switchroom 0.19.31 → 0.19.33

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.
@@ -90,14 +90,37 @@ SIDECHAIN_MAX_READ_BYTES = 8 * 1024 * 1024
90
90
  SIDECHAIN_SCAN_FRESH_WINDOW_S = 300
91
91
 
92
92
  # Extraction-framing header prepended to the retained content. The retain API
93
- # has no per-call mission (mission is bank-level), so we bias the consolidation
94
- # engine toward PROCESS facts with a short in-content note. Deterministic text —
95
- # it does not change the document_id (that is computed from the raw slice before
96
- # this is prepended).
93
+ # has no per-call mission (mission is bank-level), so the only lever available
94
+ # here is a short in-content note. Deterministic text — it does not change the
95
+ # document_id (that is computed from the raw slice before this is prepended).
96
+ #
97
+ # 2026-07-29 rewrite. The original text asked the extractor for "PROCESS facts:
98
+ # files/paths touched, commands that worked, ..." — i.e. it explicitly SOLICITED
99
+ # the exact classes the bank-level mission (`DEFAULT_RETAIN_MISSION` in
100
+ # src/memory/hindsight.ts) enumerates as NEVER-extract: file paths, tool-result
101
+ # exhaust, narration of what the assistant did, volatile state. Because this
102
+ # header sits inside the content the extractor reads, it read as a local
103
+ # override and countermanded the bank mission on the sidechain path. The header
104
+ # now REINFORCES the bank mission instead of fighting it: it keeps the genuinely
105
+ # durable half of the old wording (decisions with their rationale, non-obvious
106
+ # technique, structural dead ends) and names the ephemera to drop.
107
+ # Governing test: would this still be worth knowing in a month?
97
108
  SIDECHAIN_MISSION_HEADER = (
98
- "[sidechain sub-agent work log extract PROCESS facts: files/paths touched, "
99
- "commands that worked, decisions made, dead ends hit and why. Ignore the "
100
- "restated mission prose and routine tool chatter.]"
109
+ "[sidechain sub-agent work log. Apply the bank's retain mission unchanged; "
110
+ "this is raw work exhaust and most of it is NOT memorable. Extract only "
111
+ "durable knowledge something that would still be worth knowing in a month: "
112
+ "decisions made and the reasoning behind them; a non-obvious technique or "
113
+ "constraint that would save the next agent real time; a STRUCTURAL dead end "
114
+ "(\"X cannot work because Y\") as opposed to an incidental one (\"the command "
115
+ "failed so I retried\"). "
116
+ "Do NOT extract: the status of in-flight or unfinished work; PR, issue, "
117
+ "branch, review or CI state; counts, metrics, versions, sizes or any other "
118
+ "value that changes; container, process or runtime snapshots; narration of "
119
+ "what the agent did, including lists of the files, paths or commands it "
120
+ "touched; paths under /tmp or a scratchpad directory; session, agent, "
121
+ "request or operation IDs; the restated mission prose; routine tool chatter. "
122
+ "Anything whose truth expires when this session does is not a memory. "
123
+ "If nothing durable remains, extract nothing.]"
101
124
  )
102
125
 
103
126
 
@@ -331,8 +331,10 @@ class RunSubagentRetain(unittest.TestCase):
331
331
  self.assertIn("agent_type:worker", captured["tags"])
332
332
  # Deterministic id in a DISTINCT namespace from parent retains.
333
333
  self.assertTrue(captured["document_id"].startswith("parentsess-sub-af5-r"))
334
- # Process-fact framing header is prepended.
335
- self.assertIn("extract PROCESS facts", captured["content"])
334
+ # Durable-knowledge framing header is prepended, verbatim and first.
335
+ self.assertTrue(
336
+ captured["content"].startswith(subagent_retain.SIDECHAIN_MISSION_HEADER)
337
+ )
336
338
  # Distinct provenance context.
337
339
  self.assertEqual(captured["context"], "claude-code-sidechain")
338
340
  self.assertEqual(captured["metadata"]["parent_session_id"], "parentsess")
@@ -459,6 +461,102 @@ class RunSubagentRetain(unittest.TestCase):
459
461
  self.assertIn(k, result["payload"])
460
462
 
461
463
 
464
+ class SidechainMissionHeader(unittest.TestCase):
465
+ """The header must REINFORCE the bank-level retain mission, not countermand it.
466
+
467
+ The header is prepended into the retained content, where the extractor reads
468
+ it as a local instruction — so anything it solicits is written to memory even
469
+ when `DEFAULT_RETAIN_MISSION` (src/memory/hindsight.ts) lists that same class
470
+ under NEVER extract. The pre-2026-07-29 text asked for "PROCESS facts:
471
+ files/paths touched, commands that worked, ...", which is that exact
472
+ contradiction. These tests pin the fix: the solicit half asks only for
473
+ durable knowledge, and the ephemeral classes appear ONLY as exclusions.
474
+ """
475
+
476
+ SPLIT = "Do NOT extract"
477
+
478
+ def _halves(self):
479
+ header = subagent_retain.SIDECHAIN_MISSION_HEADER
480
+ self.assertIn(
481
+ self.SPLIT,
482
+ header,
483
+ "header must carry an explicit exclusion clause",
484
+ )
485
+ solicit, _, exclude = header.partition(self.SPLIT)
486
+ return header, solicit, exclude
487
+
488
+ def test_header_does_not_solicit_ephemeral_classes(self):
489
+ """Ephemera may be named as exclusions, never in the 'extract X' half."""
490
+ header = subagent_retain.SIDECHAIN_MISSION_HEADER
491
+ # The pre-fix solicitations, banned anywhere in the header — there is no
492
+ # phrasing of these that is a request for durable knowledge.
493
+ for gone in ("PROCESS facts", "files/paths touched", "commands that worked"):
494
+ self.assertNotIn(
495
+ gone,
496
+ header,
497
+ f"header still asks for {gone!r}, which the bank mission forbids",
498
+ )
499
+ _, solicit, _ = self._halves()
500
+ lowered = solicit.lower()
501
+ for banned in (
502
+ "process fact",
503
+ "files/paths",
504
+ "paths touched",
505
+ "commands that worked",
506
+ "/tmp",
507
+ "status",
508
+ "session id",
509
+ "container",
510
+ "metric",
511
+ ):
512
+ self.assertNotIn(
513
+ banned,
514
+ lowered,
515
+ f"header solicits ephemeral class {banned!r}: {solicit!r}",
516
+ )
517
+
518
+ def test_header_excludes_every_expiring_class(self):
519
+ _, _, exclude = self._halves()
520
+ lowered = exclude.lower()
521
+ for required in (
522
+ "in-flight",
523
+ "pr,", # PR / issue / branch / CI state
524
+ "ci state",
525
+ "counts",
526
+ "versions",
527
+ "container",
528
+ "narration",
529
+ "/tmp",
530
+ "scratchpad",
531
+ "session, agent, request or operation ids",
532
+ ):
533
+ self.assertIn(
534
+ required,
535
+ lowered,
536
+ f"header fails to exclude {required!r}",
537
+ )
538
+
539
+ def test_header_still_solicits_decisions_with_rationale(self):
540
+ """The single most valuable sub-agent output must survive the rewrite."""
541
+ _, solicit, _ = self._halves()
542
+ lowered = solicit.lower()
543
+ self.assertIn("decisions made", lowered)
544
+ self.assertTrue(
545
+ any(w in lowered for w in ("reasoning", "rationale", "why")),
546
+ f"decisions are solicited without their rationale: {solicit!r}",
547
+ )
548
+ # And the other two durable classes the old wording earned.
549
+ self.assertIn("dead end", lowered)
550
+ self.assertIn("structural", lowered)
551
+
552
+ def test_header_states_the_durability_bar(self):
553
+ header, solicit, _ = self._halves()
554
+ self.assertIn("durable", solicit.lower())
555
+ self.assertIn("worth knowing in a month", solicit.lower())
556
+ # Bounded: this is prepended to every sidechain retain payload.
557
+ self.assertLess(len(header), 1500)
558
+
559
+
462
560
  class HookRegistration(unittest.TestCase):
463
561
  def test_subagentstop_registered_in_plugin_hooks_json(self):
464
562
  hooks_path = os.path.abspath(
@@ -2,7 +2,7 @@
2
2
  "hindsightApiUrl": "",
3
3
  "bankId": "claude_code",
4
4
  "bankMission": "You are a Claude Code AI assistant. Focus on technical discussions, decisions, and context relevant to the user's projects.",
5
- "retainMission": "Extract durable facts that will still be true and useful weeks from now: user preferences and standing rules, ongoing projects and recurring commitments, technical and architectural decisions with their rationale, and people/tool relationships. A preference revealed by a request is durable — record the preference (what the user likes, wants, or always does), not the request itself.\n\nA TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this\ncandidate a file path, a command/process/agent/session id, a temp directory, or\nthe location where some output was written? If yes, drop it — it is transcript\nexhaust, not memory.\n\nNEVER extract:\n- Tool results verbatim or paraphrased. Concretely, never produce a fact whose\n text resembles any of these: \"File created successfully at /path/to/file\",\n \"A background command with ID bctz4yskm is running, and its output will be\n written to /tmp/...\", \"Async agent a745598ba84e71df1 was launched successfully\n and is running in the background\", \"User executed a Bash command to sleep for\n 200 seconds\", \"The assistant used grep to locate 'truncateSync' in src/foo.ts\".\n- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.\n- Agent tool-use traces or narration of what the assistant did (e.g. \"the\n assistant used X to query Y\", \"ran a search\", \"sent the message\").\n- In-flight workflow/process narration (a sub-task started, paused, or is still\n running) — retain the outcome only once the task completes or a decision is made.\n- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.\n- Slash commands the user typed and their effects (e.g. \"User issued /clear to\n reset assistant state\").\n- Hindsight's own errors, retries, backlogs, or internal state — the memory\n system's self-reports are not memories.\n- Restatements of the user's current request or the task in progress.\n- Volatile state written as a timeless assertion. A version, count, size,\n backlog, status, or any \"X is running Y\" / \"X is at Y\" / \"X is currently Y\"\n claim is true only at the instant it was said. Concretely, never produce a\n fact whose text resembles any of these: \"Switchroom fleet is running image\n version v0.18.19\", \"The switchroom repo is at /path/to/fleet, version\n v0.19.5\", \"Bank overlord has 43155 pending consolidations\", \"The build is\n currently green\". If the claim is worth keeping, put the date INSIDE the\n fact text (\"As of 2026-07-19 the fleet was running v0.18.19\"); if you\n cannot date it, drop it. An undated one is recalled forever as though it\n were still true, which is worse than not remembering it at all.\n- Transient state (unread counts, build status, what is running right now) unless\n the fact is explicitly dated, in which case record it as a dated observation.\n- Greetings, acknowledgements, and routine operational chatter.\n\nIf a candidate fact matches an exclusion, drop it rather than rewording it. If\nnothing durable remains, return an empty facts list.",
5
+ "retainMission": "Extract durable facts that will still be true and useful weeks from now, once this session is forgotten.\n\nDURABILITY GATE. Emit a candidate ONLY if it is one of these five classes:\n- PREFERENCE — what the user likes, wants, or always does; a standing rule or correction.\n- DECISION — a settled choice that changes how future work is done, including a choice NOT to do something. A decision about the mechanics of the CURRENT task (which worker to dispatch, which branch to rebase, which PR to merge now, what to do next) is process narration, not a durable decision — drop it unless it establishes a standing rule or permanently changes a system.\n- FINDING — a root cause, a measurement, or verified behaviour of a system. Include the number.\n- OUTCOME — a completed result that changed the world: what shipped, what a thing turned out to be. Not the act of shipping it.\n- RELATIONSHIP — who a person is, what a project or tool is, and how they connect.\nIf a candidate fits none of the five, it is not a memory. Drop it; do not reword it into one.\n\nA preference revealed by a request is durable — record the preference (what the user likes, wants, or always does), not the request itself.\n\nA TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this\ncandidate a file path, a command/process/agent/session id, a temp directory, or\nthe location where some output was written? If yes, drop it — it is transcript\nexhaust, not memory.\n\nNEVER extract:\n- Tool results verbatim or paraphrased. Concretely, never produce a fact whose\n text resembles any of these: \"File created successfully at /path/to/file\",\n \"A background command with ID bctz4yskm is running, and its output will be\n written to /tmp/...\", \"Async agent a745598ba84e71df1 was launched successfully\n and is running in the background\", \"User executed a Bash command to sleep for\n 200 seconds\", \"The assistant used grep to locate 'truncateSync' in src/foo.ts\".\n- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.\n- Agent tool-use traces or narration of what the assistant did (e.g. \"the\n assistant used X to query Y\", \"ran a search\", \"sent the message\").\n- The act of delegating, dispatching, spawning, launching, steering or merging\n work — including when it succeeded. \"X was dispatched and completed\" is the\n session describing itself. Record only what the work LEARNED or CHANGED.\n- In-flight workflow/process narration (a sub-task started, paused, or is still\n running) — retain the outcome only once the task completes or a decision is made.\n- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.\n- Slash commands the user typed and their effects (e.g. \"User issued /clear to\n reset assistant state\").\n- Hindsight's own errors, retries, backlogs, or internal state — the memory\n system's self-reports are not memories.\n- Restatements of the user's current request or the task in progress.\n- Volatile state written as a timeless assertion. A version, count, size,\n backlog, status, or any \"X is running Y\" / \"X is at Y\" / \"X is currently Y\"\n claim is true only at the instant it was said. Concretely, never produce a\n fact whose text resembles any of these: \"Switchroom fleet is running image\n version v0.18.19\", \"The switchroom repo is at /path/to/fleet, version\n v0.19.5\", \"Bank overlord has 43155 pending consolidations\", \"The build is\n currently green\". If the claim is worth keeping, put the date INSIDE the\n fact text (\"As of 2026-07-19 the fleet was running v0.18.19\"); if you\n cannot date it, drop it. An undated one is recalled forever as though it\n were still true, which is worse than not remembering it at all.\n- Transient state (unread counts, build status, what is running right now) unless\n the fact is explicitly dated, in which case record it as a dated observation.\n- Greetings, acknowledgements, and routine operational chatter.\n\nWrite each fact so it stands alone: name the thing, the number, and the date. A\nsentence that only makes sense while reading this transcript is not durable.\n\nIf a candidate fact matches an exclusion, drop it rather than rewording it. If\nnothing durable remains, return an empty facts list.\n",
6
6
  "autoRecall": true,
7
7
  "autoRetain": true,
8
8
  "retainMode": "full-session",