dsh-switchman 1.9.0 → 1.10.3
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 +13 -10
- package/README.es.md +13 -10
- package/README.fr.md +13 -10
- package/README.it.md +13 -10
- package/README.ja.md +13 -10
- package/README.ko.md +13 -10
- package/README.md +12 -9
- package/README.pt.md +13 -10
- package/README.ru.md +13 -10
- package/README.zh-TW.md +13 -10
- package/README.zh.md +13 -10
- package/client.js +56 -56
- package/host/config.js +8 -2
- package/host/context-trim/fork.js +16 -6
- package/host/context-trim/index.js +239 -15
- package/host/context-trim/metrics.js +32 -8
- package/host/context-trim/summarizer.js +1 -1
- package/host/context-trim/surface.js +920 -0
- package/host/context-trim/trigger.js +7 -0
- package/host/context-trim/view.js +81 -27
- package/host/context-watch.js +17 -67
- package/host/handover-state.js +2 -2
- package/host/routes.js +1 -1
- package/index.js +3 -3
- package/package.json +1 -1
|
@@ -47,6 +47,13 @@ export function computeFoldableRange(messages, ledger, protectedTail) {
|
|
|
47
47
|
const list = positions.get(ledger.frontier);
|
|
48
48
|
if (list !== undefined && list.length > 0) startFrom = list[list.length - 1] + 1;
|
|
49
49
|
}
|
|
50
|
+
// A leading system message is the frozen head of the wire array: a fold
|
|
51
|
+
// anchored on it would put a surface replace op (and a view splice) over
|
|
52
|
+
// the system prompt, which the host rejects outright
|
|
53
|
+
// (assertSystemHeadRewrite — node 0 may be rewritten only by a
|
|
54
|
+
// system/message over exactly that node). Covers both the frontier-null
|
|
55
|
+
// first fold and any frontier-set fold that lands on the head.
|
|
56
|
+
if (messages[0]?.role === "system") startFrom = Math.max(startFrom, 1);
|
|
50
57
|
let best = null;
|
|
51
58
|
for (const [start, end] of segmentBounds(messages)) {
|
|
52
59
|
if (start < startFrom) continue; // behind the frontier
|
|
@@ -25,9 +25,39 @@
|
|
|
25
25
|
|
|
26
26
|
import { createHash } from "node:crypto";
|
|
27
27
|
|
|
28
|
-
/** Cheap char → token estimate (the brief's char/4 heuristic).
|
|
28
|
+
/** Cheap char → token estimate (the brief's char/4 heuristic). Applies to
|
|
29
|
+
* ASCII/latin text; CJK text tokenizes far denser (see below). */
|
|
29
30
|
export const TOKENS_PER_CHAR = 1 / 4;
|
|
30
31
|
|
|
32
|
+
/** CJK/fullwidth chars cost roughly ONE token each in the glm/deepseek BPE
|
|
33
|
+
* vocabularies (vs 0.25 for the char/4 ASCII heuristic). Pinned at 1.0:
|
|
34
|
+
* slightly high for common hanzi bigrams, which errs toward earlier folds
|
|
35
|
+
* (the safe direction). NOT the 2026-11-04 incident root cause (that session
|
|
36
|
+
* was only 4.8% CJK — the real bug was frozen-dispatch view injection); this
|
|
37
|
+
* is a general accuracy fix for CJK-heavy sessions. */
|
|
38
|
+
export const TOKENS_PER_CJK_CHAR = 1;
|
|
39
|
+
|
|
40
|
+
/** CJK-ish ranges: kana, Hangul jamo+syllables, CJK radicals/blocks and
|
|
41
|
+
* unified ideographs (incl. Ext-A), compat ideographs, vertical forms,
|
|
42
|
+
* fullwidth forms, and the supplementary-plane CJK extensions (Ext-B..F,
|
|
43
|
+
* /u flag — astral chars otherwise count as two ascii units). Halfwidth
|
|
44
|
+
* katakana (\uFF61-\uFF9F) rides in the fullwidth range at the CJK weight:
|
|
45
|
+
* slightly high, the safe direction for fold scheduling. */
|
|
46
|
+
const CJK_CHAR_RE = /[\u1100-\u11FF\u2E80-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFFEF\u{20000}-\u{2FA1F}]/gu;
|
|
47
|
+
|
|
48
|
+
/** Split one text into (asciiChars, cjkChars) in one pass over the string.
|
|
49
|
+
* The replace allocates a second string — same order of cost as the
|
|
50
|
+
* JSON.stringify this sits next to. */
|
|
51
|
+
function countChars(text) {
|
|
52
|
+
let plain;
|
|
53
|
+
try {
|
|
54
|
+
plain = text.replace(CJK_CHAR_RE, "");
|
|
55
|
+
} catch {
|
|
56
|
+
return { ascii: text.length, cjk: 0 };
|
|
57
|
+
}
|
|
58
|
+
return { ascii: plain.length, cjk: text.length - plain.length };
|
|
59
|
+
}
|
|
60
|
+
|
|
31
61
|
/** Object keys dropped before hashing (volatility would churn refs). */
|
|
32
62
|
const VOLATILE_KEYS = new Set(["ts", "time", "timestamp", "createdAt", "updatedAt", "receivedAt", "sentAt"]);
|
|
33
63
|
|
|
@@ -94,23 +124,34 @@ export function refPositions(messages) {
|
|
|
94
124
|
}
|
|
95
125
|
|
|
96
126
|
/** Estimated token count of one message (text content + tool-call payloads
|
|
97
|
-
* + small per-message framing overhead).
|
|
127
|
+
* + small per-message framing overhead). CJK-aware — see TOKENS_PER_CJK_CHAR.
|
|
128
|
+
* Never throws. */
|
|
98
129
|
export function messageTokens(message) {
|
|
99
|
-
let
|
|
130
|
+
let ascii = 8; // per-message framing overhead, counted in the ascii pool
|
|
131
|
+
let cjk = 0;
|
|
100
132
|
const content = message?.content;
|
|
101
133
|
try {
|
|
102
|
-
if (typeof content === "string")
|
|
103
|
-
|
|
134
|
+
if (typeof content === "string") {
|
|
135
|
+
const counted = countChars(content);
|
|
136
|
+
ascii += counted.ascii;
|
|
137
|
+
cjk += counted.cjk;
|
|
138
|
+
} else if (Array.isArray(content)) {
|
|
104
139
|
for (const block of content) {
|
|
105
|
-
|
|
106
|
-
|
|
140
|
+
const text = block !== null && typeof block === "object" && typeof block.text === "string" ? block.text : JSON.stringify(block ?? null);
|
|
141
|
+
const counted = countChars(text);
|
|
142
|
+
ascii += counted.ascii;
|
|
143
|
+
cjk += counted.cjk;
|
|
107
144
|
}
|
|
108
145
|
}
|
|
109
|
-
if (Array.isArray(message?.tool_calls))
|
|
146
|
+
if (Array.isArray(message?.tool_calls)) {
|
|
147
|
+
const counted = countChars(JSON.stringify(message.tool_calls));
|
|
148
|
+
ascii += counted.ascii;
|
|
149
|
+
cjk += counted.cjk;
|
|
150
|
+
}
|
|
110
151
|
} catch {
|
|
111
152
|
/* unserializable block: keep the framing estimate */
|
|
112
153
|
}
|
|
113
|
-
return Math.max(1, Math.round(
|
|
154
|
+
return Math.max(1, Math.round(ascii * TOKENS_PER_CHAR + cjk * TOKENS_PER_CJK_CHAR));
|
|
114
155
|
}
|
|
115
156
|
|
|
116
157
|
/** Estimated token count of a message range (inclusive indices). */
|
|
@@ -122,18 +163,26 @@ export function estimateTokensOf(messages, startIdx = 0, endIdx = messages.lengt
|
|
|
122
163
|
|
|
123
164
|
// ---- tool-pair segmentation -------------------------------------------------
|
|
124
165
|
|
|
125
|
-
/** True when the message OPENS tool calls
|
|
166
|
+
/** True when the message OPENS tool calls. Covers the DSH-native shape
|
|
167
|
+
* (assistant content block {type:'tool-call', id} — packages/llm/llm/src/
|
|
168
|
+
* types.ts ToolCallBlock) plus the provider-shaped variants: the OpenAI
|
|
169
|
+
* message.tool_calls array and the Anthropic {type:'tool_use'} block. The
|
|
170
|
+
* DSH-native shape is what actually reaches this plugin's captures; missing
|
|
171
|
+
* it made every native assistant tool call look like a plain message, so
|
|
172
|
+
* segmentBounds below split the call from its results. */
|
|
126
173
|
function opensToolCalls(message) {
|
|
127
174
|
if (message?.role !== "assistant") return false;
|
|
128
175
|
if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) return true;
|
|
129
|
-
if (Array.isArray(message.content)) return message.content.some((block) => block?.type === "tool_use");
|
|
176
|
+
if (Array.isArray(message.content)) return message.content.some((block) => block?.type === "tool_use" || block?.type === "tool-call");
|
|
130
177
|
return false;
|
|
131
178
|
}
|
|
132
179
|
|
|
133
|
-
/** True when the message CARRIES tool results
|
|
180
|
+
/** True when the message CARRIES tool results. DSH-native results are
|
|
181
|
+
* role:'tool' messages (with their call id on the message); provider-shaped
|
|
182
|
+
* variants keep a tool_result/tool-result content block. */
|
|
134
183
|
function carriesToolResults(message) {
|
|
135
184
|
if (message?.role === "tool") return true;
|
|
136
|
-
if (Array.isArray(message.content)) return message.content.some((block) => block?.type === "tool_result");
|
|
185
|
+
if (Array.isArray(message.content)) return message.content.some((block) => block?.type === "tool_result" || block?.type === "tool-result");
|
|
137
186
|
return false;
|
|
138
187
|
}
|
|
139
188
|
|
|
@@ -261,6 +310,10 @@ export function buildView(messages, ledger, options = {}) {
|
|
|
261
310
|
for (const entry of located) {
|
|
262
311
|
if (entry.firstIdx < at) continue; // overlaps an applied block — skip
|
|
263
312
|
if (protectedTail > 0 && entry.lastIdx >= messages.length - protectedTail) continue;
|
|
313
|
+
// The system prompt at index 0 is never foldable: splicing it off the
|
|
314
|
+
// wire would drop the prompt itself. Plain skip (not unlocated) so
|
|
315
|
+
// the ledgerIsStale epoch semantics stay untouched.
|
|
316
|
+
if (entry.firstIdx === 0 && messages[0]?.role === "system") continue;
|
|
264
317
|
if (!rangeIsSegmentAligned(messages, entry.firstIdx, entry.lastIdx)) continue; // never split a tool pair (design §4)
|
|
265
318
|
for (let i = at; i < entry.firstIdx; i++) out.push(messages[i]);
|
|
266
319
|
out.push(summaryMessageOf(entry.block));
|
|
@@ -287,21 +340,22 @@ export function ledgerIsStale(view, ledger) {
|
|
|
287
340
|
* identity-preserving), resumed sessions re-send FROZEN host-assembled
|
|
288
341
|
* history (splice throws on its internal index deletes), and some dispatch
|
|
289
342
|
* options are frozen objects on top of that. Returns the strategy used so
|
|
290
|
-
* the caller can log it: "in-place" | "swapped" | "none"
|
|
343
|
+
* the caller can log it: "in-place" | "swapped" | "none" (a fold existed but
|
|
344
|
+
* the dispatch could not carry it — the wire ships raw history) |
|
|
345
|
+
* "nothing-to-apply" (no located fold blocks; the raw history IS the view). */
|
|
291
346
|
export function injectView(options, messages, view) {
|
|
292
|
-
if (view.applied
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
347
|
+
if (view.applied <= 0) return "nothing-to-apply";
|
|
348
|
+
try {
|
|
349
|
+
replaceInPlace(messages, view.messages);
|
|
350
|
+
return "in-place";
|
|
351
|
+
} catch {
|
|
352
|
+
/* frozen array — fall through to the reference swap */
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
options.messages = view.messages;
|
|
356
|
+
return "swapped";
|
|
357
|
+
} catch {
|
|
358
|
+
/* frozen options — this dispatch cannot carry the view; fail open */
|
|
305
359
|
}
|
|
306
360
|
return "none";
|
|
307
361
|
}
|
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
|
|
@@ -54,7 +54,6 @@ 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
56
|
import { recentTrimViewTokensOf } from "./context-trim/index.js";
|
|
57
|
-
import { recordForkLink, trimStateDirOf } from "./context-trim/fork.js";
|
|
58
57
|
|
|
59
58
|
/** Tools whose result text is accounted against the per-turn read budget. */
|
|
60
59
|
const ACCOUNT_TOOLS = new Set(["read", "glob", "grep", "bash"]);
|
|
@@ -187,7 +186,7 @@ function bucketKilo(tokens) {
|
|
|
187
186
|
* invariant); the numbers live in the tail context line only. */
|
|
188
187
|
const PROTOCOL_ROOT_TEXT = [
|
|
189
188
|
"[SWITCHMAN:WATERMARK] protocol: this session is guarded by a context watermark measured every step.",
|
|
190
|
-
"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.",
|
|
191
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.",
|
|
192
191
|
].join("\n");
|
|
193
192
|
|
|
@@ -215,7 +214,7 @@ function renderRootWatermark(level, used, th, status) {
|
|
|
215
214
|
const lines = [head, "force tier: MANDATORY wrap-up now — produce the final answer from what you already hold."];
|
|
216
215
|
const phase = status?.phase;
|
|
217
216
|
if (phase === "backup") {
|
|
218
|
-
lines.push(`context handover running:
|
|
217
|
+
lines.push(`context handover running: writing the handover document…`);
|
|
219
218
|
} else if (phase === "compacting") {
|
|
220
219
|
lines.push(`context handover running: compaction attempt ${status?.attempt ?? "?"} (retries while the turn is still running)…`);
|
|
221
220
|
} else if (phase === "continuation") {
|
|
@@ -224,7 +223,7 @@ function renderRootWatermark(level, used, th, status) {
|
|
|
224
223
|
lines.push(
|
|
225
224
|
status?.cooldownMin > 0
|
|
226
225
|
? `automatic handover cooldown: ${status.cooldownMin} min left; run /ctx-handover${status?.tool ? " or call the ctx_handover tool" : ""} to force one now.`
|
|
227
|
-
: `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" : ""}.`,
|
|
228
227
|
);
|
|
229
228
|
} else {
|
|
230
229
|
lines.push(`run /ctx-handover${status?.tool ? " or call the ctx_handover tool" : ""} to trigger a manual context handover.`);
|
|
@@ -423,56 +422,6 @@ function applyContextWatch(ctx, config) {
|
|
|
423
422
|
return typeof first.provider === "string" && typeof first.model === "string" ? first : null;
|
|
424
423
|
};
|
|
425
424
|
|
|
426
|
-
/** Manual handover step 1 — fork a dormant backup child of this session
|
|
427
|
-
* through the subagents service's fork provider (the child inherits the
|
|
428
|
-
* completed turns, runs one tiny confirmation turn on the economy pool
|
|
429
|
-
* model when configured, then stays idle as a browsable snapshot). The
|
|
430
|
-
* spec MUST carry a signal: the manager calls spec.signal.throwIfAborted()
|
|
431
|
-
* unconditionally, so a missing signal fails the whole fork with a
|
|
432
|
-
* TypeError (the live failure observed on 2026-10-01). The append-only
|
|
433
|
-
* session log always remains the full-fidelity backup, so a failed fork
|
|
434
|
-
* only downgrades the UX, never the data. Returns {childId, reason}. */
|
|
435
|
-
const forkBackup = async (agent, sessionId, signal) => {
|
|
436
|
-
try {
|
|
437
|
-
const economy = firstPoolRoute(config.poolEconomy);
|
|
438
|
-
const started = await ctx.subagents?.startContinuable?.({
|
|
439
|
-
provider: "fork",
|
|
440
|
-
label: "dsh-switchman handover backup",
|
|
441
|
-
signal: signal ?? new AbortController().signal,
|
|
442
|
-
request: {
|
|
443
|
-
parent: agent,
|
|
444
|
-
prompt: [
|
|
445
|
-
{
|
|
446
|
-
type: "text",
|
|
447
|
-
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.",
|
|
448
|
-
},
|
|
449
|
-
],
|
|
450
|
-
...(economy === null ? {} : { agentOptions: { provider: economy.provider, model: economy.model } }),
|
|
451
|
-
},
|
|
452
|
-
});
|
|
453
|
-
if (typeof started?.childId !== "string") {
|
|
454
|
-
const reason = `startContinuable returned no child id (${JSON.stringify(started ?? null)})`;
|
|
455
|
-
warn(
|
|
456
|
-
`handover backup fork failed for session ${sessionId} (continuing; the append-only log remains the full backup): ${reason}`,
|
|
457
|
-
);
|
|
458
|
-
return { childId: null, reason };
|
|
459
|
-
}
|
|
460
|
-
// Copy-on-fork: the backup child inherits this session's trim fold
|
|
461
|
-
// ledger at its first model call (host/context-trim/fork.js). The
|
|
462
|
-
// link is best-effort bookkeeping on an already-successful fork —
|
|
463
|
-
// recordForkLink never throws; a lost link only downgrades the
|
|
464
|
-
// child to a clean ledger.
|
|
465
|
-
recordForkLink(sessionId, started.childId, { stateDir: trimStateDirOf(config), warn });
|
|
466
|
-
return { childId: started.childId, reason: null };
|
|
467
|
-
} catch (error) {
|
|
468
|
-
const reason = error?.message ?? String(error);
|
|
469
|
-
warn(
|
|
470
|
-
`handover backup fork failed for session ${sessionId} (continuing; the append-only log remains the full backup): ${reason}`,
|
|
471
|
-
);
|
|
472
|
-
return { childId: null, reason };
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
|
|
476
425
|
/** One handover-document path per handover run (stamp frozen at run
|
|
477
426
|
* start so refreshes overwrite the same file). Null when the session
|
|
478
427
|
* exposes no usable cwd. */
|
|
@@ -531,7 +480,7 @@ function applyContextWatch(ctx, config) {
|
|
|
531
480
|
``,
|
|
532
481
|
`- handed over at: ${new Date().toISOString()} (document refreshed before every compaction attempt)`,
|
|
533
482
|
`- context used before compaction: ~${used ?? "unknown"} tokens`,
|
|
534
|
-
`- backup
|
|
483
|
+
`- backup snapshot: none — the append-only session log holds the full pre-handover history`,
|
|
535
484
|
``,
|
|
536
485
|
...liveSection,
|
|
537
486
|
`## Task state (filled in by the agent right after the handover)`,
|
|
@@ -644,9 +593,7 @@ function applyContextWatch(ctx, config) {
|
|
|
644
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.`;
|
|
645
594
|
const instruction = [
|
|
646
595
|
"[SWITCHMAN:HANDOVER] A context handover just completed for this session.",
|
|
647
|
-
|
|
648
|
-
? `A dormant fork backup holding the full pre-handover state is session ${backupId}.`
|
|
649
|
-
: `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).`,
|
|
650
597
|
docFile !== null
|
|
651
598
|
? `The handover document is at ${docFile}.`
|
|
652
599
|
: `Create the handover document under .dsh-switchman/handover/ in the working directory.`,
|
|
@@ -690,8 +637,11 @@ function applyContextWatch(ctx, config) {
|
|
|
690
637
|
state.inflight.add(sessionId);
|
|
691
638
|
try {
|
|
692
639
|
recordHandover(sessionId, { phase: "backup", source, at: Date.now() });
|
|
693
|
-
|
|
694
|
-
|
|
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;
|
|
695
645
|
// One document path per handover run (the stamp freezes here so
|
|
696
646
|
// refreshes overwrite the same file). The document itself is
|
|
697
647
|
// written as the LAST action before every compaction attempt —
|
|
@@ -806,7 +756,7 @@ function applyContextWatch(ctx, config) {
|
|
|
806
756
|
if (!outcome.ok)
|
|
807
757
|
return {
|
|
808
758
|
kind: "error",
|
|
809
|
-
text: `/ctx-handover: compaction failed
|
|
759
|
+
text: `/ctx-handover: compaction failed — ${outcome.reason}`,
|
|
810
760
|
};
|
|
811
761
|
recordHandover(sessionId, { phase: "continuation", source, at: Date.now() });
|
|
812
762
|
// Final enumeration right after compaction settled: anything that
|
|
@@ -840,7 +790,7 @@ function applyContextWatch(ctx, config) {
|
|
|
840
790
|
}
|
|
841
791
|
await armContinuation(agent, backupId, docFile, live, appeared);
|
|
842
792
|
scheduleMaterializationRecheck(agent, sessionId, backupId, docFile, live);
|
|
843
|
-
const backupText =
|
|
793
|
+
const backupText = "session log (full pre-handover history; no fork snapshot)";
|
|
844
794
|
return {
|
|
845
795
|
kind: "success",
|
|
846
796
|
text: `[SWITCHMAN:WATERMARK] handover complete — backup ${backupText}, document ${docFile ?? "agent-created"}, compaction done, continuation armed.`,
|
|
@@ -978,7 +928,7 @@ function applyContextWatch(ctx, config) {
|
|
|
978
928
|
});
|
|
979
929
|
ctx.commands.register({
|
|
980
930
|
name: "ctx-handover",
|
|
981
|
-
description: "Trigger a full context handover for this session now (
|
|
931
|
+
description: "Trigger a full context handover for this session now (document + compaction + continuation).",
|
|
982
932
|
handler: (invocation) =>
|
|
983
933
|
runHandover(invocation?.agent, sessionIdOf(invocation?.agent), invocation?.commandId, invocation?.signal, "manual"),
|
|
984
934
|
});
|
|
@@ -996,7 +946,7 @@ function applyContextWatch(ctx, config) {
|
|
|
996
946
|
ctx.tools?.register?.({
|
|
997
947
|
name: "ctx_handover",
|
|
998
948
|
description:
|
|
999
|
-
"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.",
|
|
1000
950
|
parameters: {
|
|
1001
951
|
type: "object",
|
|
1002
952
|
properties: {
|
|
@@ -1022,7 +972,7 @@ function applyContextWatch(ctx, config) {
|
|
|
1022
972
|
),
|
|
1023
973
|
(error) => warn(`ctx_handover handover crashed: ${error?.message ?? error}`),
|
|
1024
974
|
);
|
|
1025
|
-
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.";
|
|
1026
976
|
},
|
|
1027
977
|
presentCall: (args) => ({
|
|
1028
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/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
|
*
|
|
@@ -89,7 +89,7 @@ const name = "dsh-switchman";
|
|
|
89
89
|
* system-prompt section registry, the settings store it writes back to, the
|
|
90
90
|
* token meter and agent registry the watermark reads, the command registry
|
|
91
91
|
* for /ctx-*, the tools registry for the ctx_handover agent tool, the
|
|
92
|
-
* subagents service the
|
|
92
|
+
* subagents service the teams layer dispatch goes through, the sessions
|
|
93
93
|
* persistence the handover's continuation flush uses, the plugin-manager
|
|
94
94
|
* service the teams layer's bundle enable goes through, and the session
|
|
95
95
|
* query engine the subagent model annotation + per-session vision verdicts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-switchman",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.3",
|
|
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": [
|