switchroom 0.15.39 → 0.15.41

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.
@@ -13811,6 +13811,8 @@ var AgentMemorySchema = exports_external.object({
13811
13811
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
13812
13812
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
13813
13813
  min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0–1.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default — " + "current behaviour). Try 0.10–0.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`."),
13814
+ types: exports_external.array(exports_external.string()).optional().describe("Hindsight fact types to recall. Switchroom default is " + '["world", "experience", "observation"] — the synthesized ' + "`observation` tier is on by default. Set to " + '["world", "experience"] to opt out of observation-backed ' + "recall for this agent (or fleet-wide under defaults)."),
13815
+ skip_trivial: exports_external.boolean().optional().describe("Skip recall on plausibly-stateless trivial turns (time/date/" + "greeting). Switchroom default true — saves the recall arm + " + "injected tokens on turns that never need memory, guarded so it " + "never skips a turn that references user/project/session state. " + "Set false to always run recall."),
13814
13816
  topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) → soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: …' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
13815
13817
  }).optional().describe("Auto-recall tuning knobs")
13816
13818
  }).optional();
@@ -11419,6 +11419,8 @@ var init_schema = __esm(() => {
11419
11419
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
11420
11420
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
11421
11421
  min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0–1.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default — " + "current behaviour). Try 0.10–0.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`."),
11422
+ types: exports_external.array(exports_external.string()).optional().describe("Hindsight fact types to recall. Switchroom default is " + '["world", "experience", "observation"] — the synthesized ' + "`observation` tier is on by default. Set to " + '["world", "experience"] to opt out of observation-backed ' + "recall for this agent (or fleet-wide under defaults)."),
11423
+ skip_trivial: exports_external.boolean().optional().describe("Skip recall on plausibly-stateless trivial turns (time/date/" + "greeting). Switchroom default true — saves the recall arm + " + "injected tokens on turns that never need memory, guarded so it " + "never skips a turn that references user/project/session state. " + "Set false to always run recall."),
11422
11424
  topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) → soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: …' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
11423
11425
  }).optional().describe("Auto-recall tuning knobs")
11424
11426
  }).optional();
@@ -11419,6 +11419,8 @@ var init_schema = __esm(() => {
11419
11419
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
11420
11420
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
11421
11421
  min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0–1.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default — " + "current behaviour). Try 0.10–0.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`."),
11422
+ types: exports_external.array(exports_external.string()).optional().describe("Hindsight fact types to recall. Switchroom default is " + '["world", "experience", "observation"] — the synthesized ' + "`observation` tier is on by default. Set to " + '["world", "experience"] to opt out of observation-backed ' + "recall for this agent (or fleet-wide under defaults)."),
11423
+ skip_trivial: exports_external.boolean().optional().describe("Skip recall on plausibly-stateless trivial turns (time/date/" + "greeting). Switchroom default true — saves the recall arm + " + "injected tokens on turns that never need memory, guarded so it " + "never skips a turn that references user/project/session state. " + "Set false to always run recall."),
11422
11424
  topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) → soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: …' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
11423
11425
  }).optional().describe("Auto-recall tuning knobs")
11424
11426
  }).optional();
@@ -16355,6 +16357,9 @@ class VaultBroker {
16355
16357
  this.passphrase = passphrase;
16356
16358
  this._setReadinessSentinel(true);
16357
16359
  }
16360
+ reload(config) {
16361
+ this.config = config;
16362
+ }
16358
16363
  _setReadinessSentinel(ready) {
16359
16364
  const p = process.env.SWITCHROOM_VAULT_BROKER_READY_PATH;
16360
16365
  if (!p || p.length === 0)
@@ -16425,6 +16430,9 @@ class VaultBroker {
16425
16430
  _getSecretsRef() {
16426
16431
  return this.secrets;
16427
16432
  }
16433
+ _getConfigRef() {
16434
+ return this.config;
16435
+ }
16428
16436
  bindAgentSocket(socketPath) {
16429
16437
  const abs = resolve6(socketPath);
16430
16438
  const agentName = socketPathToAgent(abs);
@@ -17856,6 +17864,19 @@ async function main() {
17856
17864
  }
17857
17865
  const broker = new VaultBroker;
17858
17866
  registerShutdownHandlers(broker);
17867
+ process.on("SIGHUP", () => {
17868
+ (async () => {
17869
+ try {
17870
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_loader(), exports_loader));
17871
+ broker.reload(loadConfig2(configPath));
17872
+ process.stdout.write(`vault-broker: SIGHUP reload — config refreshed
17873
+ `);
17874
+ } catch (err) {
17875
+ process.stderr.write(`vault-broker: SIGHUP reload failed (keeping previous config): ${err.message}
17876
+ `);
17877
+ }
17878
+ })();
17879
+ });
17859
17880
  if (perAgentTargets.length > 0) {
17860
17881
  await broker.start(legacySocketPath, configPath, vaultPath);
17861
17882
  process.stdout.write(`vault-broker: legacy socket listening on ${legacySocketPath}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.15.39",
3
+ "version": "0.15.41",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -332,6 +332,21 @@ export HINDSIGHT_RECALL_CACHE_TTL_SECS={{hindsightRecallCacheTtlSecs}}
332
332
  {{#if (isNumber hindsightRecallMinOverlap)}}
333
333
  export HINDSIGHT_RECALL_MIN_OVERLAP={{hindsightRecallMinOverlap}}
334
334
  {{/if}}
335
+ # Recall fact types (memory.recall.types cascade). Switchroom default is
336
+ # world,experience,observation (the synthesized `observation` tier is ON
337
+ # by default, set in the plugin settings.json). Export only when the
338
+ # operator OPTED OUT via switchroom.yaml — comma-separated; the env value
339
+ # wins over settings.json. e.g. ["world","experience"] drops
340
+ # observation-backed recall for this agent.
341
+ {{#if hindsightRecallTypes}}
342
+ export HINDSIGHT_RECALL_TYPES="{{hindsightRecallTypes}}"
343
+ {{/if}}
344
+ # Trivial-turn recall skip (memory.recall.skip_trivial cascade). On by
345
+ # default (plugin settings.json). Export only when the operator overrode
346
+ # it; set false to always run recall.
347
+ {{#if hindsightRecallSkipTrivial}}
348
+ export HINDSIGHT_RECALL_SKIP_TRIVIAL={{hindsightRecallSkipTrivial}}
349
+ {{/if}}
335
350
  # PR6 — supergroup-mode topic tagging. JSON map of {alias: thread_id}
336
351
  # parsed by retain.py + recall.py to (a) stamp chat_id/thread_id/topic_alias
337
352
  # into retained memory metadata and (b) emit a "Current topic: …" preamble
@@ -23922,6 +23922,8 @@ var init_schema = __esm(() => {
23922
23922
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
23923
23923
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
23924
23924
  min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0\u20131.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default \u2014 " + "current behaviour). Try 0.10\u20130.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`."),
23925
+ types: exports_external.array(exports_external.string()).optional().describe("Hindsight fact types to recall. Switchroom default is " + '["world", "experience", "observation"] \u2014 the synthesized ' + "`observation` tier is on by default. Set to " + '["world", "experience"] to opt out of observation-backed ' + "recall for this agent (or fleet-wide under defaults)."),
23926
+ skip_trivial: exports_external.boolean().optional().describe("Skip recall on plausibly-stateless trivial turns (time/date/" + "greeting). Switchroom default true \u2014 saves the recall arm + " + "injected tokens on turns that never need memory, guarded so it " + "never skips a turn that references user/project/session state. " + "Set false to always run recall."),
23925
23927
  topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) \u2192 soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: \u2026' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
23926
23928
  }).optional().describe("Auto-recall tuning knobs")
23927
23929
  }).optional();
@@ -54519,9 +54521,9 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
54519
54521
  }
54520
54522
 
54521
54523
  // ../src/build-info.ts
54522
- var VERSION = "0.15.39";
54523
- var COMMIT_SHA = "69e5a2e8";
54524
- var COMMIT_DATE = "2026-06-18T12:27:21+10:00";
54524
+ var VERSION = "0.15.41";
54525
+ var COMMIT_SHA = "01f41098";
54526
+ var COMMIT_DATE = "2026-06-18T14:13:42+10:00";
54525
54527
  var LATEST_PR = null;
54526
54528
  var COMMITS_AHEAD_OF_TAG = 4;
54527
54529
 
@@ -98,6 +98,15 @@ ENV_OVERRIDES = {
98
98
  # [0.0, 1.0]. Set by start.sh from agents.<name>.memory.recall.min_overlap
99
99
  # (cascading through defaults). 0.0 = off (current behaviour).
100
100
  "HINDSIGHT_RECALL_MIN_OVERLAP": ("recallMinOverlap", float),
101
+ # Switchroom-local: recall fact types (comma-separated). Set by start.sh
102
+ # from agents.<name>.memory.recall.types only when the operator overrode
103
+ # the switchroom default (world,experience,observation) — i.e. the
104
+ # opt-out path for the synthesized `observation` tier.
105
+ "HINDSIGHT_RECALL_TYPES": ("recallTypes", list),
106
+ # Switchroom-local: trivial-turn recall skip (Phase 6a). Set by start.sh
107
+ # from agents.<name>.memory.recall.skip_trivial only on override; the
108
+ # switchroom default is on (recall.py falls back to True).
109
+ "HINDSIGHT_RECALL_SKIP_TRIVIAL": ("recallSkipTrivial", bool),
101
110
  "HINDSIGHT_RECALL_MAX_QUERY_CHARS": ("recallMaxQueryChars", int),
102
111
  "HINDSIGHT_RECALL_CONTEXT_TURNS": ("recallContextTurns", int),
103
112
  "HINDSIGHT_API_PORT": ("apiPort", int),
@@ -121,6 +130,9 @@ def _cast_env(value: str, typ):
121
130
  return int(value)
122
131
  if typ is float:
123
132
  return float(value)
133
+ if typ is list:
134
+ # Comma-separated → list of trimmed, non-empty strings.
135
+ return [t.strip() for t in value.split(",") if t.strip()]
124
136
  return value
125
137
  except (ValueError, AttributeError):
126
138
  return None
@@ -42,6 +42,7 @@ Exit codes:
42
42
  import hashlib
43
43
  import json
44
44
  import os
45
+ import re
45
46
  import sys
46
47
  import time
47
48
 
@@ -466,6 +467,57 @@ def read_transcript_messages(transcript_path: str) -> list:
466
467
  return messages
467
468
 
468
469
 
470
+ # Switchroom Phase 6a — stateless-prompt classifier for the recall skip.
471
+ # Returns True ONLY for prompts that provably never need user memory: the
472
+ # current time/date/day, or a bare greeting. Biased hard toward False —
473
+ # any personal pronoun, memory verb, or context word means "could need
474
+ # memory → recall anyway". Trap case: "what host am I on" reads trivial
475
+ # but needs memory; the "i" token blocks the skip.
476
+ _TRIVIAL_GREETINGS = frozenset({
477
+ "hi", "hello", "hey", "heya", "hiya", "yo", "howdy", "sup",
478
+ "hey there", "hello there", "hi there",
479
+ "morning", "good morning", "good afternoon", "good evening", "evening",
480
+ })
481
+ # Any of these as a whole word → do NOT skip (prompt may depend on stored
482
+ # user / project / session state).
483
+ _STATEFUL_SIGNALS = frozenset({
484
+ "i", "im", "ive", "id", "me", "my", "mine", "myself",
485
+ "we", "our", "ours", "us", "you", "your", "yours",
486
+ "remember", "recall", "forget", "forgot", "remind",
487
+ "last", "earlier", "yesterday", "before", "again", "previously", "recent",
488
+ "project", "task", "status", "config", "setup", "host", "machine",
489
+ "running", "deploy", "agent", "memory", "note", "noted",
490
+ })
491
+ # Matched against an apostrophe-stripped form, so "what's"→"whats",
492
+ # "today's"→"todays". Covers "what time is it", "what's the time",
493
+ # "what day is it", "what's today's date", "current time", "time?".
494
+ _STATELESS_QUESTION_RE = re.compile(
495
+ r"^(?:what(?:s| is)?\s+)?"
496
+ r"(?:the\s+|current\s+|todays\s+)?"
497
+ r"(?:time|date|day(?:\s+of\s+(?:the\s+)?week)?)"
498
+ r"(?:\s+is\s+it)?(?:\s+(?:right\s+now|now|today))?"
499
+ r"\s*\??$"
500
+ )
501
+
502
+
503
+ def _is_trivial_stateless(ack_form, stripped):
504
+ text = (stripped or "").lower().strip()
505
+ core = text.strip(" \t\n\r.,!?…👍👌✅🆗🙏")
506
+ if not core:
507
+ return False
508
+ core_noapos = core.replace("'", "")
509
+ # If any token signals personal / project / session state, bail —
510
+ # apostrophes stripped so "i'm"/"i've" tokenise to im/ive (stateful).
511
+ tokens = re.findall(r"[a-z]+", core_noapos)
512
+ if any(tok in _STATEFUL_SIGNALS for tok in tokens):
513
+ return False
514
+ if core in _TRIVIAL_GREETINGS:
515
+ return True
516
+ if _STATELESS_QUESTION_RE.match(core_noapos):
517
+ return True
518
+ return False
519
+
520
+
469
521
  def main():
470
522
  config = load_config()
471
523
 
@@ -522,6 +574,18 @@ def main():
522
574
  debug_log(config, f"Prompt is ack-only ({_ack_form!r}), skipping recall")
523
575
  return
524
576
 
577
+ # Switchroom Phase 6a (RFC hindsight-synthesis-layers.md) — skip recall
578
+ # on plausibly-stateless trivial asks (time/date/day, bare greetings)
579
+ # when `recallSkipTrivial` is on. Same conservatism as the ack-skip:
580
+ # a false negative (skipping a turn that DID need memory) costs the
581
+ # remember-across-sessions continuity, so `_is_trivial_stateless`
582
+ # bails the instant the prompt carries any personal/stateful signal
583
+ # (a pronoun, a memory verb, "project", etc.). It only skips an exact
584
+ # stateless form — never a content-classifier guess.
585
+ if config.get("recallSkipTrivial", True) and _is_trivial_stateless(_ack_form, _stripped):
586
+ debug_log(config, f"Prompt is trivial/stateless ({_ack_form!r}), skipping recall")
587
+ return
588
+
525
589
  session_id = hook_input.get("session_id") or ""
526
590
 
527
591
  # Switchroom #303 — push a "📚 recalling memories" status to the
@@ -0,0 +1,101 @@
1
+ """Switchroom Phase 6a — unit tests for recall.py's trivial-stateless skip.
2
+
3
+ `_is_trivial_stateless` gates the auto-recall hook: it returns True only
4
+ for prompts that provably never need user memory (current time/date/day,
5
+ bare greetings), so the ~1-2s recall arm + ~1024 injected tokens are
6
+ skipped on those turns. The load-bearing property is the NO-FALSE-NEGATIVE
7
+ guarantee: a prompt that could need memory must never be skipped, because
8
+ skipping it would breach the remember-across-sessions continuity job. The
9
+ KEEP corpus below is that no-regression gate.
10
+
11
+ Stdlib-only.
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import unittest
17
+
18
+ SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
19
+ if SCRIPTS_DIR not in sys.path:
20
+ sys.path.insert(0, SCRIPTS_DIR)
21
+
22
+ from recall import _is_trivial_stateless # noqa: E402
23
+
24
+
25
+ # Prompts that provably never need user/session memory — safe to skip.
26
+ TRIVIAL = [
27
+ "what time is it",
28
+ "what time is it?",
29
+ "what time is it now",
30
+ "what's the time",
31
+ "time?",
32
+ "current time",
33
+ "what's the date",
34
+ "whats the date",
35
+ "what's today's date",
36
+ "what day is it",
37
+ "what day is it?",
38
+ "what day is it today",
39
+ "what day of the week is it",
40
+ "hi",
41
+ "hello",
42
+ "hey",
43
+ "hey there",
44
+ "good morning",
45
+ "morning",
46
+ ]
47
+
48
+ # Prompts that depend on (or might depend on) stored user/project/session
49
+ # state — must NEVER be skipped. This is the no-regression gate.
50
+ NEEDS_MEMORY = [
51
+ "what host am I on?", # reads trivial, but needs memory — the "i" trap
52
+ "what's my config",
53
+ "what was I frustrated about",
54
+ "remind me what we discussed",
55
+ "what's the date for my deadline", # has 'my'
56
+ "what's the date of our last deploy", # 'our'/'last'/'deploy'
57
+ "what projects do I have",
58
+ "do you remember my preference",
59
+ "what's the status of the deploy",
60
+ "summarize our last conversation",
61
+ "hello, can you check my open items", # greeting + real ask
62
+ "what time did I say the meeting was", # 'time' but 'i'/'say'
63
+ "what should I work on",
64
+ "tell me about the project",
65
+ "what's the weather", # stateless-ish but not in the time/date set → recall anyway
66
+ "how do I fix this",
67
+ ]
68
+
69
+
70
+ class TestTrivialStatelessSkip(unittest.TestCase):
71
+ def test_trivial_prompts_are_skipped(self):
72
+ for p in TRIVIAL:
73
+ with self.subTest(prompt=p):
74
+ self.assertTrue(
75
+ _is_trivial_stateless("", p),
76
+ f"expected trivial/stateless skip for {p!r}",
77
+ )
78
+
79
+ def test_memory_prompts_are_never_skipped(self):
80
+ # The critical safety invariant — zero false negatives.
81
+ for p in NEEDS_MEMORY:
82
+ with self.subTest(prompt=p):
83
+ self.assertFalse(
84
+ _is_trivial_stateless("", p),
85
+ f"FALSE NEGATIVE: would skip recall for {p!r} which may need memory",
86
+ )
87
+
88
+ def test_empty_and_whitespace_do_not_skip_here(self):
89
+ # The <5-char / empty guards live earlier in main(); the classifier
90
+ # itself returns False for empty so it never short-circuits oddly.
91
+ self.assertFalse(_is_trivial_stateless("", ""))
92
+ self.assertFalse(_is_trivial_stateless("", " "))
93
+
94
+ def test_personal_pronoun_blocks_skip(self):
95
+ # Even a time question is kept the moment a stateful token appears.
96
+ self.assertTrue(_is_trivial_stateless("", "what time is it"))
97
+ self.assertFalse(_is_trivial_stateless("", "what time is it for my call"))
98
+
99
+
100
+ if __name__ == "__main__":
101
+ unittest.main()
@@ -42,7 +42,7 @@ class TestLoadConfig:
42
42
  cfg = load_config()
43
43
  assert cfg["autoRecall"] is True
44
44
  assert cfg["autoRetain"] is True
45
- assert cfg["recallBudget"] == "mid"
45
+ assert cfg["recallBudget"] == "low"
46
46
  assert cfg["retainEveryNTurns"] == 10
47
47
 
48
48
  def test_settings_json_overrides_defaults(self, tmp_path, monkeypatch):
@@ -75,7 +75,7 @@ class TestLoadConfig:
75
75
  monkeypatch.setenv("CLAUDE_PLUGIN_ROOT", str(tmp_path))
76
76
  (tmp_path / "settings.json").write_text("not valid json{{")
77
77
  cfg = load_config()
78
- assert cfg["recallBudget"] == "mid" # default still applies
78
+ assert cfg["recallBudget"] == "low" # default still applies
79
79
 
80
80
  def test_null_values_in_settings_json_not_applied(self, tmp_path, monkeypatch):
81
81
  monkeypatch.setenv("CLAUDE_PLUGIN_ROOT", str(tmp_path))
@@ -112,7 +112,7 @@ class TestLoadConfig:
112
112
  # HOME points to tmp_path where no .hindsight/claude-code.json exists
113
113
  monkeypatch.setenv("HOME", str(tmp_path))
114
114
  cfg = load_config()
115
- assert cfg["recallBudget"] == "mid" # default
115
+ assert cfg["recallBudget"] == "low" # default
116
116
 
117
117
  def test_env_var_wins_over_user_config(self, tmp_path, monkeypatch):
118
118
  plugin_root = tmp_path / "plugin"