dsh-switchman 1.8.1 → 1.10.2
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.de.md +50 -7
- package/README.es.md +50 -7
- package/README.fr.md +50 -7
- package/README.it.md +50 -7
- package/README.ja.md +50 -7
- package/README.ko.md +50 -7
- package/README.md +49 -6
- package/README.pt.md +50 -7
- package/README.ru.md +50 -7
- package/README.zh-TW.md +50 -7
- package/README.zh.md +50 -7
- package/client.js +455 -58
- package/cordis.patch.yml +32 -0
- package/host/config.js +48 -2
- package/host/context-trim/fork.js +178 -0
- package/host/context-trim/index.js +1054 -0
- package/host/context-trim/metrics.js +128 -0
- package/host/context-trim/state.js +241 -0
- package/host/context-trim/summarizer.js +179 -0
- package/host/context-trim/surface.js +736 -0
- package/host/context-trim/trigger.js +129 -0
- package/host/context-trim/view.js +362 -0
- package/host/context-watch.js +32 -62
- package/host/handover-state.js +2 -2
- package/host/routes.js +1 -1
- package/host/subagent-model.js +1 -1
- package/index.js +18 -4
- package/package.json +4 -1
package/host/context-watch.js
CHANGED
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
* reach it — the borrowed handler's closure carries the right composition.
|
|
27
27
|
* Every handover trigger — the force-tier automation (fire-and-forget,
|
|
28
28
|
* session-level in-flight flag plus a 10-minute cooldown) and manual
|
|
29
|
-
* /ctx-handover — shares one
|
|
30
|
-
*
|
|
29
|
+
* /ctx-handover — shares one three-step flow: write the handover
|
|
30
|
+
* document; then, on
|
|
31
31
|
* every compaction attempt, re-enumerate the live dispatched
|
|
32
32
|
* subagents and refresh the handover document as the very last
|
|
33
33
|
* action before compacting via the borrowed per-agent /compact
|
|
@@ -53,6 +53,7 @@ import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
53
53
|
import { dirname, join } from "node:path";
|
|
54
54
|
import { clearHandover, handoverOf, recordHandover } from "./handover-state.js";
|
|
55
55
|
import { enumerateLiveSubagents, sessionRecordPath, sessionRecordsDir } from "./subagents.js";
|
|
56
|
+
import { recentTrimViewTokensOf } from "./context-trim/index.js";
|
|
56
57
|
|
|
57
58
|
/** Tools whose result text is accounted against the per-turn read budget. */
|
|
58
59
|
const ACCOUNT_TOOLS = new Set(["read", "glob", "grep", "bash"]);
|
|
@@ -146,11 +147,23 @@ function readThresholds(config) {
|
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
/** Measure one agent's session now; null when the meter is missing/failing. */
|
|
149
|
-
function measureNow(ctx, agent) {
|
|
150
|
+
export function measureNow(ctx, agent) {
|
|
150
151
|
try {
|
|
151
152
|
const measurement = agent?.session ? ctx.tokenMeter?.measure?.(agent.session) : undefined;
|
|
152
153
|
const used = Number.isFinite(measurement?.totalTokens) ? measurement.totalTokens : null;
|
|
153
|
-
|
|
154
|
+
// Meter duality (design §8): prefer the trim layer's fresh outbound-view
|
|
155
|
+
// estimate when present — billion-context can fold far more away than the
|
|
156
|
+
// meter's stored tally admits, and the watermark must track what the
|
|
157
|
+
// model actually sees. Strictly additive + fail-open: any trim failure
|
|
158
|
+
// falls back to the stored measure.
|
|
159
|
+
try {
|
|
160
|
+
const sessionId = agent?.session?.id ?? (typeof agent?.id === "string" ? agent.id : null);
|
|
161
|
+
const viewTokens = sessionId === null ? null : recentTrimViewTokensOf(sessionId);
|
|
162
|
+
if (viewTokens !== null) return viewTokens;
|
|
163
|
+
} catch {
|
|
164
|
+
// fall through to the stored measure
|
|
165
|
+
}
|
|
166
|
+
return used;
|
|
154
167
|
} catch {
|
|
155
168
|
return null;
|
|
156
169
|
}
|
|
@@ -173,7 +186,7 @@ function bucketKilo(tokens) {
|
|
|
173
186
|
* invariant); the numbers live in the tail context line only. */
|
|
174
187
|
const PROTOCOL_ROOT_TEXT = [
|
|
175
188
|
"[SWITCHMAN:WATERMARK] protocol: this session is guarded by a context watermark measured every step.",
|
|
176
|
-
"ok tier is silent. soft tier: prefer delegating lightweight work (subagent/workflow) over reading more context yourself. hard tier: stop broad reading (read/glob/grep are budget-limited and may be denied) and start wrapping up. force tier: MANDATORY wrap-up now — produce the final answer from what you already hold; an automatic context handover (
|
|
189
|
+
"ok tier is silent. soft tier: prefer delegating lightweight work (subagent/workflow) over reading more context yourself. hard tier: stop broad reading (read/glob/grep are budget-limited and may be denied) and start wrapping up. force tier: MANDATORY wrap-up now — produce the final answer from what you already hold; an automatic context handover (document + compaction + continuation) may run and will tell you to end the turn.",
|
|
177
190
|
"The live tier and the bucketed used-token reading (against the soft/hard/force budget) arrive ONLY as [SWITCHMAN:WATERMARK] lines inside the appended runtime-context snapshot at the conversation tail; a tier change is announced on the step it happens. /ctx-pause pauses every watermark enforcement (measurement continues), /ctx-resume restores it, /ctx-handover starts a handover immediately.",
|
|
178
191
|
].join("\n");
|
|
179
192
|
|
|
@@ -201,7 +214,7 @@ function renderRootWatermark(level, used, th, status) {
|
|
|
201
214
|
const lines = [head, "force tier: MANDATORY wrap-up now — produce the final answer from what you already hold."];
|
|
202
215
|
const phase = status?.phase;
|
|
203
216
|
if (phase === "backup") {
|
|
204
|
-
lines.push(`context handover running:
|
|
217
|
+
lines.push(`context handover running: writing the handover document…`);
|
|
205
218
|
} else if (phase === "compacting") {
|
|
206
219
|
lines.push(`context handover running: compaction attempt ${status?.attempt ?? "?"} (retries while the turn is still running)…`);
|
|
207
220
|
} else if (phase === "continuation") {
|
|
@@ -210,7 +223,7 @@ function renderRootWatermark(level, used, th, status) {
|
|
|
210
223
|
lines.push(
|
|
211
224
|
status?.cooldownMin > 0
|
|
212
225
|
? `automatic handover cooldown: ${status.cooldownMin} min left; run /ctx-handover${status?.tool ? " or call the ctx_handover tool" : ""} to force one now.`
|
|
213
|
-
: `an automatic context handover (
|
|
226
|
+
: `an automatic context handover (document + compaction + continuation) is armed and will fire at the next step boundary${status?.tool ? "; calling the ctx_handover tool triggers it immediately" : ""}.`,
|
|
214
227
|
);
|
|
215
228
|
} else {
|
|
216
229
|
lines.push(`run /ctx-handover${status?.tool ? " or call the ctx_handover tool" : ""} to trigger a manual context handover.`);
|
|
@@ -409,50 +422,6 @@ function applyContextWatch(ctx, config) {
|
|
|
409
422
|
return typeof first.provider === "string" && typeof first.model === "string" ? first : null;
|
|
410
423
|
};
|
|
411
424
|
|
|
412
|
-
/** Manual handover step 1 — fork a dormant backup child of this session
|
|
413
|
-
* through the subagents service's fork provider (the child inherits the
|
|
414
|
-
* completed turns, runs one tiny confirmation turn on the economy pool
|
|
415
|
-
* model when configured, then stays idle as a browsable snapshot). The
|
|
416
|
-
* spec MUST carry a signal: the manager calls spec.signal.throwIfAborted()
|
|
417
|
-
* unconditionally, so a missing signal fails the whole fork with a
|
|
418
|
-
* TypeError (the live failure observed on 2026-10-01). The append-only
|
|
419
|
-
* session log always remains the full-fidelity backup, so a failed fork
|
|
420
|
-
* only downgrades the UX, never the data. Returns {childId, reason}. */
|
|
421
|
-
const forkBackup = async (agent, sessionId, signal) => {
|
|
422
|
-
try {
|
|
423
|
-
const economy = firstPoolRoute(config.poolEconomy);
|
|
424
|
-
const started = await ctx.subagents?.startContinuable?.({
|
|
425
|
-
provider: "fork",
|
|
426
|
-
label: "dsh-switchman handover backup",
|
|
427
|
-
signal: signal ?? new AbortController().signal,
|
|
428
|
-
request: {
|
|
429
|
-
parent: agent,
|
|
430
|
-
prompt: [
|
|
431
|
-
{
|
|
432
|
-
type: "text",
|
|
433
|
-
text: "[SWITCHMAN:HANDOVER-BACKUP] This session is a dormant fork snapshot taken immediately before a context handover in the parent session. Do not perform any work. Reply with exactly one line confirming you hold the complete pre-handover state, then stay idle.",
|
|
434
|
-
},
|
|
435
|
-
],
|
|
436
|
-
...(economy === null ? {} : { agentOptions: { provider: economy.provider, model: economy.model } }),
|
|
437
|
-
},
|
|
438
|
-
});
|
|
439
|
-
if (typeof started?.childId !== "string") {
|
|
440
|
-
const reason = `startContinuable returned no child id (${JSON.stringify(started ?? null)})`;
|
|
441
|
-
warn(
|
|
442
|
-
`handover backup fork failed for session ${sessionId} (continuing; the append-only log remains the full backup): ${reason}`,
|
|
443
|
-
);
|
|
444
|
-
return { childId: null, reason };
|
|
445
|
-
}
|
|
446
|
-
return { childId: started.childId, reason: null };
|
|
447
|
-
} catch (error) {
|
|
448
|
-
const reason = error?.message ?? String(error);
|
|
449
|
-
warn(
|
|
450
|
-
`handover backup fork failed for session ${sessionId} (continuing; the append-only log remains the full backup): ${reason}`,
|
|
451
|
-
);
|
|
452
|
-
return { childId: null, reason };
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
|
|
456
425
|
/** One handover-document path per handover run (stamp frozen at run
|
|
457
426
|
* start so refreshes overwrite the same file). Null when the session
|
|
458
427
|
* exposes no usable cwd. */
|
|
@@ -511,7 +480,7 @@ function applyContextWatch(ctx, config) {
|
|
|
511
480
|
``,
|
|
512
481
|
`- handed over at: ${new Date().toISOString()} (document refreshed before every compaction attempt)`,
|
|
513
482
|
`- context used before compaction: ~${used ?? "unknown"} tokens`,
|
|
514
|
-
`- backup
|
|
483
|
+
`- backup snapshot: none — the append-only session log holds the full pre-handover history`,
|
|
515
484
|
``,
|
|
516
485
|
...liveSection,
|
|
517
486
|
`## Task state (filled in by the agent right after the handover)`,
|
|
@@ -624,9 +593,7 @@ function applyContextWatch(ctx, config) {
|
|
|
624
593
|
: `If the compaction summary mentions dispatched subagents that are NOT listed above, they materialized after this enumeration — a post-wake recheck will announce them within seconds; do not re-dispatch anything before it fires.`;
|
|
625
594
|
const instruction = [
|
|
626
595
|
"[SWITCHMAN:HANDOVER] A context handover just completed for this session.",
|
|
627
|
-
|
|
628
|
-
? `A dormant fork backup holding the full pre-handover state is session ${backupId}.`
|
|
629
|
-
: `No fork backup was created; the append-only session log still holds the full history.`,
|
|
596
|
+
`The append-only session log holds the full pre-handover history (no fork snapshot).`,
|
|
630
597
|
docFile !== null
|
|
631
598
|
? `The handover document is at ${docFile}.`
|
|
632
599
|
: `Create the handover document under .dsh-switchman/handover/ in the working directory.`,
|
|
@@ -670,8 +637,11 @@ function applyContextWatch(ctx, config) {
|
|
|
670
637
|
state.inflight.add(sessionId);
|
|
671
638
|
try {
|
|
672
639
|
recordHandover(sessionId, { phase: "backup", source, at: Date.now() });
|
|
673
|
-
|
|
674
|
-
|
|
640
|
+
// Backup = the append-only session log itself (full fidelity). The
|
|
641
|
+
// old fork-snapshot step was removed 2026-11-05: the subagents
|
|
642
|
+
// fork API (ctx.subagents.startContinuable) is absent from the
|
|
643
|
+
// installed host, so the fork failed on every single run.
|
|
644
|
+
const backupId = null;
|
|
675
645
|
// One document path per handover run (the stamp freezes here so
|
|
676
646
|
// refreshes overwrite the same file). The document itself is
|
|
677
647
|
// written as the LAST action before every compaction attempt —
|
|
@@ -786,7 +756,7 @@ function applyContextWatch(ctx, config) {
|
|
|
786
756
|
if (!outcome.ok)
|
|
787
757
|
return {
|
|
788
758
|
kind: "error",
|
|
789
|
-
text: `/ctx-handover: compaction failed
|
|
759
|
+
text: `/ctx-handover: compaction failed — ${outcome.reason}`,
|
|
790
760
|
};
|
|
791
761
|
recordHandover(sessionId, { phase: "continuation", source, at: Date.now() });
|
|
792
762
|
// Final enumeration right after compaction settled: anything that
|
|
@@ -820,7 +790,7 @@ function applyContextWatch(ctx, config) {
|
|
|
820
790
|
}
|
|
821
791
|
await armContinuation(agent, backupId, docFile, live, appeared);
|
|
822
792
|
scheduleMaterializationRecheck(agent, sessionId, backupId, docFile, live);
|
|
823
|
-
const backupText =
|
|
793
|
+
const backupText = "session log (full pre-handover history; no fork snapshot)";
|
|
824
794
|
return {
|
|
825
795
|
kind: "success",
|
|
826
796
|
text: `[SWITCHMAN:WATERMARK] handover complete — backup ${backupText}, document ${docFile ?? "agent-created"}, compaction done, continuation armed.`,
|
|
@@ -958,7 +928,7 @@ function applyContextWatch(ctx, config) {
|
|
|
958
928
|
});
|
|
959
929
|
ctx.commands.register({
|
|
960
930
|
name: "ctx-handover",
|
|
961
|
-
description: "Trigger a full context handover for this session now (
|
|
931
|
+
description: "Trigger a full context handover for this session now (document + compaction + continuation).",
|
|
962
932
|
handler: (invocation) =>
|
|
963
933
|
runHandover(invocation?.agent, sessionIdOf(invocation?.agent), invocation?.commandId, invocation?.signal, "manual"),
|
|
964
934
|
});
|
|
@@ -976,7 +946,7 @@ function applyContextWatch(ctx, config) {
|
|
|
976
946
|
ctx.tools?.register?.({
|
|
977
947
|
name: "ctx_handover",
|
|
978
948
|
description:
|
|
979
|
-
"Trigger a full dsh-switchman context handover for this session NOW:
|
|
949
|
+
"Trigger a full dsh-switchman context handover for this session NOW: write the handover document, compact this session at the idle boundary, then wake it to fill in the document and continue the task. Call it when the watermark banner reaches the force tier or the user asks for a handover, then END the turn immediately.",
|
|
980
950
|
parameters: {
|
|
981
951
|
type: "object",
|
|
982
952
|
properties: {
|
|
@@ -1002,7 +972,7 @@ function applyContextWatch(ctx, config) {
|
|
|
1002
972
|
),
|
|
1003
973
|
(error) => warn(`ctx_handover handover crashed: ${error?.message ?? error}`),
|
|
1004
974
|
);
|
|
1005
|
-
return "[SWITCHMAN:WATERMARK] ctx_handover scheduled:
|
|
975
|
+
return "[SWITCHMAN:WATERMARK] ctx_handover scheduled: handover document + compaction + continuation are running. END this turn now — reply briefly and call no more tools, so the compaction lands at the idle boundary. Your closing line must tell the user the handover is running in the background and will continue automatically (the session-header badge shows the live state) — otherwise the GUI looks idle during the handover window.";
|
|
1006
976
|
},
|
|
1007
977
|
presentCall: (args) => ({
|
|
1008
978
|
card: "generic",
|
package/host/handover-state.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Shared in-flight handover registry (process-lifetime, never persisted).
|
|
3
3
|
*
|
|
4
4
|
* context-watch.js records phase transitions from the first synchronous
|
|
5
|
-
* line of runHandover (t=0
|
|
5
|
+
* line of runHandover (t=0); routes.js
|
|
6
6
|
* exposes the read-only snapshot behind GET /api/dsh-switchman/
|
|
7
7
|
* handover-state so the client badge can show a live "handover in
|
|
8
|
-
* progress" cue during the otherwise-silent
|
|
8
|
+
* progress" cue during the otherwise-silent handover window.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/** sessionId -> { sessionId, phase, source?, attempt?, at }. */
|
package/host/routes.js
CHANGED
|
@@ -587,7 +587,7 @@ export function makeRoutes(ctx, config) {
|
|
|
587
587
|
handler: async (req, res) => {
|
|
588
588
|
if (!guard(req, res, "GET")) return;
|
|
589
589
|
// Read-only live registry: the client badge polls this during
|
|
590
|
-
// the otherwise-silent
|
|
590
|
+
// the otherwise-silent handover window ({count, handovers}),
|
|
591
591
|
// so the GUI shows an animated "handover in progress" cue.
|
|
592
592
|
const handovers = handoverSnapshot();
|
|
593
593
|
writeJson(res, 200, { count: handovers.length, handovers });
|
package/host/subagent-model.js
CHANGED
package/index.js
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Phase 3 mounts the context-watermark layer (host/context-watch.js): the
|
|
22
22
|
* `[SWITCHMAN:WATERMARK]` tiered banner from the live token meter, a per-turn
|
|
23
|
-
* read budget with deny/cap gates, the
|
|
24
|
-
* (
|
|
23
|
+
* read budget with deny/cap gates, the force-tier handover
|
|
24
|
+
* (document + compaction + continuation) plus the
|
|
25
25
|
* ctx_handover agent tool, and the /ctx-pause, /ctx-resume,
|
|
26
26
|
* /ctx-handover commands.
|
|
27
27
|
*
|
|
@@ -69,6 +69,9 @@ import { applySubagentModel } from "./host/subagent-model.js";
|
|
|
69
69
|
|
|
70
70
|
// Context-watermark layer (banner + read budget + handover + /ctx-* commands).
|
|
71
71
|
import { applyContextWatch } from "./host/context-watch.js";
|
|
72
|
+
// Context-trim layer (deterministic outbound-view folding + idle-boundary
|
|
73
|
+
// automatic folds + ctx_trim_fold/ctx_trim_status advisory tools).
|
|
74
|
+
import { applyContextTrim } from "./host/context-trim/index.js";
|
|
72
75
|
// Vision dispatch gate (/vision command + vision-state route for the
|
|
73
76
|
// composer-dock hint): unlocks image sending on text-only models.
|
|
74
77
|
import { applyVision } from "./host/vision.js";
|
|
@@ -86,7 +89,7 @@ const name = "dsh-switchman";
|
|
|
86
89
|
* system-prompt section registry, the settings store it writes back to, the
|
|
87
90
|
* token meter and agent registry the watermark reads, the command registry
|
|
88
91
|
* for /ctx-*, the tools registry for the ctx_handover agent tool, the
|
|
89
|
-
* subagents service the
|
|
92
|
+
* subagents service the teams layer dispatch goes through, the sessions
|
|
90
93
|
* persistence the handover's continuation flush uses, the plugin-manager
|
|
91
94
|
* service the teams layer's bundle enable goes through, and the session
|
|
92
95
|
* query engine the subagent model annotation + per-session vision verdicts
|
|
@@ -100,7 +103,17 @@ const name = "dsh-switchman";
|
|
|
100
103
|
* only inside each preset's per-agent isolated group (isolate: { compaction:
|
|
101
104
|
* true }), never at this root — host/context-watch.js reaches it by borrowing
|
|
102
105
|
* the session's own per-agent /compact handler through
|
|
103
|
-
* ctx.commands.find(agent, "compact").
|
|
106
|
+
* ctx.commands.find(agent, "compact").
|
|
107
|
+
*
|
|
108
|
+
* The context-trim layer (host/context-trim/) adds fully-automatic context
|
|
109
|
+
* compression: an llm/stream listener rebuilds a deterministic outbound
|
|
110
|
+
* view (folded ranges replaced by ONE synthetic summary message each,
|
|
111
|
+
* per-session ledger) in place before every agent-loop model call, an
|
|
112
|
+
* agent/status idle hook runs the fold decisions (growth gate / pressure /
|
|
113
|
+
* emergency tiers; side-model summaries with mechanical emergency
|
|
114
|
+
* fallback), and the ctx_trim_fold / ctx_trim_status tools expose the
|
|
115
|
+
* advisory surface. Fail-open throughout: the listener always calls
|
|
116
|
+
* next(), and trimming degrades to a no-op on any internal failure. */
|
|
104
117
|
const inject = ["skills", "systemPrompt", "settings", "tokenMeter", "commands", "tools", "agents", "webServer", "subagents", "sessions", "pluginManager", "sessionQuery"];
|
|
105
118
|
|
|
106
119
|
/** Must equal `BUNDLED_SKILL_RANK` of @deepseek-ai/dsh-skill (packaged skill
|
|
@@ -193,6 +206,7 @@ function apply(ctx, config) {
|
|
|
193
206
|
applyDispatch(ctx, config);
|
|
194
207
|
applyTeams(ctx, config);
|
|
195
208
|
applyContextWatch(ctx, config);
|
|
209
|
+
applyContextTrim(ctx, config);
|
|
196
210
|
applyVision(ctx, config);
|
|
197
211
|
applyIntent(ctx, config);
|
|
198
212
|
applySubagentModel(ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-switchman",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.2",
|
|
4
4
|
"description": "Autonomous Agent Teams doctrine for the four shipped DeepSeek Harness presets, plus language preferences, dispatch pools with model ranking, and context watermark control.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
},
|
|
23
23
|
"type": "module",
|
|
24
24
|
"main": "./index.js",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "node --test scripts/test/*.test.mjs"
|
|
27
|
+
},
|
|
25
28
|
"icon": "./icon.svg",
|
|
26
29
|
"engines": {
|
|
27
30
|
"node": ">=24",
|