switchroom 0.19.27 → 0.19.28

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.
Files changed (32) hide show
  1. package/dist/agent-scheduler/index.js +5 -2
  2. package/dist/auth-broker/index.js +129 -8
  3. package/dist/cli/autoaccept-poll.js +225 -17
  4. package/dist/cli/notion-write-pretool.mjs +5 -2
  5. package/dist/cli/switchroom.js +796 -35
  6. package/dist/host-control/main.js +130 -9
  7. package/dist/vault/approvals/kernel-server.js +129 -8
  8. package/dist/vault/broker/server.js +129 -8
  9. package/package.json +3 -2
  10. package/profiles/_base/start.sh.hbs +70 -15
  11. package/telegram-plugin/dist/bridge/bridge.js +1 -0
  12. package/telegram-plugin/dist/gateway/gateway.js +568 -49
  13. package/telegram-plugin/dist/server.js +1 -0
  14. package/telegram-plugin/edit-flood-fuse.ts +230 -27
  15. package/telegram-plugin/gateway/callback-query-handlers.ts +6 -0
  16. package/telegram-plugin/gateway/gateway.ts +9 -2
  17. package/telegram-plugin/gateway/mcp-failure-hook.ts +74 -0
  18. package/telegram-plugin/inline-keyboard-callbacks.ts +202 -21
  19. package/telegram-plugin/mcp-credential-failure.ts +459 -0
  20. package/telegram-plugin/operator-events.ts +38 -0
  21. package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +58 -1
  22. package/telegram-plugin/tests/edit-flood-fuse-reply-reserve.test.ts +340 -0
  23. package/telegram-plugin/tests/finalize-callback-flood-policy.test.ts +298 -0
  24. package/telegram-plugin/tests/finalize-callback.test.ts +41 -8
  25. package/telegram-plugin/tests/mcp-credential-failure.test.ts +310 -0
  26. package/vendor/hindsight-memory/scripts/drain_pending.py +433 -11
  27. package/vendor/hindsight-memory/scripts/lib/pending.py +193 -28
  28. package/vendor/hindsight-memory/scripts/tests/test_drain_circuit_breaker.py +401 -0
  29. package/vendor/hindsight-memory/scripts/tests/test_drain_serialisation.py +286 -0
  30. package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +817 -8
  31. package/vendor/hindsight-memory/settings.json +1 -1
  32. package/vendor/hindsight-memory/tests/test_hooks.py +11 -2
@@ -4404,7 +4404,10 @@ var init_schema = __esm(() => {
4404
4404
  admin_key: exports_external.string().optional().describe("LiteLLM master/admin key used at apply time to provision the team + " + "virtual key. Supports a vault reference (e.g. " + "'vault:litellm/master-key') — resolution happens at apply time via " + "the vault-broker. Never injected into the agent container."),
4405
4405
  team: exports_external.string().optional().describe("LiteLLM team alias the per-agent key is created under. Defaults to " + "'switchroom' (applied in code, not as a schema default)."),
4406
4406
  small_fast_model: exports_external.string().optional().describe("Model id exported as ANTHROPIC_SMALL_FAST_MODEL for the claude CLI's " + "background/fast lane, e.g. 'claude-haiku-4-5-20251001'."),
4407
- tags: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Extra key/value metadata tags attached to the provisioned LiteLLM " + "virtual key. Merged per-key across cascade layers (agent wins).")
4407
+ tags: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Extra key/value metadata tags attached to the provisioned LiteLLM " + "virtual key. Merged per-key across cascade layers (agent wins)."),
4408
+ max_budget: exports_external.number().positive().optional().describe("HARD spend cap in USD for this agent's virtual key over one " + "`budget_duration` window. LiteLLM refuses the request once the key's " + "tracked spend exceeds it, so a runaway loop costs at most this much " + "before it is stopped. Defaults to " + "DEFAULT_KEY_MAX_BUDGET_USD (see src/litellm/budget.ts) — deliberately " + "conservative; raise it per-agent rather than removing it. Set 0 or " + "omit `budget_duration` at your own risk: an uncapped key is only as " + "bounded as the upstream account balance."),
4409
+ soft_budget: exports_external.number().positive().optional().describe("ADVISORY spend threshold in USD. LiteLLM keeps serving past it and " + "raises a budget alert instead. Must be < max_budget. NOTE: LiteLLM " + "accepts soft_budget only on POST /key/generate (GenerateKeyRequest); " + "UpdateKeyRequest does NOT carry it, so changing this value only takes " + "effect on a key that is (re)generated, not on an existing one."),
4410
+ budget_duration: exports_external.string().regex(/^\d+(s|m|h|d|mo)$/, "budget_duration must be a LiteLLM duration like '30d', '24h', '1mo'").optional().describe("Rolling window the budget resets on, in LiteLLM duration syntax " + "('30d', '24h', '1mo'). Defaults to DEFAULT_KEY_BUDGET_DURATION. " + "WITHOUT a duration LiteLLM treats max_budget as a LIFETIME cap that " + "never resets — the key silently dies for good once it is hit.")
4408
4411
  }).optional().describe("LiteLLM routing config — opt-in per-agent virtual-key auto-provisioning " + "+ routing env. Default OFF. See LiteLLMConfigSchema doc for the full flow.");
4409
4412
  HindsightPerOpLlmSchema = exports_external.object({
4410
4413
  model: exports_external.string().min(1).optional().describe("Per-op model (upstream `HINDSIGHT_API_<OP>_LLM_MODEL`). Absent → " + "inherit the global `hindsight.llm.model`."),
@@ -4423,7 +4426,7 @@ var init_schema = __esm(() => {
4423
4426
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
4424
4427
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
4425
4428
  }).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults."),
4426
- env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY — a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP — the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
4429
+ env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS — switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY — a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP — the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS — only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS — the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_MAX_SLOTS reserves out of; " + "unset means upstream's own default), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
4427
4430
  });
4428
4431
  MicrosoftWorkspaceConfigSchema = exports_external.object({
4429
4432
  microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL — omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
@@ -18970,7 +18973,13 @@ var HINDSIGHT_DEFAULT_LINK_EXPANSION_TIMEOUT_S = 2;
18970
18973
  var HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT = "low";
18971
18974
  var HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT = 4;
18972
18975
  var HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT = 1;
18973
- var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT = 1;
18976
+ function hindsightConsolidationLlmMaxConcurrentDefault(globalMaxConcurrent = HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT, retainMaxConcurrent = HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT) {
18977
+ const globalCap = Number.isFinite(globalMaxConcurrent) && globalMaxConcurrent >= 1 ? Math.floor(globalMaxConcurrent) : HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT;
18978
+ const retainCap = Number.isFinite(retainMaxConcurrent) && retainMaxConcurrent >= 0 ? Math.floor(retainMaxConcurrent) : HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT;
18979
+ const headroomBound = Math.max(1, globalCap - 1);
18980
+ return Math.min(headroomBound, Math.max(1, globalCap - retainCap - 1));
18981
+ }
18982
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT = hindsightConsolidationLlmMaxConcurrentDefault();
18974
18983
  var HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16 = "true";
18975
18984
  var HINDSIGHT_DEFAULT_RERANKER_LOCAL_BATCH_SIZE = 128;
18976
18985
  var HINDSIGHT_DEFAULT_LLM_STRICT_SCHEMA = "true";
@@ -18985,6 +18994,8 @@ var HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600;
18985
18994
  var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 500;
18986
18995
  var HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT = 6;
18987
18996
  var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1;
18997
+ var HINDSIGHT_DEFAULT_RECENCY_DECAY_FUNCTION = "exponential";
18998
+ var HINDSIGHT_DEFAULT_RECENCY_DECAY_HALFLIFE_DAYS = 30;
18988
18999
  var HINDSIGHT_PERF_DEFAULTS_UNGATED = [
18989
19000
  [
18990
19001
  "HINDSIGHT_API_RECALL_MAX_CANDIDATES_PER_SOURCE",
@@ -19038,6 +19049,14 @@ var HINDSIGHT_PERF_DEFAULTS_UNGATED = [
19038
19049
  [
19039
19050
  "HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND",
19040
19051
  String(HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND)
19052
+ ],
19053
+ [
19054
+ "HINDSIGHT_API_RECENCY_DECAY_FUNCTION",
19055
+ HINDSIGHT_DEFAULT_RECENCY_DECAY_FUNCTION
19056
+ ],
19057
+ [
19058
+ "HINDSIGHT_API_RECENCY_DECAY_HALFLIFE_DAYS",
19059
+ String(HINDSIGHT_DEFAULT_RECENCY_DECAY_HALFLIFE_DAYS)
19041
19060
  ]
19042
19061
  ];
19043
19062
  var HINDSIGHT_PERF_DEFAULTS_GPU = [
@@ -19062,7 +19081,9 @@ var HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM = [
19062
19081
  ];
19063
19082
  var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
19064
19083
  "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY",
19065
- "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP"
19084
+ "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP",
19085
+ "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS",
19086
+ "HINDSIGHT_API_WORKER_MAX_SLOTS"
19066
19087
  ]);
19067
19088
  var HINDSIGHT_PERF_ENV_KEYS = new Set([
19068
19089
  ...[
@@ -19128,6 +19149,16 @@ var DEFAULT_RETAIN_MISSION = `Extract durable facts that will still be true and
19128
19149
  ` + `- Hindsight's own errors, retries, backlogs, or internal state — the memory
19129
19150
  ` + ` system's self-reports are not memories.
19130
19151
  ` + `- Restatements of the user's current request or the task in progress.
19152
+ ` + `- Volatile state written as a timeless assertion. A version, count, size,
19153
+ ` + ` backlog, status, or any "X is running Y" / "X is at Y" / "X is currently Y"
19154
+ ` + ` claim is true only at the instant it was said. Concretely, never produce a
19155
+ ` + ` fact whose text resembles any of these: "Switchroom fleet is running image
19156
+ ` + ` version v0.18.19", "The switchroom repo is at /path/to/fleet, version
19157
+ ` + ` v0.19.5", "Bank overlord has 43155 pending consolidations", "The build is
19158
+ ` + ` currently green". If the claim is worth keeping, put the date INSIDE the
19159
+ ` + ` fact text ("As of 2026-07-19 the fleet was running v0.18.19"); if you
19160
+ ` + ` cannot date it, drop it. An undated one is recalled forever as though it
19161
+ ` + ` were still true, which is worse than not remembering it at all.
19131
19162
  ` + `- Transient state (unread counts, build status, what is running right now) unless
19132
19163
  ` + ` the fact is explicitly dated, in which case record it as a dated observation.
19133
19164
  ` + `- Greetings, acknowledgements, and routine operational chatter.
@@ -19149,18 +19180,108 @@ var SUPERSEDED_RETAIN_MISSIONS = [
19149
19180
  ` + "- Transient state (unread counts, build status, what is running right now) " + "unless the fact is explicitly dated, in which case record it as a dated " + `observation.
19150
19181
  ` + `- Greetings, acknowledgements, and routine operational chatter.
19151
19182
 
19152
- ` + "If a candidate fact matches an exclusion, drop it rather than rewording " + "it. If nothing durable remains, return an empty facts list."
19183
+ ` + "If a candidate fact matches an exclusion, drop it rather than rewording " + "it. If nothing durable remains, return an empty facts list.",
19184
+ `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.
19185
+ ` + `
19186
+ ` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
19187
+ ` + `candidate a file path, a command/process/agent/session id, a temp directory, or
19188
+ ` + `the location where some output was written? If yes, drop it — it is transcript
19189
+ ` + `exhaust, not memory.
19190
+ ` + `
19191
+ ` + `NEVER extract:
19192
+ ` + `- Tool results verbatim or paraphrased. Concretely, never produce a fact whose
19193
+ ` + ` text resembles any of these: "File created successfully at /path/to/file",
19194
+ ` + ` "A background command with ID bctz4yskm is running, and its output will be
19195
+ ` + ` written to /tmp/...", "Async agent a745598ba84e71df1 was launched successfully
19196
+ ` + ` and is running in the background", "User executed a Bash command to sleep for
19197
+ ` + ` 200 seconds", "The assistant used grep to locate 'truncateSync' in src/foo.ts".
19198
+ ` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
19199
+ ` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
19200
+ ` + ` assistant used X to query Y", "ran a search", "sent the message").
19201
+ ` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
19202
+ ` + ` running) — retain the outcome only once the task completes or a decision is made.
19203
+ ` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
19204
+ ` + `- Slash commands the user typed and their effects (e.g. "User issued /clear to
19205
+ ` + ` reset assistant state").
19206
+ ` + `- Hindsight's own errors, retries, backlogs, or internal state — the memory
19207
+ ` + ` system's self-reports are not memories.
19208
+ ` + `- Restatements of the user's current request or the task in progress.
19209
+ ` + `- Transient state (unread counts, build status, what is running right now) unless
19210
+ ` + ` the fact is explicitly dated, in which case record it as a dated observation.
19211
+ ` + `- Greetings, acknowledgements, and routine operational chatter.
19212
+ ` + `
19213
+ ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
19214
+ ` + "nothing durable remains, return an empty facts list."
19215
+ ];
19216
+ var DEFAULT_OBSERVATIONS_MISSION = `Synthesise durable, standing knowledge about the people, projects, and systems this agent works with: preferences and standing rules, roles and relationships, skills and recurring patterns, technical and operational decisions with their rationale, and the state of long-running work once it lands.
19217
+ ` + `
19218
+ ` + `The test is durability, not notability: an observation must still be worth reading weeks from now. A single dated event belongs in an observation only when it establishes or changes a standing fact.
19219
+ ` + `
19220
+ ` + `Do NOT synthesise observations from:
19221
+ ` + `- Transcript exhaust — tool calls and their results, file paths, temp or scratchpad directories, where some output was written, or narration of what an assistant did.
19222
+ ` + `- Identifiers with no standing meaning: session, agent, request, batch or command IDs, UUIDs, hashes, error codes.
19223
+ ` + `- In-flight process narration — a task started, paused, or still running. Record the outcome once it lands, not the running state.
19224
+ ` + `- The memory system's own errors, retries, backlogs, or internal state.
19225
+ ` + `- Transient state (what is running right now, unread counts, build status) unless the fact is explicitly dated, in which case record it as dated.
19226
+ ` + `
19227
+ ` + "If the new facts contain nothing durable, record nothing rather than synthesising a weak observation.";
19228
+ var SUPERSEDED_OBSERVATIONS_MISSIONS = [
19229
+ "Synthesise the person's wellbeing patterns, motivations, and emotional " + "context — how habits, setbacks, and encouragement connect over time."
19153
19230
  ];
19154
19231
  var PROFILE_MEMORY_DEFAULTS = {
19155
19232
  "health-coach": {
19156
19233
  disposition: { skepticism: 2, literalism: 2, empathy: 5 },
19157
- observations_mission: "Synthesise the person's wellbeing patterns, motivations, and emotional " + "context — how habits, setbacks, and encouragement connect over time."
19234
+ observations_mission: `You consolidate the memory of a health and fitness coach working with one person. This bank records how that person actually lives and trains.
19235
+ ` + `
19236
+ ` + `Synthesise into durable observations:
19237
+ ` + `- Goals, targets, and the plan currently in force, with the reasoning behind each.
19238
+ ` + `- Training, nutrition, sleep, and alcohol patterns as they hold over weeks — what the person reliably does, not what they did once.
19239
+ ` + `- Constraints that shape the plan: injuries, medical guidance, schedule, equipment, foods and sessions they refuse.
19240
+ ` + `- Motivations, and what actually helps or backfires when they slip.
19241
+ ` + `- Trends in the numbers: direction and range over time, not any single reading.
19242
+ ` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
19243
+ ` + `
19244
+ ` + `A single day's log, weight reading, or session is evidence, not an observation. Record one only when it establishes or changes a standing pattern, target, or constraint — then fold it into the observation for that pattern and say what changed and roughly when.
19245
+ ` + `
19246
+ ` + `Granularity: one observation per habit, target, constraint, or trend. Aggregate repeated daily evidence into the observation for that pattern rather than creating one per day, and keep training, nutrition, and sleep as separate observations.
19247
+ ` + `
19248
+ ` + "Word observations as the person's own pattern and framing, never as a verdict on them."
19158
19249
  },
19159
19250
  "executive-assistant": {
19160
- disposition: { skepticism: 4, literalism: 4, empathy: 3 }
19251
+ disposition: { skepticism: 4, literalism: 4, empathy: 3 },
19252
+ observations_mission: `You consolidate the memory of an executive assistant working for one person. This bank records that person's commitments, people, and standing arrangements.
19253
+ ` + `
19254
+ ` + `Synthesise into durable observations:
19255
+ ` + `- Standing rules and preferences: how they want things scheduled, written, and filed, and when they want to be interrupted.
19256
+ ` + `- People and organisations, and the relationship: who they are, what they are involved in, how to reach them.
19257
+ ` + `- Recurring commitments and routines, and the constraints around them.
19258
+ ` + `- Obligations and their state: what was promised to whom, the deadline, and what is still outstanding.
19259
+ ` + `- Decisions made and decisions deferred, with the reasoning and the trade accepted.
19260
+ ` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
19261
+ ` + `
19262
+ ` + `Live commitment state IS durable knowledge here, not ephemeral chatter. An outstanding obligation, a travel window, an unanswered request, a "currently X" arrangement — these are precisely what this agent must recall later. Keep them, and embed the date inside the observation text so a later reader can judge staleness. Do not drop them as ephemeral.
19263
+ ` + `
19264
+ ` + `Granularity: one observation per person, arrangement, or obligation. Aggregate repeated mentions into that observation rather than creating siblings; never merge two different people or two different commitments into one.
19265
+ ` + `
19266
+ ` + "Do not synthesise from transcript exhaust: tool calls and their results, message or event identifiers, or narration of what the assistant did."
19161
19267
  },
19162
19268
  coding: {
19163
- disposition: { skepticism: 4, literalism: 5, empathy: 2 }
19269
+ disposition: { skepticism: 4, literalism: 5, empathy: 2 },
19270
+ observations_mission: `You consolidate the memory of a software-engineering agent. This bank records real work on real codebases.
19271
+ ` + `
19272
+ ` + `Synthesise into durable observations:
19273
+ ` + `- Architecture and design decisions, each with its rationale and the trade accepted.
19274
+ ` + `- Root causes, with the evidence chain, and negative results — what was ruled out matters as much as what was found.
19275
+ ` + `- How the repository works: build, test, and lint commands, conventions, CI gates, and where things live.
19276
+ ` + `- Outcomes of code work: issue and PR numbers, what changed, whether it merged, what review found.
19277
+ ` + `- The user's standing rules, preferences, and corrections to this agent's behaviour.
19278
+ ` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
19279
+ ` + `
19280
+ ` + `Repository and service state IS durable knowledge here, not ephemeral chatter. Versions, a failing gate, an open PR, a measured number, a "currently X" claim — these are precisely what this agent must recall later. Keep them, and embed the date inside the observation text so a later reader can judge staleness. Do not drop them as ephemeral.
19281
+ ` + `
19282
+ ` + `Granularity: one observation per distinct decision, cause, convention, or work item. Aggregate repeated evidence about the same one into that observation rather than creating siblings; never merge two separate decisions into a single summary.
19283
+ ` + `
19284
+ ` + "Do not synthesise from transcript exhaust: tool calls and their results, scratch paths, session or request identifiers, or narration of what the agent did. Prefer specific and falsifiable — naming the file, the number, the commit, or the decision beats summarising the topic."
19164
19285
  }
19165
19286
  };
19166
19287
  // src/agents/reconcile-default-skills.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.19.27",
4
+ "version": "0.19.28",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -28,7 +28,7 @@
28
28
  "test:vitest": "vitest run",
29
29
  "test:bun": "bun test telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-scope-persist.test.ts src/vault/broker/server-tokenless-scope.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/self-approval-bypass.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
30
30
  "test:watch": "vitest",
31
- "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-agent-attribution-trailers.mjs",
31
+ "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-agent-attribution-trailers.mjs",
32
32
  "lint:tsc": "tsc --noEmit",
33
33
  "lint:plugin-references": "node scripts/check-plugin-references.mjs",
34
34
  "lint:bot-api-wrapping": "bash scripts/check-bot-api-wrapping.sh",
@@ -42,6 +42,7 @@
42
42
  "lint:no-broadcast-delivery": "node scripts/check-no-broadcast-delivery.mjs",
43
43
  "lint:gateway-line-ratchet": "node scripts/check-gateway-line-ratchet.mjs",
44
44
  "lint:retry-flood-hooks": "node scripts/check-retry-flood-hooks.mjs",
45
+ "lint:callback-ctx-wrapping": "node scripts/check-callback-ctx-wrapping.mjs",
45
46
  "lint:status-pin-single-path": "node scripts/check-status-pin-single-path.mjs",
46
47
  "lint:mcp-instructions-budget": "node scripts/check-mcp-instructions-budget.mjs",
47
48
  "lint:litellm-config-guard": "node scripts/check-litellm-config-guard.mjs",
@@ -475,11 +475,25 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
475
475
  # mechanism — it runs because the container is up, needs no LLM turn of
476
476
  # its own, and cannot be prompted away.
477
477
  #
478
- # SERIALISED. `flock -n` on a per-agent lockfile makes overlapping runs
479
- # impossible: a run that overshoots the interval causes the next tick to
480
- # SKIP, never to stack. The lock path is deliberately the one the
481
- # out-of-band recovery tooling also takes, so a manual sweep and this
482
- # loop mutually exclude instead of double-processing an entry.
478
+ # SERIALISED, BUT NOT BY THIS FILE. `drain_pending.py` takes an
479
+ # exclusive `fcntl.flock` on `$HOME/.hindsight/drain-pending.lock` for
480
+ # the whole of `drain()` and does nothing at all if another drain holds
481
+ # it. That is where the guarantee has to live: the queue has three
482
+ # independent drain paths — the SessionStart hook (which imports
483
+ # `drain` in-process, and boots while this loop may be mid-run), this
484
+ # sidecar, and the `docker exec … drain_pending.py --backlog` that
485
+ # `switchroom doctor` documents for operators — and a lock wrapped
486
+ # around only ONE of them serialises nothing. Two drains on one queue
487
+ # means the same entry POSTed twice, i.e. ~168s of a 4-slot fleet-wide
488
+ # lane spent twice on one memory.
489
+ #
490
+ # DO NOT WRAP THE CALL BELOW IN `flock`. It would take the very lock
491
+ # the python then asks for, and the drain would skip every tick,
492
+ # silently, forever. (This is also why the lock file is NOT the
493
+ # `drain.lock` the interim host cron wraps its `docker exec` in — with
494
+ # distinct names that wrapper stays merely redundant instead of
495
+ # starving the drain it launched.) `tests/hindsight-drain-sidecar.test.ts`
496
+ # fails if a `flock` reappears here.
483
497
  #
484
498
  # LANE-RESPECTING. Retain extraction is served by a small fixed pool of
485
499
  # LLM lanes (4 on this fleet, 2 boxes x 2 slots) SHARED with live
@@ -516,6 +530,22 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
516
530
  # already queued ABOVE the current bound are recovered by re-splitting
517
531
  # them, not by waiting longer. An operator who still wants a longer
518
532
  # deadline moves HINDSIGHT_RETAIN_CLIENT_DEADLINE_S, which moves both.
533
+ # THE ROWS OUTLIVE THE STOPGAP THAT PRODUCED THEM; the serialisation in
534
+ # it does not. That stopgap is the interim host cron named above, whose
535
+ # outer `flock` on `drain.lock` serialised only that cron against
536
+ # itself, while the SessionStart hook's in-process drain and a bare
537
+ # `docker exec` walked straight past it — the widening to all three is
538
+ # the whole point of moving the lock inside `drain_pending.py`. What
539
+ # the rows measure is one drain completing (or not) inside a deadline,
540
+ # which is a property of the drain and not of who serialised it, so
541
+ # they still stand. They are not a licence to reinstate the wrapper.
542
+ #
543
+ # UNATTENDED, SO BOUNDED PER ENTRY. `drain_pending` parks an entry past
544
+ # its attempt ceiling (default 20) instead of retrying it forever —
545
+ # without that, one permanently-failing entry on a 3600s backlog budget
546
+ # would burn most of this agent's wall-clock, and a share of the 4
547
+ # lanes, indefinitely and with nobody watching. Parked entries stay
548
+ # queued and are still reconciled for free; `--force` replays them.
519
549
  #
520
550
  # Failure is never terminal: the drain exits non-zero only when it
521
551
  # promoted entries to `.dead`, and the loop below ignores the status
@@ -524,13 +554,24 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
524
554
  # (those are exported later, in the inner pass) — only $HOME, to find
525
555
  # the queue.
526
556
  #
557
+ # DOCKER RUNTIME ONLY. This whole block is inside the
558
+ # `[ "$SWITCHROOM_RUNTIME" = "docker" ]` branch that ends at the `exec
559
+ # tmux` below, so an agent on the legacy systemd path gets NO drain
560
+ # sidecar and keeps the boot-only SessionStart behaviour. The fleet is
561
+ # entirely docker; if systemd ever comes back, this needs an equivalent
562
+ # timer, and until then the honest statement is that it is unsupported
563
+ # there rather than silently absent.
564
+ #
527
565
  # Kill switch: SWITCHROOM_HINDSIGHT_DRAIN=0 at the container level.
528
- # Interval: SWITCHROOM_HINDSIGHT_DRAIN_INTERVAL_S (default 900s).
566
+ # Interval: SWITCHROOM_HINDSIGHT_DRAIN_INTERVAL_S (default 900s). That
567
+ # is a COOLDOWN BETWEEN RUNS, not a fixed period — the loop is strictly
568
+ # sequential, so the true tick-to-tick spacing is one drain's duration
569
+ # (up to the 3600s backlog budget) plus the interval. Deliberate: a
570
+ # fixed period would give a long, lane-heavy drain no cooldown at all.
529
571
  _hs_drain_script="{{agentDir}}/.claude/plugins/hindsight-memory/scripts/drain_pending.py"
530
- if [ "$SWITCHROOM_HINDSIGHT_DRAIN" != "0" ] \
572
+ if [ "${SWITCHROOM_HINDSIGHT_DRAIN:-1}" != "0" ] \
531
573
  && [ -f "$_hs_drain_script" ] \
532
- && command -v python3 >/dev/null 2>&1 \
533
- && command -v flock >/dev/null 2>&1; then
574
+ && command -v python3 >/dev/null 2>&1; then
534
575
  _switchroom_hindsight_drain_loop() {
535
576
  local _script="$1"
536
577
  local _interval="${SWITCHROOM_HINDSIGHT_DRAIN_INTERVAL_S:-900}"
@@ -538,24 +579,38 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
538
579
  # against the LLM lane. 60s is the floor.
539
580
  case "$_interval" in ''|*[!0-9]*) _interval=900 ;; esac
540
581
  [ "$_interval" -lt 60 ] && _interval=60
541
- local _lock="$HOME/.hindsight/drain.lock"
542
- mkdir -p "$HOME/.hindsight" 2>/dev/null || true
543
582
  # Deterministic per-agent phase offset (see the note above).
544
583
  local _offset=0
545
584
  if command -v cksum >/dev/null 2>&1; then
546
585
  _offset=$(( $(printf '%s' "{{name}}" | cksum | cut -d' ' -f1) % _interval ))
547
586
  fi
548
- echo "[hindsight-drain] starting: interval=${_interval}s offset=${_offset}s lock=${_lock}"
587
+ echo "[hindsight-drain] starting: interval=${_interval}s (cooldown between runs, not a fixed period) offset=${_offset}s serialised-by=drain_pending.py"
549
588
  sleep "$_offset"
550
589
  while true; do
551
- # -n: if a drain (ours or an operator sweep) already holds the
552
- # lock, skip this tick entirely instead of queueing behind it.
553
- flock -n "$_lock" python3 "$_script" --backlog || true
590
+ # No `flock` here, by design — drain_pending.py locks itself, and a
591
+ # wrapper here would hold the lock it needs. See the note above.
592
+ python3 "$_script" --backlog || true
554
593
  sleep "$_interval"
555
594
  done
556
595
  }
557
596
  _switchroom_supervise hindsight-drain /var/log/switchroom/hindsight-drain.log \
558
597
  _switchroom_hindsight_drain_loop "$_hs_drain_script" &
598
+ else
599
+ # NEVER FAIL SILENTLY. Without this branch a failed precondition left no
600
+ # log line and no logfile at all, so "is the drain running?" was
601
+ # unanswerable from the logs — indistinguishable from a drain that was
602
+ # running and finding nothing to do.
603
+ if [ "${SWITCHROOM_HINDSIGHT_DRAIN:-1}" = "0" ]; then
604
+ _hs_drain_why="disabled by SWITCHROOM_HINDSIGHT_DRAIN=0"
605
+ elif [ ! -f "$_hs_drain_script" ]; then
606
+ _hs_drain_why="no drain script at $_hs_drain_script (hindsight plugin not installed, or an interrupted \`switchroom apply\`)"
607
+ else
608
+ _hs_drain_why="python3 is not on PATH in this image"
609
+ fi
610
+ _hs_drain_msg="[hindsight-drain] NOT STARTED: ${_hs_drain_why}. The memory queue will only be drained at session boot, which cannot clear a backlog."
611
+ echo "$_hs_drain_msg"
612
+ mkdir -p /var/log/switchroom 2>/dev/null || true
613
+ echo "$_hs_drain_msg" >> /var/log/switchroom/hindsight-drain.log 2>/dev/null || true
559
614
  fi
560
615
  {{/if}}
561
616
 
@@ -23195,6 +23195,7 @@ var OPERATOR_ACTIONABLE_KINDS = new Set([
23195
23195
  "credentials-invalid",
23196
23196
  "credit-exhausted",
23197
23197
  "provider-credit-exhausted",
23198
+ "mcp-dependency-blocked",
23198
23199
  "proxy-misconfig"
23199
23200
  ]);
23200
23201