switchroom 0.17.10 → 0.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/bin/workspace-dynamic-hook.sh +12 -13
  2. package/dist/agent-scheduler/index.js +27 -1
  3. package/dist/auth-broker/index.js +6161 -151
  4. package/dist/cli/notion-write-pretool.mjs +29 -2
  5. package/dist/cli/switchroom.js +578 -454
  6. package/dist/host-control/main.js +6182 -172
  7. package/dist/vault/approvals/kernel-server.js +5891 -164
  8. package/dist/vault/broker/server.js +6597 -881
  9. package/package.json +1 -1
  10. package/profiles/_base/settings.json.hbs +2 -2
  11. package/profiles/_base/start.sh.hbs +170 -21
  12. package/profiles/coding/CLAUDE.md.hbs +1 -1
  13. package/profiles/default/CLAUDE.md +2 -2
  14. package/profiles/default/CLAUDE.md.hbs +2 -2
  15. package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
  16. package/profiles/health-coach/CLAUDE.md.hbs +1 -1
  17. package/telegram-plugin/auth-snapshot-format.ts +22 -24
  18. package/telegram-plugin/context-exhaustion.ts +124 -0
  19. package/telegram-plugin/dist/gateway/gateway.js +24086 -8727
  20. package/telegram-plugin/gateway/activity-card-store.ts +76 -0
  21. package/telegram-plugin/gateway/gateway.ts +480 -85
  22. package/telegram-plugin/gateway/inbound-delivery-gate.ts +26 -0
  23. package/telegram-plugin/gateway/model-command.ts +70 -10
  24. package/telegram-plugin/package.json +6 -0
  25. package/telegram-plugin/quota-watch.ts +4 -6
  26. package/telegram-plugin/registry/turns-schema.test.ts +97 -0
  27. package/telegram-plugin/registry/turns-schema.ts +78 -0
  28. package/telegram-plugin/render/ir.ts +209 -0
  29. package/telegram-plugin/render/parse.ts +363 -0
  30. package/telegram-plugin/render/render.ts +440 -0
  31. package/telegram-plugin/render/rich-render.ts +72 -0
  32. package/telegram-plugin/stream-controller.ts +14 -3
  33. package/telegram-plugin/tests/activity-card-store.test.ts +94 -0
  34. package/telegram-plugin/tests/auth-command-format2.test.ts +1 -1
  35. package/telegram-plugin/tests/auth-snapshot-format.test.ts +30 -16
  36. package/telegram-plugin/tests/claude-code-event-contract.test.ts +48 -0
  37. package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +11 -0
  38. package/telegram-plugin/tests/feed-survival.test.ts +39 -0
  39. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +81 -0
  40. package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +82 -0
  41. package/telegram-plugin/tests/liveness-tracker.test.ts +228 -0
  42. package/telegram-plugin/tests/model-command.test.ts +193 -16
  43. package/telegram-plugin/tests/narrative-render.test.ts +125 -0
  44. package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +123 -163
  45. package/telegram-plugin/tests/quota-watch.test.ts +1 -4
  46. package/telegram-plugin/tests/rapid-fire-delivery-ordering.test.ts +149 -0
  47. package/telegram-plugin/tests/render/parse-torture.test.ts +136 -0
  48. package/telegram-plugin/tests/render/parse.test.ts +393 -0
  49. package/telegram-plugin/tests/render/render.test.ts +436 -0
  50. package/telegram-plugin/tests/render/rich-render.test.ts +85 -0
  51. package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +155 -1
  52. package/telegram-plugin/tests/worktree-watch-cwds.test.ts +98 -3
  53. package/telegram-plugin/turn-liveness-floor.ts +35 -1
  54. package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +99 -7
  55. package/telegram-plugin/worktree-watch-cwds.ts +92 -17
  56. package/vendor/hindsight-memory/scripts/lib/client.py +11 -1
  57. package/vendor/hindsight-memory/scripts/lib/config.py +9 -2
  58. package/vendor/hindsight-memory/scripts/recall.py +64 -6
  59. package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +1 -0
  60. package/vendor/hindsight-memory/tests/test_client.py +43 -0
  61. package/vendor/hindsight-memory/tests/test_recall_precision.py +114 -0
@@ -10,14 +10,13 @@
10
10
  # Configuration is via env vars (set at start.sh time):
11
11
  #
12
12
  # SWITCHROOM_AGENT_NAME - The agent name (required, set in start.sh)
13
- # SWITCHROOM_INJECT_ON_CHANGE - When "1", enables inject-on-change
14
- # semantics: content is only emitted when the
15
- # session_id changes or file content changes.
16
- # When "0" or unset (legacy mode), content is
17
- # emitted every turn as before (full emit).
18
- # Default in new agents is "1" (set by
19
- # scaffold.ts when inject_on_change is true,
20
- # the default).
13
+ # SWITCHROOM_INJECT_ON_CHANGE - Inject-on-change semantics are the default:
14
+ # content is only emitted when the session_id
15
+ # changes or file content changes. Set to "0"
16
+ # to opt out and emit every turn (legacy full
17
+ # emit) scaffold.ts threads this negation
18
+ # only when an agent sets
19
+ # channels.telegram.inject_on_change: false.
21
20
  #
22
21
  # Failure modes (all silent — workspace injection must never block the turn):
23
22
  # - switchroom CLI missing → exit 0 with no output
@@ -29,7 +28,7 @@
29
28
  set -u
30
29
 
31
30
  AGENT_NAME="${SWITCHROOM_AGENT_NAME:-}"
32
- INJECT_ON_CHANGE="${SWITCHROOM_INJECT_ON_CHANGE:-0}"
31
+ INJECT_ON_CHANGE="${SWITCHROOM_INJECT_ON_CHANGE:-1}"
33
32
 
34
33
  if [ -z "$AGENT_NAME" ]; then
35
34
  exit 0
@@ -43,7 +42,7 @@ fi
43
42
  # Inject-on-change: read session_id from stdin JSON (when enabled)
44
43
  # ---------------------------------------------------------------------------
45
44
  SESSION_ID=""
46
- if [ "$INJECT_ON_CHANGE" = "1" ]; then
45
+ if [ "$INJECT_ON_CHANGE" != "0" ]; then
47
46
  # Read stdin (non-blocking: if no stdin supplied in tests, just skip).
48
47
  if ! [ -t 0 ]; then
49
48
  STDIN_JSON=$(cat 2>/dev/null || true)
@@ -118,7 +117,7 @@ if [ -f "$BODY_FILE" ]; then
118
117
  # In inject-on-change mode, also check the session-state file — if the
119
118
  # session_id matches the last-emitted session AND the hash matches, we
120
119
  # can suppress entirely (model already has this content in context).
121
- if [ "$INJECT_ON_CHANGE" = "1" ] && [ -n "$SESSION_ID" ]; then
120
+ if [ "$INJECT_ON_CHANGE" != "0" ] && [ -n "$SESSION_ID" ]; then
122
121
  SESSION_STATE_DIR="${TELEGRAM_STATE_DIR:-${HOME:-/tmp}/.claude/switchroom-hookcache}/.hook-state"
123
122
  SESSION_STATE_FILE="$SESSION_STATE_DIR/ws-dynamic.$SESSION_ID"
124
123
  if [ -f "$SESSION_STATE_FILE" ]; then
@@ -201,7 +200,7 @@ _ws_record_session_state() {
201
200
  if [ -n "$NEW_HASH" ] && [ "$NEW_HASH" = "$OLD_HASH" ] && [ -f "$BODY_FILE" ]; then
202
201
  # Content unchanged since last render. In inject-on-change mode, check if
203
202
  # we already injected this content in the current session — if so, suppress.
204
- if [ "$INJECT_ON_CHANGE" = "1" ] && [ -n "$SESSION_ID" ]; then
203
+ if [ "$INJECT_ON_CHANGE" != "0" ] && [ -n "$SESSION_ID" ]; then
205
204
  SESSION_STATE_DIR="${TELEGRAM_STATE_DIR:-${HOME:-/tmp}/.claude/switchroom-hookcache}/.hook-state"
206
205
  SESSION_STATE_FILE="$SESSION_STATE_DIR/ws-dynamic.$SESSION_ID"
207
206
  if [ -f "$SESSION_STATE_FILE" ]; then
@@ -222,7 +221,7 @@ else
222
221
  if [ -n "$NEW_HASH" ]; then
223
222
  printf '%s\n' "$NEW_HASH" > "$CACHE_FILE" 2>/dev/null || true
224
223
  printf '%s\n' "$WS_DYNAMIC" > "$BODY_FILE" 2>/dev/null || true
225
- if [ "$INJECT_ON_CHANGE" = "1" ] && [ -n "$SESSION_ID" ]; then
224
+ if [ "$INJECT_ON_CHANGE" != "0" ] && [ -n "$SESSION_ID" ]; then
226
225
  _ws_record_session_state "$NEW_HASH" "$SESSION_ID"
227
226
  fi
228
227
  fi
@@ -11108,7 +11108,11 @@ var AgentMemorySchema = exports_external.object({
11108
11108
  sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Per-speaker recall routing: a map of Telegram sender → extra " + "recall bank. When a message arrives, the agent also recalls the " + "speaker's bank (matched by Telegram username — a leading @ is " + "optional — or numeric user_id), merged " + "into its own results — so each trusted user gets their own " + "profile context. Additive recall scoping within the single " + "tenant: never an access boundary (who may drive an agent stays " + "the per-agent user assignment in `access.allowFrom`). Author the " + "banks via `switchroom memory profile`."),
11109
11109
  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."),
11110
11110
  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).")
11111
- }).optional().describe("Auto-recall tuning knobs")
11111
+ }).optional().describe("Auto-recall tuning knobs"),
11112
+ retain: exports_external.object({
11113
+ every_n_turns: exports_external.number().int().min(1).optional().describe("How often the Stop hook fires auto-retention, in turns. The " + "vendor plugin default is 10 (a short session can end before " + "retention ever fires); switchroom's scaffold default is 3. " + "Lower = more frequent, smaller retains + tighter crash " + "durability (at 1, every turn is retained before a restart can " + "lose it); higher = fewer, larger retains + less LLM churn. " + "Raised from the historical 1 to 3 because the local reasoning " + "consolidation model (Ollama gpt-oss-20b) ran away on the large " + "overlapping every-turn payloads. Set to 1 for the old " + "every-turn crash-durability guarantee. Min 1. Cascade: " + "per-field merge (agent wins over default)."),
11114
+ overlap_turns: exports_external.number().int().min(0).optional().describe("Extra recent turns included in each chunked retain window on " + "top of `every_n_turns`, so window = overlap_turns + " + "every_n_turns recent turns. Vendor default is 2; switchroom's " + "scaffold default is 1 (smaller payloads for the local " + "reasoning consolidation model). Higher = more redundant " + "context re-sent per fire. Min 0. Cascade: per-field merge.")
11115
+ }).optional().describe("Auto-retain (Stop-hook consolidation) cadence knobs")
11112
11116
  }).optional();
11113
11117
  var HookEntrySchema = exports_external.object({
11114
11118
  command: exports_external.string().describe("Shell command to run. Supports ${CLAUDE_CONFIG_DIR} and ${CLAUDE_PLUGIN_ROOT} substitution."),
@@ -11286,6 +11290,21 @@ var LiteLLMConfigSchema = exports_external.object({
11286
11290
  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'."),
11287
11291
  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).")
11288
11292
  }).optional().describe("LiteLLM routing config — opt-in per-agent virtual-key auto-provisioning " + "+ routing env. Default OFF. See LiteLLMConfigSchema doc for the full flow.");
11293
+ var HindsightPerOpLlmSchema = exports_external.object({
11294
+ model: exports_external.string().min(1).optional().describe("Per-op model (upstream `HINDSIGHT_API_<OP>_LLM_MODEL`). Absent → " + "inherit the global `hindsight.llm.model`."),
11295
+ provider: exports_external.string().min(1).optional().describe("Per-op provider (upstream `HINDSIGHT_API_<OP>_LLM_PROVIDER`). " + "Absent → inherit the global `hindsight.llm.provider`."),
11296
+ base_url: exports_external.string().min(1).optional().describe("Per-op base URL (upstream `HINDSIGHT_API_<OP>_LLM_BASE_URL`). " + "Optional passthrough; absent → inherit the global."),
11297
+ api_key: exports_external.string().min(1).optional().describe("Per-op API key (upstream `HINDSIGHT_API_<OP>_LLM_API_KEY`). Literal " + "or `vault:` reference. Optional passthrough; absent → inherit global.")
11298
+ }).describe("Per-operation LLM override. Every field optional; an unset field (or " + "an omitted op block) inherits the global `hindsight.llm.*`, which is " + "already the engine's fallback — switchroom emits only the vars set.");
11299
+ var HindsightConfigSchema = exports_external.object({
11300
+ llm: exports_external.object({
11301
+ provider: exports_external.string().min(1).optional().describe("Hindsight LLM provider (upstream `HINDSIGHT_API_LLM_PROVIDER`). " + "Defaults to `claude-code` (subscription-honest, broker-fed OAuth). " + "Any litellm-routable provider the upstream image supports is valid. " + "Serves as the GLOBAL default for every op absent a per-op override."),
11302
+ model: exports_external.string().min(1).optional().describe("Hindsight LLM model (upstream `HINDSIGHT_API_LLM_MODEL`). Defaults " + "to HINDSIGHT_DEFAULT_MODEL. Any model your LiteLLM proxy can route " + "is valid, e.g. `openrouter/z-ai/glm-5.2` when routing through the " + "fleet proxy. With provider=claude-code this value is ALSO exported " + "as `ANTHROPIC_MODEL` to the claude subprocess. Serves as the GLOBAL " + "default for every op absent a per-op override."),
11303
+ retain: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `retain` LLM op (memory ingestion). Emits " + "`HINDSIGHT_API_RETAIN_LLM_*`. Absent → uses the global model/provider."),
11304
+ reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
11305
+ consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
11306
+ }).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.")
11307
+ });
11289
11308
  var MicrosoftWorkspaceConfigSchema = exports_external.object({
11290
11309
  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)."),
11291
11310
  microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional — public-client apps " + "(Mobile + Desktop platform with 'Allow public client flows' " + "enabled) work without a secret; confidential clients pass " + "one. Either literal or vault reference e.g. " + "'vault:microsoft-oauth-client-secret'."),
@@ -11534,6 +11553,7 @@ var MemoryBackendConfigSchema = exports_external.object({
11534
11553
  config: exports_external.object({
11535
11554
  provider: exports_external.string().default("ollama").describe("Embedding provider (ollama, openai, anthropic)"),
11536
11555
  model: exports_external.string().optional().describe("Embedding model (e.g., 'nomic-embed-text')"),
11556
+ llm_model: exports_external.string().optional().describe("LiteLLM model name for Hindsight's LLM ops (retain/reflect/" + "consolidation) when the top-level `litellm.enabled` carve-out is " + "on. Defaults to a cheap OpenRouter model (routed via LiteLLM's " + "model-mapped path, not the Anthropic OAuth pass-through) to keep " + "background memory-op cost off the Claude subscription quota. Set " + "to a `claude-*` model name to route it back through the OAuth " + "pass-through instead. Has no effect when litellm is disabled."),
11537
11557
  api_key: exports_external.string().optional().describe("API key or vault reference for embedding provider"),
11538
11558
  docker_service: exports_external.boolean().default(true).describe("Whether to include Hindsight in docker-compose"),
11539
11559
  url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/.")
@@ -11611,6 +11631,7 @@ var SwitchroomConfigSchema = exports_external.object({
11611
11631
  telegram: TelegramConfigSchema,
11612
11632
  release: ReleaseBlock.optional().describe("Fleet-wide default release-channel pin / pointer for the update " + "flow. Either `channel` (dev|rc|latest) or `pin` (sha-<hex>|v<semver>) " + "— mutually exclusive. Per-agent `release` REPLACES this entirely."),
11613
11633
  memory: MemoryBackendConfigSchema.optional(),
11634
+ hindsight: HindsightConfigSchema.optional().describe("Fleet-singleton Hindsight (memory backend) configuration. Currently " + "just the LLM knob (provider + model) used for retain/reflect/" + "consolidation. Both fields optional; when unset the container falls " + "back to provider=claude-code + the hard-coded HINDSIGHT_DEFAULT_MODEL " + "so nothing changes for operators who don't set it. Read at container " + "launch by startHindsight() (src/setup/hindsight.ts) — takes effect on " + "the next `switchroom apply` / `memory setup --recreate`."),
11614
11635
  vault: VaultConfigSchema.optional(),
11615
11636
  auth: exports_external.object({
11616
11637
  active: exports_external.string().min(1).optional().describe("Fleet-wide active Anthropic account label. Every agent without " + "an explicit `agent.auth.override` uses this account. See " + "docs/auth.md for the full model. Set by `switchroom auth use <label>`."),
@@ -12007,6 +12028,11 @@ function mergeAgentConfig(defaultsIn, agentIn) {
12007
12028
  continue;
12008
12029
  if (k === "recall" && base.recall && typeof v === "object" && v !== null && !Array.isArray(v)) {
12009
12030
  combined[k] = { ...base.recall, ...v };
12031
+ } else if (k === "retain" && base.retain && typeof v === "object" && v !== null && !Array.isArray(v)) {
12032
+ combined[k] = {
12033
+ ...base.retain,
12034
+ ...v
12035
+ };
12010
12036
  } else if (k === "disposition" && base.disposition && typeof v === "object" && v !== null && !Array.isArray(v)) {
12011
12037
  combined[k] = {
12012
12038
  ...base.disposition,