switchroom 0.19.23 → 0.19.25

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 (52) hide show
  1. package/dist/agent-scheduler/index.js +18 -7
  2. package/dist/auth-broker/index.js +117 -33
  3. package/dist/cli/autoaccept-poll.js +0 -1
  4. package/dist/cli/drive-write-pretool.mjs +5 -0
  5. package/dist/cli/ms-365-write-pretool.mjs +5 -0
  6. package/dist/cli/notion-write-pretool.mjs +18 -6
  7. package/dist/cli/switchroom.js +2916 -1481
  8. package/dist/host-control/main.js +116 -34
  9. package/dist/vault/approvals/kernel-server.js +115 -33
  10. package/dist/vault/broker/server.js +281 -76
  11. package/examples/switchroom.yaml +1 -1
  12. package/package.json +1 -1
  13. package/profiles/_base/start.sh.hbs +52 -13
  14. package/profiles/_shared/dev-protocol.md.hbs +3 -4
  15. package/skills/dev-protocol/SKILL.md +22 -15
  16. package/skills/switchroom-health/SKILL.md +19 -0
  17. package/skills/switchroom-release/SKILL.md +2 -1
  18. package/skills/switchroom-status/SKILL.md +1 -1
  19. package/telegram-plugin/auth-snapshot-format.ts +9 -2
  20. package/telegram-plugin/dist/gateway/gateway.js +6925 -6734
  21. package/telegram-plugin/gateway/gateway.ts +34 -35
  22. package/telegram-plugin/gateway/latest-turn-lookup.ts +60 -0
  23. package/telegram-plugin/gateway/outbound-send-path.ts +53 -21
  24. package/telegram-plugin/gateway/subagent-handback-marker.ts +1 -1
  25. package/telegram-plugin/gateway/turn-end.ts +1 -1
  26. package/telegram-plugin/quota-bar-format.ts +4 -1
  27. package/telegram-plugin/reply-owner-resolve.ts +110 -9
  28. package/telegram-plugin/send-gate-degraded.test.ts +45 -16
  29. package/telegram-plugin/send-gate.ts +185 -24
  30. package/telegram-plugin/tests/activity-card-send-gate.test.ts +9 -9
  31. package/telegram-plugin/tests/auth-snapshot-format.test.ts +42 -0
  32. package/telegram-plugin/tests/latest-turn-lookup.test.ts +77 -0
  33. package/telegram-plugin/tests/narrative-lane-golden.test.ts +23 -1
  34. package/telegram-plugin/tests/quota-bar-format.test.ts +50 -0
  35. package/telegram-plugin/tests/reply-owner-resolve.test.ts +531 -0
  36. package/telegram-plugin/tests/secret-detect-false-positives.test.ts +1 -1
  37. package/telegram-plugin/tests/send-reply-golden.test.ts +296 -28
  38. package/telegram-plugin/tests/stream-controller-send-gate.test.ts +134 -28
  39. package/telegram-plugin/tests/stream-render-golden.test.ts +25 -3
  40. package/vendor/hindsight-memory/scripts/lib/config.py +61 -19
  41. package/vendor/hindsight-memory/scripts/lib/content.py +376 -1
  42. package/vendor/hindsight-memory/scripts/lib/english_words.txt +10799 -0
  43. package/vendor/hindsight-memory/scripts/recall.py +503 -252
  44. package/vendor/hindsight-memory/scripts/tests/test_recall_bank_slots.py +509 -0
  45. package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +22 -5
  46. package/vendor/hindsight-memory/scripts/tests/test_recall_error_text.py +147 -0
  47. package/vendor/hindsight-memory/scripts/tests/test_recall_hook_budget.py +266 -0
  48. package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +0 -401
  49. package/vendor/hindsight-memory/scripts/tests/test_recall_no_lexical_gate.py +261 -0
  50. package/vendor/hindsight-memory/scripts/tests/test_recall_query_shaping.py +473 -0
  51. package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +25 -8
  52. package/vendor/hindsight-memory/tests/test_content.py +218 -0
@@ -4164,7 +4164,11 @@ var init_schema = __esm(() => {
4164
4164
  recall: exports_external.object({
4165
4165
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
4166
4166
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
4167
- min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum containment token overlap [0.0–1.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "A cheap FLOOR that removes candidates with (near-)zero lexical " + "relationship to the prompt — NOT a precision control: at the " + "recommended 0.10 the gate is near-passthrough, and the " + "effective precision control is the engine rerank plus the " + "max_memories head-slice. 0.0 disables (default — current " + "behaviour). Use 0.10; values at or above 0.20 measurably " + "starve recall — on production replay 0.20 leaves ~41.9% of " + "turns with NO memories at all, re-creating the bug #3541 " + "fixed. Observe the `overlap_dropped` field via " + "`switchroom memory recall-log`."),
4167
+ query_max_tokens: exports_external.number().int().min(0).optional().describe("Cap on the number of DISTINCT BM25 terms the recall hook may " + "put on the wire. `recallMaxQueryChars` bounds characters, which " + "is not the cost driver: Hindsight OR-joins every query token " + "into one tsquery and Postgres native FTS ranks the entire " + "matched set before the top-60 heapsort, so cost tracks TERMS. " + "An 800-char composed query is ~96 distinct terms and matched " + "119,510 of 135,565 rows on the `overlord` bank (14.0s for the " + "3-arm " + "BM25 UNION), past the per-bank timeout — 96.8% of that agent's " + "own-bank recalls returned nothing. Plugin default is 24 " + "(measured 48,433 rows / 2.7s on the same bank). Terms are " + "chosen recency-first (the latest turn beats prior context), " + "then by selectivity. 0 disables shaping (rollback lever)."),
4168
+ query_stop_terms: exports_external.array(exports_external.string().min(1).regex(/^[\w./-]+$/)).optional().describe("Extra terms dropped from the BM25 recall query, on top of the " + "built-in English stopword list. For BANK-SPECIFIC " + "high-document-frequency words a generic stoplist cannot know " + "about: on `overlord`, `switchroom` matches 20% of the bank and " + "`agent` another 20%, purely because that is what the corpus is " + "about, and each such term drags tens of thousands of rows into " + "the ranking. Defaults to []."),
4169
+ request_timeout_seconds: exports_external.number().int().min(1).optional().describe("Per-bank HTTP read timeout, in seconds, for one recall " + "request. Plugin default is 12, matching the UserPromptSubmit " + "hook ceiling; the shared `recallParallelDeadlineSeconds` (10) " + "is the tighter outer guard in the default configuration, so " + "this is a per-request safety net. Was a hardcoded 8 in the " + "plugin before #3757."),
4170
+ own_bank_min_slots: exports_external.number().int().min(0).optional().describe("Slots inside `max_memories` reserved as a FLOOR for the agent's " + "own bank when recall fans out to more than one bank. The merged " + "set is sorted globally by relevance and head-sliced, which is " + "winner-take-all across banks: when both banks return more " + "candidates than the cap, one bank's score distribution can fill " + "every slot and the agent gets a dossier about its operator with " + "none of its own session memory. A floor, not a quota: at most " + "this many slots, only if the own bank returned that many, and " + "only up to HALF the cap shared with `additional_bank_min_slots` " + "— the rest is always won on pure relevance, so composition still " + "moves with the scores. Fixes score-based crowd-out only; a " + "timed-out bank returns no candidates and reservation is a no-op " + "there. 0 disables (default). Switchroom-managed agents use 2 " + "against the fleet-deployed cap of 6."),
4171
+ additional_bank_min_slots: exports_external.number().int().min(0).optional().describe("Slots inside `max_memories` reserved as a FLOOR for the " + "additional (profile / shared / sender) banks. Symmetric with " + "`own_bank_min_slots` — same floor-not-quota semantics, and the " + "two share the same half-of-cap reservation budget. When they sum " + "above that budget the own-bank floor is honoured first. 0 " + "disables (default). Switchroom-managed agents use 1 against the " + "fleet-deployed cap of 6. Observe `injected_own_bank_count` / " + "`injected_additional_bank_count` via " + "`switchroom memory recall-log`."),
4168
4172
  types: exports_external.array(exports_external.string()).optional().describe("Hindsight fact types to recall. Switchroom default is " + '["world", "experience", "observation"] — the synthesized ' + "`observation` tier is on by default. Set to " + '["world", "experience"] to opt out of observation-backed ' + "recall for this agent (or fleet-wide under defaults)."),
4169
4173
  additional_banks: exports_external.array(exports_external.string()).optional().describe("Extra Hindsight banks to recall from on every turn, merged into " + "the agent's own bank results — e.g. a shared operator/household " + "profile bank authored via `switchroom memory profile`. Each is " + "recalled with an 8s timeout and is non-fatal on failure. Stays " + "within the single tenant: all banks are the operator's data, in " + "the operator's Hindsight instance (see the `single-tenant` " + "invariant). Defaults to [] (no extra banks)."),
4170
4174
  sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Per-speaker recall routing: a map of Telegram sender → extra " + "recall bank. When a message arrives, the agent also recalls the " + "speaker's bank (matched by Telegram username — a leading @ is " + "optional — or numeric user_id), merged " + "into its own results — so each trusted user gets their own " + "profile context. Additive recall scoping within the single " + "tenant: never an access boundary (who may drive an agent stays " + "the per-agent user assignment in `access.allowFrom`). Author the " + "banks via `switchroom memory profile`."),
@@ -4377,17 +4381,19 @@ var init_schema = __esm(() => {
4377
4381
  model: exports_external.string().min(1).optional().describe("Per-op model (upstream `HINDSIGHT_API_<OP>_LLM_MODEL`). Absent → " + "inherit the global `hindsight.llm.model`."),
4378
4382
  provider: exports_external.string().min(1).optional().describe("Per-op provider (upstream `HINDSIGHT_API_<OP>_LLM_PROVIDER`). " + "Absent → inherit the global `hindsight.llm.provider`."),
4379
4383
  base_url: exports_external.string().min(1).optional().describe("Per-op base URL (upstream `HINDSIGHT_API_<OP>_LLM_BASE_URL`). " + "Optional passthrough; absent → inherit the global."),
4380
- api_key: exports_external.string().min(1).optional().describe("Per-op API key (upstream `HINDSIGHT_API_<OP>_LLM_API_KEY`). Literal " + "or `vault:` reference. Optional passthrough; absent → inherit global.")
4384
+ api_key: exports_external.string().min(1).optional().describe("Per-op API key (upstream `HINDSIGHT_API_<OP>_LLM_API_KEY`). Literal " + "or `vault:` reference. Optional passthrough; absent → inherit global."),
4385
+ context_window: exports_external.number().int().positive().optional().describe("Context window (tokens) of the backend serving THIS op. NOT an " + "upstream env var — switchroom derives the op's token budget " + "(consolidation batch size / max-completion caps / reflect " + "max-context cap) from it so a single call can never overflow the " + "window. Absent → inherit " + "`hindsight.llm.context_window`, else a per-provider default " + "(conservative for non-`claude-code` providers, which usually mean " + "a local llama.cpp/Ollama slot; a self-hosted `base_url` — loopback, " + "RFC1918, `.local`/`.internal` — forces the conservative default too, " + "regardless of the provider NAME, since the endpoint is where the " + "traffic actually terminates). All three lanes (`retain`, " + "`reflect`, `consolidation`) are budgeted independently.")
4381
4386
  }).describe("Per-operation LLM override. Every field optional; an unset field (or " + "an omitted op block) inherits the global `hindsight.llm.*`, which is " + "already the engine's fallback — switchroom emits only the vars set.");
4382
4387
  HindsightConfigSchema = exports_external.object({
4383
4388
  llm: exports_external.object({
4384
4389
  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."),
4385
4390
  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."),
4391
+ context_window: exports_external.number().int().positive().optional().describe("GLOBAL context window (tokens) of the backend serving hindsight's " + "LLM ops — the declared size switchroom derives every token budget " + "from. Set this to the real window of whatever you point " + "`hindsight.llm` at (e.g. 32768 for a llama.cpp slot launched with " + "`-c 65536 -np 2`, 131072 for a large-window OpenRouter model). " + "Absent → a per-provider default: 200000 for `claude-code`, a " + "conservative 32768 for everything else. Overflowing a local " + "backend's window does NOT error — llama.cpp context-shift silently " + "drops the system prompt and the model answers conversationally " + "with HTTP 200 — so this value is what makes the failure " + "detectable at setup time instead of never."),
4386
4392
  retain: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `retain` LLM op (memory ingestion). Emits " + "`HINDSIGHT_API_RETAIN_LLM_*`. Absent → uses the global model/provider."),
4387
4393
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
4388
4394
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
4389
4395
  }).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."),
4390
- env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "LLM_MAX_CONCURRENT, RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
4396
+ env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY — a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP — the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
4391
4397
  });
4392
4398
  MicrosoftWorkspaceConfigSchema = exports_external.object({
4393
4399
  microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL — omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
@@ -4523,7 +4529,11 @@ var init_schema = __esm(() => {
4523
4529
  recall: exports_external.object({
4524
4530
  max_memories: exports_external.number().int().min(0).optional(),
4525
4531
  cache_ttl_secs: exports_external.number().int().min(0).optional(),
4526
- min_overlap: exports_external.number().min(0).max(1).optional(),
4532
+ query_max_tokens: exports_external.number().int().min(0).optional(),
4533
+ query_stop_terms: exports_external.array(exports_external.string().min(1).regex(/^[\w./-]+$/)).optional(),
4534
+ request_timeout_seconds: exports_external.number().int().min(1).optional(),
4535
+ own_bank_min_slots: exports_external.number().int().min(0).optional(),
4536
+ additional_bank_min_slots: exports_external.number().int().min(0).optional(),
4527
4537
  additional_banks: exports_external.array(exports_external.string()).optional(),
4528
4538
  sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional()
4529
4539
  }).optional()
@@ -4557,7 +4567,8 @@ var init_schema = __esm(() => {
4557
4567
  memory: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "memory must be a Docker size string like '6g', '512m', '1.5g'").optional().describe("Hard memory cap (Docker `mem_limit` → cgroup memory.max). When the " + "container exceeds this, the kernel OOM-kills processes in the cgroup. " + "Format: '6g', '1.5g', '512m'. When unset at every cascade layer the " + "compose generator falls back to the hard-coded per-profile defaults " + "in src/agents/compose.ts (klanker 6g, coding 2g, conversational 1.5g, " + "lightweight 1g, default 1.5g)."),
4558
4568
  memory_reservation: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "memory_reservation must be a Docker size string like '4g', '256m'").optional().describe("Soft memory floor (Docker `mem_reservation` → cgroup memory.low). " + "Under host-wide memory pressure, the kernel protects at least this " + "much from being reclaimed from the cgroup. Must be ≤ memory. Use to " + "keep an agent RAM-resident when the host has other tenants that " + "might push the box (Coolify apps, build jobs). Default: unset."),
4559
4569
  pids_limit: exports_external.number().int().positive().optional().describe("Max processes the cgroup can spawn (cgroup pids.max). Prevents " + "fork bombs and runaway test runners. Counts every process in the " + "cgroup including bash subprocesses, claude itself, sidecars, and " + "any test/build worker. A typical agent at idle uses ~30 PIDs; " + "`npm test`-style workloads can spike to 200+. Set generously " + "(2000 is a comfortable cap for test-running agents). Default: " + "unset (no cgroup pid cap)."),
4560
- cpus: exports_external.number().positive().optional().describe("CPU quota (Docker `cpus`). Fractional values OK (e.g. 0.5, 2.0). " + "When unset at every cascade layer the compose generator falls " + "back to the per-profile default (klanker/coding 2.0, default 1.0, " + "lightweight 0.5).")
4570
+ cpus: exports_external.number().positive().optional().describe("CPU quota (Docker `cpus`). Fractional values OK (e.g. 0.5, 2.0). " + "When unset at every cascade layer the compose generator falls " + "back to the per-profile default (klanker/coding 2.0, default 1.0, " + "lightweight 0.5)."),
4571
+ tmp_size: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "tmp_size must be a Docker size string like '1g', '4g', '512m'").refine((v) => parseFloat(v) > 0, "tmp_size must be greater than zero").optional().describe("Size of the agent container's RAM-backed `/tmp` (Docker " + "`tmpfs: - /tmp:size=<this>,mode=1777`). The container root FS is " + "read-only, so /tmp is the only scratch space the agent and its " + "sub-agents get for repo clones and toolchain caches (bunx/npx/pip) " + "— a fan-out of several sub-agents exhausts the 1g default. A tmpfs " + "only consumes host RAM for the pages actually written, so raising " + "the ceiling costs nothing until it is used; those pages are still " + "charged to the container's `memory` cap, so raise both together. " + "Format: '1g', '4g', '512m'. Default when unset at every cascade " + "layer: '1g'.")
4561
4572
  }).optional().describe("Per-agent resource limits. Cascades through defaults → profile → " + "per-agent with per-field merge (agent wins on each field independently). " + "Any field left unset at every layer falls back to the hard-coded " + "per-profile defaults in src/agents/compose.ts."),
4562
4573
  experimental: exports_external.object({
4563
4574
  legacy_pty: exports_external.boolean().optional().describe("Opt out of the default tmux supervisor (#725) and run the agent under " + "the legacy PTY supervisor instead. Default: false (tmux is the default)."),
@@ -4650,7 +4661,8 @@ var init_schema = __esm(() => {
4650
4661
  memory: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/).optional(),
4651
4662
  memory_reservation: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/).optional(),
4652
4663
  pids_limit: exports_external.number().int().positive().optional(),
4653
- cpus: exports_external.number().positive().optional()
4664
+ cpus: exports_external.number().positive().optional(),
4665
+ tmp_size: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "tmp_size must be a Docker size string like '1g', '4g', '512m'").refine((v) => parseFloat(v) > 0, "tmp_size must be greater than zero").optional()
4654
4666
  }).optional()
4655
4667
  }).superRefine((agent, ctx) => {
4656
4668
  if (agent.dm_only !== true)
@@ -18223,7 +18235,7 @@ var require_lib = __commonJS((exports, module) => {
18223
18235
 
18224
18236
  // src/vault/broker/server.ts
18225
18237
  import * as net from "node:net";
18226
- import { mkdirSync as mkdirSync7, chmodSync as chmodSync5, chownSync, existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync7, unlinkSync as unlinkSync6, writeFileSync as writeFileSync3, renameSync as renameSync8 } from "node:fs";
18238
+ import { mkdirSync as mkdirSync8, chmodSync as chmodSync5, chownSync as chownSync2, existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync5, statSync as statSync7, unlinkSync as unlinkSync6, writeFileSync as writeFileSync5 } from "node:fs";
18227
18239
 
18228
18240
  // src/agents/compose.ts
18229
18241
  init_schema();
@@ -18865,6 +18877,27 @@ function assertPositive(value, label) {
18865
18877
  // src/setup/host-capabilities.ts
18866
18878
  init_paths();
18867
18879
 
18880
+ // src/setup/hindsight-pg-defaults.ts
18881
+ var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION = 8 * 1024;
18882
+ var HINDSIGHT_PG_APP_ANON_MIB = 2560;
18883
+ var HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB = 2048;
18884
+ var HINDSIGHT_PG_SHARED_BUFFERS_BUDGET_MIB = HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION - HINDSIGHT_PG_APP_ANON_MIB - HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB;
18885
+ var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 1536;
18886
+ var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 4096;
18887
+ function pgMib(mib) {
18888
+ return `${mib}MB`;
18889
+ }
18890
+ var HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV = "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE";
18891
+ var HINDSIGHT_PG_SHARED_BUFFERS_ENV = "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS";
18892
+ var HINDSIGHT_PG_DEFAULTS = [
18893
+ [
18894
+ HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV,
18895
+ pgMib(HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB)
18896
+ ],
18897
+ [HINDSIGHT_PG_SHARED_BUFFERS_ENV, pgMib(HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB)]
18898
+ ];
18899
+ var HINDSIGHT_PG_ENV_KEYS = new Set(HINDSIGHT_PG_DEFAULTS.map(([k]) => k));
18900
+
18868
18901
  // src/setup/hindsight-perf-defaults.ts
18869
18902
  var HINDSIGHT_RERANKER_MAX_CANDIDATES_FOR_DERIVATION = 150;
18870
18903
  var HINDSIGHT_DEFAULT_RECALL_MAX_CANDIDATES_PER_SOURCE = Math.ceil(HINDSIGHT_RERANKER_MAX_CANDIDATES_FOR_DERIVATION * 0.4);
@@ -18875,6 +18908,19 @@ var HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT = 4;
18875
18908
  var HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT = 1;
18876
18909
  var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT = 1;
18877
18910
  var HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16 = "true";
18911
+ var HINDSIGHT_DEFAULT_RERANKER_LOCAL_BATCH_SIZE = 128;
18912
+ var HINDSIGHT_DEFAULT_LLM_STRICT_SCHEMA = "true";
18913
+ var HINDSIGHT_DEFAULT_LLM_MAX_RETRIES = 2;
18914
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 2;
18915
+ var HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000;
18916
+ var HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true";
18917
+ var HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150;
18918
+ var HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4;
18919
+ var HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8;
18920
+ var HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600;
18921
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 500;
18922
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT = 6;
18923
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1;
18878
18924
  var HINDSIGHT_PERF_DEFAULTS_UNGATED = [
18879
18925
  [
18880
18926
  "HINDSIGHT_API_RECALL_MAX_CANDIDATES_PER_SOURCE",
@@ -18888,10 +18934,54 @@ var HINDSIGHT_PERF_DEFAULTS_UNGATED = [
18888
18934
  "HINDSIGHT_API_LINK_EXPANSION_TIMEOUT",
18889
18935
  String(HINDSIGHT_DEFAULT_LINK_EXPANSION_TIMEOUT_S)
18890
18936
  ],
18891
- ["HINDSIGHT_API_LLM_REASONING_EFFORT", HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT]
18937
+ ["HINDSIGHT_API_LLM_REASONING_EFFORT", HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT],
18938
+ [
18939
+ "HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM",
18940
+ String(HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM)
18941
+ ],
18942
+ [
18943
+ "HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE",
18944
+ String(HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE)
18945
+ ],
18946
+ [
18947
+ "HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING",
18948
+ HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING
18949
+ ],
18950
+ [
18951
+ "HINDSIGHT_API_RERANKER_MAX_CANDIDATES",
18952
+ String(HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES)
18953
+ ],
18954
+ [
18955
+ "HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT",
18956
+ String(HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT)
18957
+ ],
18958
+ [
18959
+ "HINDSIGHT_API_RECALL_MAX_CONCURRENT",
18960
+ String(HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT)
18961
+ ],
18962
+ [
18963
+ "HINDSIGHT_API_REFLECT_WALL_TIMEOUT",
18964
+ String(HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S)
18965
+ ],
18966
+ [
18967
+ "HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS",
18968
+ String(HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS)
18969
+ ],
18970
+ [
18971
+ "HINDSIGHT_API_WORKER_CONSOLIDATION_SLOT_LIMIT",
18972
+ String(HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT)
18973
+ ],
18974
+ [
18975
+ "HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND",
18976
+ String(HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND)
18977
+ ]
18892
18978
  ];
18893
18979
  var HINDSIGHT_PERF_DEFAULTS_GPU = [
18894
- ["HINDSIGHT_API_RERANKER_LOCAL_FP16", HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16]
18980
+ ["HINDSIGHT_API_RERANKER_LOCAL_FP16", HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16],
18981
+ [
18982
+ "HINDSIGHT_API_RERANKER_LOCAL_BATCH_SIZE",
18983
+ String(HINDSIGHT_DEFAULT_RERANKER_LOCAL_BATCH_SIZE)
18984
+ ]
18895
18985
  ];
18896
18986
  var HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM = [
18897
18987
  ["HINDSIGHT_API_LLM_MAX_CONCURRENT", String(HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT)],
@@ -18902,34 +18992,26 @@ var HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM = [
18902
18992
  [
18903
18993
  "HINDSIGHT_API_CONSOLIDATION_LLM_MAX_CONCURRENT",
18904
18994
  String(HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT)
18905
- ]
18995
+ ],
18996
+ ["HINDSIGHT_API_LLM_STRICT_SCHEMA", HINDSIGHT_DEFAULT_LLM_STRICT_SCHEMA],
18997
+ ["HINDSIGHT_API_LLM_MAX_RETRIES", String(HINDSIGHT_DEFAULT_LLM_MAX_RETRIES)]
18906
18998
  ];
18999
+ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
19000
+ "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY",
19001
+ "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP"
19002
+ ]);
18907
19003
  var HINDSIGHT_PERF_ENV_KEYS = new Set([
18908
- ...HINDSIGHT_PERF_DEFAULTS_UNGATED,
18909
- ...HINDSIGHT_PERF_DEFAULTS_GPU,
18910
- ...HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM
18911
- ].map(([k]) => k));
19004
+ ...[
19005
+ ...HINDSIGHT_PERF_DEFAULTS_UNGATED,
19006
+ ...HINDSIGHT_PERF_DEFAULTS_GPU,
19007
+ ...HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM
19008
+ ].map(([k]) => k),
19009
+ ...HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS
19010
+ ]);
18912
19011
 
18913
- // src/setup/hindsight-pg-defaults.ts
18914
- var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION = 8 * 1024;
18915
- var HINDSIGHT_PG_APP_ANON_MIB = 2560;
18916
- var HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB = 2048;
18917
- var HINDSIGHT_PG_SHARED_BUFFERS_BUDGET_MIB = HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION - HINDSIGHT_PG_APP_ANON_MIB - HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB;
18918
- var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 1536;
18919
- var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 4096;
18920
- function pgMib(mib) {
18921
- return `${mib}MB`;
18922
- }
18923
- var HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV = "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE";
18924
- var HINDSIGHT_PG_SHARED_BUFFERS_ENV = "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS";
18925
- var HINDSIGHT_PG_DEFAULTS = [
18926
- [
18927
- HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV,
18928
- pgMib(HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB)
18929
- ],
18930
- [HINDSIGHT_PG_SHARED_BUFFERS_ENV, pgMib(HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB)]
18931
- ];
18932
- var HINDSIGHT_PG_ENV_KEYS = new Set(HINDSIGHT_PG_DEFAULTS.map(([k]) => k));
19012
+ // src/setup/hindsight-context-budget.ts
19013
+ var HINDSIGHT_UPSTREAM_RETAIN_CHUNK_SIZE = 3000;
19014
+ var HINDSIGHT_RETAIN_MAX_COMPLETION_FLOOR = HINDSIGHT_UPSTREAM_RETAIN_CHUNK_SIZE + 72;
18933
19015
 
18934
19016
  // src/setup/hindsight.ts
18935
19017
  var HINDSIGHT_DEFAULT_API_PORT = 18888;
@@ -19866,6 +19948,73 @@ function migrateLegacyGrantsDbLocation(newDbPath = getGrantsDbPath(), deps) {
19866
19948
  // src/agents/compose.ts
19867
19949
  var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
19868
19950
 
19951
+ // src/vault/broker/write-token-file.ts
19952
+ import { chownSync, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
19953
+ import { join as join6 } from "node:path";
19954
+ class TokenFileWriteError extends Error {
19955
+ tokenPath;
19956
+ constructor(tokenPath, message, options) {
19957
+ super(message, options);
19958
+ this.name = "TokenFileWriteError";
19959
+ this.tokenPath = tokenPath;
19960
+ }
19961
+ }
19962
+ function writeAgentTokenFile(args, deps = {}) {
19963
+ const { agentsDir, agent, token } = args;
19964
+ const mkdir = deps.mkdir ?? ((p) => {
19965
+ mkdirSync4(p, { recursive: true });
19966
+ });
19967
+ const writeFile = deps.writeFile ?? ((p, d) => {
19968
+ writeFileSync2(p, d, { mode: 384 });
19969
+ });
19970
+ const readFile = deps.readFile ?? ((p) => readFileSync7(p, "utf8"));
19971
+ const chown = deps.chown ?? ((p, uid, gid) => {
19972
+ chownSync(p, uid, gid);
19973
+ });
19974
+ const agentUid = deps.agentUid ?? allocateAgentUid;
19975
+ const tokenDir = join6(agentsDir, agent);
19976
+ const tokenPath = join6(tokenDir, ".vault-token");
19977
+ try {
19978
+ mkdir(tokenDir);
19979
+ } catch (err) {
19980
+ throw new TokenFileWriteError(tokenPath, `cannot create agent token dir ${tokenDir}: ${errText(err)}`, { cause: err });
19981
+ }
19982
+ let preExisting;
19983
+ try {
19984
+ readFile(tokenPath);
19985
+ preExisting = true;
19986
+ } catch {
19987
+ preExisting = false;
19988
+ }
19989
+ try {
19990
+ writeFile(tokenPath, token);
19991
+ } catch (err) {
19992
+ throw new TokenFileWriteError(tokenPath, `cannot write token file ${tokenPath}: ${errText(err)}`, { cause: err });
19993
+ }
19994
+ let onDisk;
19995
+ try {
19996
+ onDisk = readFile(tokenPath);
19997
+ } catch (err) {
19998
+ throw new TokenFileWriteError(tokenPath, `token file ${tokenPath} is unreadable after write: ${errText(err)}`, { cause: err });
19999
+ }
20000
+ if (onDisk !== token) {
20001
+ throw new TokenFileWriteError(tokenPath, `token file ${tokenPath} did not verify after write ` + `(${onDisk.length} bytes on disk, ${token.length} expected)`);
20002
+ }
20003
+ let chownWarning = null;
20004
+ if (!preExisting) {
20005
+ try {
20006
+ const uid = agentUid(agent);
20007
+ chown(tokenPath, uid, uid);
20008
+ } catch (err) {
20009
+ chownWarning = `token file ${tokenPath} created but chown to the agent UID failed: ` + `${errText(err)} (CAP_CHOWN missing?)`;
20010
+ }
20011
+ }
20012
+ return { tokenPath, chownWarning };
20013
+ }
20014
+ function errText(err) {
20015
+ return err instanceof Error ? err.message : String(err);
20016
+ }
20017
+
19869
20018
  // src/vault/broker/server.ts
19870
20019
  import { dirname as dirname7, resolve as resolve9, basename as basename5 } from "node:path";
19871
20020
  import * as os3 from "node:os";
@@ -19878,23 +20027,23 @@ import {
19878
20027
  existsSync as existsSync8,
19879
20028
  fsyncSync as fsyncSync4,
19880
20029
  lstatSync as lstatSync2,
19881
- mkdirSync as mkdirSync4,
20030
+ mkdirSync as mkdirSync5,
19882
20031
  openSync as openSync4,
19883
20032
  closeSync as closeSync4,
19884
- readFileSync as readFileSync7,
20033
+ readFileSync as readFileSync8,
19885
20034
  renameSync as renameSync4,
19886
20035
  statSync as statSync4,
19887
20036
  symlinkSync,
19888
20037
  unlinkSync as unlinkSync3
19889
20038
  } from "node:fs";
19890
20039
  import { createHash as createHash2 } from "node:crypto";
19891
- import { basename as basename3, dirname as dirname4, join as join6 } from "node:path";
20040
+ import { basename as basename3, dirname as dirname4, join as join7 } from "node:path";
19892
20041
  function vaultLayoutPaths(home2) {
19893
- const switchroomRoot = join6(home2, ".switchroom");
20042
+ const switchroomRoot = join7(home2, ".switchroom");
19894
20043
  return {
19895
- oldPath: join6(switchroomRoot, "vault.enc"),
19896
- newPath: join6(switchroomRoot, "vault", "vault.enc"),
19897
- parent: join6(switchroomRoot, "vault"),
20044
+ oldPath: join7(switchroomRoot, "vault.enc"),
20045
+ newPath: join7(switchroomRoot, "vault", "vault.enc"),
20046
+ parent: join7(switchroomRoot, "vault"),
19898
20047
  switchroomRoot
19899
20048
  };
19900
20049
  }
@@ -19943,7 +20092,7 @@ function runMigration(home2, opts) {
19943
20092
  if (oldStat?.isFile() && !newExists) {
19944
20093
  if (opts.dryRun)
19945
20094
  return { kind: "migrated" };
19946
- mkdirSync4(parent, { recursive: true, mode: 448 });
20095
+ mkdirSync5(parent, { recursive: true, mode: 448 });
19947
20096
  const tempNew = `${newPath}.tmp`;
19948
20097
  copyFileSync2(oldPath, tempNew);
19949
20098
  chmodSync2(tempNew, 384);
@@ -19971,11 +20120,11 @@ function lstatSyncOrNull(path2) {
19971
20120
  }
19972
20121
  }
19973
20122
  function sha256File(path2) {
19974
- const data = readFileSync7(path2);
20123
+ const data = readFileSync8(path2);
19975
20124
  return createHash2("sha256").update(data).digest("hex");
19976
20125
  }
19977
20126
  function atomicReplaceWithSymlink(target, linkTarget) {
19978
- const tmp = join6(dirname4(target), `.${basename3(target)}.symlink-tmp`);
20127
+ const tmp = join7(dirname4(target), `.${basename3(target)}.symlink-tmp`);
19979
20128
  if (existsSync8(tmp)) {
19980
20129
  try {
19981
20130
  unlinkSync3(tmp);
@@ -20011,9 +20160,9 @@ import {
20011
20160
  closeSync as closeSync5,
20012
20161
  existsSync as existsSync9,
20013
20162
  fsyncSync as fsyncSync5,
20014
- mkdirSync as mkdirSync5,
20163
+ mkdirSync as mkdirSync6,
20015
20164
  openSync as openSync5,
20016
- readFileSync as readFileSync8,
20165
+ readFileSync as readFileSync9,
20017
20166
  renameSync as renameSync5,
20018
20167
  unlinkSync as unlinkSync4,
20019
20168
  writeSync as writeSync4
@@ -20052,7 +20201,7 @@ function readMachineId() {
20052
20201
  }
20053
20202
  for (const path2 of [MACHINE_ID_PRIMARY, MACHINE_ID_FALLBACK]) {
20054
20203
  try {
20055
- const id = readFileSync8(path2, "utf8").trim();
20204
+ const id = readFileSync9(path2, "utf8").trim();
20056
20205
  if (id.length > 0)
20057
20206
  return id;
20058
20207
  } catch {}
@@ -20094,7 +20243,7 @@ function readAutoUnlockFile(filePath) {
20094
20243
  }
20095
20244
  let blob;
20096
20245
  try {
20097
- blob = readFileSync8(filePath);
20246
+ blob = readFileSync9(filePath);
20098
20247
  } catch {
20099
20248
  throw new AutoUnlockDecryptError("io");
20100
20249
  }
@@ -20175,7 +20324,7 @@ function seedChain(path2) {
20175
20324
  // src/vault/broker/test-isolation-guard.ts
20176
20325
  import { mkdtempSync } from "node:fs";
20177
20326
  import { homedir as homedir5, tmpdir } from "node:os";
20178
- import { join as join7, resolve as resolve8, sep } from "node:path";
20327
+ import { join as join8, resolve as resolve8, sep } from "node:path";
20179
20328
  function isTestRuntime() {
20180
20329
  return process.env.VITEST !== undefined;
20181
20330
  }
@@ -20196,7 +20345,7 @@ function safeVaultPath(requestedPath) {
20196
20345
  return requestedPath;
20197
20346
  if (!isUnderRealSwitchroomHome(requestedPath))
20198
20347
  return requestedPath;
20199
- const redirected = join7(mkdtempSync(join7(tmpdir(), "sw-test-vault-")), "vault.enc");
20348
+ const redirected = join8(mkdtempSync(join8(tmpdir(), "sw-test-vault-")), "vault.enc");
20200
20349
  if (!warnedVault) {
20201
20350
  warnedVault = true;
20202
20351
  process.stderr.write(`[test-isolation guard] vault path redirected away from the ` + `production tree to an isolated tmpdir — a vault/broker test ` + `resolved its vault path inside ~/.switchroom. See CLAUDE.md ` + `"Vault & shared-state test discipline".
@@ -20212,7 +20361,7 @@ function safeAuditLogPath(requestedPath) {
20212
20361
  if (!isUnderRealSwitchroomHome(requestedPath))
20213
20362
  return requestedPath;
20214
20363
  if (!cachedTmpAuditLog) {
20215
- cachedTmpAuditLog = join7(mkdtempSync(join7(tmpdir(), "sw-test-audit-")), "vault-audit.log");
20364
+ cachedTmpAuditLog = join8(mkdtempSync(join8(tmpdir(), "sw-test-audit-")), "vault-audit.log");
20216
20365
  }
20217
20366
  if (!warnedAudit) {
20218
20367
  warnedAudit = true;
@@ -22358,6 +22507,37 @@ function listGrants(db, agent_slug) {
22358
22507
  return rows.map(rowToGrant);
22359
22508
  }
22360
22509
 
22510
+ // src/vault/broker/reap-token-file.ts
22511
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "node:fs";
22512
+ import { join as join11 } from "node:path";
22513
+ var REAPABLE_REASONS = new Set(["grant-invalid", "grant-expired"]);
22514
+ function reapUnusableGrantToken(args, deps = {}) {
22515
+ const { agentsDir, agent, presentedToken, reason } = args;
22516
+ if (!REAPABLE_REASONS.has(reason))
22517
+ return "kept:reason-not-reapable";
22518
+ if (!agent)
22519
+ return "kept:no-agent-identity";
22520
+ const readFile = deps.readFile ?? ((p) => readFileSync11(p, "utf8"));
22521
+ const writeFile = deps.writeFile ?? ((p, d) => writeFileSync4(p, d, { mode: 384 }));
22522
+ const tokenPath = join11(agentsDir, agent, ".vault-token");
22523
+ let onDisk;
22524
+ try {
22525
+ onDisk = readFile(tokenPath);
22526
+ } catch {
22527
+ return "kept:nothing-on-disk";
22528
+ }
22529
+ if (onDisk.trim() === "")
22530
+ return "kept:nothing-on-disk";
22531
+ if (onDisk.trim() !== presentedToken.trim())
22532
+ return "kept:superseded";
22533
+ try {
22534
+ writeFile(tokenPath, "");
22535
+ } catch {
22536
+ return "kept:io-error";
22537
+ }
22538
+ return "reaped";
22539
+ }
22540
+
22361
22541
  // src/vault/admin-only-keys.ts
22362
22542
  function globToRegExp(pattern) {
22363
22543
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
@@ -22919,7 +23099,7 @@ class VaultBroker {
22919
23099
  }
22920
23100
  process.umask(63);
22921
23101
  const parentDir = dirname7(this.socketPath);
22922
- mkdirSync7(parentDir, { recursive: true, mode: 448 });
23102
+ mkdirSync8(parentDir, { recursive: true, mode: 448 });
22923
23103
  try {
22924
23104
  chmodSync5(parentDir, 448);
22925
23105
  } catch {}
@@ -22959,7 +23139,7 @@ class VaultBroker {
22959
23139
  return;
22960
23140
  try {
22961
23141
  if (ready) {
22962
- writeFileSync3(p, "", { mode: 384 });
23142
+ writeFileSync5(p, "", { mode: 384 });
22963
23143
  } else if (existsSync12(p)) {
22964
23144
  unlinkSync6(p);
22965
23145
  }
@@ -23037,7 +23217,7 @@ class VaultBroker {
23037
23217
  const dir = abs.slice(0, -"/sock".length);
23038
23218
  if (existsSync12(dir)) {
23039
23219
  try {
23040
- chownSync(dir, 0, 0);
23220
+ chownSync2(dir, 0, 0);
23041
23221
  } catch {}
23042
23222
  try {
23043
23223
  chmodSync5(dir, 448);
@@ -23065,12 +23245,12 @@ class VaultBroker {
23065
23245
  try {
23066
23246
  agentUid = allocateAgentUid(agentName);
23067
23247
  try {
23068
- chownSync(abs, agentUid, agentUid);
23248
+ chownSync2(abs, agentUid, agentUid);
23069
23249
  } catch {}
23070
23250
  if (abs.endsWith("/sock")) {
23071
23251
  const dir = abs.slice(0, -"/sock".length);
23072
23252
  try {
23073
- chownSync(dir, agentUid, agentUid);
23253
+ chownSync2(dir, agentUid, agentUid);
23074
23254
  } catch {}
23075
23255
  }
23076
23256
  } catch {}
@@ -23097,7 +23277,7 @@ class VaultBroker {
23097
23277
  } catch {}
23098
23278
  if (agentUid !== null) {
23099
23279
  try {
23100
- chownSync(unlockAbs, agentUid, agentUid);
23280
+ chownSync2(unlockAbs, agentUid, agentUid);
23101
23281
  } catch {}
23102
23282
  }
23103
23283
  this.agentServers.set(unlockAbs, { server: unlockServer, agentName });
@@ -23132,7 +23312,7 @@ class VaultBroker {
23132
23312
  return;
23133
23313
  try {
23134
23314
  if (existsSync12(this.vaultPath))
23135
- chownSync(this.vaultPath, uid, uid);
23315
+ chownSync2(this.vaultPath, uid, uid);
23136
23316
  } catch {}
23137
23317
  }
23138
23318
  bindOperatorListener(socketPath, operatorUid) {
@@ -23146,7 +23326,7 @@ class VaultBroker {
23146
23326
  const dir = abs.slice(0, -"/sock".length);
23147
23327
  if (existsSync12(dir)) {
23148
23328
  try {
23149
- chownSync(dir, 0, 0);
23329
+ chownSync2(dir, 0, 0);
23150
23330
  } catch {}
23151
23331
  try {
23152
23332
  chmodSync5(dir, 448);
@@ -23170,7 +23350,7 @@ class VaultBroker {
23170
23350
  chmodSync5(abs, 384);
23171
23351
  } catch {}
23172
23352
  try {
23173
- chownSync(abs, operatorUid, operatorUid);
23353
+ chownSync2(abs, operatorUid, operatorUid);
23174
23354
  } catch {}
23175
23355
  const unlockServer = net.createServer((sock) => {
23176
23356
  this._handleUnlockConnection(sock, true);
@@ -23181,12 +23361,12 @@ class VaultBroker {
23181
23361
  chmodSync5(unlockAbs, 384);
23182
23362
  } catch {}
23183
23363
  try {
23184
- chownSync(unlockAbs, operatorUid, operatorUid);
23364
+ chownSync2(unlockAbs, operatorUid, operatorUid);
23185
23365
  } catch {}
23186
23366
  if (abs.endsWith("/sock")) {
23187
23367
  const dir = abs.slice(0, -"/sock".length);
23188
23368
  try {
23189
- chownSync(dir, operatorUid, operatorUid);
23369
+ chownSync2(dir, operatorUid, operatorUid);
23190
23370
  } catch {}
23191
23371
  try {
23192
23372
  chmodSync5(dir, 448);
@@ -23488,6 +23668,14 @@ class VaultBroker {
23488
23668
  socket.write(encodeResponse(errorResponse("DENIED", "grant-revoked")));
23489
23669
  return;
23490
23670
  }
23671
+ if (!grantResult.ok) {
23672
+ reapUnusableGrantToken({
23673
+ agentsDir: this.config ? resolveAgentsDir(this.config) : path5.join(os3.homedir(), ".switchroom", "agents"),
23674
+ agent: agentName,
23675
+ presentedToken: req.token,
23676
+ reason: grantResult.reason
23677
+ });
23678
+ }
23491
23679
  }
23492
23680
  if (isOperator) {
23493
23681
  const entry2 = this.secrets[req.key];
@@ -24078,22 +24266,39 @@ class VaultBroker {
24078
24266
  }
24079
24267
  try {
24080
24268
  const agentsDir = this.config ? resolveAgentsDir(this.config) : path5.join(os3.homedir(), ".switchroom", "agents");
24081
- const tokenDir = path5.join(agentsDir, agent);
24082
- mkdirSync7(tokenDir, { recursive: true });
24083
- const tokenPath = path5.join(tokenDir, ".vault-token");
24084
- const tmpPath = `${tokenPath}.tmp.${process.pid}`;
24085
- writeFileSync3(tmpPath, mintResult.token, { mode: 384 });
24086
- renameSync8(tmpPath, tokenPath);
24087
- try {
24088
- const uid = allocateAgentUid(agent);
24089
- chownSync(tokenPath, uid, uid);
24090
- } catch (chownErr) {
24091
- process.stderr.write(`[vault-broker] mint_grant: token written but chown failed for agent ${agent}: ${chownErr.message} (CAP_CHOWN missing?)
24269
+ const { chownWarning } = writeAgentTokenFile({
24270
+ agentsDir,
24271
+ agent,
24272
+ token: mintResult.token
24273
+ });
24274
+ if (chownWarning !== null) {
24275
+ process.stderr.write(`[vault-broker] mint_grant: ${chownWarning} (agent ${agent})
24092
24276
  `);
24093
24277
  }
24094
24278
  } catch (err) {
24095
- process.stderr.write(`[vault-broker] mint_grant: failed to write token file for agent ${agent}: ${err.message}
24279
+ const msg = err instanceof Error ? err.message : String(err);
24280
+ let rolledBack = false;
24281
+ let rollbackErr = null;
24282
+ try {
24283
+ rolledBack = revokeGrant(this.grantsDb, mintResult.id);
24284
+ } catch (revokeError) {
24285
+ rollbackErr = revokeError instanceof Error ? revokeError.message : String(revokeError);
24286
+ }
24287
+ const rollbackNote = rolledBack ? "grant rolled back (revoked)" : `GRANT ROLLBACK FAILED — revoke ${mintResult.id} manually${rollbackErr ? `: ${rollbackErr}` : ""}`;
24288
+ process.stderr.write(`[vault-broker] mint_grant: failed to write token file for agent ${agent}: ${msg}; ${rollbackNote}
24096
24289
  `);
24290
+ this.auditLogger.write({
24291
+ ts: new Date().toISOString(),
24292
+ op: "mint_grant",
24293
+ caller: auditCaller,
24294
+ pid: auditPid,
24295
+ cgroup: auditCgroup,
24296
+ result: `error:token-write-failed:${msg}`,
24297
+ method: "grant",
24298
+ grant_id: mintResult.id
24299
+ });
24300
+ socket.write(encodeResponse(errorResponse("INTERNAL", `Failed to write token file for agent ${agent}: ${msg} (${rollbackNote})`)));
24301
+ return;
24097
24302
  }
24098
24303
  this.auditLogger.write({
24099
24304
  ts: new Date().toISOString(),
@@ -24369,7 +24574,7 @@ class VaultBroker {
24369
24574
  _writePidFile() {
24370
24575
  try {
24371
24576
  const pidPath = resolvePath(PID_FILE_DEFAULT);
24372
- writeFileSync3(pidPath, String(process.pid) + `
24577
+ writeFileSync5(pidPath, String(process.pid) + `
24373
24578
  `, { mode: 384 });
24374
24579
  } catch {}
24375
24580
  }
@@ -24425,7 +24630,7 @@ class VaultBroker {
24425
24630
  const credPath = `${dir}/vault-passphrase`;
24426
24631
  let passphrase;
24427
24632
  try {
24428
- passphrase = readFileSync10(credPath, "utf8").replace(/\n+$/, "");
24633
+ passphrase = readFileSync12(credPath, "utf8").replace(/\n+$/, "");
24429
24634
  } catch (err) {
24430
24635
  const code = err.code;
24431
24636
  if (code === "ENOENT") {