switchroom 0.14.76 → 0.14.78

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.
@@ -18,21 +18,45 @@ if [ -f "$MARKER" ]; then
18
18
  fi
19
19
  # Fire-and-forget refresh via MCP. Timeout bounded by Claude Code hook.
20
20
  # Output nothing on success or failure; errors are surfaced via doctor.
21
+ #
22
+ # Two bugs this fixes (2026-06-07):
23
+ # 1. The Hindsight server is STATELESS (HINDSIGHT_API_MCP_STATELESS=true) and
24
+ # returns NO mcp-session-id header, so the old code's `[ -n "$SESSION" ]`
25
+ # gate was ALWAYS false → the refresh NEVER fired. Stateless MCP needs no
26
+ # session — drop the gate (mirrors src/memory/hindsight.ts, which tolerates
27
+ # a missing session id).
28
+ # 2. refresh_mental_model takes `mental_model_id` (a UUID), NOT `name`. Passing
29
+ # `name` returns isError "Missing required argument: mental_model_id". So we
30
+ # list_mental_models, resolve the `user-profile` model's id, then refresh
31
+ # by id.
21
32
  {
22
- # JSON-RPC flow: initialize tools/call refresh_mental_model
23
- SESSION=$(curl -sS -X POST "$API_URL/mcp/" \
33
+ # initialize (no session id needed on the stateless server).
34
+ curl -sS -X POST "$API_URL/mcp/" \
24
35
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
25
- -H "X-Bank-Id: $BANK_ID" -D /tmp/mm-refresh-$$.headers \
36
+ -H "X-Bank-Id: $BANK_ID" \
26
37
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"mm-refresh","version":"0.1"}}}' \
27
- -m 3 -o /dev/null 2>/dev/null && grep -i mcp-session-id /tmp/mm-refresh-$$.headers | cut -d' ' -f2 | tr -d '\r\n')
28
- if [ -n "$SESSION" ]; then
38
+ -m 3 -o /dev/null 2>/dev/null || true
39
+
40
+ # Resolve the user-profile mental model's id (refresh is by id, not name).
41
+ # list_mental_models returns the items as a JSON STRING nested in
42
+ # .result.structuredContent.result, so parse with jq (fromjson). The agent
43
+ # image ships jq; if it's somehow absent, MM_ID stays empty → skip (the next
44
+ # Stop fires it again; fire-and-forget, no wedge).
45
+ MM_ID=$(curl -sS -X POST "$API_URL/mcp/" \
46
+ -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
47
+ -H "X-Bank-Id: $BANK_ID" \
48
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_mental_models","arguments":{}}}' \
49
+ -m 5 2>/dev/null \
50
+ | grep '^data:' | sed 's/^data: //' \
51
+ | jq -r '.result.structuredContent.result | fromjson | .items[] | select(.name=="user-profile") | .id' 2>/dev/null | head -1)
52
+
53
+ if [ -n "$MM_ID" ]; then
29
54
  curl -sS -X POST "$API_URL/mcp/" \
30
55
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
31
- -H "X-Bank-Id: $BANK_ID" -H "mcp-session-id: $SESSION" \
32
- -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"refresh_mental_model","arguments":{"name":"user-profile"}}}' \
56
+ -H "X-Bank-Id: $BANK_ID" \
57
+ -d "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"refresh_mental_model\",\"arguments\":{\"mental_model_id\":\"$MM_ID\"}}}" \
33
58
  -m 5 -o /dev/null 2>/dev/null || true
34
59
  fi
35
- rm -f /tmp/mm-refresh-$$.headers 2>/dev/null || true
36
60
  date +%s > "$MARKER"
37
61
  } &
38
62
  exit 0
@@ -14987,7 +14987,7 @@ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
14987
14987
  name: "create_mental_model",
14988
14988
  arguments: {
14989
14989
  name: "user-profile",
14990
- query: "What are the key facts, preferences, context, and communication style about the user I talk to? Summarize what matters for making the agent feel like it knows them.",
14990
+ source_query: "What are the key facts, preferences, context, and communication style about the user I talk to? Summarize what matters for making the agent feel like it knows them.",
14991
14991
  types: ["world", "experience"]
14992
14992
  }
14993
14993
  }
@@ -14998,6 +14998,13 @@ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
14998
14998
  if (!createResponse.ok) {
14999
14999
  return { ok: false, reason: `Create MM HTTP ${createResponse.status}` };
15000
15000
  }
15001
+ try {
15002
+ const created = await parseSseOrJson(createResponse);
15003
+ if (created.result?.isError === true) {
15004
+ const msg = created.result.content?.[0]?.text ?? "create returned isError";
15005
+ return { ok: false, reason: msg };
15006
+ }
15007
+ } catch {}
15001
15008
  return { ok: true };
15002
15009
  } catch (err) {
15003
15010
  if (err.name === "AbortError") {
@@ -15204,6 +15211,13 @@ async function addMemoryTag(apiUrl, bankId, memoryId, tag, opts) {
15204
15211
  if (!toolResponse.ok) {
15205
15212
  return { ok: false, reason: `Tool call HTTP ${toolResponse.status}` };
15206
15213
  }
15214
+ try {
15215
+ const parsed = await parseSseOrJson(toolResponse);
15216
+ if (parsed.result?.isError === true) {
15217
+ const msg = parsed.result.content?.[0]?.text ?? "tool returned isError";
15218
+ return { ok: false, reason: msg };
15219
+ }
15220
+ } catch {}
15207
15221
  return { ok: true };
15208
15222
  } catch (err) {
15209
15223
  if (err.name === "AbortError") {
@@ -20876,7 +20890,7 @@ function getHindsightSettingsEntry(agentName, config) {
20876
20890
  }
20877
20891
  const collection = getCollectionForAgent(agentName, config);
20878
20892
  const mcpConfig = generateHindsightMcpConfig(collection, memoryConfig);
20879
- return { key: "hindsight", value: mcpConfig };
20893
+ return { key: "hindsight", value: { ...mcpConfig, alwaysLoad: true } };
20880
20894
  }
20881
20895
  function getPlaywrightMcpSettingsEntry() {
20882
20896
  return {
@@ -49681,8 +49695,8 @@ var {
49681
49695
  } = import__.default;
49682
49696
 
49683
49697
  // src/build-info.ts
49684
- var VERSION = "0.14.76";
49685
- var COMMIT_SHA = "e7ab9ec6";
49698
+ var VERSION = "0.14.78";
49699
+ var COMMIT_SHA = "ac0dae98";
49686
49700
 
49687
49701
  // src/cli/agent.ts
49688
49702
  init_source();
@@ -50534,16 +50548,16 @@ Every turn that answers a user message ends with a user-visible
50534
50548
  sees; your terminal output never reaches them.`;
50535
50549
  var MEMORY_GUIDANCE = `## Memory \u2014 proactive, conversational
50536
50550
 
50537
- You have Hindsight tools: \`mcp__hindsight__sync_retain\`, \`mcp__hindsight__delete_memory\`, \`mcp__hindsight__recall\`, \`mcp__hindsight__reflect\`. Use them without being asked.
50551
+ You have Hindsight tools: \`mcp__hindsight__sync_retain\`, \`mcp__hindsight__delete_document\`, \`mcp__hindsight__recall\`, \`mcp__hindsight__reflect\`. Use them without being asked.
50538
50552
 
50539
50553
  ### Retain proactively
50540
50554
  When the user shares a fact, preference, decision, or plan worth keeping across sessions, call \`sync_retain\` in the same turn. Briefly acknowledge in your reply ("got it, April 2nd anniversary"). Don't narrate the tool call. Skip small talk and transient tool output, the auto-retain hook handles conversation-level signal.
50541
50555
 
50542
50556
  ### Correct proactively
50543
- When the user corrects you or contradicts a prior memory, call \`delete_memory\` on the wrong entry, then \`sync_retain\` the correction. Acknowledge the correction in one line ("noted, Alice not Bob").
50557
+ When the user corrects you or contradicts a prior memory, call \`delete_document\` on the wrong entry, then \`sync_retain\` the correction. Acknowledge the correction in one line ("noted, Alice not Bob").
50544
50558
 
50545
50559
  ### Forget proactively
50546
- When the user asks you to forget something ("forget that", "delete X", "drop what I said about Y"), call \`delete_memory\` for matching entries and confirm what was removed.
50560
+ When the user asks you to forget something ("forget that", "delete X", "drop what I said about Y"), call \`delete_document\` for matching entries and confirm what was removed.
50547
50561
 
50548
50562
  ### Inspect proactively
50549
50563
  When the user asks "what do you know about X / me", "what do you remember about Y", or any memory audit, use \`reflect\` to synthesize an answer across the bank. Return it as honest prose, not a raw dump. If the bank has little on the topic, say so.
@@ -50600,7 +50614,18 @@ name from that list.
50600
50614
  Only call the \`vault_request_access\` MCP tool \u2014 which pings the
50601
50615
  operator for a Telegram approval \u2014 for a key you've **confirmed via
50602
50616
  \`vault list\` you don't already have.** Requesting access to a guessed
50603
- or already-held key wastes the operator's tap and fails.`;
50617
+ or already-held key wastes the operator's tap and fails.
50618
+
50619
+ **Never put a secret's value \u2014 or a \`vault:<key>\` placeholder \u2014 in a
50620
+ tool call.** Tool arguments are sent to the underlying API verbatim;
50621
+ nothing resolves a \`vault:\` reference for them. A real secret never
50622
+ needs to appear in a tool call \u2014 the launcher injects it into the
50623
+ tool's environment for you. A value you genuinely must pass literally
50624
+ (an account or customer ID) is a public identifier, not a secret, and
50625
+ belongs in plain config, not the vault. If a tool call is **blocked for
50626
+ containing a vaulted secret**, report that exact block message to the
50627
+ operator and stop \u2014 don't theorise about auth or tokens; it just means
50628
+ a stored value reached a tool argument.`;
50604
50629
  function renderFleetInvariants() {
50605
50630
  return [
50606
50631
  "<!--",
@@ -51111,6 +51136,11 @@ function buildHumanizerEnvVars(agentDir, agent) {
51111
51136
  const resolved = expanded.startsWith("/") ? expanded : resolve11(agentDir, expanded);
51112
51137
  return { HUMANIZER_VOICE_FILE: resolved };
51113
51138
  }
51139
+ function buildToolSearchEnvVars() {
51140
+ if (process.env.SWITCHROOM_DISABLE_TOOL_SEARCH === "1")
51141
+ return {};
51142
+ return { ENABLE_TOOL_SEARCH: process.env.SWITCHROOM_TOOL_SEARCH_MODE ?? "auto" };
51143
+ }
51114
51144
  var SWITCHROOM_OWNED_SETTINGS_KEYS = new Set([
51115
51145
  "permissions",
51116
51146
  "mcpServers",
@@ -51440,6 +51470,7 @@ function buildWorkspaceContext(args) {
51440
51470
  fallbackModelQ: agentConfig.fallback_model ? shellSingleQuote(agentConfig.fallback_model) : undefined,
51441
51471
  userEnvQuoted: (() => {
51442
51472
  const combined = {
51473
+ ...buildToolSearchEnvVars(),
51443
51474
  ...channelsToEnv(agentConfig),
51444
51475
  ...agentConfig.env ?? {},
51445
51476
  ...buildRepoEnvVars(name, agentDir, agentConfig),
@@ -51750,7 +51781,8 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
51750
51781
  TELEGRAM_STATE_DIR: telegramStateDir,
51751
51782
  SWITCHROOM_CONFIG: resolvedConfigPath,
51752
51783
  SWITCHROOM_CLI_PATH: switchroomCliPath
51753
- }
51784
+ },
51785
+ alwaysLoad: true
51754
51786
  },
51755
51787
  "agent-config": {
51756
51788
  command: switchroomCliPath,
@@ -51758,7 +51790,8 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
51758
51790
  env: {
51759
51791
  SWITCHROOM_AGENT_NAME: name,
51760
51792
  SWITCHROOM_CONFIG: resolvedConfigPath
51761
- }
51793
+ },
51794
+ alwaysLoad: true
51762
51795
  }
51763
51796
  };
51764
51797
  if (agentConfig.admin === true) {
@@ -52503,6 +52536,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
52503
52536
  fallbackModelQ: agentConfig.fallback_model ? shellSingleQuote(agentConfig.fallback_model) : undefined,
52504
52537
  userEnvQuoted: (() => {
52505
52538
  const combined = {
52539
+ ...buildToolSearchEnvVars(),
52506
52540
  ...channelsToEnv(agentConfig),
52507
52541
  ...agentConfig.env ?? {},
52508
52542
  ...buildRepoEnvVars(name, agentDir, agentConfig)
@@ -52785,7 +52819,8 @@ ${body}
52785
52819
  TELEGRAM_STATE_DIR: telegramStateDir,
52786
52820
  SWITCHROOM_CONFIG: resolvedConfigPath,
52787
52821
  SWITCHROOM_CLI_PATH: switchroomCliPath
52788
- }
52822
+ },
52823
+ alwaysLoad: true
52789
52824
  },
52790
52825
  "agent-config": {
52791
52826
  command: switchroomCliPath,
@@ -52793,7 +52828,8 @@ ${body}
52793
52828
  env: {
52794
52829
  SWITCHROOM_AGENT_NAME: name,
52795
52830
  SWITCHROOM_CONFIG: resolvedConfigPath
52796
- }
52831
+ },
52832
+ alwaysLoad: true
52797
52833
  }
52798
52834
  };
52799
52835
  if (agentConfig.admin === true) {
@@ -59152,9 +59188,8 @@ function makeHttpHindsightClient(apiUrl, opts) {
59152
59188
  });
59153
59189
  return { items: res.items ?? [], total: res.total ?? 0 };
59154
59190
  },
59155
- async deleteMemory(bankId, memoryId) {
59156
- const sid = await initSession(bankId);
59157
- await callTool(bankId, sid, "delete_memory", { bank_id: bankId, memory_id: memoryId });
59191
+ async deleteMemory(_bankId, _memoryId) {
59192
+ throw new Error("hindsight exposes no single-memory delete API (delete_memory is not a " + "real tool; a memory id is not a document_id) \u2014 matched secrets cannot " + "be auto-scrubbed; redact manually via the bank UI");
59158
59193
  }
59159
59194
  };
59160
59195
  }
@@ -59184,11 +59219,18 @@ ${mem.context ?? ""}`;
59184
59219
  }
59185
59220
  let deleted = 0;
59186
59221
  if (!opts.dryRun) {
59222
+ let warnedOnce = false;
59187
59223
  for (const m of matched) {
59188
59224
  try {
59189
59225
  await client.deleteMemory(bankId, m.id);
59190
59226
  deleted++;
59191
- } catch {}
59227
+ } catch (err) {
59228
+ if (!warnedOnce) {
59229
+ warnedOnce = true;
59230
+ process.stderr.write(`vault-sweep: ${matched.length} secret(s) matched in bank '${bankId}' but ` + `could NOT be auto-deleted: ${err.message}
59231
+ `);
59232
+ }
59233
+ }
59192
59234
  }
59193
59235
  }
59194
59236
  return { matched, deleted };
@@ -64069,6 +64111,15 @@ import { existsSync as existsSync37 } from "node:fs";
64069
64111
 
64070
64112
  // src/vault/doctor.ts
64071
64113
  var SENSITIVE_KEY_RE = /oauth(?![a-zA-Z])|token(?![a-zA-Z])|secret(?![a-zA-Z])|api[-_]?key(?![a-zA-Z])|password(?![a-zA-Z])/i;
64114
+ function looksLikeIdentifierValue(value) {
64115
+ const v = value.trim();
64116
+ if (v.length < 8 || v.length > 24)
64117
+ return false;
64118
+ if (!/^[0-9][0-9 -]*[0-9]$/.test(v))
64119
+ return false;
64120
+ const digitCount = v.replace(/[^0-9]/g, "").length;
64121
+ return digitCount >= 8;
64122
+ }
64072
64123
  function analyseVaultHealth(input) {
64073
64124
  const diagnostics = [];
64074
64125
  if (input.brokerConfigured && input.brokerRunning === false) {
@@ -64124,6 +64175,23 @@ ${keyList}`,
64124
64175
  fix: "Consider `switchroom vault set <key> --allow <agent>` to restrict access"
64125
64176
  });
64126
64177
  }
64178
+ const identifierKeys = [];
64179
+ for (const [keyName, entry] of Object.entries(input.vaultKeys)) {
64180
+ if (entry.looksLikeIdentifier)
64181
+ identifierKeys.push(keyName);
64182
+ }
64183
+ if (identifierKeys.length > 0) {
64184
+ const keyList = identifierKeys.map((k) => ` ${k}`).join(`
64185
+ `);
64186
+ diagnostics.push({
64187
+ level: "warn",
64188
+ check: "identifier-stored-as-secret",
64189
+ message: `${identifierKeys.length} vault key(s) hold an identifier-like value (digits only):
64190
+ ${keyList}
64191
+ ` + `If an agent must pass one of these in a tool call, the secret-guard hook will block it \u2014 it cannot tell a public ID from a secret.`,
64192
+ fix: "If the value is a public identifier (account/customer ID), remove it from the vault and pass it as plain config (e.g. hardcode it in the MCP launcher). If it is genuinely secret, ignore this."
64193
+ });
64194
+ }
64127
64195
  const unreferencedKeys = [];
64128
64196
  for (const keyName of Object.keys(input.vaultKeys)) {
64129
64197
  if (!referencedKeys.has(keyName)) {
@@ -64215,7 +64283,8 @@ function registerVaultDoctorCommand(vault, program3) {
64215
64283
  vaultKeys = {};
64216
64284
  for (const [name, entry] of Object.entries(entries)) {
64217
64285
  vaultKeys[name] = {
64218
- scope: "scope" in entry && entry.scope ? entry.scope : undefined
64286
+ scope: "scope" in entry && entry.scope ? entry.scope : undefined,
64287
+ looksLikeIdentifier: entry.kind === "string" ? looksLikeIdentifierValue(entry.value) : false
64219
64288
  };
64220
64289
  }
64221
64290
  } catch (err) {
@@ -77207,7 +77276,18 @@ function formatBytes(bytes) {
77207
77276
  return `${bytes.toLocaleString()} bytes`;
77208
77277
  }
77209
77278
  function estimateTokens(bytes) {
77210
- return Math.round(bytes / 4);
77279
+ return Math.round(bytes / 3.7);
77280
+ }
77281
+ function readMcpServerNames(agentDir) {
77282
+ const mcpPath = join64(agentDir, ".mcp.json");
77283
+ if (!existsSync64(mcpPath))
77284
+ return [];
77285
+ try {
77286
+ const parsed = JSON.parse(readFileSync56(mcpPath, "utf-8"));
77287
+ return Object.keys(parsed.mcpServers ?? {});
77288
+ } catch {
77289
+ return null;
77290
+ }
77211
77291
  }
77212
77292
  function sha256(content) {
77213
77293
  return createHash13("sha256").update(content).digest("hex").slice(0, 16);
@@ -77425,14 +77505,31 @@ function registerDebugCommand(program3) {
77425
77505
  const soulMdBytes = soulMdContent.length;
77426
77506
  const perTurnBytes = dynamicResult.concatenated.length;
77427
77507
  const userBytes = userMessage?.text.length ?? 0;
77428
- const totalBytes = stableBytes + perSessionBytes + claudeMdBytes + soulMdBytes + perTurnBytes + userBytes;
77429
- console.log(`Stable prefix: ${formatBytes(stableBytes).padEnd(20)} (cache-hot)`);
77508
+ const fleetDir = join64(agentsDir, "..", "fleet");
77509
+ const fleetInvPath = join64(fleetDir, "switchroom-invariants.md");
77510
+ const fleetClaudePath = join64(fleetDir, "CLAUDE.md");
77511
+ const fleetInvBytes = existsSync64(fleetInvPath) ? readFileSync56(fleetInvPath, "utf-8").length : 0;
77512
+ const fleetClaudeBytes = existsSync64(fleetClaudePath) ? readFileSync56(fleetClaudePath, "utf-8").length : 0;
77513
+ const fleetBytes = fleetInvBytes + fleetClaudeBytes;
77514
+ const totalBytes = stableBytes + perSessionBytes + claudeMdBytes + fleetBytes + perTurnBytes + userBytes;
77515
+ console.log(`Stable prefix: ${formatBytes(stableBytes).padEnd(20)} (cache-hot; includes SOUL.md ${soulMdBytes.toLocaleString()}B)`);
77430
77516
  console.log(`Per-session: ${formatBytes(perSessionBytes).padEnd(20)} (cache-warm until next session)`);
77431
- console.log(`CLAUDE.md: ${formatBytes(claudeMdBytes).padEnd(20)}`);
77432
- console.log(`SOUL.md: ${formatBytes(soulMdBytes).padEnd(20)}`);
77433
- console.log(`Per-turn: ${formatBytes(perTurnBytes).padEnd(20)} (never cached)`);
77517
+ console.log(`CLAUDE.md (cwd): ${formatBytes(claudeMdBytes).padEnd(20)} (cache-hot)`);
77518
+ console.log(`Fleet invariants: ${formatBytes(fleetBytes).padEnd(20)} (--add-dir, cache-hot)`);
77519
+ console.log(`Per-turn: ${formatBytes(perTurnBytes).padEnd(20)} (never cached \u2014 the real per-turn $ cost)`);
77434
77520
  console.log(`User message: ${formatBytes(userBytes).padEnd(20)}`);
77435
- console.log(`Total: ${formatBytes(totalBytes).padEnd(20)} (~${estimateTokens(totalBytes).toLocaleString()} tokens est.)`);
77521
+ console.log(`Authored text: ${formatBytes(totalBytes).padEnd(20)} (~${estimateTokens(totalBytes).toLocaleString()} tokens est.)`);
77522
+ const mcpServers = readMcpServerNames(agentDir);
77523
+ const mcpCount = mcpServers?.length ?? null;
77524
+ const mcpEstK = mcpCount != null ? mcpCount * 3 : null;
77525
+ const mcpLabel = mcpCount != null ? `${mcpCount} servers` : "(unreadable \u2014 agent-private .mcp.json)";
77526
+ const mcpEstLabel = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k tok` : "~30k tok (audit est.)";
77527
+ console.log(`MCP tool surface: ${mcpLabel.padEnd(20)} (NOT counted above; ~3k tok/server \u2248 ${mcpEstLabel} \u2014 deferred under tool search)`);
77528
+ if (mcpServers && mcpServers.length > 0) {
77529
+ console.log(` [${mcpServers.join(", ")}]`);
77530
+ }
77531
+ const floorMcp = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k` : "~30k";
77532
+ console.log(`Per-turn FLOOR: ${`~${estimateTokens(totalBytes).toLocaleString()} + ${floorMcp} MCP + ~13k CLI-base`.padEnd(20)} tokens est. (before the user msg, recall, or tool results)`);
77436
77533
  const stableCacheInput = stableResult.concatenated + progressGuidance + baseSystemPromptAppend;
77437
77534
  const stableHash = sha256(stableCacheInput);
77438
77535
  console.log(`Cache stable hash: sha256:${stableHash}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.76",
3
+ "version": "0.14.78",
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": {
@@ -11,23 +11,21 @@ Before answering anything non-trivial:
11
11
 
12
12
  1. Confirm who you are (see `IDENTITY.md` if present).
13
13
  2. Confirm who you're talking to (see `USER.md` if present).
14
- 3. Remember what matters — `MEMORY.md` is your long-term memory; the
15
- `memory/` subdirectory has daily notes (`memory/YYYY-MM-DD.md`) with
16
- recent context.
14
+ 3. Recall what matters — your memory is **Hindsight**: call
15
+ `mcp__hindsight__recall` when you need prior context, decisions, or who
16
+ someone is. (Full memory protocol is in your `CLAUDE.md`.)
17
17
  4. If tools are configured, `TOOLS.md` captures setup-specific notes
18
18
  (host paths, credentials, endpoints) that save you discovery work.
19
19
 
20
20
  Don't ask permission to read these files. They exist so you can use them.
21
21
 
22
- ## Memory discipline
22
+ ## Memory
23
23
 
24
- - **Short-term / recent context** `memory/YYYY-MM-DD.md` raw log-style
25
- notes of what happened today. Create the day's file when you need it.
26
- - **Long-term / curated** `MEMORY.md` — distilled learnings, decisions,
27
- preferences, identity of people you interact with.
28
- - **Lessons** → When you make a mistake or learn something worth keeping,
29
- write it down. Future-you is fresh every session. Files are your
30
- continuity.
24
+ Memory is **Hindsight**, not files `recall` to read, `sync_retain` to keep a
25
+ fact across sessions, `delete_document` to forget. Claude Code's built-in
26
+ file-based auto-memory is disabled for this agent; do not maintain a `MEMORY.md`
27
+ index or a `memory/` daily-notes directory. Your `CLAUDE.md` has the full
28
+ protocol.
31
29
 
32
30
  ## Safety defaults
33
31
 
@@ -55,8 +53,8 @@ Failed edits burn your own context and the user's patience. Verify first.
55
53
  tool exists.
56
54
  - Long-running commands: use the background flag and poll, rather than
57
55
  blocking.
58
- - `memory_search` and `memory_get` (when available) are your primary
59
- interface to workspace knowledge; use them before asking the user.
56
+ - `mcp__hindsight__recall` is your interface to past context and decisions;
57
+ recall before asking the user something you may already know.
60
58
 
61
59
  ## Working on code repositories
62
60
 
@@ -52821,11 +52821,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
52821
52821
  }
52822
52822
 
52823
52823
  // ../src/build-info.ts
52824
- var VERSION = "0.14.76";
52825
- var COMMIT_SHA = "e7ab9ec6";
52826
- var COMMIT_DATE = "2026-06-06T08:50:42Z";
52827
- var LATEST_PR = 2196;
52828
- var COMMITS_AHEAD_OF_TAG = 0;
52824
+ var VERSION = "0.14.78";
52825
+ var COMMIT_SHA = "ac0dae98";
52826
+ var COMMIT_DATE = "2026-06-07T09:07:56+10:00";
52827
+ var LATEST_PR = null;
52828
+ var COMMITS_AHEAD_OF_TAG = 3;
52829
52829
 
52830
52830
  // gateway/boot-version.ts
52831
52831
  function formatRelativeAgo(iso) {
@@ -194,10 +194,19 @@ async function main() {
194
194
  }
195
195
  const hit = scanToolInput(toolInput, vaultValues)
196
196
  if (hit) {
197
+ // The reason is read by the model. It must be CORRECT and actionable:
198
+ // earlier versions said "reference it as vault:<key> instead", which
199
+ // suggested a `vault:` resolver that does not exist for tool arguments
200
+ // — sending agents (and operators) chasing a phantom mechanism. State
201
+ // the two real resolutions instead, and do NOT imply a resolver.
197
202
  process.stdout.write(
198
203
  JSON.stringify({
199
204
  decision: 'block',
200
- reason: `tool_input contains a vaulted secret — reference it as vault:${hit.key} instead`,
205
+ reason:
206
+ `Blocked: this tool input contains the value of vault secret '${hit.key}'. ` +
207
+ `Secrets must never appear in a tool call. ` +
208
+ `If '${hit.key}' is a real secret, do not pass it as an argument — the launcher injects it into the tool's environment, so you never type it. ` +
209
+ `If '${hit.key}' is actually a public identifier (e.g. an account or customer ID that must appear in the call), it should not be in the vault — ask the operator to move it to plain config.`,
201
210
  }),
202
211
  )
203
212
  process.exit(0)
@@ -132,7 +132,13 @@ describe('secret-guard-pretool.mjs (broker-direct)', () => {
132
132
  })
133
133
  expect(r.status).toBe(0)
134
134
  expect(r.stdout).toContain('"decision":"block"')
135
- expect(r.stdout).toContain('vault:gh-token')
135
+ // The reason must name the offending key and explain the real fix.
136
+ expect(r.stdout).toContain('gh-token')
137
+ expect(r.stdout).toContain('Secrets must never appear in a tool call')
138
+ // It must NOT resurrect the old, misleading "reference it as vault:<key>"
139
+ // suggestion — there is no vault: resolver for tool arguments, and that
140
+ // advice sent agents down a dead end (see the hook's block-reason note).
141
+ expect(r.stdout).not.toContain('reference it as vault:')
136
142
  })
137
143
 
138
144
  it('allows when tool_input contains no vaulted value', async () => {