switchroom 0.20.11 → 0.20.13

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 (50) hide show
  1. package/dist/agent-scheduler/index.js +5 -2
  2. package/dist/auth-broker/index.js +32 -25
  3. package/dist/cli/notion-write-pretool.mjs +5 -2
  4. package/dist/cli/self-improve-stop.mjs +13 -1
  5. package/dist/cli/switchroom.js +3069 -1146
  6. package/dist/host-control/main.js +33 -26
  7. package/dist/vault/approvals/kernel-server.js +32 -25
  8. package/dist/vault/broker/server.js +32 -25
  9. package/examples/personal-google-workspace-mcp/compose.yaml +1 -1
  10. package/package.json +7 -4
  11. package/skills/switchroom-architecture/telegram.md +0 -1
  12. package/skills/switchroom-cli/SKILL.md +0 -1
  13. package/skills/switchroom-release/SKILL.md +3 -2
  14. package/telegram-plugin/README.md +2 -11
  15. package/telegram-plugin/bridge/bridge.ts +0 -12
  16. package/telegram-plugin/bunfig.toml +9 -5
  17. package/telegram-plugin/chat-lock.ts +1 -1
  18. package/telegram-plugin/dist/bridge/bridge.js +0 -12
  19. package/telegram-plugin/dist/gateway/gateway.js +219 -127
  20. package/telegram-plugin/dist/server.js +0 -12
  21. package/telegram-plugin/gateway/captured-answer-resume.ts +23 -1
  22. package/telegram-plugin/gateway/gateway.ts +25 -59
  23. package/telegram-plugin/gateway/liveness-wiring.ts +6 -1
  24. package/telegram-plugin/gateway/outbound-send-path.ts +111 -6
  25. package/telegram-plugin/gateway/outbox-sweep.ts +69 -0
  26. package/telegram-plugin/gateway/stale-pin-sweep.ts +4 -3
  27. package/telegram-plugin/gateway/status-pin-store.ts +10 -9
  28. package/telegram-plugin/gateway/stream-render.ts +8 -4
  29. package/telegram-plugin/gateway/turn-record-status.ts +32 -1
  30. package/telegram-plugin/hooks/audience-classify.d.mts +26 -0
  31. package/telegram-plugin/hooks/audience-classify.mjs +193 -0
  32. package/telegram-plugin/hooks/hooks.json +13 -12
  33. package/telegram-plugin/hooks/narration-classify.mjs +1 -2
  34. package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +31 -1
  35. package/telegram-plugin/hooks/silent-end-scan.mjs +9 -2
  36. package/telegram-plugin/outbox.ts +69 -3
  37. package/telegram-plugin/silent-end.ts +48 -5
  38. package/telegram-plugin/status-pin.ts +2 -5
  39. package/telegram-plugin/tests/backstop-exactly-once.test.ts +8 -2
  40. package/telegram-plugin/tests/captured-answer-resume.test.ts +26 -11
  41. package/telegram-plugin/tests/framework-fallback-duration-guard.test.ts +125 -0
  42. package/telegram-plugin/tests/hindsight-bank-preload.test.ts +50 -0
  43. package/telegram-plugin/tests/outbox-live-path-review-4490.test.ts +613 -0
  44. package/telegram-plugin/tests/outbox-self-improve-review.test.ts +401 -0
  45. package/telegram-plugin/tests/pin-message-tool-retired.test.ts +64 -0
  46. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +38 -0
  47. package/telegram-plugin/tests/worker-activity-feed.test.ts +40 -1
  48. package/telegram-plugin/worker-activity-feed.ts +1 -1
  49. package/vendor/hindsight-memory/scripts/recall.py +140 -0
  50. package/vendor/hindsight-memory/scripts/tests/test_recall_latency_instrumentation.py +277 -0
@@ -22025,6 +22025,7 @@ var init_schema = __esm(() => {
22025
22025
  }).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.");
22026
22026
  HindsightConfigSchema = exports_external.object({
22027
22027
  gpu: exports_external.boolean().optional().describe("Force GPU passthrough for the hindsight container on (`true`) or off " + "(`false`), overriding host autodetection in BOTH directions. Absent " + "(the default) \u2192 autodetect from the persisted host-capabilities " + "verdict (`~/.switchroom/host-capabilities.json`), which enables " + "`--gpus all` only when that file proves BOTH a GPU and the nvidia " + "container toolkit. Set `true` when that verdict is wrong or unreadable " + "and you know the host has a working toolkit \u2014 switchroom cannot verify " + "it for you, and `docker run --gpus all` hard-fails container create on " + "a host without one. Set `false` to pin the container to CPU on a GPU " + "host. This is also the declarative opt-out for the recreate-time GPU " + "drop guard (`switchroom memory setup --recreate` refuses to silently " + "turn a GPU container into a CPU one). `--gpu`/`--no-gpu` on `memory " + "setup` override this for a single run."),
22028
+ mem_limit: exports_external.string().regex(/^\d+(\.\d+)?\s*[bkmgt]?b?$/i, "hindsight.mem_limit must be a docker memory size, e.g. `24g` or `16384m`").optional().describe("Hard memory cap for the hindsight container \u2014 docker `--memory` on the " + "`memory setup` path and `mem_limit:` in the emitted compose snippet. A " + "docker size string (`24g`, `16384m`). Absent \u2192 switchroom's default " + "(`HINDSIGHT_DEFAULT_MEM_LIMIT`, 16g). This is a docker HostConfig flag, " + "not a container env var, which is why it is a first-class field here " + "rather than an `env:` key. **Move it together with " + "`env.SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS`**: Postgres pins the " + "buffer pool as unreclaimable shared memory inside this cap, so a cap " + "that does not clear `shared_buffers` by the container's app working " + "set plus its page-cache floor puts Postgres in a near-OOM " + "configuration. switchroom warns, naming both numbers, when it does " + "not. A value below the container's memory RESERVATION (4g) is " + "rejected outright \u2014 docker will not create such a container."),
22028
22029
  cp_access_key: exports_external.string().min(1).optional().describe("Access key for the hindsight control-plane DASHBOARD (upstream " + "`HINDSIGHT_CP_ACCESS_KEY`, port 9999). Literal or \u2014 strongly preferred " + "\u2014 a `vault:` reference such as `vault:hindsight_cp_access_key`, read " + "through the broker at container-launch time. This is the ONLY thing " + "that arms the dashboard's login: upstream's middleware short-circuits " + "to `next()` when the var is unset, so an unset key means the dashboard " + "has no authentication at all, not weak authentication. Because of that, " + "leaving it unset is FAIL-CLOSED: switchroom pins " + "`HINDSIGHT_CP_HOSTNAME=127.0.0.1` so the loginless dashboard is " + "reachable only from the host, and warns. Set this to serve the " + "dashboard on the LAN/tailnet."),
22029
22030
  llm: exports_external.object({
22030
22031
  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."),
@@ -22036,7 +22037,7 @@ var init_schema = __esm(() => {
22036
22037
  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."),
22037
22038
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
22038
22039
  }).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."),
22039
- 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_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS \u2014 only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS \u2014 the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY \u2014 the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE \u2014 the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT \u2014 upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT \u2014 the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S \u2014 the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES \u2014 the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing; and " + "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS \u2014 the max chars of query text " + "dateparser.search_dates() is handed during temporal analysis (0 = " + "unlimited), made live by switchroom's temporal-offload image patch " + "(which moved the synchronous extraction off the shared asyncio loop onto " + "a single-worker thread and bounds its input, ending multi-second loop " + "stalls on multi-KB consolidation queries); unset means the image's baked " + "2000, set 0 to restore an unbounded full scan), 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 \u2014 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 \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
22040
+ 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_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS \u2014 only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS \u2014 the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY \u2014 the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE \u2014 the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT \u2014 upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT \u2014 the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S \u2014 the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES \u2014 the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing; and " + "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS \u2014 the max chars of query text " + "dateparser.search_dates() is handed during temporal analysis (0 = " + "unlimited), made live by switchroom's temporal-offload image patch " + "(which moved the synchronous extraction off the shared asyncio loop onto " + "a single-worker thread and bounds its input, ending multi-second loop " + "stalls on multi-KB consolidation queries); unset means the image's baked " + "2000, set 0 to restore an unbounded full scan); and " + "HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE \u2014 the Postgres " + "text-search regconfig the native BM25 arms and the search_vector index " + "write run to_tsvector/to_tsquery against; unset means upstream's " + "always-safe `english`, set `hindsight_english` to use switchroom's " + "junk-stopping config (dict + stopword file provisioned durably by the " + "image entrypoint, recall degrading to semantic-only if it is ever " + "missing); 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 \u2014 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 \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
22040
22041
  });
22041
22042
  MicrosoftWorkspaceConfigSchema = exports_external.object({
22042
22043
  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)."),
@@ -22489,7 +22490,9 @@ var init_schema = __esm(() => {
22489
22490
  path: exports_external.string().regex(/^[a-zA-Z0-9~._\-/]+$/, "config_repo.path must not contain shell-special characters ($, `, \", ', \\, etc.)").default("~/.switchroom-config").describe("Filesystem path to the operator's private config repo. Must be a git " + "repo (else `switchroom doctor` FAILs). Tilde-expanded at read time."),
22490
22491
  push: exports_external.boolean().default(true).describe("When true, `config-repo sync` pushes after committing (subject to the " + "`require_private` gate). When false, it commits locally only \u2014 useful " + "for offline hosts or a review-before-push workflow."),
22491
22492
  remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
22492
- require_private: exports_external.boolean().default(true).describe("Refuse to PUSH unless the GitHub API confirms the remote repo is " + "private. On a public remote \u2014 or when the API is unreachable \u2014 the " + "push is skipped (commits still land locally) and a WARN is emitted. " + "Fail-safe against exfiltrating memory files / workspace state to a " + "repo that has been flipped public. Default true.")
22493
+ require_private: exports_external.boolean().default(true).describe("Refuse to PUSH unless the GitHub API confirms the remote repo is " + "private. On a public remote \u2014 or when the API is unreachable \u2014 the " + "push is skipped (commits still land locally) and a WARN is emitted. " + "Fail-safe against exfiltrating memory files / workspace state to a " + "repo that has been flipped public. Default true."),
22494
+ interval_minutes: exports_external.number().int().min(5).max(59).default(30).describe("Cadence (minutes) of the scheduled `config-repo sync` tick installed " + "by `switchroom config-repo --install-cron`. Rendered as a cron " + "`*/N * * * *` minute field, so it must be 5..59 (the 5-min floor " + "matches the rest of the fleet's cron floor). Default 30 (Ken's " + "approved cadence). Changing it takes effect on the next " + "`--install-cron` / `switchroom update` reconcile."),
22495
+ include_vault_backup: exports_external.enum(["off", "daily", "every_tick"]).default("daily").describe("Whether the scheduled tick also runs `switchroom vault backup` " + "(an encrypted vault/memory snapshot into <path>/vault-backups/) " + "before the sync. `off`: sync only. `daily`: an extra once-per-day " + "cron leg runs `vault backup && config-repo sync` (the 30-min legs " + "stay sync-only). `every_tick`: every tick runs the backup first. " + "Default `daily` \u2014 the first automated vault backup on this host.")
22493
22496
  });
22494
22497
  SwitchroomConfigSchema = exports_external.object({
22495
22498
  switchroom: exports_external.object({
@@ -49357,7 +49360,7 @@ function createWorkerActivityFeed(opts) {
49357
49360
  const floodWaitRemainingMs = opts.floodWaitRemainingMs ?? (() => 0);
49358
49361
  const minEditInterval = opts.minEditIntervalMs ?? 2500;
49359
49362
  const elapsedRefreshMs = Math.max(minEditInterval, Math.floor(opts.elapsedRefreshMs ?? 15000));
49360
- const firstPaintMin = opts.firstPaintMinMs ?? 8000;
49363
+ const firstPaintMin = opts.firstPaintMinMs ?? 4000;
49361
49364
  const heartbeatTickMs = opts.heartbeatTickMs ?? 6000;
49362
49365
  const maxRows = Math.max(1, Math.floor(opts.maxRows ?? 8));
49363
49366
  const staleWorkerTtlMs = Math.max(1, Math.floor(opts.staleWorkerTtlMs ?? 50 * 60000));
@@ -79040,6 +79043,17 @@ import { homedir as homedir9 } from "node:os";
79040
79043
  // hooks/audience-classify.mjs
79041
79044
  var AUDIENCE_USER = "user";
79042
79045
  var AUDIENCE_INTERNAL = "internal";
79046
+ function decideCaptureAudience(signals) {
79047
+ if (signals?.reviewOriginated === true) {
79048
+ return signals?.reviewTextIsCard === true ? AUDIENCE_USER : AUDIENCE_INTERNAL;
79049
+ }
79050
+ const threw = signals?.replyToolThrewThisTurn === true;
79051
+ if (!threw)
79052
+ return AUDIENCE_USER;
79053
+ if (signals?.openInboundObligation !== false)
79054
+ return AUDIENCE_USER;
79055
+ return AUDIENCE_INTERNAL;
79056
+ }
79043
79057
  function resolveRecordAudience(value) {
79044
79058
  return value === AUDIENCE_INTERNAL ? AUDIENCE_INTERNAL : AUDIENCE_USER;
79045
79059
  }
@@ -79066,6 +79080,31 @@ function formatReplyThrowFraming(s) {
79066
79080
  return `telegram gateway: outbox provenance framing nonce=${s.turnNonce} ` + `turnId=${s.turnId ?? "unknown"} chatId=${s.chatId ?? "unresolved"} ` + `sha=${(s.textSha256 ?? "").slice(0, 12)} source=${s.source ?? "unknown"} ` + `audience=user replyToolThrew=true \u2014 captured prose delivered with its ` + `provenance stated, not suppressed
79067
79081
  `;
79068
79082
  }
79083
+ var SELF_IMPROVEMENT_TITLE = "\uD83D\uDD27 **Self-improvement**";
79084
+ function isSelfImprovementCard(text4) {
79085
+ if (typeof text4 !== "string")
79086
+ return false;
79087
+ return text4.trimStart().startsWith(SELF_IMPROVEMENT_TITLE);
79088
+ }
79089
+ function shouldFrameSelfImprovement(record, opts) {
79090
+ if (opts?.frameEnabled === false)
79091
+ return false;
79092
+ return record?.reviewOriginated === true;
79093
+ }
79094
+ function applySelfImprovementFraming(text4) {
79095
+ const body = typeof text4 === "string" ? text4 : "";
79096
+ if (body.trim().length === 0)
79097
+ return body;
79098
+ if (isSelfImprovementCard(body))
79099
+ return body;
79100
+ return `${SELF_IMPROVEMENT_TITLE}
79101
+
79102
+ ${body}`;
79103
+ }
79104
+ function formatSelfImprovementFraming(s) {
79105
+ return `telegram gateway: outbox self-improvement framing nonce=${s.turnNonce} ` + `turnId=${s.turnId ?? "unknown"} chatId=${s.chatId ?? "unresolved"} ` + `sha=${(s.textSha256 ?? "").slice(0, 12)} source=${s.source ?? "unknown"} ` + `reviewOriginated=true \u2014 review-turn prose delivered with its self-improvement ` + `title, never as raw unlabelled reasoning
79106
+ `;
79107
+ }
79069
79108
  function formatInternalSuppression(s) {
79070
79109
  return `telegram gateway: outbox audience suppression nonce=${s.turnNonce} ` + `turnId=${s.turnId ?? "unknown"} chatId=${s.chatId ?? "unresolved"} ` + `sha=${(s.textSha256 ?? "").slice(0, 12)} ageMs=${s.ageMs ?? 0} ` + `source=${s.source ?? "unknown"} pastWindow=${s.pastWindow === true} ` + `audience=internal \u2014 internal prose withheld from chat, journaled terminally
79071
79110
  `;
@@ -79252,7 +79291,8 @@ function decideOutboxSweep(input) {
79252
79291
  quietMs = OUTBOX_QUIET_MS,
79253
79292
  maxAgeMs = OUTBOX_MAX_AGE_MS,
79254
79293
  shownLedgerHit = false,
79255
- provenanceFraming = true
79294
+ provenanceFraming = true,
79295
+ selfImprovementFraming = true
79256
79296
  } = input;
79257
79297
  if (deliveredNonces.has(record.turnNonce))
79258
79298
  return { action: "skip-journaled" };
@@ -79268,11 +79308,14 @@ function decideOutboxSweep(input) {
79268
79308
  const delayed = age > maxAgeMs;
79269
79309
  const prefix = (delayed ? "(delayed) " : "") + routePrefix;
79270
79310
  const framed = shouldFrameReplyThrow(record, { frameEnabled: provenanceFraming });
79271
- const body = framed ? applyReplyThrowFraming(record.text) : record.text;
79311
+ const provenanceBody = framed ? applyReplyThrowFraming(record.text) : record.text;
79312
+ const selfImproveFramed = shouldFrameSelfImprovement(record, { frameEnabled: selfImprovementFraming }) && !isSelfImprovementCard(record.text);
79313
+ const body = selfImproveFramed ? applySelfImprovementFraming(provenanceBody) : provenanceBody;
79272
79314
  return {
79273
79315
  action: delayed ? "send-delayed" : "send",
79274
79316
  text: prefix + body,
79275
- ...framed && body !== record.text ? { framedProvenance: "reply-throw" } : {}
79317
+ ...framed && provenanceBody !== record.text ? { framedProvenance: "reply-throw" } : {},
79318
+ ...selfImproveFramed && body !== provenanceBody ? { framedSelfImprovement: "self-improve" } : {}
79276
79319
  };
79277
79320
  }
79278
79321
  function resolveOutboxChat(record, deps) {
@@ -79336,7 +79379,6 @@ function isStructuralNarration(text4, followedByToolUse) {
79336
79379
  var EPHEMERAL_TOOLS = new Set([
79337
79380
  "react",
79338
79381
  "send_typing",
79339
- "pin_message",
79340
79382
  "delete_message",
79341
79383
  "edit_message"
79342
79384
  ]);
@@ -79604,7 +79646,10 @@ function journalExternalDelivery(args, stateDir, now = Date.now()) {
79604
79646
  ts: now,
79605
79647
  deliverySource: args.deliverySource ?? "reply-tool",
79606
79648
  ...args.replyAlreadyDeliveredThisTurn == null ? {} : { replyAlreadyDeliveredThisTurn: args.replyAlreadyDeliveredThisTurn },
79607
- ...args.framedProvenance == null ? {} : { framedProvenance: args.framedProvenance }
79649
+ ...args.framedProvenance == null ? {} : { framedProvenance: args.framedProvenance },
79650
+ ...args.audience == null ? {} : { audience: args.audience },
79651
+ ...args.suppressedAudience == null ? {} : { suppressedAudience: args.suppressedAudience },
79652
+ ...args.framedSelfImprovement == null ? {} : { framedSelfImprovement: args.framedSelfImprovement }
79608
79653
  }, stateDir);
79609
79654
  clearOutboxRecord(nonce, stateDir);
79610
79655
  }
@@ -80284,7 +80329,8 @@ function decideCapturedProseDelivery(args, deps) {
80284
80329
  deliver: true,
80285
80330
  text: text4,
80286
80331
  reason: "captured-prose",
80287
- ...state3.replyToolThrewThisTurn === true ? { replyToolThrewThisTurn: true } : {}
80332
+ ...state3.replyToolThrewThisTurn === true ? { replyToolThrewThisTurn: true } : {},
80333
+ ...state3.reviewOriginated === true ? { reviewOriginated: true } : {}
80288
80334
  };
80289
80335
  }
80290
80336
  function settleCapturedProseDelivery(outcome, effects) {
@@ -81365,67 +81411,104 @@ async function deliverCapturedProse(deps, args) {
81365
81411
  const { chatId, threadId, statusKeyStr, registryKey, originTurnId, text: text4, turnDurationMs } = args;
81366
81412
  const now = Date.now();
81367
81413
  const framed = shouldFrameReplyThrow({ replyToolThrewThisTurn: args.replyToolThrewThisTurn }, { frameEnabled: process.env.SWITCHROOM_TG_OUTBOX_PROVENANCE_FRAMING !== "0" });
81414
+ const audienceGateEnabled = process.env.SWITCHROOM_TG_OUTBOX_AUDIENCE_GATE !== "0";
81415
+ const audience = decideCaptureAudience({
81416
+ reviewOriginated: args.reviewOriginated === true,
81417
+ reviewTextIsCard: isSelfImprovementCard(text4)
81418
+ });
81419
+ const suppressed = audienceGateEnabled && audience === AUDIENCE_INTERNAL;
81420
+ const selfImproveFramed = !suppressed && shouldFrameSelfImprovement({ reviewOriginated: args.reviewOriginated }, { frameEnabled: process.env.SWITCHROOM_TG_OUTBOX_SELF_IMPROVE_FRAMING !== "0" }) && !isSelfImprovementCard(text4);
81368
81421
  let outcome;
81369
- const already = outboundDedup.check(chatId, threadId, text4, now, registryKey);
81370
- if (already == null) {
81371
- let out = normalizeParagraphBreaks(repairEscapedWhitespace(framed ? applyReplyThrowFraming(text4) : text4));
81372
- out = redactOutboundText(out, "captured_prose");
81373
- const chunks = splitMarkdownChunks(out, RICH_MESSAGE_MAX_CHARS);
81374
- const sentIds = [];
81375
- try {
81376
- let liveThreadId = threadId;
81377
- for (const c of chunks) {
81378
- const sent = await retryWithThreadFallback(robustApiCall, (tid) => {
81379
- const opts = {
81380
- link_preview_options: { is_disabled: true },
81381
- ...tid != null ? { message_thread_id: tid } : {}
81382
- };
81383
- return bot.api.sendRichMessage(chatId, richMessage(c), opts);
81384
- }, { threadId: liveThreadId, chat_id: chatId, verb: "captured-prose.sendMessage" });
81385
- if (liveThreadId != null && sent.message_thread_id == null) {
81386
- liveThreadId = undefined;
81422
+ if (suppressed) {
81423
+ journalExternalDelivery({
81424
+ turnNonce: originTurnId,
81425
+ text: text4,
81426
+ replyAlreadyDeliveredThisTurn: false,
81427
+ audience,
81428
+ suppressedAudience: AUDIENCE_INTERNAL
81429
+ });
81430
+ process.stderr.write(formatInternalSuppression({
81431
+ turnNonce: originTurnId,
81432
+ turnId: originTurnId,
81433
+ chatId,
81434
+ textSha256: sha256Hex(text4),
81435
+ source: "captured-prose-bridge"
81436
+ }));
81437
+ outcome = "suppressed-internal";
81438
+ } else {
81439
+ const already = outboundDedup.check(chatId, threadId, text4, now, registryKey);
81440
+ if (already == null) {
81441
+ let out = normalizeParagraphBreaks(repairEscapedWhitespace(framed ? applyReplyThrowFraming(text4) : text4));
81442
+ out = selfImproveFramed ? applySelfImprovementFraming(out) : out;
81443
+ out = redactOutboundText(out, "captured_prose");
81444
+ const chunks = splitMarkdownChunks(out, RICH_MESSAGE_MAX_CHARS);
81445
+ const sentIds = [];
81446
+ try {
81447
+ let liveThreadId = threadId;
81448
+ for (const c of chunks) {
81449
+ const sent = await retryWithThreadFallback(robustApiCall, (tid) => {
81450
+ const opts = {
81451
+ link_preview_options: { is_disabled: true },
81452
+ ...tid != null ? { message_thread_id: tid } : {}
81453
+ };
81454
+ return bot.api.sendRichMessage(chatId, richMessage(c), opts);
81455
+ }, { threadId: liveThreadId, chat_id: chatId, verb: "captured-prose.sendMessage" });
81456
+ if (liveThreadId != null && sent.message_thread_id == null) {
81457
+ liveThreadId = undefined;
81458
+ }
81459
+ sentIds.push(sent.message_id);
81387
81460
  }
81388
- sentIds.push(sent.message_id);
81389
- }
81390
- if (HISTORY_ENABLED && sentIds.length > 0) {
81391
- try {
81392
- recordOutbound2({
81393
- chat_id: chatId,
81394
- thread_id: threadId ?? null,
81395
- message_ids: sentIds,
81396
- texts: chunks
81397
- });
81398
- } catch {}
81399
- }
81400
- outboundDedup.record(chatId, threadId, text4, now, registryKey);
81401
- journalExternalDelivery({
81402
- turnNonce: originTurnId,
81403
- text: text4,
81404
- tgMessageId: sentIds[sentIds.length - 1],
81405
- replyAlreadyDeliveredThisTurn: false,
81406
- ...framed ? { framedProvenance: "reply-throw" } : {}
81407
- });
81408
- process.stderr.write(`telegram gateway: captured-prose delivery \u2014 sent ${out.length} chars recovered from ` + `transcript scan (chat=${chatId} origin=${originTurnId})
81409
- `);
81410
- if (framed) {
81411
- process.stderr.write(formatReplyThrowFraming({
81461
+ if (HISTORY_ENABLED && sentIds.length > 0) {
81462
+ try {
81463
+ recordOutbound2({
81464
+ chat_id: chatId,
81465
+ thread_id: threadId ?? null,
81466
+ message_ids: sentIds,
81467
+ texts: chunks
81468
+ });
81469
+ } catch {}
81470
+ }
81471
+ outboundDedup.record(chatId, threadId, text4, now, registryKey);
81472
+ journalExternalDelivery({
81412
81473
  turnNonce: originTurnId,
81413
- turnId: originTurnId,
81414
- chatId,
81415
- textSha256: sha256Hex(text4),
81416
- source: "captured-prose-bridge"
81417
- }));
81474
+ text: text4,
81475
+ tgMessageId: sentIds[sentIds.length - 1],
81476
+ replyAlreadyDeliveredThisTurn: false,
81477
+ ...framed ? { framedProvenance: "reply-throw" } : {},
81478
+ ...selfImproveFramed ? { framedSelfImprovement: "self-improve" } : {},
81479
+ audience
81480
+ });
81481
+ process.stderr.write(`telegram gateway: captured-prose delivery \u2014 sent ${out.length} chars recovered from ` + `transcript scan (chat=${chatId} origin=${originTurnId})
81482
+ `);
81483
+ if (framed) {
81484
+ process.stderr.write(formatReplyThrowFraming({
81485
+ turnNonce: originTurnId,
81486
+ turnId: originTurnId,
81487
+ chatId,
81488
+ textSha256: sha256Hex(text4),
81489
+ source: "captured-prose-bridge"
81490
+ }));
81491
+ }
81492
+ if (selfImproveFramed) {
81493
+ process.stderr.write(formatSelfImprovementFraming({
81494
+ turnNonce: originTurnId,
81495
+ turnId: originTurnId,
81496
+ chatId,
81497
+ textSha256: sha256Hex(text4),
81498
+ source: "captured-prose-bridge"
81499
+ }));
81500
+ }
81501
+ outcome = "sent";
81502
+ } catch (err) {
81503
+ process.stderr.write(`telegram gateway: captured-prose delivery failed: ${err.message} \u2014 ` + `arming the silent-end re-prompt net (recordUndeliveredTurnEnd) so the ` + `answer is recoverable (chat=${chatId} origin=${originTurnId})
81504
+ `);
81505
+ outcome = "failed";
81418
81506
  }
81419
- outcome = "sent";
81420
- } catch (err) {
81421
- process.stderr.write(`telegram gateway: captured-prose delivery failed: ${err.message} \u2014 ` + `arming the silent-end re-prompt net (recordUndeliveredTurnEnd) so the ` + `answer is recoverable (chat=${chatId} origin=${originTurnId})
81507
+ } else {
81508
+ process.stderr.write(`telegram gateway: captured-prose delivery skipped \u2014 this answer already went out ` + `(dedup age=${already.ageMs}ms chat=${chatId} origin=${originTurnId}); settling bookkeeping
81422
81509
  `);
81423
- outcome = "failed";
81510
+ outcome = "skipped-dedup";
81424
81511
  }
81425
- } else {
81426
- process.stderr.write(`telegram gateway: captured-prose delivery skipped \u2014 this answer already went out ` + `(dedup age=${already.ageMs}ms chat=${chatId} origin=${originTurnId}); settling bookkeeping
81427
- `);
81428
- outcome = "skipped-dedup";
81429
81512
  }
81430
81513
  const settlement = settleCapturedProseDelivery(outcome, {
81431
81514
  closeObligation: () => {
@@ -82650,6 +82733,12 @@ function decideTurnEndGate(snapshot) {
82650
82733
  }
82651
82734
 
82652
82735
  // gateway/turn-record-status.ts
82736
+ function computeTurnDurationMs(startedAt, now) {
82737
+ if (typeof startedAt !== "number" || !Number.isFinite(startedAt) || startedAt <= 0) {
82738
+ return 0;
82739
+ }
82740
+ return Math.max(0, now - startedAt);
82741
+ }
82653
82742
  function backstopSendOutcomeGated(args) {
82654
82743
  if (args.threw)
82655
82744
  return "failed";
@@ -83605,7 +83694,7 @@ function handleSessionEvent(deps, ev) {
83605
83694
  chatId,
83606
83695
  threadId,
83607
83696
  turnId: turn.turnId,
83608
- turnDurationMs: turn.startedAt > 0 ? Date.now() - turn.startedAt : 0,
83697
+ turnDurationMs: computeTurnDurationMs(turn.startedAt, Date.now()),
83609
83698
  reactionCount: reactionTransitionCounts.get(tKey) ?? 0
83610
83699
  });
83611
83700
  }
@@ -83623,7 +83712,7 @@ function handleSessionEvent(deps, ev) {
83623
83712
  finalizeStatusReaction(chatId, threadId, "done");
83624
83713
  {
83625
83714
  const sKey = streamKey2(chatId, threadId);
83626
- const turnDurationMs = turn.startedAt > 0 ? Date.now() - turn.startedAt : 0;
83715
+ const turnDurationMs = computeTurnDurationMs(turn.startedAt, Date.now());
83627
83716
  logStreamingEvent2({
83628
83717
  kind: "turn_end",
83629
83718
  chatId,
@@ -83857,7 +83946,7 @@ function handleSessionEvent(deps, ev) {
83857
83946
  finalizeStatusReaction(chatId, threadId, terminalReason);
83858
83947
  {
83859
83948
  const sKey = streamKey2(chatId, threadId);
83860
- const turnDurationMs = turn.startedAt > 0 ? Date.now() - turn.startedAt : 0;
83949
+ const turnDurationMs = computeTurnDurationMs(turn.startedAt, Date.now());
83861
83950
  logStreamingEvent2({
83862
83951
  kind: "turn_end",
83863
83952
  chatId,
@@ -83902,6 +83991,7 @@ function handleSessionEvent(deps, ev) {
83902
83991
  originTurnId: turn.turnId,
83903
83992
  text: proseDecision.text,
83904
83993
  replyToolThrewThisTurn: proseDecision.replyToolThrewThisTurn === true,
83994
+ reviewOriginated: proseDecision.reviewOriginated === true,
83905
83995
  turnDurationMs
83906
83996
  });
83907
83997
  } else {
@@ -87743,6 +87833,27 @@ init_peercred();
87743
87833
  // ../src/agents/scaffold.ts
87744
87834
  init_generation_stamp();
87745
87835
 
87836
+ // ../src/setup/hindsight-recall-passthrough.ts
87837
+ var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
87838
+ var HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT = "Relevant memories from past conversations (prioritize recent when " + "conflicting). Only use memories that are directly useful to continue " + "this conversation; ignore the rest:";
87839
+ var RECALL_PASSTHROUGH_DEFAULTS = Object.freeze({
87840
+ budget: "low",
87841
+ maxTokens: 1024,
87842
+ preferObservations: true,
87843
+ contextTurns: 2,
87844
+ roles: ["user", "assistant"],
87845
+ promptPreamble: HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT,
87846
+ tags: [],
87847
+ tagsMatch: "any",
87848
+ tagGroups: {},
87849
+ tagWeights: HINDSIGHT_RECALL_TAG_WEIGHT_SEED,
87850
+ additionalBankFilters: {},
87851
+ transcriptFallback: true,
87852
+ transcriptTailBytes: 262144,
87853
+ maxQueryChars: 800,
87854
+ parallel: true
87855
+ });
87856
+
87746
87857
  // ../src/setup/hindsight-recall-tunables.ts
87747
87858
  var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
87748
87859
  var MIN_RECALL_HOOK_TIMEOUT_SECONDS = RECALL_DEADLINE_HEADROOM_SECONDS + 1;
@@ -87870,6 +87981,7 @@ var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 12288;
87870
87981
  function pgMib(mib) {
87871
87982
  return `${mib}MB`;
87872
87983
  }
87984
+ var HINDSIGHT_PG_MIN_NON_BUFFER_MIB = HINDSIGHT_PG_APP_ANON_MIB + HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB;
87873
87985
  var HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV = "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE";
87874
87986
  var HINDSIGHT_PG_SHARED_BUFFERS_ENV = "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS";
87875
87987
  var HINDSIGHT_PG_FSYNC_ENV = "SWITCHROOM_HINDSIGHT_PG_FSYNC";
@@ -87917,6 +88029,7 @@ var HINDSIGHT_DEFAULT_GRAPH_SEED_MIN_SIMILARITY = 0.3;
87917
88029
  var HINDSIGHT_DEFAULT_LLM_SUPPORTS_MAX_ITEMS = "false";
87918
88030
  var HINDSIGHT_DEFAULT_RECENCY_DECAY_FUNCTION = "exponential";
87919
88031
  var HINDSIGHT_DEFAULT_RECENCY_DECAY_HALFLIFE_DAYS = 30;
88032
+ var HINDSIGHT_DEFAULT_TEXT_SEARCH_EXTENSION = "pg_search";
87920
88033
  var HINDSIGHT_PERF_DEFAULTS_UNGATED = [
87921
88034
  [
87922
88035
  "HINDSIGHT_API_RECALL_MAX_CANDIDATES_PER_SOURCE",
@@ -87982,7 +88095,8 @@ var HINDSIGHT_PERF_DEFAULTS_UNGATED = [
87982
88095
  [
87983
88096
  "HINDSIGHT_API_GRAPH_SEED_MIN_SIMILARITY",
87984
88097
  String(HINDSIGHT_DEFAULT_GRAPH_SEED_MIN_SIMILARITY)
87985
- ]
88098
+ ],
88099
+ ["HINDSIGHT_API_TEXT_SEARCH_EXTENSION", HINDSIGHT_DEFAULT_TEXT_SEARCH_EXTENSION]
87986
88100
  ];
87987
88101
  var HINDSIGHT_PERF_DEFAULTS_GPU = [
87988
88102
  ["HINDSIGHT_API_RERANKER_LOCAL_FP16", HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16],
@@ -88018,7 +88132,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
88018
88132
  "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
88019
88133
  "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S",
88020
88134
  "HINDSIGHT_API_TEMPORAL_LANGUAGES",
88021
- "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS"
88135
+ "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS",
88136
+ "HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE"
88022
88137
  ]);
88023
88138
  var HINDSIGHT_WORKER_SLOT_TYPES = [
88024
88139
  "consolidation",
@@ -88440,27 +88555,6 @@ init_vault();
88440
88555
  // ../src/setup/onboarding.ts
88441
88556
  init_paths();
88442
88557
 
88443
- // ../src/setup/hindsight-recall-passthrough.ts
88444
- var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
88445
- var HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT = "Relevant memories from past conversations (prioritize recent when " + "conflicting). Only use memories that are directly useful to continue " + "this conversation; ignore the rest:";
88446
- var RECALL_PASSTHROUGH_DEFAULTS = Object.freeze({
88447
- budget: "low",
88448
- maxTokens: 1024,
88449
- preferObservations: true,
88450
- contextTurns: 2,
88451
- roles: ["user", "assistant"],
88452
- promptPreamble: HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT,
88453
- tags: [],
88454
- tagsMatch: "any",
88455
- tagGroups: {},
88456
- tagWeights: HINDSIGHT_RECALL_TAG_WEIGHT_SEED,
88457
- additionalBankFilters: {},
88458
- transcriptFallback: true,
88459
- transcriptTailBytes: 262144,
88460
- maxQueryChars: 800,
88461
- parallel: true
88462
- });
88463
-
88464
88558
  // ../src/repos/bare-clone.ts
88465
88559
  init_paths();
88466
88560
 
@@ -94204,6 +94298,7 @@ function hydrateResumeLedger(ledger, turnId, snapshot, hasLanded) {
94204
94298
  }
94205
94299
  function createCapturedResumeDispatcher(ports) {
94206
94300
  const stderr = ports.stderr ?? (() => {});
94301
+ const hasOutboundWithText3 = ports.hasOutboundWithText ?? hasOutboundWithText2;
94207
94302
  const inFlight = new Set;
94208
94303
  function deliver(o, snapshot) {
94209
94304
  return ports.deliverAnswer({
@@ -94215,7 +94310,7 @@ function createCapturedResumeDispatcher(ports) {
94215
94310
  replyToMessageId: o.messageId,
94216
94311
  resume: {
94217
94312
  snapshot,
94218
- hydrate: (ledger, turnId) => hydrateResumeLedger(ledger, turnId, snapshot, (txt) => ports.historyEnabled && hasOutboundWithText2(o.chatId, txt, o.threadId ?? null, o.openedAt))
94313
+ hydrate: (ledger, turnId) => hydrateResumeLedger(ledger, turnId, snapshot, (txt) => ports.historyEnabled && hasOutboundWithText3(o.chatId, txt, o.threadId ?? null, o.openedAt))
94219
94314
  }
94220
94315
  });
94221
94316
  }
@@ -95142,6 +95237,12 @@ function maybeRotate(path3, fs3, maxBytes = TURNS_JSONL_MAX_BYTES) {
95142
95237
  }
95143
95238
 
95144
95239
  // gateway/turn-record-status.ts
95240
+ function computeTurnDurationMs2(startedAt, now) {
95241
+ if (typeof startedAt !== "number" || !Number.isFinite(startedAt) || startedAt <= 0) {
95242
+ return 0;
95243
+ }
95244
+ return Math.max(0, now - startedAt);
95245
+ }
95145
95246
  function computeTurnStatus(turn) {
95146
95247
  switch (turn.deliveryOutcome) {
95147
95248
  case "failed":
@@ -95170,7 +95271,7 @@ function buildTurnRecord(turn, endedAt) {
95170
95271
  return {
95171
95272
  ts: Math.floor(endedAt / 1000),
95172
95273
  agent: turn.agent,
95173
- duration_ms: turn.startedAt > 0 ? endedAt - turn.startedAt : 0,
95274
+ duration_ms: computeTurnDurationMs2(turn.startedAt, endedAt),
95174
95275
  tools: turn.toolCallCount ?? 0,
95175
95276
  status: computeTurnStatus(turn),
95176
95277
  turn_id: turn.turnId,
@@ -96933,7 +97034,7 @@ function buildSilencePokeOptions(deps) {
96933
97034
  const tearsDownLiveTurn = turnMatchesFallback && wedgedTurn != null && turnLiveForItsTopic(wedgedTurn);
96934
97035
  const turnStartedAt = activeTurnStartedAt.get(fbKey);
96935
97036
  if (turnStartedAt != null) {
96936
- const turnDurationMs = Date.now() - turnStartedAt;
97037
+ const turnDurationMs = computeTurnDurationMs(turnStartedAt, Date.now());
96937
97038
  const outboundMetrics = getOutboundMetrics2(fbKey);
96938
97039
  emitRuntimeMetric2({
96939
97040
  kind: "turn_ended",
@@ -103016,6 +103117,8 @@ async function sweepOutbox(deps) {
103016
103117
  const escalateInternalSuppression = deps.escalateInternalSuppression ?? log;
103017
103118
  const provenanceFramingEnabled = deps.provenanceFramingEnabled?.() ?? process.env.SWITCHROOM_TG_OUTBOX_PROVENANCE_FRAMING !== "0";
103018
103119
  const logProvenanceFraming = deps.logProvenanceFraming ?? log;
103120
+ const selfImprovementFramingEnabled = deps.selfImprovementFramingEnabled?.() ?? process.env.SWITCHROOM_TG_OUTBOX_SELF_IMPROVE_FRAMING !== "0";
103121
+ const logSelfImprovementFraming = deps.logSelfImprovementFraming ?? log;
103019
103122
  for (const record2 of records) {
103020
103123
  summary.scanned++;
103021
103124
  if (shouldSuppressForAudience(record2, { gateEnabled: audienceGateEnabled })) {
@@ -103061,7 +103164,8 @@ async function sweepOutbox(deps) {
103061
103164
  routePrefix,
103062
103165
  quietMs: deps.quietMs ?? OUTBOX_QUIET_MS,
103063
103166
  shownLedgerHit: isShownBlock(record2.turnNonce, record2.text, deps.stateDir),
103064
- provenanceFraming: provenanceFramingEnabled
103167
+ provenanceFraming: provenanceFramingEnabled,
103168
+ selfImprovementFraming: selfImprovementFramingEnabled
103065
103169
  });
103066
103170
  if (decision.action !== "send" && decision.action !== "send-delayed") {
103067
103171
  summary.skipped++;
@@ -103105,7 +103209,8 @@ async function sweepOutbox(deps) {
103105
103209
  ts: now,
103106
103210
  deliverySource: "sweep",
103107
103211
  replyAlreadyDeliveredThisTurn: record2.replyAlreadyDeliveredThisTurn === true,
103108
- ...decision.framedProvenance != null ? { framedProvenance: decision.framedProvenance } : {}
103212
+ ...decision.framedProvenance != null ? { framedProvenance: decision.framedProvenance } : {},
103213
+ ...decision.framedSelfImprovement != null ? { framedSelfImprovement: decision.framedSelfImprovement } : {}
103109
103214
  }, deps.stateDir);
103110
103215
  if (decision.framedProvenance != null) {
103111
103216
  summary.provenanceFramed = (summary.provenanceFramed ?? 0) + 1;
@@ -103117,6 +103222,16 @@ async function sweepOutbox(deps) {
103117
103222
  source: record2.source
103118
103223
  }));
103119
103224
  }
103225
+ if (decision.framedSelfImprovement != null) {
103226
+ summary.selfImprovementFramed = (summary.selfImprovementFramed ?? 0) + 1;
103227
+ logSelfImprovementFraming(formatSelfImprovementFraming({
103228
+ turnNonce: record2.turnNonce,
103229
+ turnId: turnIdFromNonce(record2.turnNonce),
103230
+ chatId: resolvedChat.chatId,
103231
+ textSha256: record2.textSha256,
103232
+ source: record2.source
103233
+ }));
103234
+ }
103120
103235
  if (deps.recordOutbound != null && sendResult.chunks.length > 0) {
103121
103236
  try {
103122
103237
  deps.recordOutbound(resolvedChat.chatId, resolvedChat.threadId, sendResult.chunks.map((c) => c.messageId), sendResult.chunks.map((c) => c.text));
@@ -103323,10 +103438,10 @@ function startOutboxSweep(deps) {
103323
103438
  }
103324
103439
 
103325
103440
  // ../src/build-info.ts
103326
- var VERSION2 = "0.20.11";
103327
- var COMMIT_SHA = "e083a99d";
103328
- var COMMIT_DATE = "2026-08-06T04:35:43Z";
103329
- var LATEST_PR = 4449;
103441
+ var VERSION2 = "0.20.13";
103442
+ var COMMIT_SHA = "a73b0fd2";
103443
+ var COMMIT_DATE = "2026-08-07T03:01:37Z";
103444
+ var LATEST_PR = 4502;
103330
103445
  var COMMITS_AHEAD_OF_TAG = 0;
103331
103446
 
103332
103447
  // gateway/boot-version.ts
@@ -108773,10 +108888,6 @@ function clearQueuedCard(key, messageId) {
108773
108888
  }
108774
108889
  var bannerPinPersistEnabled = !STATIC;
108775
108890
  var toolPinPersistEnabled = !STATIC;
108776
- var TOOL_PIN_TTL_MS = (() => {
108777
- const v = Number(process.env.SWITCHROOM_TOOL_PIN_TTL_MS);
108778
- return Number.isFinite(v) && v > 0 ? v : 604800000;
108779
- })();
108780
108891
  function statusPinApi() {
108781
108892
  return createStatusPinApi(() => lockedBot, robustApiCall);
108782
108893
  }
@@ -110377,7 +110488,6 @@ var ALLOWED_TOOLS = new Set([
110377
110488
  "download_attachment",
110378
110489
  "edit_message",
110379
110490
  "send_typing",
110380
- "pin_message",
110381
110491
  "delete_message",
110382
110492
  "forward_message",
110383
110493
  "get_recent_messages",
@@ -110411,8 +110521,6 @@ async function executeToolCall(tool, args, callerAgentName = null) {
110411
110521
  return executeEditMessage(args);
110412
110522
  case "send_typing":
110413
110523
  return executeSendTyping(args);
110414
- case "pin_message":
110415
- return executePinMessage(args);
110416
110524
  case "delete_message":
110417
110525
  return executeDeleteMessage(args);
110418
110526
  case "forward_message":
@@ -111218,27 +111326,6 @@ async function executeSendTyping(args) {
111218
111326
  }
111219
111327
  return { content: [{ type: "text", text: `${action} indicator sent (auto-refreshes every 4s, stops after 30s or next reply)` }] };
111220
111328
  }
111221
- async function executePinMessage(args) {
111222
- if (!args.chat_id)
111223
- throw new Error("pin_message: chat_id is required");
111224
- if (!args.message_id)
111225
- throw new Error("pin_message: message_id is required");
111226
- const pinChatId = String(args.chat_id ?? "");
111227
- assertAllowedChat(pinChatId);
111228
- const pinMsgId = Number(args.message_id);
111229
- await robustApiCall(() => lockedBot.api.pinChatMessage(pinChatId, pinMsgId), { chat_id: pinChatId, verb: "pin_message" });
111230
- statusPinRightsCache.clear(pinChatId);
111231
- if (toolPinPersistEnabled) {
111232
- const toolPinKey = `tool:${pinChatId}:${pinMsgId}`;
111233
- mutateStatusPinRow(STATUS_PIN_STORE_PATH, statusPinStoreFs, toolPinKey, {
111234
- pinKey: toolPinKey,
111235
- chatId: pinChatId,
111236
- messageId: pinMsgId,
111237
- expiresAt: Date.now() + TOOL_PIN_TTL_MS
111238
- });
111239
- }
111240
- return { content: [{ type: "text", text: `pinned message ${args.message_id}` }] };
111241
- }
111242
111329
  async function executeDeleteMessage(args) {
111243
111330
  if (!args.chat_id)
111244
111331
  throw new Error("delete_message: chat_id is required");
@@ -117137,6 +117224,10 @@ async function startGateway() {
117137
117224
  const raw = Number(process.env.SWITCHROOM_TG_WORKER_FEED_MAX_ROWS);
117138
117225
  return Number.isInteger(raw) && raw > 0 ? raw : undefined;
117139
117226
  })();
117227
+ const workerFeedFirstPaintMs = (() => {
117228
+ const raw = Number(process.env.SWITCHROOM_TG_WORKER_FEED_FIRST_PAINT_MS);
117229
+ return Number.isInteger(raw) && raw > 0 ? raw : undefined;
117230
+ })();
117140
117231
  const foregroundNestingEnabled = process.env.SWITCHROOM_FOREGROUND_SUBAGENT_NESTING !== "0";
117141
117232
  const orphanStatusEnabled = isOrphanSubagentStatusEnabled(process.env.SWITCHROOM_ORPHAN_SUBAGENT_STATUS);
117142
117233
  if (workerActivityFeed != null) {
@@ -117159,6 +117250,7 @@ async function startGateway() {
117159
117250
  },
117160
117251
  floodWaitRemainingMs: probeFloodWaitRemainingMs,
117161
117252
  maxRows: workerFeedMaxRows,
117253
+ firstPaintMinMs: workerFeedFirstPaintMs,
117162
117254
  staleWorkerTtlMs: resolveInflightTerminalCapMs() + WORKER_FEED_STALE_TTL_MARGIN_MS,
117163
117255
  absoluteRowLifetimeCapMs: resolveInflightTerminalCapMs() * WORKER_FEED_ABSOLUTE_ROW_LIFETIME_CAP_MULTIPLE,
117164
117256
  groupMessageLifetimeCapMs: WORKER_FEED_GROUP_MESSAGE_LIFETIME_CAP_MS,