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
@@ -13542,7 +13542,7 @@ var init_zod = __esm(() => {
13542
13542
  function isValidTimezone(value) {
13543
13543
  return TIMEZONE_REGEX.test(value);
13544
13544
  }
13545
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
13545
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
13546
13546
  var init_schema = __esm(() => {
13547
13547
  init_zod();
13548
13548
  CodeRepoEntrySchema = exports_external.object({
@@ -13694,7 +13694,11 @@ var init_schema = __esm(() => {
13694
13694
  sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Per-speaker recall routing: a map of Telegram sender \u2192 extra " + "recall bank. When a message arrives, the agent also recalls the " + "speaker's bank (matched by Telegram username \u2014 a leading @ is " + "optional \u2014 or numeric user_id), merged " + "into its own results \u2014 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`."),
13695
13695
  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."),
13696
13696
  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).")
13697
- }).optional().describe("Auto-recall tuning knobs")
13697
+ }).optional().describe("Auto-recall tuning knobs"),
13698
+ retain: exports_external.object({
13699
+ 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)."),
13700
+ 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.")
13701
+ }).optional().describe("Auto-retain (Stop-hook consolidation) cadence knobs")
13698
13702
  }).optional();
13699
13703
  HookEntrySchema = exports_external.object({
13700
13704
  command: exports_external.string().describe("Shell command to run. Supports ${CLAUDE_CONFIG_DIR} and ${CLAUDE_PLUGIN_ROOT} substitution."),
@@ -13872,6 +13876,21 @@ var init_schema = __esm(() => {
13872
13876
  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'."),
13873
13877
  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).")
13874
13878
  }).optional().describe("LiteLLM routing config \u2014 opt-in per-agent virtual-key auto-provisioning " + "+ routing env. Default OFF. See LiteLLMConfigSchema doc for the full flow.");
13879
+ HindsightPerOpLlmSchema = exports_external.object({
13880
+ model: exports_external.string().min(1).optional().describe("Per-op model (upstream `HINDSIGHT_API_<OP>_LLM_MODEL`). Absent \u2192 " + "inherit the global `hindsight.llm.model`."),
13881
+ provider: exports_external.string().min(1).optional().describe("Per-op provider (upstream `HINDSIGHT_API_<OP>_LLM_PROVIDER`). " + "Absent \u2192 inherit the global `hindsight.llm.provider`."),
13882
+ base_url: exports_external.string().min(1).optional().describe("Per-op base URL (upstream `HINDSIGHT_API_<OP>_LLM_BASE_URL`). " + "Optional passthrough; absent \u2192 inherit the global."),
13883
+ 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 \u2192 inherit global.")
13884
+ }).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 \u2014 switchroom emits only the vars set.");
13885
+ HindsightConfigSchema = exports_external.object({
13886
+ llm: exports_external.object({
13887
+ 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."),
13888
+ 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."),
13889
+ retain: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `retain` LLM op (memory ingestion). Emits " + "`HINDSIGHT_API_RETAIN_LLM_*`. Absent \u2192 uses the global model/provider."),
13890
+ reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent \u2192 uses global."),
13891
+ consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
13892
+ }).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.")
13893
+ });
13875
13894
  MicrosoftWorkspaceConfigSchema = exports_external.object({
13876
13895
  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 \u2014 omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
13877
13896
  microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional \u2014 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'."),
@@ -14120,6 +14139,7 @@ var init_schema = __esm(() => {
14120
14139
  config: exports_external.object({
14121
14140
  provider: exports_external.string().default("ollama").describe("Embedding provider (ollama, openai, anthropic)"),
14122
14141
  model: exports_external.string().optional().describe("Embedding model (e.g., 'nomic-embed-text')"),
14142
+ 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."),
14123
14143
  api_key: exports_external.string().optional().describe("API key or vault reference for embedding provider"),
14124
14144
  docker_service: exports_external.boolean().default(true).describe("Whether to include Hindsight in docker-compose"),
14125
14145
  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/.")
@@ -14197,6 +14217,7 @@ var init_schema = __esm(() => {
14197
14217
  telegram: TelegramConfigSchema,
14198
14218
  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>) " + "\u2014 mutually exclusive. Per-agent `release` REPLACES this entirely."),
14199
14219
  memory: MemoryBackendConfigSchema.optional(),
14220
+ 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) \u2014 takes effect on " + "the next `switchroom apply` / `memory setup --recreate`."),
14200
14221
  vault: VaultConfigSchema.optional(),
14201
14222
  auth: exports_external.object({
14202
14223
  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>`."),
@@ -14645,6 +14666,11 @@ function mergeAgentConfig(defaultsIn, agentIn) {
14645
14666
  continue;
14646
14667
  if (k === "recall" && base.recall && typeof v === "object" && v !== null && !Array.isArray(v)) {
14647
14668
  combined[k] = { ...base.recall, ...v };
14669
+ } else if (k === "retain" && base.retain && typeof v === "object" && v !== null && !Array.isArray(v)) {
14670
+ combined[k] = {
14671
+ ...base.retain,
14672
+ ...v
14673
+ };
14648
14674
  } else if (k === "disposition" && base.disposition && typeof v === "object" && v !== null && !Array.isArray(v)) {
14649
14675
  combined[k] = {
14650
14676
  ...base.disposition,
@@ -15098,7 +15124,7 @@ function resolveAgentsDir(config) {
15098
15124
  if (override && override.length > 0 && override.startsWith("/")) {
15099
15125
  return override;
15100
15126
  }
15101
- return resolveDualPath(config.switchroom.agents_dir);
15127
+ return resolveDualPath(config.switchroom.agents_dir || "~/.switchroom/agents");
15102
15128
  }
15103
15129
  function resolvePath(pathStr) {
15104
15130
  return resolveDualPath(pathStr);
@@ -15155,6 +15181,374 @@ var init_users = __esm(() => {
15155
15181
  init_merge();
15156
15182
  });
15157
15183
 
15184
+ // src/agents/pane-lock.ts
15185
+ function withPaneLock(key, fn) {
15186
+ const tail = tails.get(key) ?? Promise.resolve();
15187
+ const run = tail.then(() => fn());
15188
+ const next = run.then(() => {}, () => {});
15189
+ tails.set(key, next);
15190
+ next.then(() => {
15191
+ if (tails.get(key) === next)
15192
+ tails.delete(key);
15193
+ });
15194
+ return run;
15195
+ }
15196
+ var tails;
15197
+ var init_pane_lock = __esm(() => {
15198
+ tails = new Map;
15199
+ });
15200
+
15201
+ // src/agents/inject.ts
15202
+ var exports_inject = {};
15203
+ __export(exports_inject, {
15204
+ validateInjectCommand: () => validateInjectCommand,
15205
+ normalizeInjectCommand: () => normalizeInjectCommand,
15206
+ makeTmuxRunner: () => makeTmuxRunner,
15207
+ isTuiChromeLine: () => isTuiChromeLine,
15208
+ injectSlashCommandWith: () => injectSlashCommandWith,
15209
+ injectSlashCommand: () => injectSlashCommand,
15210
+ diffPane: () => diffPane,
15211
+ dedupeInjectQueue: () => dedupeInjectQueue,
15212
+ InjectError: () => InjectError,
15213
+ INJECT_COMMANDS: () => INJECT_COMMANDS,
15214
+ INJECT_BLOCKLIST: () => INJECT_BLOCKLIST,
15215
+ INJECT_BLOCKED: () => INJECT_BLOCKED,
15216
+ INJECT_ALLOWLIST: () => INJECT_ALLOWLIST
15217
+ });
15218
+ import { execFileSync } from "node:child_process";
15219
+ function validateInjectCommand(command) {
15220
+ if (typeof command !== "string" || command.trim().length === 0) {
15221
+ throw new InjectError("invalid", "command is empty");
15222
+ }
15223
+ const trimmed = command.trim();
15224
+ if (!trimmed.startsWith("/")) {
15225
+ throw new InjectError("invalid", `command must start with "/": got ${trimmed}`);
15226
+ }
15227
+ const bare = trimmed.split(/\s+/, 1)[0].toLowerCase();
15228
+ const blockedMeta = INJECT_BLOCKED.get(bare);
15229
+ if (blockedMeta) {
15230
+ throw new InjectError("blocked", `${bare} is explicitly blocked from inject (${blockedMeta.reason}).`);
15231
+ }
15232
+ if (!INJECT_COMMANDS.has(bare)) {
15233
+ const allowed = [...INJECT_COMMANDS.keys()].sort().join(", ");
15234
+ throw new InjectError("not_allowed", `${bare} is not in the inject allowlist. Allowed: ${allowed}`);
15235
+ }
15236
+ return bare;
15237
+ }
15238
+ function normalizeInjectCommand(command) {
15239
+ return command.trim().replace(/\s+/g, " ").toLowerCase();
15240
+ }
15241
+ function dedupeInjectQueue(commands) {
15242
+ const seen = new Set;
15243
+ const out = [];
15244
+ for (const raw of commands) {
15245
+ if (typeof raw !== "string")
15246
+ continue;
15247
+ const key = normalizeInjectCommand(raw);
15248
+ if (key.length === 0)
15249
+ continue;
15250
+ if (seen.has(key))
15251
+ continue;
15252
+ seen.add(key);
15253
+ out.push(raw.trim());
15254
+ }
15255
+ return out;
15256
+ }
15257
+ function defaultSocketName(agentName) {
15258
+ return `switchroom-${agentName}`;
15259
+ }
15260
+ function makeTmuxRunner(tmuxBin) {
15261
+ return {
15262
+ capture(socket, session) {
15263
+ try {
15264
+ return execFileSync(tmuxBin, ["-L", socket, "capture-pane", "-p", "-t", session, "-S", "-200"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
15265
+ } catch {
15266
+ return null;
15267
+ }
15268
+ },
15269
+ send(socket, session, args) {
15270
+ const [subcmd, ...rest] = args;
15271
+ const flagEnd = rest.findIndex((a) => !a.startsWith("-"));
15272
+ const flagsBeforeKeys = flagEnd === -1 ? rest : rest.slice(0, flagEnd);
15273
+ const keys = flagEnd === -1 ? [] : rest.slice(flagEnd);
15274
+ execFileSync(tmuxBin, ["-L", socket, subcmd, ...flagsBeforeKeys, "-t", session, ...keys], { stdio: ["pipe", "pipe", "pipe"] });
15275
+ },
15276
+ hasSession(socket, session) {
15277
+ try {
15278
+ execFileSync(tmuxBin, ["-L", socket, "has-session", "-t", session], {
15279
+ stdio: ["pipe", "pipe", "pipe"]
15280
+ });
15281
+ return true;
15282
+ } catch {
15283
+ return false;
15284
+ }
15285
+ }
15286
+ };
15287
+ }
15288
+ function sleep(ms) {
15289
+ return new Promise((res) => setTimeout(res, ms));
15290
+ }
15291
+ function isTuiChromeLine(line) {
15292
+ const t = line.trim();
15293
+ if (t.length === 0)
15294
+ return false;
15295
+ if (/^[\s\-_\u2502\u2500\u256d\u256e\u2570\u256f\u250c\u2510\u2514\u2518|]+$/.test(t))
15296
+ return true;
15297
+ if (/^[\u256d\u256e\u2570\u256f\u2502\s]*[>)\u276f][\u256d\u256e\u2570\u256f\u2502\s]*$/.test(t))
15298
+ return true;
15299
+ if (/accept edits on/i.test(t))
15300
+ return true;
15301
+ if (/shift\+tab to cycle/i.test(t))
15302
+ return true;
15303
+ if (/for agents/i.test(t))
15304
+ return true;
15305
+ if (/\? for shortcuts/i.test(t))
15306
+ return true;
15307
+ if (/bypassing permissions/i.test(t))
15308
+ return true;
15309
+ if (t === "copy")
15310
+ return true;
15311
+ return false;
15312
+ }
15313
+ function diffPane(before, after, command) {
15314
+ if (command) {
15315
+ const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15316
+ const lines = after.split(`
15317
+ `);
15318
+ let anchorIdx = -1;
15319
+ for (let i = lines.length - 1;i >= 0; i--) {
15320
+ if (/[\u276f>]\s+/.test(lines[i]) && lines[i].includes(escaped)) {
15321
+ anchorIdx = i;
15322
+ break;
15323
+ }
15324
+ }
15325
+ if (anchorIdx >= 0) {
15326
+ const tail = lines.slice(anchorIdx + 1);
15327
+ const trimmed = [];
15328
+ for (const raw of tail) {
15329
+ const line = raw.trimEnd();
15330
+ if (line.length === 0 && trimmed.length === 0)
15331
+ continue;
15332
+ if (isTuiChromeLine(line))
15333
+ continue;
15334
+ trimmed.push(line);
15335
+ }
15336
+ while (trimmed.length > 0 && trimmed[trimmed.length - 1].length === 0) {
15337
+ trimmed.pop();
15338
+ }
15339
+ const affordances = /^(esc to cancel|press any key|\u21b5 select)/i;
15340
+ while (trimmed.length > 0 && affordances.test(trimmed[trimmed.length - 1].trim())) {
15341
+ trimmed.pop();
15342
+ }
15343
+ if (trimmed.length > 0) {
15344
+ return { output: trimmed.join(`
15345
+ `), anchored: true };
15346
+ }
15347
+ }
15348
+ }
15349
+ const beforeSet = new Set(before.split(`
15350
+ `).map((l) => l.trimEnd()));
15351
+ const newLines = [];
15352
+ for (const raw of after.split(`
15353
+ `)) {
15354
+ const line = raw.trimEnd();
15355
+ if (line.length === 0)
15356
+ continue;
15357
+ if (beforeSet.has(line))
15358
+ continue;
15359
+ if (isTuiChromeLine(line))
15360
+ continue;
15361
+ newLines.push(line);
15362
+ }
15363
+ return { output: newLines.join(`
15364
+ `), anchored: false };
15365
+ }
15366
+ async function injectSlashCommand(agentName, command, opts = {}) {
15367
+ validateInjectCommand(command);
15368
+ const tmuxBin = opts.tmuxBin ?? "tmux";
15369
+ const socket = opts.socketName ?? defaultSocketName(agentName);
15370
+ const session = opts.sessionName ?? agentName;
15371
+ const settleMs = opts.settleMs ?? 2000;
15372
+ const timeoutMs = opts.timeoutMs ?? 5000;
15373
+ return withPaneLock(`${socket}:${session}`, () => injectSlashCommandWith(makeTmuxRunner(tmuxBin), {
15374
+ socket,
15375
+ session,
15376
+ command: command.trim(),
15377
+ settleMs,
15378
+ timeoutMs
15379
+ }));
15380
+ }
15381
+ async function injectSlashCommandWith(runner, args) {
15382
+ const { socket, session, command, settleMs, timeoutMs } = args;
15383
+ let bareVerb;
15384
+ try {
15385
+ bareVerb = validateInjectCommand(command);
15386
+ } catch (err) {
15387
+ if (err instanceof InjectError) {
15388
+ return {
15389
+ outcome: "failed",
15390
+ output: "",
15391
+ truncated: false,
15392
+ command: command.trim().split(/\s+/, 1)[0]?.toLowerCase() ?? "",
15393
+ meta: null,
15394
+ errorCode: err.code,
15395
+ errorMessage: err.message
15396
+ };
15397
+ }
15398
+ throw err;
15399
+ }
15400
+ const meta = INJECT_COMMANDS.get(bareVerb) ?? null;
15401
+ if (!runner.hasSession(socket, session)) {
15402
+ return {
15403
+ outcome: "failed",
15404
+ output: "",
15405
+ truncated: false,
15406
+ command: bareVerb,
15407
+ meta,
15408
+ errorCode: "session_missing",
15409
+ errorMessage: `tmux session "${session}" on socket "${socket}" not found. ` + `Is the agent running under the tmux supervisor (the default)? ` + `If experimental.legacy_pty=true is set, inject is unsupported.`
15410
+ };
15411
+ }
15412
+ const before = runner.capture(socket, session) ?? "";
15413
+ try {
15414
+ runner.send(socket, session, ["send-keys", "-l", command]);
15415
+ runner.send(socket, session, ["send-keys", "Enter"]);
15416
+ } catch (err) {
15417
+ return {
15418
+ outcome: "failed",
15419
+ output: "",
15420
+ truncated: false,
15421
+ command: bareVerb,
15422
+ meta,
15423
+ errorCode: "tmux_failed",
15424
+ errorMessage: `tmux send-keys failed: ${err instanceof Error ? err.message : String(err)}`
15425
+ };
15426
+ }
15427
+ const start = Date.now();
15428
+ let last = before;
15429
+ let stableSince = null;
15430
+ while (Date.now() - start < timeoutMs) {
15431
+ await sleep(POLL_INTERVAL_MS);
15432
+ const cur = runner.capture(socket, session) ?? "";
15433
+ if (cur === last && cur !== before) {
15434
+ if (stableSince === null) {
15435
+ stableSince = Date.now();
15436
+ } else if (Date.now() - stableSince >= POLL_INTERVAL_MS) {
15437
+ last = cur;
15438
+ break;
15439
+ }
15440
+ } else {
15441
+ stableSince = null;
15442
+ }
15443
+ last = cur;
15444
+ if (Date.now() - start >= settleMs && cur !== before) {
15445
+ break;
15446
+ }
15447
+ }
15448
+ const { output: rawOutput, anchored } = diffPane(before, last, command);
15449
+ let output = rawOutput;
15450
+ let truncated = false;
15451
+ const bytes = Buffer.byteLength(output, "utf-8");
15452
+ if (bytes > OUTPUT_BYTE_CAP) {
15453
+ while (Buffer.byteLength(output, "utf-8") > OUTPUT_BYTE_CAP) {
15454
+ output = output.slice(Math.floor(output.length * 0.1) + 1);
15455
+ }
15456
+ truncated = true;
15457
+ }
15458
+ if (meta?.dialog) {
15459
+ try {
15460
+ runner.send(socket, session, ["send-keys", "Escape"]);
15461
+ } catch {}
15462
+ }
15463
+ if (output.trim().length === 0) {
15464
+ return {
15465
+ outcome: "ok_no_output",
15466
+ output: "",
15467
+ truncated: false,
15468
+ command: bareVerb,
15469
+ meta,
15470
+ ...anchored ? {} : { diagnostic: "anchor_missing" }
15471
+ };
15472
+ }
15473
+ return {
15474
+ outcome: "ok",
15475
+ output,
15476
+ truncated,
15477
+ command: bareVerb,
15478
+ meta,
15479
+ ...truncated ? { diagnostic: "truncated_output" } : {}
15480
+ };
15481
+ }
15482
+ var INJECT_COMMANDS, INJECT_ALLOWLIST, INJECT_BLOCKED, INJECT_BLOCKLIST, InjectError, POLL_INTERVAL_MS = 150, OUTPUT_BYTE_CAP = 3000;
15483
+ var init_inject = __esm(() => {
15484
+ init_pane_lock();
15485
+ INJECT_COMMANDS = new Map([
15486
+ ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true }],
15487
+ ["/status", { description: "Show session status", expectsOutput: true, dialog: true }],
15488
+ ["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true }],
15489
+ ["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true }],
15490
+ ["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true }],
15491
+ ["/model", { description: "Open model picker", expectsOutput: true }],
15492
+ [
15493
+ "/clear",
15494
+ {
15495
+ description: "Clear session screen",
15496
+ expectsOutput: false,
15497
+ silentNote: "context cleared \u2014 fresh slate"
15498
+ }
15499
+ ],
15500
+ [
15501
+ "/compact",
15502
+ {
15503
+ description: "Compact conversation history",
15504
+ expectsOutput: false,
15505
+ silentNote: "compaction runs silently"
15506
+ }
15507
+ ]
15508
+ ]);
15509
+ INJECT_ALLOWLIST = new Set(INJECT_COMMANDS.keys());
15510
+ INJECT_BLOCKED = new Map([
15511
+ ["/login", { reason: "would mutate auth state" }],
15512
+ ["/logout", { reason: "would terminate the agent's auth session" }],
15513
+ ["/exit", { reason: "would kill the agent process" }],
15514
+ ["/quit", { reason: "would kill the agent process" }],
15515
+ [
15516
+ "/effort",
15517
+ {
15518
+ reason: "leaves a blocking confirmation modal open and wedges the pane; use the /effort command (it drives the modal), not raw inject"
15519
+ }
15520
+ ]
15521
+ ]);
15522
+ INJECT_BLOCKLIST = new Set(INJECT_BLOCKED.keys());
15523
+ InjectError = class InjectError extends Error {
15524
+ code;
15525
+ constructor(code, message) {
15526
+ super(message);
15527
+ this.name = "InjectError";
15528
+ this.code = code;
15529
+ }
15530
+ };
15531
+ });
15532
+
15533
+ // src/agents/model-picker.ts
15534
+ var init_model_picker = __esm(() => {
15535
+ init_inject();
15536
+ init_pane_lock();
15537
+ });
15538
+
15539
+ // telegram-plugin/gateway/model-command.ts
15540
+ function isClaudeModel(name) {
15541
+ const lower = name.toLowerCase();
15542
+ if (MODEL_ALIASES.includes(lower))
15543
+ return true;
15544
+ return lower.startsWith("claude-");
15545
+ }
15546
+ var MODEL_ALIASES;
15547
+ var init_model_command = __esm(() => {
15548
+ init_model_picker();
15549
+ MODEL_ALIASES = ["opus", "sonnet", "haiku", "fable", "default"];
15550
+ });
15551
+
15158
15552
  // src/util/atomic.ts
15159
15553
  var exports_atomic = {};
15160
15554
  __export(exports_atomic, {
@@ -15223,7 +15617,7 @@ var init_atomic = __esm(() => {
15223
15617
  });
15224
15618
 
15225
15619
  // src/setup/hindsight.ts
15226
- import { execFileSync } from "node:child_process";
15620
+ import { execFileSync as execFileSync2 } from "node:child_process";
15227
15621
  import { createServer } from "node:net";
15228
15622
  function hindsightImageRef(tag) {
15229
15623
  const t = tag?.trim();
@@ -15249,7 +15643,7 @@ function describePortHolder(port) {
15249
15643
  ];
15250
15644
  for (const [cmd, args] of probes) {
15251
15645
  try {
15252
- const out = execFileSync(cmd, args, {
15646
+ const out = execFileSync2(cmd, args, {
15253
15647
  stdio: "pipe",
15254
15648
  encoding: "utf-8"
15255
15649
  }).trim();
@@ -15306,7 +15700,7 @@ async function pickHindsightPorts() {
15306
15700
  }
15307
15701
  function isDockerAvailable() {
15308
15702
  try {
15309
- execFileSync("docker", ["--version"], { stdio: "pipe" });
15703
+ execFileSync2("docker", ["--version"], { stdio: "pipe" });
15310
15704
  return true;
15311
15705
  } catch {
15312
15706
  return false;
@@ -15314,7 +15708,7 @@ function isDockerAvailable() {
15314
15708
  }
15315
15709
  function isHindsightRunning() {
15316
15710
  try {
15317
- const output = execFileSync("docker", ["ps", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
15711
+ const output = execFileSync2("docker", ["ps", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
15318
15712
  return output.trim().length > 0;
15319
15713
  } catch {
15320
15714
  return false;
@@ -15322,24 +15716,54 @@ function isHindsightRunning() {
15322
15716
  }
15323
15717
  function isHindsightContainerExists() {
15324
15718
  try {
15325
- const output = execFileSync("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf-8" });
15719
+ const output = execFileSync2("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf-8" });
15326
15720
  return output.trim().length > 0;
15327
15721
  } catch {
15328
15722
  return false;
15329
15723
  }
15330
15724
  }
15331
- function startHindsight(ports, litellm, imageTag) {
15725
+ function resolveHindsightPerOpLlm(llm) {
15726
+ const out = [];
15727
+ if (!llm)
15728
+ return out;
15729
+ for (const op of HINDSIGHT_LLM_OPS) {
15730
+ const cfg = llm[op];
15731
+ if (!cfg)
15732
+ continue;
15733
+ const prefix = `HINDSIGHT_API_${op.toUpperCase()}_LLM`;
15734
+ const model = cfg.model?.trim();
15735
+ const provider = cfg.provider?.trim();
15736
+ const baseUrl = cfg.base_url?.trim();
15737
+ const apiKey = cfg.api_key?.trim();
15738
+ if (model)
15739
+ out.push([`${prefix}_MODEL`, model]);
15740
+ if (provider)
15741
+ out.push([`${prefix}_PROVIDER`, provider]);
15742
+ if (baseUrl)
15743
+ out.push([`${prefix}_BASE_URL`, baseUrl]);
15744
+ if (apiKey)
15745
+ out.push([`${prefix}_API_KEY`, apiKey]);
15746
+ }
15747
+ return out;
15748
+ }
15749
+ function resolveHindsightLlm(llm, litellm) {
15750
+ const provider = llm?.provider?.trim() || "claude-code";
15751
+ const model = llm?.model?.trim() || (litellm ? litellm.model ?? HINDSIGHT_DEFAULT_LITELLM_MODEL : HINDSIGHT_DEFAULT_MODEL);
15752
+ return { provider, model };
15753
+ }
15754
+ function startHindsight(ports, litellm, imageTag, llm) {
15332
15755
  const apiPort = ports?.apiPort ?? HINDSIGHT_DEFAULT_API_PORT;
15333
15756
  const uiPort = ports?.uiPort ?? HINDSIGHT_DEFAULT_UI_PORT;
15757
+ const { provider: llmProvider, model: llmModel } = resolveHindsightLlm(llm, litellm);
15758
+ const perOpLlm = resolveHindsightPerOpLlm(llm);
15334
15759
  const envArgs = [
15335
15760
  "-e",
15336
15761
  `HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
15337
15762
  "-e",
15338
- "HINDSIGHT_API_LLM_PROVIDER=claude-code",
15339
- "-e",
15340
- `HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
15763
+ `HINDSIGHT_API_LLM_PROVIDER=${llmProvider}`,
15341
15764
  "-e",
15342
- `ANTHROPIC_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
15765
+ `HINDSIGHT_API_LLM_MODEL=${llmModel}`,
15766
+ ...perOpLlm.flatMap(([k, v]) => ["-e", `${k}=${v}`]),
15343
15767
  "-e",
15344
15768
  `HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
15345
15769
  "-e",
@@ -15361,8 +15785,13 @@ function startHindsight(ports, litellm, imageTag) {
15361
15785
  "-e",
15362
15786
  `HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`
15363
15787
  ];
15788
+ if (llmProvider === "claude-code") {
15789
+ envArgs.push("-e", `ANTHROPIC_MODEL=${llmModel}`);
15790
+ }
15364
15791
  if (litellm) {
15365
- envArgs.push("-e", `HINDSIGHT_API_PORT=${apiPort}`, "-e", `ANTHROPIC_BASE_URL=${litellm.baseUrl.replace(/\/+$/, "")}/anthropic`, "-e", `ANTHROPIC_CUSTOM_HEADERS=x-litellm-api-key: Bearer ${litellm.apiKey}
15792
+ const litellmRoot = litellm.baseUrl.replace(/\/+$/, "");
15793
+ const anthropicBaseUrl = isClaudeModel(llmModel) ? `${litellmRoot}/anthropic` : litellmRoot;
15794
+ envArgs.push("-e", `HINDSIGHT_API_PORT=${apiPort}`, "-e", `HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:${apiPort}`, "-e", `ANTHROPIC_BASE_URL=${anthropicBaseUrl}`, "-e", `ANTHROPIC_CUSTOM_HEADERS=x-litellm-api-key: Bearer ${litellm.apiKey}
15366
15795
  x-litellm-customer-id: hindsight
15367
15796
  x-litellm-tags: service:hindsight`);
15368
15797
  }
@@ -15394,25 +15823,25 @@ x-litellm-tags: service:hindsight`);
15394
15823
  args.push("-p", `127.0.0.1:${apiPort}:8888`, "-p", `127.0.0.1:${uiPort}:9999`);
15395
15824
  }
15396
15825
  args.push("-v", "switchroom-hindsight-data:/home/hindsight/.pg0", "-v", "switchroom-hindsight-backups:/backups", "-v", `${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`, "--tmpfs", `/run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`, ...envArgs, hindsightImageRef(imageTag));
15397
- execFileSync("docker", args, { stdio: "pipe" });
15826
+ execFileSync2("docker", args, { stdio: "pipe" });
15398
15827
  }
15399
15828
  function stopHindsight() {
15400
15829
  try {
15401
- execFileSync("docker", ["stop", "switchroom-hindsight"], { stdio: "pipe" });
15830
+ execFileSync2("docker", ["stop", "switchroom-hindsight"], { stdio: "pipe" });
15402
15831
  } catch {}
15403
15832
  try {
15404
- execFileSync("docker", ["rm", "switchroom-hindsight"], { stdio: "pipe" });
15833
+ execFileSync2("docker", ["rm", "switchroom-hindsight"], { stdio: "pipe" });
15405
15834
  } catch {}
15406
15835
  }
15407
15836
  function pullHindsightImage(imageTag) {
15408
- execFileSync("docker", ["pull", hindsightImageRef(imageTag)], { stdio: "inherit" });
15837
+ execFileSync2("docker", ["pull", hindsightImageRef(imageTag)], { stdio: "inherit" });
15409
15838
  }
15410
15839
  function getRunningHindsightPorts() {
15411
15840
  const CONTAINER_API_PORT = 8888;
15412
15841
  const CONTAINER_UI_PORT = 9999;
15413
15842
  const readPort = (containerPort) => {
15414
15843
  try {
15415
- const out = execFileSync("docker", ["port", "switchroom-hindsight", `${containerPort}/tcp`], { stdio: "pipe", encoding: "utf-8" });
15844
+ const out = execFileSync2("docker", ["port", "switchroom-hindsight", `${containerPort}/tcp`], { stdio: "pipe", encoding: "utf-8" });
15416
15845
  const m = out.split(`
15417
15846
  `).map((l) => l.trim()).find((l) => l.length > 0);
15418
15847
  if (!m)
@@ -15433,7 +15862,7 @@ function getRunningHindsightPorts() {
15433
15862
  function getHindsightPortsFromEnv() {
15434
15863
  let env2;
15435
15864
  try {
15436
- env2 = execFileSync("docker", ["inspect", "--format", "{{.Config.Env}}", "switchroom-hindsight"], { stdio: "pipe", encoding: "utf-8" });
15865
+ env2 = execFileSync2("docker", ["inspect", "--format", "{{.Config.Env}}", "switchroom-hindsight"], { stdio: "pipe", encoding: "utf-8" });
15437
15866
  } catch {
15438
15867
  return null;
15439
15868
  }
@@ -15453,14 +15882,24 @@ function getHindsightPortsFromEnv() {
15453
15882
  }
15454
15883
  function getHindsightStatus() {
15455
15884
  try {
15456
- const output = execFileSync("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
15885
+ const output = execFileSync2("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
15457
15886
  const status = output.trim();
15458
15887
  return status.length > 0 ? status : null;
15459
15888
  } catch {
15460
15889
  return null;
15461
15890
  }
15462
15891
  }
15463
- function generateHindsightComposeSnippet() {
15892
+ function generateHindsightComposeSnippet(llm) {
15893
+ const { provider: llmProvider, model: llmModel } = resolveHindsightLlm(llm);
15894
+ const perOpLlm = resolveHindsightPerOpLlm(llm);
15895
+ const environment = [
15896
+ ` - HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
15897
+ ` - HINDSIGHT_API_LLM_PROVIDER=${llmProvider}`,
15898
+ ` - HINDSIGHT_API_LLM_MODEL=${llmModel}`,
15899
+ ...perOpLlm.map(([k, v]) => ` - ${k}=${v}`),
15900
+ ...llmProvider === "claude-code" ? [` - ANTHROPIC_MODEL=${llmModel}`] : [],
15901
+ ` - HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888`
15902
+ ];
15464
15903
  return [
15465
15904
  "services:",
15466
15905
  " switchroom-hindsight:",
@@ -15470,10 +15909,7 @@ function generateHindsightComposeSnippet() {
15470
15909
  ` - "127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}:8888"`,
15471
15910
  ' - "127.0.0.1:19999:9999"',
15472
15911
  " environment:",
15473
- ` - HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
15474
- " - HINDSIGHT_API_LLM_PROVIDER=claude-code",
15475
- ` - HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
15476
- ` - ANTHROPIC_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
15912
+ ...environment,
15477
15913
  ` - HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
15478
15914
  ` - HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING=${HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING}`,
15479
15915
  ` - HINDSIGHT_API_RERANKER_MAX_CANDIDATES=${HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES}`,
@@ -15564,13 +16000,15 @@ async function ensureHindsightConsumer(configPath, account, uid = HINDSIGHT_DEFA
15564
16000
  atomicWriteFileSync2(configPath, tail, mode);
15565
16001
  return { added: true, reason: "added" };
15566
16002
  }
15567
- var HINDSIGHT_DEFAULT_API_PORT = 18888, HINDSIGHT_DEFAULT_UI_PORT = 9999, HINDSIGHT_DEFAULT_MCP_URL, HINDSIGHT_DEFAULT_API_BASE_URL, HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000, HINDSIGHT_CONSUMER_NAME = "hindsight", HINDSIGHT_DEFAULT_UID = 11000, HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight", HINDSIGHT_IMAGE, HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-5", HINDSIGHT_DEFAULT_MCP_STATELESS = true, HINDSIGHT_BROKER_SOCK_VOLUME, HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true", HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150, HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8, HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 12, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 2, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 100, HINDSIGHT_DEFAULT_MEM_LIMIT = "8g", HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g", HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000, HINDSIGHT_DEFAULT_SHM_SIZE = "2g", HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)', HINDSIGHT_HEALTHCHECK_CMD;
16003
+ var HINDSIGHT_DEFAULT_API_PORT = 18888, HINDSIGHT_DEFAULT_UI_PORT = 9999, HINDSIGHT_DEFAULT_MCP_URL, HINDSIGHT_DEFAULT_API_BASE_URL, HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000, HINDSIGHT_CONSUMER_NAME = "hindsight", HINDSIGHT_DEFAULT_UID = 11000, HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight", HINDSIGHT_IMAGE, HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-5", HINDSIGHT_DEFAULT_LITELLM_MODEL = "openrouter/google/gemini-3.1-flash-lite", HINDSIGHT_DEFAULT_MCP_STATELESS = true, HINDSIGHT_BROKER_SOCK_VOLUME, HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true", HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150, HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8, HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 12, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 2, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 100, HINDSIGHT_DEFAULT_MEM_LIMIT = "8g", HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g", HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000, HINDSIGHT_DEFAULT_SHM_SIZE = "2g", HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)', HINDSIGHT_HEALTHCHECK_CMD, HINDSIGHT_LLM_OPS;
15568
16004
  var init_hindsight = __esm(() => {
16005
+ init_model_command();
15569
16006
  HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
15570
16007
  HINDSIGHT_DEFAULT_API_BASE_URL = HINDSIGHT_DEFAULT_MCP_URL.replace(/\/mcp\/?$/, "");
15571
16008
  HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
15572
16009
  HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
15573
16010
  HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
16011
+ HINDSIGHT_LLM_OPS = ["retain", "reflect", "consolidation"];
15574
16012
  });
15575
16013
 
15576
16014
  // src/memory/hindsight.ts
@@ -23797,7 +24235,7 @@ function parseSocketIdentity(socketPath, spec) {
23797
24235
  }
23798
24236
 
23799
24237
  // src/vault/broker/peercred.ts
23800
- import { execFileSync as execFileSync2 } from "node:child_process";
24238
+ import { execFileSync as execFileSync3 } from "node:child_process";
23801
24239
  import { readFileSync as readFileSync10, readlinkSync as readlinkSync3, fstatSync } from "node:fs";
23802
24240
  function socketPathToIdentity(socketPath) {
23803
24241
  return parseSocketIdentity(socketPath, {
@@ -23970,7 +24408,7 @@ function identify(socketPath, socket, execFileSyncOverride) {
23970
24408
  if (process.platform !== "linux") {
23971
24409
  return null;
23972
24410
  }
23973
- const runner = execFileSyncOverride ?? execFileSync2;
24411
+ const runner = execFileSyncOverride ?? execFileSync3;
23974
24412
  let pid = null;
23975
24413
  if (socket !== undefined) {
23976
24414
  const fd = fdFromSocket(socket);
@@ -24454,7 +24892,8 @@ function brokerIsComposeManaged() {
24454
24892
  return fs.existsSync(OPERATOR_SOCKET_PATH);
24455
24893
  }
24456
24894
  function vaultTokenFilePath(agentSlug) {
24457
- return join6(homedir4(), ".switchroom", "agents", agentSlug, ".vault-token");
24895
+ const base = process.env.SWITCHROOM_AGENTS_DIR || join6(homedir4(), ".switchroom", "agents");
24896
+ return join6(base, agentSlug, ".vault-token");
24458
24897
  }
24459
24898
  function readVaultTokenFile(agentSlug) {
24460
24899
  const filePath = vaultTokenFilePath(agentSlug);
@@ -25263,7 +25702,7 @@ var init_onboarding = __esm(() => {
25263
25702
  });
25264
25703
 
25265
25704
  // src/repos/bare-clone.ts
25266
- import { execFileSync as execFileSync3 } from "node:child_process";
25705
+ import { execFileSync as execFileSync4 } from "node:child_process";
25267
25706
  import { existsSync as existsSync13, mkdirSync as mkdirSync8 } from "node:fs";
25268
25707
  import { resolve as resolve9 } from "node:path";
25269
25708
  function bareClonePath(slug) {
@@ -25276,7 +25715,7 @@ function ensureBareClone(slug, url) {
25276
25715
  if (!existsSync13(clonePath)) {
25277
25716
  process.stderr.write(`[switchroom] repo "${slug}": cloning ${url} \u2026
25278
25717
  `);
25279
- execFileSync3("git", ["clone", "--bare", url, clonePath], {
25718
+ execFileSync4("git", ["clone", "--bare", url, clonePath], {
25280
25719
  stdio: ["ignore", "pipe", "pipe"]
25281
25720
  });
25282
25721
  process.stderr.write(`[switchroom] repo "${slug}": bare clone ready at ${clonePath}
@@ -25285,7 +25724,7 @@ function ensureBareClone(slug, url) {
25285
25724
  process.stderr.write(`[switchroom] repo "${slug}": fetching ${clonePath} \u2026
25286
25725
  `);
25287
25726
  try {
25288
- execFileSync3("git", ["fetch", "--all"], {
25727
+ execFileSync4("git", ["fetch", "--all"], {
25289
25728
  cwd: clonePath,
25290
25729
  stdio: ["ignore", "pipe", "pipe"]
25291
25730
  });
@@ -25302,7 +25741,7 @@ var init_bare_clone = __esm(() => {
25302
25741
  });
25303
25742
 
25304
25743
  // src/repos/agent-worktree.ts
25305
- import { execFileSync as execFileSync4 } from "node:child_process";
25744
+ import { execFileSync as execFileSync5 } from "node:child_process";
25306
25745
  import { existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync5 } from "node:fs";
25307
25746
  import { join as join9, resolve as resolve10 } from "node:path";
25308
25747
  function agentBranchName(agentName) {
@@ -25313,7 +25752,7 @@ function agentWorktreePath(agentDir, slug) {
25313
25752
  }
25314
25753
  function isWorktreeDirty(worktreePath) {
25315
25754
  try {
25316
- const out = execFileSync4("git", ["status", "--porcelain"], {
25755
+ const out = execFileSync5("git", ["status", "--porcelain"], {
25317
25756
  cwd: worktreePath,
25318
25757
  stdio: ["ignore", "pipe", "pipe"],
25319
25758
  encoding: "utf-8"
@@ -25325,7 +25764,7 @@ function isWorktreeDirty(worktreePath) {
25325
25764
  }
25326
25765
  function headShortSha(worktreePath) {
25327
25766
  try {
25328
- return execFileSync4("git", ["rev-parse", "--short", "HEAD"], {
25767
+ return execFileSync5("git", ["rev-parse", "--short", "HEAD"], {
25329
25768
  cwd: worktreePath,
25330
25769
  stdio: ["ignore", "pipe", "pipe"],
25331
25770
  encoding: "utf-8"
@@ -25336,7 +25775,7 @@ function headShortSha(worktreePath) {
25336
25775
  }
25337
25776
  function branchExistsInBare(bareClonePath2, branchName) {
25338
25777
  try {
25339
- execFileSync4("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], {
25778
+ execFileSync5("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], {
25340
25779
  cwd: bareClonePath2,
25341
25780
  stdio: ["ignore", "pipe", "pipe"]
25342
25781
  });
@@ -25347,7 +25786,7 @@ function branchExistsInBare(bareClonePath2, branchName) {
25347
25786
  }
25348
25787
  function resolveDefaultBranch(bareClonePath2) {
25349
25788
  try {
25350
- const out = execFileSync4("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
25789
+ const out = execFileSync5("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
25351
25790
  cwd: bareClonePath2,
25352
25791
  stdio: ["ignore", "pipe", "pipe"],
25353
25792
  encoding: "utf-8"
@@ -25365,12 +25804,12 @@ function ensureAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
25365
25804
  if (!existsSync14(worktreePath)) {
25366
25805
  mkdirSync9(join9(agentDir, "work"), { recursive: true });
25367
25806
  if (branchExistsInBare(bareClonePath2, branch)) {
25368
- execFileSync4("git", ["worktree", "add", worktreePath, branch], {
25807
+ execFileSync5("git", ["worktree", "add", worktreePath, branch], {
25369
25808
  cwd: bareClonePath2,
25370
25809
  stdio: ["ignore", "pipe", "pipe"]
25371
25810
  });
25372
25811
  } else {
25373
- execFileSync4("git", [
25812
+ execFileSync5("git", [
25374
25813
  "worktree",
25375
25814
  "add",
25376
25815
  worktreePath,
@@ -25398,11 +25837,11 @@ function ensureAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
25398
25837
  };
25399
25838
  }
25400
25839
  try {
25401
- execFileSync4("git", ["fetch", "origin"], {
25840
+ execFileSync5("git", ["fetch", "origin"], {
25402
25841
  cwd: worktreePath,
25403
25842
  stdio: ["ignore", "pipe", "pipe"]
25404
25843
  });
25405
- execFileSync4("git", ["merge", "--ff-only", `origin/${defaultBranch}`], {
25844
+ execFileSync5("git", ["merge", "--ff-only", `origin/${defaultBranch}`], {
25406
25845
  cwd: worktreePath,
25407
25846
  stdio: ["ignore", "pipe", "pipe"]
25408
25847
  });
@@ -25420,7 +25859,7 @@ function removeAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
25420
25859
  const branch = agentBranchName(agentName);
25421
25860
  if (existsSync14(worktreePath)) {
25422
25861
  try {
25423
- execFileSync4("git", ["worktree", "remove", "--force", worktreePath], {
25862
+ execFileSync5("git", ["worktree", "remove", "--force", worktreePath], {
25424
25863
  cwd: bareClonePath2,
25425
25864
  stdio: ["ignore", "pipe", "pipe"]
25426
25865
  });
@@ -25432,7 +25871,7 @@ function removeAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
25432
25871
  }
25433
25872
  if (branchExistsInBare(bareClonePath2, branch)) {
25434
25873
  try {
25435
- execFileSync4("git", ["branch", "-D", branch], {
25874
+ execFileSync5("git", ["branch", "-D", branch], {
25436
25875
  cwd: bareClonePath2,
25437
25876
  stdio: ["ignore", "pipe", "pipe"]
25438
25877
  });
@@ -25443,7 +25882,7 @@ function removeAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
25443
25882
  }
25444
25883
  }
25445
25884
  try {
25446
- execFileSync4("git", ["worktree", "prune"], {
25885
+ execFileSync5("git", ["worktree", "prune"], {
25447
25886
  cwd: bareClonePath2,
25448
25887
  stdio: ["ignore", "pipe", "pipe"]
25449
25888
  });
@@ -25472,7 +25911,7 @@ import {
25472
25911
  unlinkSync as unlinkSync4
25473
25912
  } from "node:fs";
25474
25913
  import { homedir as homedir5 } from "node:os";
25475
- import { execSync, execFileSync as execFileSync5 } from "node:child_process";
25914
+ import { execSync, execFileSync as execFileSync6 } from "node:child_process";
25476
25915
  import { join as join10, resolve as resolve11 } from "node:path";
25477
25916
  import { createHash as createHash2 } from "node:crypto";
25478
25917
  function prependReplyDiscipline(rendered, context) {
@@ -25606,13 +26045,13 @@ function alignAgentUid(name, agentDir, uid, opts = {}) {
25606
26045
  `));
25607
26046
  }
25608
26047
  try {
25609
- execFileSync5("chown", ["-R", `${uid}:${uid}`, ...paths], {
26048
+ execFileSync6("chown", ["-R", `${uid}:${uid}`, ...paths], {
25610
26049
  stdio: ["ignore", "ignore", "pipe"]
25611
26050
  });
25612
26051
  return { chowned: true, paths };
25613
26052
  } catch {}
25614
26053
  try {
25615
- execFileSync5("sudo", ["chown", "-R", `${uid}:${uid}`, ...paths], {
26054
+ execFileSync6("sudo", ["chown", "-R", `${uid}:${uid}`, ...paths], {
25616
26055
  stdio: "inherit"
25617
26056
  });
25618
26057
  return { chowned: true, paths };
@@ -26260,7 +26699,7 @@ Thumbs.db
26260
26699
  execSync("git add -A", { cwd: workspaceDir, stdio: "pipe" });
26261
26700
  const userEmail = process.env.GIT_AUTHOR_EMAIL || "switchroom@localhost";
26262
26701
  const userName = process.env.GIT_AUTHOR_NAME || "Switchroom Agent";
26263
- execFileSync5("git", [
26702
+ execFileSync6("git", [
26264
26703
  "-c",
26265
26704
  `user.email=${userEmail}`,
26266
26705
  "-c",
@@ -26298,11 +26737,12 @@ function installHindsightPlugin(agentName, agentDir, switchroomConfig) {
26298
26737
  }
26299
26738
  const destPath = join10(agentDir, ".claude", "plugins", "hindsight-memory");
26300
26739
  const additionalBanks = resolveUsers(switchroomConfig, agentName).additionalBanks;
26740
+ const retainConfig = resolveHindsightRetainConfig(switchroomConfig, agentName);
26301
26741
  let expectedSettings = null;
26302
26742
  try {
26303
26743
  const vendorSettingsPath = join10(sourcePath, "settings.json");
26304
26744
  if (existsSync15(vendorSettingsPath)) {
26305
- expectedSettings = renderHindsightSettingsOverrides(readFileSync13(vendorSettingsPath, "utf-8"), additionalBanks);
26745
+ expectedSettings = renderHindsightSettingsOverrides(readFileSync13(vendorSettingsPath, "utf-8"), additionalBanks, retainConfig);
26306
26746
  }
26307
26747
  } catch {}
26308
26748
  const upToDate = dirContentEquals(sourcePath, destPath, expectedSettings != null ? { "settings.json": expectedSettings } : undefined);
@@ -26312,13 +26752,21 @@ function installHindsightPlugin(agentName, agentDir, switchroomConfig) {
26312
26752
  }
26313
26753
  copyDirRecursive2(sourcePath, destPath);
26314
26754
  }
26315
- applyHindsightSettingsOverrides(destPath, additionalBanks);
26755
+ applyHindsightSettingsOverrides(destPath, additionalBanks, retainConfig);
26316
26756
  const bankId = agentMemory?.collection ?? agentName;
26317
26757
  const mcpUrl = memory.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
26318
26758
  const apiBaseUrl = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
26319
26759
  return { pluginDir: destPath, apiBaseUrl, bankId };
26320
26760
  }
26321
- function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks) {
26761
+ function resolveHindsightRetainConfig(switchroomConfig, agentName) {
26762
+ const resolved = switchroomConfig ? resolveAgentConfig(switchroomConfig.defaults, switchroomConfig.profiles, switchroomConfig.agents[agentName] ?? {}) : undefined;
26763
+ const retain = resolved?.memory?.retain;
26764
+ return {
26765
+ everyNTurns: retain?.every_n_turns ?? HINDSIGHT_DEFAULT_RETAIN_EVERY_N_TURNS,
26766
+ overlapTurns: retain?.overlap_turns ?? HINDSIGHT_DEFAULT_RETAIN_OVERLAP_TURNS
26767
+ };
26768
+ }
26769
+ function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks, retainConfig) {
26322
26770
  const settingsPath = join10(pluginDestPath, "settings.json");
26323
26771
  if (!existsSync15(settingsPath))
26324
26772
  return;
@@ -26328,20 +26776,21 @@ function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks) {
26328
26776
  } catch {
26329
26777
  return;
26330
26778
  }
26331
- const next = renderHindsightSettingsOverrides(raw, additionalBanks);
26779
+ const next = renderHindsightSettingsOverrides(raw, additionalBanks, retainConfig);
26332
26780
  if (next == null)
26333
26781
  return;
26334
26782
  writeFileSyncIfChanged(settingsPath, next);
26335
26783
  }
26336
- function renderHindsightSettingsOverrides(raw, additionalBanks) {
26784
+ function renderHindsightSettingsOverrides(raw, additionalBanks, retainConfig) {
26337
26785
  let settings;
26338
26786
  try {
26339
26787
  settings = JSON.parse(raw);
26340
26788
  } catch {
26341
26789
  return null;
26342
26790
  }
26343
- settings.retainEveryNTurns = 1;
26791
+ settings.retainEveryNTurns = retainConfig.everyNTurns;
26344
26792
  settings.retainMode = "chunked";
26793
+ settings.retainOverlapTurns = retainConfig.overlapTurns;
26345
26794
  settings.recallMaxMemories = 8;
26346
26795
  settings.recallMinOverlap = 0.1;
26347
26796
  settings.recallTypes = ["world", "experience", "observation"];
@@ -26353,17 +26802,25 @@ function renderHindsightSettingsOverrides(raw, additionalBanks) {
26353
26802
  return JSON.stringify(settings, null, 2) + `
26354
26803
  `;
26355
26804
  }
26805
+ function resolvePermissionDefaultMode(agentConfig, switchroomConfig) {
26806
+ const raw = agentConfig.permission_mode ?? switchroomConfig?.defaults?.permission_mode ?? SWITCHROOM_DEFAULT_PERMISSION_MODE;
26807
+ if (VALID_SETTINGS_DEFAULT_MODES.includes(raw)) {
26808
+ return raw;
26809
+ }
26810
+ console.warn(source_default.yellow(`[switchroom] permission_mode "${raw}" has no settings.json defaultMode ` + `equivalent \u2014 using "${SWITCHROOM_DEFAULT_PERMISSION_MODE}" for ` + `permissions.defaultMode (the --permission-mode CLI flag still carries "${raw}").`));
26811
+ return SWITCHROOM_DEFAULT_PERMISSION_MODE;
26812
+ }
26356
26813
  function buildWorkspaceContext(args) {
26357
26814
  const {
26358
26815
  name,
26359
26816
  agentDir,
26360
26817
  agentConfig,
26361
26818
  telegramConfig,
26819
+ switchroomConfig,
26362
26820
  switchroomConfigPath,
26363
26821
  topicId,
26364
26822
  tools,
26365
26823
  permissionAllow,
26366
- hasAllWildcard,
26367
26824
  resolvedBotToken,
26368
26825
  rawBotToken,
26369
26826
  hindsightAutoRecallEnabled,
@@ -26396,7 +26853,7 @@ function buildWorkspaceContext(args) {
26396
26853
  ...INTERACTIVE_TUI_FLEET_DENY_TOOLS
26397
26854
  ]),
26398
26855
  permissionAllow,
26399
- defaultModeAcceptEdits: hasAllWildcard,
26856
+ defaultMode: resolvePermissionDefaultMode(agentConfig, switchroomConfig),
26400
26857
  memory: agentConfig.memory,
26401
26858
  model: agentConfig.model,
26402
26859
  mcpServers: agentConfig.mcp_servers ? filterMcpServers(agentConfig.mcp_servers) : agentConfig.mcp_servers,
@@ -27312,7 +27769,7 @@ function buildSettingsHooksBlock(p) {
27312
27769
  hooks: [
27313
27770
  {
27314
27771
  type: "command",
27315
- command: wrap("hook:workspace-dynamic", `${useInjectOnChange ? `env SWITCHROOM_INJECT_ON_CHANGE=1 ` : ""}bash "${join10(DOCKER_BIN_PATH, "workspace-dynamic-hook.sh")}"`),
27772
+ command: wrap("hook:workspace-dynamic", `${useInjectOnChange ? "" : `env SWITCHROOM_INJECT_ON_CHANGE=0 `}bash "${join10(DOCKER_BIN_PATH, "workspace-dynamic-hook.sh")}"`),
27316
27773
  timeout: 5
27317
27774
  }
27318
27775
  ]
@@ -27662,11 +28119,7 @@ ${baseAppend}` : TELEGRAM_FORMATTING_FLOOR_CARD;
27662
28119
  settings.permissions = settings.permissions ?? {};
27663
28120
  settings.permissions.allow = desiredAllow;
27664
28121
  settings.permissions.deny = desiredDeny;
27665
- if (hasAllWildcard) {
27666
- settings.permissions.defaultMode = "acceptEdits";
27667
- } else {
27668
- delete settings.permissions.defaultMode;
27669
- }
28122
+ settings.permissions.defaultMode = resolvePermissionDefaultMode(agentConfig, switchroomConfig);
27670
28123
  const mcpServers = {};
27671
28124
  const hindsightEntry = getHindsightSettingsEntry(name, switchroomConfig);
27672
28125
  if (hindsightEntry) {
@@ -28470,7 +28923,7 @@ in doubt, shorter and plainer wins.
28470
28923
 
28471
28924
  Every turn that answers a user message ends with a user-visible \`reply\`
28472
28925
  \u2014 Telegram is all the user sees; your terminal output
28473
- never reaches them.`, SWITCHROOM_TELEGRAM_MCP_TOOLS, HINDSIGHT_MCP_TOOLS, AGENT_CONFIG_MCP_TOOLS, HOSTD_MCP_TOOLS, WEBKITE_MCP_TOOLS, LEGACY_SWITCHROOM_MCP_TOKENS, LEGACY_HOSTD_BLANKET_TOKENS, DEFAULT_READ_ONLY_PREAPPROVED_TOOLS, WEBKITE_FLEET_DENY_TOOLS, INTERACTIVE_TUI_FLEET_DENY_TOOLS, WEBKITE_BINARY_CONTAINER_PATH = "/usr/local/bin/webkite", SWITCHROOM_DEFAULT_MAIN_MODEL = "claude-sonnet-5", SWITCHROOM_DEFAULT_THINKING_EFFORT = "low", SWITCHROOM_OWNED_SETTINGS_KEYS, ALL_BUILTIN_TOOLS, DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin", DOCKER_HOOKS_PATH, DOCKER_BUNDLED_HOOKS_PATH = "/opt/switchroom/hooks", DOCKER_BIN_PATH = "/opt/switchroom/bin", DOCKER_SECURITY_PLUGIN_PATH = "/opt/switchroom/security-plugin", DOCKER_CONFIG_PATH = "/state/config/switchroom.yaml", DOCKER_SWITCHROOM_CLI_PATH = "/usr/local/bin/switchroom", DOCKER_AUTH_BROKER_SOCKET = "/run/switchroom/auth-broker/sock", DOCKER_VAULT_BROKER_SOCKET = "/run/switchroom/broker/sock", DOCKER_AGENT_HOME = "/state/agent/home", INTEGRATION_MCP_RESOLVERS;
28926
+ never reaches them.`, SWITCHROOM_TELEGRAM_MCP_TOOLS, HINDSIGHT_MCP_TOOLS, AGENT_CONFIG_MCP_TOOLS, HOSTD_MCP_TOOLS, WEBKITE_MCP_TOOLS, LEGACY_SWITCHROOM_MCP_TOKENS, LEGACY_HOSTD_BLANKET_TOKENS, DEFAULT_READ_ONLY_PREAPPROVED_TOOLS, WEBKITE_FLEET_DENY_TOOLS, INTERACTIVE_TUI_FLEET_DENY_TOOLS, WEBKITE_BINARY_CONTAINER_PATH = "/usr/local/bin/webkite", SWITCHROOM_DEFAULT_MAIN_MODEL = "claude-sonnet-5", SWITCHROOM_DEFAULT_THINKING_EFFORT = "low", SWITCHROOM_OWNED_SETTINGS_KEYS, ALL_BUILTIN_TOOLS, HINDSIGHT_DEFAULT_RETAIN_EVERY_N_TURNS = 3, HINDSIGHT_DEFAULT_RETAIN_OVERLAP_TURNS = 1, VALID_SETTINGS_DEFAULT_MODES, SWITCHROOM_DEFAULT_PERMISSION_MODE = "acceptEdits", DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin", DOCKER_HOOKS_PATH, DOCKER_BUNDLED_HOOKS_PATH = "/opt/switchroom/hooks", DOCKER_BIN_PATH = "/opt/switchroom/bin", DOCKER_SECURITY_PLUGIN_PATH = "/opt/switchroom/security-plugin", DOCKER_CONFIG_PATH = "/state/config/switchroom.yaml", DOCKER_SWITCHROOM_CLI_PATH = "/usr/local/bin/switchroom", DOCKER_AUTH_BROKER_SOCKET = "/run/switchroom/auth-broker/sock", DOCKER_VAULT_BROKER_SOCKET = "/run/switchroom/broker/sock", DOCKER_AGENT_HOME = "/state/agent/home", INTEGRATION_MCP_RESOLVERS;
28474
28927
  var init_scaffold = __esm(() => {
28475
28928
  init_source();
28476
28929
  init_cron_unit_name();
@@ -28585,6 +29038,12 @@ var init_scaffold = __esm(() => {
28585
29038
  "Agent",
28586
29039
  "ExitPlanMode"
28587
29040
  ];
29041
+ VALID_SETTINGS_DEFAULT_MODES = [
29042
+ "default",
29043
+ "acceptEdits",
29044
+ "plan",
29045
+ "bypassPermissions"
29046
+ ];
28588
29047
  DOCKER_HOOKS_PATH = `${DOCKER_TELEGRAM_PLUGIN_PATH}/hooks`;
28589
29048
  INTEGRATION_MCP_RESOLVERS = [
28590
29049
  {
@@ -28829,6 +29288,7 @@ function describeAgents(config, litellmConfirmedAgents) {
28829
29288
  baseUrl: resolved.litellm?.base_url ?? config.litellm?.base_url,
28830
29289
  smallFastModel: resolved.litellm?.small_fast_model ?? config.litellm?.small_fast_model ?? "claude-haiku-4-5-20251001"
28831
29290
  },
29291
+ model: resolveMainModel(resolved.model),
28832
29292
  timezone: resolveTimezone(config, resolved, {
28833
29293
  onUtcFallback: () => {
28834
29294
  console.warn(` \u26a0 timezone: no explicit timezone set and server detection resolved to UTC ` + `for agent "${name}" \u2014 cron schedules and the per-turn time hint will ` + `run in UTC. Add \`switchroom.timezone: "Region/City"\` (e.g. ` + `"Australia/Melbourne") to switchroom.yaml to silence this warning.`);
@@ -29261,7 +29721,7 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
29261
29721
  const root = a.litellm.baseUrl.replace(/\/+$/, "");
29262
29722
  env2.SWITCHROOM_LITELLM = "1";
29263
29723
  env2.SWITCHROOM_LITELLM_BASE = root;
29264
- env2.ANTHROPIC_BASE_URL = `${root}/anthropic`;
29724
+ env2.ANTHROPIC_BASE_URL = isClaudeModel(a.model) ? `${root}/anthropic` : root;
29265
29725
  env2.ANTHROPIC_SMALL_FAST_MODEL = a.litellm.smallFastModel;
29266
29726
  }
29267
29727
  env2.SWITCHROOM_VOICE_ENGINE = voiceEngine;
@@ -29359,6 +29819,8 @@ var init_compose = __esm(() => {
29359
29819
  init_schema();
29360
29820
  init_cron_routing();
29361
29821
  init_tier_selector();
29822
+ init_scaffold();
29823
+ init_model_command();
29362
29824
  init_merge();
29363
29825
  init_timezone();
29364
29826
  init_peercred();
@@ -29686,7 +30148,7 @@ var init_write_compose = __esm(() => {
29686
30148
  });
29687
30149
 
29688
30150
  // src/agents/tmux.ts
29689
- import { execFileSync as execFileSync6 } from "node:child_process";
30151
+ import { execFileSync as execFileSync7 } from "node:child_process";
29690
30152
  function sendAgentInterrupt(opts) {
29691
30153
  const { agentName } = opts;
29692
30154
  const attempts = typeof opts.attempts === "number" && opts.attempts > 0 ? opts.attempts : 1;
@@ -29696,7 +30158,7 @@ function sendAgentInterrupt(opts) {
29696
30158
  let lastError = null;
29697
30159
  for (let i = 0;i < attempts; i++) {
29698
30160
  try {
29699
- execFileSync6("tmux", args, {
30161
+ execFileSync7("tmux", args, {
29700
30162
  timeout: 3000,
29701
30163
  stdio: ["ignore", "pipe", "pipe"]
29702
30164
  });
@@ -29737,7 +30199,7 @@ var init_compose_env = () => {};
29737
30199
  import { resolve as resolve13 } from "node:path";
29738
30200
  import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync6 } from "node:fs";
29739
30201
  import { homedir as homedir7 } from "node:os";
29740
- import { execFileSync as execFileSync7 } from "node:child_process";
30202
+ import { execFileSync as execFileSync8 } from "node:child_process";
29741
30203
  function resolveSwitchroomHome(explicit) {
29742
30204
  if (explicit && explicit.length > 0)
29743
30205
  return explicit;
@@ -29768,7 +30230,7 @@ function bringUpAgentService(opts) {
29768
30230
  const dockerBin = opts.dockerBin ?? "docker";
29769
30231
  const stdio = opts.stdio ?? "inherit";
29770
30232
  for (const svc of ["vault-broker", "approval-kernel", "switchroom-auth-broker"]) {
29771
- execFileSync7(dockerBin, [
30233
+ execFileSync8(dockerBin, [
29772
30234
  "compose",
29773
30235
  "-f",
29774
30236
  composePath,
@@ -29780,7 +30242,7 @@ function bringUpAgentService(opts) {
29780
30242
  svc
29781
30243
  ], { stdio });
29782
30244
  }
29783
- execFileSync7(dockerBin, [
30245
+ execFileSync8(dockerBin, [
29784
30246
  "compose",
29785
30247
  "-f",
29786
30248
  composePath,
@@ -29799,7 +30261,7 @@ var init_docker_fleet = __esm(() => {
29799
30261
  });
29800
30262
 
29801
30263
  // src/agents/singleton-reconcile.ts
29802
- import { execFileSync as execFileSync8 } from "node:child_process";
30264
+ import { execFileSync as execFileSync9 } from "node:child_process";
29803
30265
  import { readFileSync as readFileSync16 } from "node:fs";
29804
30266
  function resolveSingletonServices(voiceEngine) {
29805
30267
  const engine = voiceEngine ?? loadHostCapabilities()?.voice.engine ?? "cloud";
@@ -29825,7 +30287,7 @@ function readPinnedSingletonImages(composeText, services = SINGLETON_SERVICES) {
29825
30287
  }
29826
30288
  function defaultInspectImage(dockerBin, container) {
29827
30289
  try {
29828
- const out = execFileSync8(dockerBin, ["inspect", "-f", "{{.Config.Image}}", container], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000 }).trim();
30290
+ const out = execFileSync9(dockerBin, ["inspect", "-f", "{{.Config.Image}}", container], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000 }).trim();
29829
30291
  return out.length > 0 ? out : null;
29830
30292
  } catch {
29831
30293
  return null;
@@ -29851,7 +30313,7 @@ function detectSingletonDrift(deps) {
29851
30313
  });
29852
30314
  }
29853
30315
  function defaultRecreate(dockerBin, project, composeFile, service) {
29854
- execFileSync8(dockerBin, ["compose", "-p", project, "-f", composeFile, ...composeEnvFileArgs(composeFile), "up", "-d", "--no-deps", service], { stdio: ["ignore", "pipe", "pipe"], timeout: 120000 });
30316
+ execFileSync9(dockerBin, ["compose", "-p", project, "-f", composeFile, ...composeEnvFileArgs(composeFile), "up", "-d", "--no-deps", service], { stdio: ["ignore", "pipe", "pipe"], timeout: 120000 });
29855
30317
  }
29856
30318
  function reconcileSingletons(deps) {
29857
30319
  const dockerBin = deps.dockerBin ?? "docker";
@@ -29894,7 +30356,7 @@ var init_singleton_reconcile = __esm(() => {
29894
30356
  });
29895
30357
 
29896
30358
  // src/agents/lifecycle.ts
29897
- import { execFileSync as execFileSync9, spawn, spawnSync } from "node:child_process";
30359
+ import { execFileSync as execFileSync10, spawn, spawnSync } from "node:child_process";
29898
30360
  import { existsSync as existsSync20, mkdirSync as mkdirSync14, writeFileSync as writeFileSync7, renameSync as renameSync5, readFileSync as readFileSync17 } from "node:fs";
29899
30361
  import { resolve as resolve14, join as join14 } from "node:path";
29900
30362
  import { connect } from "node:net";
@@ -29925,7 +30387,7 @@ function buildCliRestartReason(opts) {
29925
30387
  if (!buildCommit)
29926
30388
  return "cli: restart";
29927
30389
  try {
29928
- const head = execFileSync9("git", ["rev-parse", "HEAD"], {
30390
+ const head = execFileSync10("git", ["rev-parse", "HEAD"], {
29929
30391
  cwd: cwd ?? process.cwd(),
29930
30392
  encoding: "utf-8",
29931
30393
  stdio: ["ignore", "pipe", "ignore"]
@@ -29936,7 +30398,7 @@ function buildCliRestartReason(opts) {
29936
30398
  return "cli: restart";
29937
30399
  let subject = "";
29938
30400
  try {
29939
- subject = execFileSync9("git", ["log", "-1", "--pretty=%s", head], {
30401
+ subject = execFileSync10("git", ["log", "-1", "--pretty=%s", head], {
29940
30402
  cwd: cwd ?? process.cwd(),
29941
30403
  encoding: "utf-8",
29942
30404
  stdio: ["ignore", "pipe", "ignore"]
@@ -29966,7 +30428,7 @@ function reconcileSingletonImages(log) {
29966
30428
  }
29967
30429
  function dockerSync(args) {
29968
30430
  try {
29969
- return execFileSync9("docker", args, {
30431
+ return execFileSync10("docker", args, {
29970
30432
  encoding: "utf-8",
29971
30433
  stdio: ["ignore", "pipe", "pipe"]
29972
30434
  }).trim();
@@ -30355,12 +30817,12 @@ var init_lifecycle = __esm(() => {
30355
30817
  });
30356
30818
 
30357
30819
  // src/auth/pane-ready-probe.ts
30358
- import { execFileSync as execFileSync11 } from "node:child_process";
30820
+ import { execFileSync as execFileSync12 } from "node:child_process";
30359
30821
  function defaultPaneReadyDeps() {
30360
30822
  return {
30361
30823
  capturePane(sessionName) {
30362
30824
  try {
30363
- return execFileSync11("tmux", ["capture-pane", "-p", "-t", sessionName, "-S", "-200"], {
30825
+ return execFileSync12("tmux", ["capture-pane", "-p", "-t", sessionName, "-S", "-200"], {
30364
30826
  encoding: "utf-8",
30365
30827
  stdio: ["pipe", "pipe", "pipe"]
30366
30828
  }).trim();
@@ -30641,7 +31103,7 @@ var init_accounts = __esm(() => {
30641
31103
  });
30642
31104
 
30643
31105
  // src/auth/manager.ts
30644
- import { execFileSync as execFileSync12 } from "node:child_process";
31106
+ import { execFileSync as execFileSync13 } from "node:child_process";
30645
31107
  import {
30646
31108
  readFileSync as readFileSync22,
30647
31109
  readdirSync as readdirSync11,
@@ -30692,14 +31154,14 @@ function authSessionName(name, slot) {
30692
31154
  return `${base}-${slot.replace(/[^a-zA-Z0-9_.-]/g, "-")}`;
30693
31155
  }
30694
31156
  function tmux(args) {
30695
- return execFileSync12("tmux", args, {
31157
+ return execFileSync13("tmux", args, {
30696
31158
  encoding: "utf-8",
30697
31159
  stdio: ["pipe", "pipe", "pipe"]
30698
31160
  }).trim();
30699
31161
  }
30700
31162
  function tmuxSessionExists(sessionName) {
30701
31163
  try {
30702
- execFileSync12("tmux", ["has-session", "-t", sessionName], {
31164
+ execFileSync13("tmux", ["has-session", "-t", sessionName], {
30703
31165
  stdio: ["pipe", "pipe", "pipe"]
30704
31166
  });
30705
31167
  return true;
@@ -31215,355 +31677,6 @@ function readQuarantineMarkerForAgent(agentsDir, name) {
31215
31677
  var QUARANTINE_FILENAME = "quarantine.json";
31216
31678
  var init_quarantine = () => {};
31217
31679
 
31218
- // src/agents/pane-lock.ts
31219
- function withPaneLock(key, fn) {
31220
- const tail = tails.get(key) ?? Promise.resolve();
31221
- const run = tail.then(() => fn());
31222
- const next = run.then(() => {}, () => {});
31223
- tails.set(key, next);
31224
- next.then(() => {
31225
- if (tails.get(key) === next)
31226
- tails.delete(key);
31227
- });
31228
- return run;
31229
- }
31230
- var tails;
31231
- var init_pane_lock = __esm(() => {
31232
- tails = new Map;
31233
- });
31234
-
31235
- // src/agents/inject.ts
31236
- var exports_inject = {};
31237
- __export(exports_inject, {
31238
- validateInjectCommand: () => validateInjectCommand,
31239
- normalizeInjectCommand: () => normalizeInjectCommand,
31240
- makeTmuxRunner: () => makeTmuxRunner,
31241
- isTuiChromeLine: () => isTuiChromeLine,
31242
- injectSlashCommandWith: () => injectSlashCommandWith,
31243
- injectSlashCommand: () => injectSlashCommand,
31244
- diffPane: () => diffPane,
31245
- dedupeInjectQueue: () => dedupeInjectQueue,
31246
- InjectError: () => InjectError,
31247
- INJECT_COMMANDS: () => INJECT_COMMANDS,
31248
- INJECT_BLOCKLIST: () => INJECT_BLOCKLIST,
31249
- INJECT_BLOCKED: () => INJECT_BLOCKED,
31250
- INJECT_ALLOWLIST: () => INJECT_ALLOWLIST
31251
- });
31252
- import { execFileSync as execFileSync14 } from "node:child_process";
31253
- function validateInjectCommand(command) {
31254
- if (typeof command !== "string" || command.trim().length === 0) {
31255
- throw new InjectError("invalid", "command is empty");
31256
- }
31257
- const trimmed = command.trim();
31258
- if (!trimmed.startsWith("/")) {
31259
- throw new InjectError("invalid", `command must start with "/": got ${trimmed}`);
31260
- }
31261
- const bare = trimmed.split(/\s+/, 1)[0].toLowerCase();
31262
- const blockedMeta = INJECT_BLOCKED.get(bare);
31263
- if (blockedMeta) {
31264
- throw new InjectError("blocked", `${bare} is explicitly blocked from inject (${blockedMeta.reason}).`);
31265
- }
31266
- if (!INJECT_COMMANDS.has(bare)) {
31267
- const allowed = [...INJECT_COMMANDS.keys()].sort().join(", ");
31268
- throw new InjectError("not_allowed", `${bare} is not in the inject allowlist. Allowed: ${allowed}`);
31269
- }
31270
- return bare;
31271
- }
31272
- function normalizeInjectCommand(command) {
31273
- return command.trim().replace(/\s+/g, " ").toLowerCase();
31274
- }
31275
- function dedupeInjectQueue(commands) {
31276
- const seen = new Set;
31277
- const out = [];
31278
- for (const raw of commands) {
31279
- if (typeof raw !== "string")
31280
- continue;
31281
- const key = normalizeInjectCommand(raw);
31282
- if (key.length === 0)
31283
- continue;
31284
- if (seen.has(key))
31285
- continue;
31286
- seen.add(key);
31287
- out.push(raw.trim());
31288
- }
31289
- return out;
31290
- }
31291
- function defaultSocketName(agentName) {
31292
- return `switchroom-${agentName}`;
31293
- }
31294
- function makeTmuxRunner(tmuxBin) {
31295
- return {
31296
- capture(socket, session) {
31297
- try {
31298
- return execFileSync14(tmuxBin, ["-L", socket, "capture-pane", "-p", "-t", session, "-S", "-200"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
31299
- } catch {
31300
- return null;
31301
- }
31302
- },
31303
- send(socket, session, args) {
31304
- const [subcmd, ...rest] = args;
31305
- const flagEnd = rest.findIndex((a) => !a.startsWith("-"));
31306
- const flagsBeforeKeys = flagEnd === -1 ? rest : rest.slice(0, flagEnd);
31307
- const keys = flagEnd === -1 ? [] : rest.slice(flagEnd);
31308
- execFileSync14(tmuxBin, ["-L", socket, subcmd, ...flagsBeforeKeys, "-t", session, ...keys], { stdio: ["pipe", "pipe", "pipe"] });
31309
- },
31310
- hasSession(socket, session) {
31311
- try {
31312
- execFileSync14(tmuxBin, ["-L", socket, "has-session", "-t", session], {
31313
- stdio: ["pipe", "pipe", "pipe"]
31314
- });
31315
- return true;
31316
- } catch {
31317
- return false;
31318
- }
31319
- }
31320
- };
31321
- }
31322
- function sleep2(ms) {
31323
- return new Promise((res) => setTimeout(res, ms));
31324
- }
31325
- function isTuiChromeLine(line) {
31326
- const t = line.trim();
31327
- if (t.length === 0)
31328
- return false;
31329
- if (/^[\s\-_\u2502\u2500\u256d\u256e\u2570\u256f\u250c\u2510\u2514\u2518|]+$/.test(t))
31330
- return true;
31331
- if (/^[\u256d\u256e\u2570\u256f\u2502\s]*[>)\u276f][\u256d\u256e\u2570\u256f\u2502\s]*$/.test(t))
31332
- return true;
31333
- if (/accept edits on/i.test(t))
31334
- return true;
31335
- if (/shift\+tab to cycle/i.test(t))
31336
- return true;
31337
- if (/for agents/i.test(t))
31338
- return true;
31339
- if (/\? for shortcuts/i.test(t))
31340
- return true;
31341
- if (/bypassing permissions/i.test(t))
31342
- return true;
31343
- if (t === "copy")
31344
- return true;
31345
- return false;
31346
- }
31347
- function diffPane(before, after, command) {
31348
- if (command) {
31349
- const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31350
- const lines = after.split(`
31351
- `);
31352
- let anchorIdx = -1;
31353
- for (let i = lines.length - 1;i >= 0; i--) {
31354
- if (/[\u276f>]\s+/.test(lines[i]) && lines[i].includes(escaped)) {
31355
- anchorIdx = i;
31356
- break;
31357
- }
31358
- }
31359
- if (anchorIdx >= 0) {
31360
- const tail = lines.slice(anchorIdx + 1);
31361
- const trimmed = [];
31362
- for (const raw of tail) {
31363
- const line = raw.trimEnd();
31364
- if (line.length === 0 && trimmed.length === 0)
31365
- continue;
31366
- if (isTuiChromeLine(line))
31367
- continue;
31368
- trimmed.push(line);
31369
- }
31370
- while (trimmed.length > 0 && trimmed[trimmed.length - 1].length === 0) {
31371
- trimmed.pop();
31372
- }
31373
- const affordances = /^(esc to cancel|press any key|\u21b5 select)/i;
31374
- while (trimmed.length > 0 && affordances.test(trimmed[trimmed.length - 1].trim())) {
31375
- trimmed.pop();
31376
- }
31377
- if (trimmed.length > 0) {
31378
- return { output: trimmed.join(`
31379
- `), anchored: true };
31380
- }
31381
- }
31382
- }
31383
- const beforeSet = new Set(before.split(`
31384
- `).map((l) => l.trimEnd()));
31385
- const newLines = [];
31386
- for (const raw of after.split(`
31387
- `)) {
31388
- const line = raw.trimEnd();
31389
- if (line.length === 0)
31390
- continue;
31391
- if (beforeSet.has(line))
31392
- continue;
31393
- if (isTuiChromeLine(line))
31394
- continue;
31395
- newLines.push(line);
31396
- }
31397
- return { output: newLines.join(`
31398
- `), anchored: false };
31399
- }
31400
- async function injectSlashCommand(agentName, command, opts = {}) {
31401
- validateInjectCommand(command);
31402
- const tmuxBin = opts.tmuxBin ?? "tmux";
31403
- const socket = opts.socketName ?? defaultSocketName(agentName);
31404
- const session = opts.sessionName ?? agentName;
31405
- const settleMs = opts.settleMs ?? 2000;
31406
- const timeoutMs = opts.timeoutMs ?? 5000;
31407
- return withPaneLock(`${socket}:${session}`, () => injectSlashCommandWith(makeTmuxRunner(tmuxBin), {
31408
- socket,
31409
- session,
31410
- command: command.trim(),
31411
- settleMs,
31412
- timeoutMs
31413
- }));
31414
- }
31415
- async function injectSlashCommandWith(runner, args) {
31416
- const { socket, session, command, settleMs, timeoutMs } = args;
31417
- let bareVerb;
31418
- try {
31419
- bareVerb = validateInjectCommand(command);
31420
- } catch (err) {
31421
- if (err instanceof InjectError) {
31422
- return {
31423
- outcome: "failed",
31424
- output: "",
31425
- truncated: false,
31426
- command: command.trim().split(/\s+/, 1)[0]?.toLowerCase() ?? "",
31427
- meta: null,
31428
- errorCode: err.code,
31429
- errorMessage: err.message
31430
- };
31431
- }
31432
- throw err;
31433
- }
31434
- const meta = INJECT_COMMANDS.get(bareVerb) ?? null;
31435
- if (!runner.hasSession(socket, session)) {
31436
- return {
31437
- outcome: "failed",
31438
- output: "",
31439
- truncated: false,
31440
- command: bareVerb,
31441
- meta,
31442
- errorCode: "session_missing",
31443
- errorMessage: `tmux session "${session}" on socket "${socket}" not found. ` + `Is the agent running under the tmux supervisor (the default)? ` + `If experimental.legacy_pty=true is set, inject is unsupported.`
31444
- };
31445
- }
31446
- const before = runner.capture(socket, session) ?? "";
31447
- try {
31448
- runner.send(socket, session, ["send-keys", "-l", command]);
31449
- runner.send(socket, session, ["send-keys", "Enter"]);
31450
- } catch (err) {
31451
- return {
31452
- outcome: "failed",
31453
- output: "",
31454
- truncated: false,
31455
- command: bareVerb,
31456
- meta,
31457
- errorCode: "tmux_failed",
31458
- errorMessage: `tmux send-keys failed: ${err instanceof Error ? err.message : String(err)}`
31459
- };
31460
- }
31461
- const start = Date.now();
31462
- let last = before;
31463
- let stableSince = null;
31464
- while (Date.now() - start < timeoutMs) {
31465
- await sleep2(POLL_INTERVAL_MS);
31466
- const cur = runner.capture(socket, session) ?? "";
31467
- if (cur === last && cur !== before) {
31468
- if (stableSince === null) {
31469
- stableSince = Date.now();
31470
- } else if (Date.now() - stableSince >= POLL_INTERVAL_MS) {
31471
- last = cur;
31472
- break;
31473
- }
31474
- } else {
31475
- stableSince = null;
31476
- }
31477
- last = cur;
31478
- if (Date.now() - start >= settleMs && cur !== before) {
31479
- break;
31480
- }
31481
- }
31482
- const { output: rawOutput, anchored } = diffPane(before, last, command);
31483
- let output = rawOutput;
31484
- let truncated = false;
31485
- const bytes = Buffer.byteLength(output, "utf-8");
31486
- if (bytes > OUTPUT_BYTE_CAP) {
31487
- while (Buffer.byteLength(output, "utf-8") > OUTPUT_BYTE_CAP) {
31488
- output = output.slice(Math.floor(output.length * 0.1) + 1);
31489
- }
31490
- truncated = true;
31491
- }
31492
- if (meta?.dialog) {
31493
- try {
31494
- runner.send(socket, session, ["send-keys", "Escape"]);
31495
- } catch {}
31496
- }
31497
- if (output.trim().length === 0) {
31498
- return {
31499
- outcome: "ok_no_output",
31500
- output: "",
31501
- truncated: false,
31502
- command: bareVerb,
31503
- meta,
31504
- ...anchored ? {} : { diagnostic: "anchor_missing" }
31505
- };
31506
- }
31507
- return {
31508
- outcome: "ok",
31509
- output,
31510
- truncated,
31511
- command: bareVerb,
31512
- meta,
31513
- ...truncated ? { diagnostic: "truncated_output" } : {}
31514
- };
31515
- }
31516
- var INJECT_COMMANDS, INJECT_ALLOWLIST, INJECT_BLOCKED, INJECT_BLOCKLIST, InjectError, POLL_INTERVAL_MS = 150, OUTPUT_BYTE_CAP = 3000;
31517
- var init_inject = __esm(() => {
31518
- init_pane_lock();
31519
- INJECT_COMMANDS = new Map([
31520
- ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true }],
31521
- ["/status", { description: "Show session status", expectsOutput: true, dialog: true }],
31522
- ["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true }],
31523
- ["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true }],
31524
- ["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true }],
31525
- ["/model", { description: "Open model picker", expectsOutput: true }],
31526
- [
31527
- "/clear",
31528
- {
31529
- description: "Clear session screen",
31530
- expectsOutput: false,
31531
- silentNote: "context cleared \u2014 fresh slate"
31532
- }
31533
- ],
31534
- [
31535
- "/compact",
31536
- {
31537
- description: "Compact conversation history",
31538
- expectsOutput: false,
31539
- silentNote: "compaction runs silently"
31540
- }
31541
- ]
31542
- ]);
31543
- INJECT_ALLOWLIST = new Set(INJECT_COMMANDS.keys());
31544
- INJECT_BLOCKED = new Map([
31545
- ["/login", { reason: "would mutate auth state" }],
31546
- ["/logout", { reason: "would terminate the agent's auth session" }],
31547
- ["/exit", { reason: "would kill the agent process" }],
31548
- ["/quit", { reason: "would kill the agent process" }],
31549
- [
31550
- "/effort",
31551
- {
31552
- reason: "leaves a blocking confirmation modal open and wedges the pane; use the /effort command (it drives the modal), not raw inject"
31553
- }
31554
- ]
31555
- ]);
31556
- INJECT_BLOCKLIST = new Set(INJECT_BLOCKED.keys());
31557
- InjectError = class InjectError extends Error {
31558
- code;
31559
- constructor(code, message) {
31560
- super(message);
31561
- this.name = "InjectError";
31562
- this.code = code;
31563
- }
31564
- };
31565
- });
31566
-
31567
31680
  // src/auth/broker/protocol.ts
31568
31681
  function encodeRequest2(req) {
31569
31682
  const line = JSON.stringify(RequestSchema2.parse(req)) + `
@@ -64552,8 +64665,8 @@ import { existsSync, readFileSync } from "node:fs";
64552
64665
  import { dirname, join } from "node:path";
64553
64666
 
64554
64667
  // src/build-info.ts
64555
- var VERSION = "0.17.10";
64556
- var COMMIT_SHA = "d38372bf";
64668
+ var VERSION = "0.18.3";
64669
+ var COMMIT_SHA = "dbd8b112";
64557
64670
 
64558
64671
  // src/cli/resolve-version.ts
64559
64672
  function readPackageVersion() {
@@ -64767,11 +64880,11 @@ function detectInFlight(opts) {
64767
64880
  async function waitUntilIdle(opts) {
64768
64881
  const pollMs = opts.pollMs ?? 2000;
64769
64882
  const clock = opts.now ?? Date.now;
64770
- const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
64883
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
64771
64884
  const deadline = clock() + opts.timeoutMs;
64772
64885
  let last = detectInFlight(opts);
64773
64886
  while (last.busy && clock() < deadline) {
64774
- await sleep(pollMs);
64887
+ await sleep2(pollMs);
64775
64888
  last = detectInFlight(opts);
64776
64889
  }
64777
64890
  return last;
@@ -64883,7 +64996,7 @@ function spinner(message) {
64883
64996
  // src/agents/status.ts
64884
64997
  import { existsSync as existsSync23, readFileSync as readFileSync20, statSync as statSync11 } from "node:fs";
64885
64998
  import { join as join16 } from "node:path";
64886
- import { execFileSync as execFileSync10 } from "node:child_process";
64999
+ import { execFileSync as execFileSync11 } from "node:child_process";
64887
65000
 
64888
65001
  // src/agents/handoff-summarizer.ts
64889
65002
  import { readFileSync as readFileSync18, writeFileSync as writeFileSync8, renameSync as renameSync6, mkdirSync as mkdirSync15, existsSync as existsSync21, statSync as statSync10, readdirSync as readdirSync9 } from "node:fs";
@@ -65468,7 +65581,7 @@ async function buildAgentStatusReport(inputs) {
65468
65581
  async function waitForAgentReady(inputs, options = {}) {
65469
65582
  const timeoutMs = options.timeoutMs ?? 30000;
65470
65583
  const pollIntervalMs = options.pollIntervalMs ?? 750;
65471
- const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
65584
+ const sleep2 = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
65472
65585
  const now = options.now ?? Date.now;
65473
65586
  const startedAt = now();
65474
65587
  const deadline = startedAt + timeoutMs;
@@ -65483,7 +65596,7 @@ async function waitForAgentReady(inputs, options = {}) {
65483
65596
  notReady
65484
65597
  };
65485
65598
  }
65486
- await sleep(pollIntervalMs);
65599
+ await sleep2(pollIntervalMs);
65487
65600
  report = await buildAgentStatusReport(inputs);
65488
65601
  notReady = readinessGaps(report);
65489
65602
  }
@@ -65560,14 +65673,14 @@ function readLastMessages(historyDbPath) {
65560
65673
  };
65561
65674
  }
65562
65675
  try {
65563
- const out = execFileSync10("bun", [
65676
+ const out = execFileSync11("bun", [
65564
65677
  "-e",
65565
65678
  `const { Database } = require("bun:sqlite"); ` + `const db = new Database(${JSON.stringify(historyDbPath)}, { readonly: true }); ` + `const rows = db.prepare("SELECT role, MAX(ts) as ts FROM messages GROUP BY role").all(); ` + `for (const r of rows) console.log(r.role + "|" + r.ts); ` + `db.close();`
65566
65679
  ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
65567
65680
  return parseSqliteRoleTsOutput(out);
65568
65681
  } catch {}
65569
65682
  try {
65570
- const out = execFileSync10("sqlite3", [
65683
+ const out = execFileSync11("sqlite3", [
65571
65684
  historyDbPath,
65572
65685
  "SELECT role, MAX(ts) FROM messages GROUP BY role;"
65573
65686
  ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
@@ -65679,7 +65792,7 @@ function escapeRegex(s) {
65679
65792
  }
65680
65793
  function readDockerContainer(containerName2) {
65681
65794
  try {
65682
- const out = execFileSync10("docker", ["inspect", "--format", "{{json .State}}", containerName2], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
65795
+ const out = execFileSync11("docker", ["inspect", "--format", "{{json .State}}", containerName2], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
65683
65796
  const state = JSON.parse(out.trim());
65684
65797
  const active = state.Status === "running" ? "active" : state.Status === "restarting" ? "restarting" : "inactive";
65685
65798
  const pid = typeof state.Pid === "number" && state.Pid > 0 ? state.Pid : null;
@@ -65794,7 +65907,7 @@ async function pollForDmStart(token, timeoutMs = 120000) {
65794
65907
  const response = await fetch(url);
65795
65908
  data = await response.json();
65796
65909
  } catch {
65797
- await sleep(2000);
65910
+ await sleep2(2000);
65798
65911
  continue;
65799
65912
  }
65800
65913
  if (data.ok && Array.isArray(data.result)) {
@@ -65810,11 +65923,11 @@ async function pollForDmStart(token, timeoutMs = 120000) {
65810
65923
  }
65811
65924
  }
65812
65925
  }
65813
- await sleep(2000);
65926
+ await sleep2(2000);
65814
65927
  }
65815
65928
  throw new Error("Timed out waiting for /start DM");
65816
65929
  }
65817
- function sleep(ms) {
65930
+ function sleep2(ms) {
65818
65931
  return new Promise((resolve17) => setTimeout(resolve17, ms));
65819
65932
  }
65820
65933
 
@@ -65958,7 +66071,7 @@ async function completeCreation(name, code, opts = {}) {
65958
66071
  // src/agents/add-orchestrator.ts
65959
66072
  import { resolve as resolve19, join as join19 } from "node:path";
65960
66073
  import { existsSync as existsSync28, rmSync as rmSync8, statSync as statSync15 } from "node:fs";
65961
- import { execFileSync as execFileSync13 } from "node:child_process";
66074
+ import { execFileSync as execFileSync14 } from "node:child_process";
65962
66075
  init_onboarding();
65963
66076
 
65964
66077
  // src/setup/botfather-walkthrough.ts
@@ -66532,7 +66645,7 @@ function pruneBundledSkills(agentDir, keep, scope) {
66532
66645
  }
66533
66646
  function defaultIsUnitActive(unitName) {
66534
66647
  try {
66535
- const out = execFileSync13("systemctl", ["--user", "is-active", unitName], {
66648
+ const out = execFileSync14("systemctl", ["--user", "is-active", unitName], {
66536
66649
  encoding: "utf-8",
66537
66650
  stdio: ["ignore", "pipe", "pipe"]
66538
66651
  }).trim();
@@ -74986,7 +75099,8 @@ class VaultBroker {
74986
75099
  return;
74987
75100
  }
74988
75101
  try {
74989
- const tokenDir = path3.join(os4.homedir(), ".switchroom", "agents", agent);
75102
+ const agentsDir = this.config ? resolveAgentsDir(this.config) : path3.join(os4.homedir(), ".switchroom", "agents");
75103
+ const tokenDir = path3.join(agentsDir, agent);
74990
75104
  mkdirSync24(tokenDir, { recursive: true });
74991
75105
  const tokenPath = path3.join(tokenDir, ".vault-token");
74992
75106
  const tmpPath = `${tokenPath}.tmp.${process.pid}`;
@@ -75050,7 +75164,8 @@ class VaultBroker {
75050
75164
  try {
75051
75165
  const row = this.grantsDb.query("SELECT agent_slug FROM vault_grants WHERE id = ?").get(id);
75052
75166
  if (row && AgentNameSchema.safeParse(row.agent_slug).success) {
75053
- const tokenPath = path3.join(os4.homedir(), ".switchroom", "agents", row.agent_slug, ".vault-token");
75167
+ const agentsDir = this.config ? resolveAgentsDir(this.config) : path3.join(os4.homedir(), ".switchroom", "agents");
75168
+ const tokenPath = path3.join(agentsDir, row.agent_slug, ".vault-token");
75054
75169
  if (existsSync41(tokenPath)) {
75055
75170
  try {
75056
75171
  unlinkSync10(tokenPath);
@@ -78288,7 +78403,11 @@ async function resolveLiteLLMForHindsight(config) {
78288
78403
  const result = await getViaBrokerStructured("litellm/hindsight/api-key").catch(() => null);
78289
78404
  if (!result || result.kind !== "ok" || result.entry.kind !== "string")
78290
78405
  return;
78291
- return { baseUrl: topLiteLLM.base_url, apiKey: result.entry.value };
78406
+ return {
78407
+ baseUrl: topLiteLLM.base_url,
78408
+ apiKey: result.entry.value,
78409
+ model: config.memory?.config?.llm_model
78410
+ };
78292
78411
  }
78293
78412
  function readRecallLog(agentDir, limit) {
78294
78413
  const path4 = join40(agentDir, ".claude", "plugins", "data", "hindsight-memory-inline", "state", "recall_log.jsonl");
@@ -78552,8 +78671,9 @@ Cross-agent reflection plan
78552
78671
  const provider = "claude-code";
78553
78672
  console.log(source_default.gray(" Starting Hindsight Docker container..."));
78554
78673
  try {
78555
- const litellmCfg = await resolveLiteLLMForHindsight(getConfig(program3));
78556
- startHindsight(ports, litellmCfg, opts.tag);
78674
+ const hindsightConfig = getConfig(program3);
78675
+ const litellmCfg = await resolveLiteLLMForHindsight(hindsightConfig);
78676
+ startHindsight(ports, litellmCfg, opts.tag, hindsightConfig.hindsight?.llm);
78557
78677
  if (litellmCfg) {
78558
78678
  console.log(source_default.gray(" LiteLLM routing enabled for hindsight (--network host)."));
78559
78679
  }
@@ -78599,7 +78719,7 @@ Cross-agent reflection plan
78599
78719
  console.log(source_default.bold(`
78600
78720
  # Add this to your docker-compose.yml:
78601
78721
  `));
78602
- console.log(generateHindsightComposeSnippet());
78722
+ console.log(generateHindsightComposeSnippet(getConfig(program3).hindsight?.llm));
78603
78723
  console.log();
78604
78724
  });
78605
78725
  memory.command("recall-log [agent]").description("Show recent auto-recall events (per-turn JSONL log) \u2014 see what was injected, when the cap fired, hit rate hints").option("-n, --limit <n>", "Tail the last N events per agent (default 20)", "20").option("--json", "Emit raw JSONL (one entry per line)").action(withConfigError(async (agent, opts) => {
@@ -83403,7 +83523,11 @@ async function resolveLiteLLMForHindsight2(config) {
83403
83523
  const result = await getViaBrokerStructured("litellm/hindsight/api-key").catch(() => null);
83404
83524
  if (!result || result.kind !== "ok" || result.entry.kind !== "string")
83405
83525
  return;
83406
- return { baseUrl: topLiteLLM.base_url, apiKey: result.entry.value };
83526
+ return {
83527
+ baseUrl: topLiteLLM.base_url,
83528
+ apiKey: result.entry.value,
83529
+ model: config.memory?.config?.llm_model
83530
+ };
83407
83531
  }
83408
83532
  async function stepMemoryBackend(config, nonInteractive, switchroomConfigPath) {
83409
83533
  stepHeader(6, "Memory backend", STEP_ACTIVE);
@@ -83495,7 +83619,7 @@ async function stepMemoryBackend(config, nonInteractive, switchroomConfigPath) {
83495
83619
  const spin = spinner("Starting Hindsight Docker container...");
83496
83620
  try {
83497
83621
  const litellmCfg = await resolveLiteLLMForHindsight2(config);
83498
- startHindsight(ports, litellmCfg);
83622
+ startHindsight(ports, litellmCfg, undefined, config.hindsight?.llm);
83499
83623
  if (litellmCfg) {
83500
83624
  console.log(source_default.gray(" LiteLLM routing enabled (--network host, ANTHROPIC_BASE_URL set)."));
83501
83625
  }