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.
@@ -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). Never throws. */
127
+ * + small per-message framing overhead). CJK-aware — see TOKENS_PER_CJK_CHAR.
128
+ * Never throws. */
98
129
  export function messageTokens(message) {
99
- let chars = 8;
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") chars += content.length;
103
- else if (Array.isArray(content)) {
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
- if (block !== null && typeof block === "object" && typeof block.text === "string") chars += block.text.length;
106
- else chars += JSON.stringify(block ?? null).length;
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)) chars += JSON.stringify(message.tool_calls).length;
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(chars * TOKENS_PER_CHAR));
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 (either wire shape). */
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 (either wire shape). */
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 > 0) {
293
- try {
294
- replaceInPlace(messages, view.messages);
295
- return "in-place";
296
- } catch {
297
- /* frozen array — fall through to the reference swap */
298
- }
299
- try {
300
- options.messages = view.messages;
301
- return "swapped";
302
- } catch {
303
- /* frozen options — this dispatch cannot carry the view; fail open */
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
  }
@@ -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 four-step flow: fork a dormant backup
30
- * session (subagents' fork provider, economy pool model); then, on
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 (backup + document + compaction + continuation) may run and will tell you to end the turn.",
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: forking the backup session and writing the handover document…`);
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 (backup + document + compaction + continuation) is armed and will fire at the next step boundary${status?.tool ? "; calling the ctx_handover tool triggers it immediately" : ""}.`,
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 fork session: ${backupId ?? "(fork failed — the full history remains in this session's append-only log)"}`,
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
- backupId !== null
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
- const backup = await forkBackup(agent, sessionId, signal);
694
- const backupId = backup.childId;
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 after backup ${backupId ?? "n/a"} — ${outcome.reason}`,
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 = backupId ?? `n/a (${backup.reason ?? "log retains full history"})`;
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 (backup fork + document + compaction + continuation).",
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: fork a dormant backup session, 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.",
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: backup fork + 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 fork+compact.";
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",
@@ -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, before the fork even starts); routes.js
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 fork+compact window.
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 fork+compact window ({count, handovers}),
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 four-step force-tier handover
24
- * (backup fork + document + compaction + continuation) plus the
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 handover's backup fork goes through, the sessions
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.9.0",
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": [