switchroom 0.19.23 → 0.19.24

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.
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
2120
2120
  });
2121
2121
 
2122
2122
  // src/build-info.ts
2123
- var VERSION = "0.19.23", COMMIT_SHA = "56e3c01e";
2123
+ var VERSION = "0.19.24", COMMIT_SHA = "0fa8ffb8";
2124
2124
 
2125
2125
  // src/cli/resolve-version.ts
2126
2126
  import { existsSync, readFileSync } from "node:fs";
@@ -13939,17 +13939,19 @@ var init_schema = __esm(() => {
13939
13939
  model: exports_external.string().min(1).optional().describe("Per-op model (upstream `HINDSIGHT_API_<OP>_LLM_MODEL`). Absent \u2192 " + "inherit the global `hindsight.llm.model`."),
13940
13940
  provider: exports_external.string().min(1).optional().describe("Per-op provider (upstream `HINDSIGHT_API_<OP>_LLM_PROVIDER`). " + "Absent \u2192 inherit the global `hindsight.llm.provider`."),
13941
13941
  base_url: exports_external.string().min(1).optional().describe("Per-op base URL (upstream `HINDSIGHT_API_<OP>_LLM_BASE_URL`). " + "Optional passthrough; absent \u2192 inherit the global."),
13942
- api_key: exports_external.string().min(1).optional().describe("Per-op API key (upstream `HINDSIGHT_API_<OP>_LLM_API_KEY`). Literal " + "or `vault:` reference. Optional passthrough; absent \u2192 inherit global.")
13942
+ api_key: exports_external.string().min(1).optional().describe("Per-op API key (upstream `HINDSIGHT_API_<OP>_LLM_API_KEY`). Literal " + "or `vault:` reference. Optional passthrough; absent \u2192 inherit global."),
13943
+ context_window: exports_external.number().int().positive().optional().describe("Context window (tokens) of the backend serving THIS op. NOT an " + "upstream env var \u2014 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 \u2192 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` \u2014 loopback, " + "RFC1918, `.local`/`.internal` \u2014 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.")
13943
13944
  }).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.");
13944
13945
  HindsightConfigSchema = exports_external.object({
13945
13946
  llm: exports_external.object({
13946
13947
  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."),
13947
13948
  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."),
13949
+ context_window: exports_external.number().int().positive().optional().describe("GLOBAL context window (tokens) of the backend serving hindsight's " + "LLM ops \u2014 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 \u2192 a per-provider default: 200000 for `claude-code`, a " + "conservative 32768 for everything else. Overflowing a local " + "backend's window does NOT error \u2014 llama.cpp context-shift silently " + "drops the system prompt and the model answers conversationally " + "with HTTP 200 \u2014 so this value is what makes the failure " + "detectable at setup time instead of never."),
13948
13950
  retain: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `retain` LLM op (memory ingestion). Emits " + "`HINDSIGHT_API_RETAIN_LLM_*`. Absent \u2192 uses the global model/provider."),
13949
13951
  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."),
13950
13952
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
13951
13953
  }).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."),
13952
- 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 \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).")
13954
+ 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, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT), 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), 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).")
13953
13955
  });
13954
13956
  MicrosoftWorkspaceConfigSchema = exports_external.object({
13955
13957
  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)."),
@@ -15645,7 +15647,7 @@ function hindsightPerfEnv(caps, overrides = new Map) {
15645
15647
  }
15646
15648
  return out;
15647
15649
  }
15648
- var HINDSIGHT_RERANKER_MAX_CANDIDATES_FOR_DERIVATION = 150, HINDSIGHT_DEFAULT_RECALL_MAX_CANDIDATES_PER_SOURCE, HINDSIGHT_DEFAULT_LINK_EXPANSION_PER_ENTITY_LIMIT = 50, HINDSIGHT_DEFAULT_LINK_EXPANSION_TIMEOUT_S = 2, HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT = "low", HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT = 1, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT = 1, HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16 = "true", HINDSIGHT_PERF_DEFAULTS_UNGATED, HINDSIGHT_PERF_DEFAULTS_GPU, HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM, HINDSIGHT_PERF_ENV_KEYS;
15650
+ var HINDSIGHT_RERANKER_MAX_CANDIDATES_FOR_DERIVATION = 150, HINDSIGHT_DEFAULT_RECALL_MAX_CANDIDATES_PER_SOURCE, HINDSIGHT_DEFAULT_LINK_EXPANSION_PER_ENTITY_LIMIT = 50, HINDSIGHT_DEFAULT_LINK_EXPANSION_TIMEOUT_S = 2, HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT = "low", HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT = 1, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT = 1, HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16 = "true", HINDSIGHT_DEFAULT_RERANKER_LOCAL_BATCH_SIZE = 128, HINDSIGHT_DEFAULT_LLM_STRICT_SCHEMA = "true", HINDSIGHT_DEFAULT_LLM_MAX_RETRIES = 2, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 2, HINDSIGHT_PERF_DEFAULTS_UNGATED, HINDSIGHT_PERF_DEFAULTS_GPU, HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM, HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS, HINDSIGHT_PERF_ENV_KEYS;
15649
15651
  var init_hindsight_perf_defaults = __esm(() => {
15650
15652
  HINDSIGHT_DEFAULT_RECALL_MAX_CANDIDATES_PER_SOURCE = Math.ceil(HINDSIGHT_RERANKER_MAX_CANDIDATES_FOR_DERIVATION * 0.4);
15651
15653
  HINDSIGHT_PERF_DEFAULTS_UNGATED = [
@@ -15661,10 +15663,18 @@ var init_hindsight_perf_defaults = __esm(() => {
15661
15663
  "HINDSIGHT_API_LINK_EXPANSION_TIMEOUT",
15662
15664
  String(HINDSIGHT_DEFAULT_LINK_EXPANSION_TIMEOUT_S)
15663
15665
  ],
15664
- ["HINDSIGHT_API_LLM_REASONING_EFFORT", HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT]
15666
+ ["HINDSIGHT_API_LLM_REASONING_EFFORT", HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT],
15667
+ [
15668
+ "HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM",
15669
+ String(HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM)
15670
+ ]
15665
15671
  ];
15666
15672
  HINDSIGHT_PERF_DEFAULTS_GPU = [
15667
- ["HINDSIGHT_API_RERANKER_LOCAL_FP16", HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16]
15673
+ ["HINDSIGHT_API_RERANKER_LOCAL_FP16", HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16],
15674
+ [
15675
+ "HINDSIGHT_API_RERANKER_LOCAL_BATCH_SIZE",
15676
+ String(HINDSIGHT_DEFAULT_RERANKER_LOCAL_BATCH_SIZE)
15677
+ ]
15668
15678
  ];
15669
15679
  HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM = [
15670
15680
  ["HINDSIGHT_API_LLM_MAX_CONCURRENT", String(HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT)],
@@ -15675,13 +15685,21 @@ var init_hindsight_perf_defaults = __esm(() => {
15675
15685
  [
15676
15686
  "HINDSIGHT_API_CONSOLIDATION_LLM_MAX_CONCURRENT",
15677
15687
  String(HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT)
15678
- ]
15688
+ ],
15689
+ ["HINDSIGHT_API_LLM_STRICT_SCHEMA", HINDSIGHT_DEFAULT_LLM_STRICT_SCHEMA],
15690
+ ["HINDSIGHT_API_LLM_MAX_RETRIES", String(HINDSIGHT_DEFAULT_LLM_MAX_RETRIES)]
15679
15691
  ];
15692
+ HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
15693
+ "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY"
15694
+ ]);
15680
15695
  HINDSIGHT_PERF_ENV_KEYS = new Set([
15681
- ...HINDSIGHT_PERF_DEFAULTS_UNGATED,
15682
- ...HINDSIGHT_PERF_DEFAULTS_GPU,
15683
- ...HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM
15684
- ].map(([k]) => k));
15696
+ ...[
15697
+ ...HINDSIGHT_PERF_DEFAULTS_UNGATED,
15698
+ ...HINDSIGHT_PERF_DEFAULTS_GPU,
15699
+ ...HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM
15700
+ ].map(([k]) => k),
15701
+ ...HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS
15702
+ ]);
15685
15703
  });
15686
15704
 
15687
15705
  // src/setup/hindsight-pg-defaults.ts
@@ -15731,6 +15749,185 @@ var init_hindsight_pg_defaults = __esm(() => {
15731
15749
  HINDSIGHT_PG_ENV_KEYS = new Set(HINDSIGHT_PG_DEFAULTS.map(([k]) => k));
15732
15750
  });
15733
15751
 
15752
+ // src/setup/self-hosted-url.ts
15753
+ function isLoopbackHttpUrl(url) {
15754
+ try {
15755
+ const u = new URL(url.includes("://") ? url : `http://${url}`);
15756
+ const h = (u.hostname || "").toLowerCase().replace(/^\[|\]$/g, "");
15757
+ return h === "localhost" || h === "127.0.0.1" || h === "::1";
15758
+ } catch {
15759
+ return false;
15760
+ }
15761
+ }
15762
+ function isSelfHostedHttpUrl(url) {
15763
+ let host;
15764
+ try {
15765
+ const u = new URL(url.includes("://") ? url : `http://${url}`);
15766
+ host = (u.hostname || "").toLowerCase().replace(/^\[|\]$/g, "");
15767
+ } catch {
15768
+ return false;
15769
+ }
15770
+ if (!host)
15771
+ return false;
15772
+ if (isLoopbackHttpUrl(url))
15773
+ return true;
15774
+ if (host === "host.docker.internal" || host === "gateway.docker.internal")
15775
+ return true;
15776
+ if (host.endsWith(".local") || host.endsWith(".internal"))
15777
+ return true;
15778
+ if (/^f[cd][0-9a-f]{2}:/.test(host) || /^fe[89ab][0-9a-f]:/.test(host))
15779
+ return true;
15780
+ const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
15781
+ if (!v4)
15782
+ return false;
15783
+ const [a, b] = [Number(v4[1]), Number(v4[2])];
15784
+ if (a === 10)
15785
+ return true;
15786
+ if (a === 127)
15787
+ return true;
15788
+ if (a === 192 && b === 168)
15789
+ return true;
15790
+ if (a === 172 && b >= 16 && b <= 31)
15791
+ return true;
15792
+ if (a === 169 && b === 254)
15793
+ return true;
15794
+ return false;
15795
+ }
15796
+
15797
+ // src/setup/hindsight-context-budget.ts
15798
+ function clamp(value, floor, ceiling) {
15799
+ return Math.min(ceiling, Math.max(floor, value));
15800
+ }
15801
+ function usableContextTokens(windowTokens) {
15802
+ return windowTokens - Math.floor(windowTokens * HINDSIGHT_CONTEXT_SAFETY_FRACTION);
15803
+ }
15804
+ function defaultContextWindowForProvider(provider, baseUrl) {
15805
+ const url = baseUrl?.trim();
15806
+ if (url && isSelfHostedHttpUrl(url))
15807
+ return HINDSIGHT_CONSERVATIVE_CONTEXT_WINDOW;
15808
+ const p = provider.trim().toLowerCase();
15809
+ if (p === "claude-code" || p === "anthropic")
15810
+ return HINDSIGHT_CLAUDE_CONTEXT_WINDOW;
15811
+ return HINDSIGHT_CONSERVATIVE_CONTEXT_WINDOW;
15812
+ }
15813
+ function resolveLaneContextWindow(lane, llm) {
15814
+ const perOp = llm?.[lane];
15815
+ const provider = perOp?.provider?.trim() || llm?.provider?.trim() || "claude-code";
15816
+ const baseUrl = perOp?.base_url?.trim() || undefined;
15817
+ const perOpWindow = perOp?.context_window;
15818
+ if (typeof perOpWindow === "number" && perOpWindow > 0) {
15819
+ return { windowTokens: Math.floor(perOpWindow), windowSource: "per-op", provider, baseUrl };
15820
+ }
15821
+ const globalWindow = llm?.context_window;
15822
+ if (typeof globalWindow === "number" && globalWindow > 0) {
15823
+ return { windowTokens: Math.floor(globalWindow), windowSource: "global", provider, baseUrl };
15824
+ }
15825
+ return {
15826
+ windowTokens: defaultContextWindowForProvider(provider, baseUrl),
15827
+ windowSource: "provider-default",
15828
+ provider,
15829
+ baseUrl
15830
+ };
15831
+ }
15832
+ function resolveHindsightContextBudget(llm) {
15833
+ const cons = resolveLaneContextWindow("consolidation", llm);
15834
+ const ret = resolveLaneContextWindow("retain", llm);
15835
+ const consMaxCompletion = clamp(Math.floor(cons.windowTokens / 4), HINDSIGHT_CONSOLIDATION_MAX_COMPLETION_FLOOR, HINDSIGHT_CONSOLIDATION_MAX_COMPLETION_CEILING);
15836
+ const consUsable = usableContextTokens(cons.windowTokens);
15837
+ const consPromptBudget = consUsable - consMaxCompletion;
15838
+ const batchSize = clamp(Math.floor((consPromptBudget - HINDSIGHT_CONSOLIDATION_PROMPT_OVERHEAD_TOKENS) / HINDSIGHT_CONSOLIDATION_TOKENS_PER_FACT), HINDSIGHT_CONSOLIDATION_BATCH_SIZE_FLOOR, HINDSIGHT_CONSOLIDATION_BATCH_SIZE_CEILING);
15839
+ const consPrompt = HINDSIGHT_CONSOLIDATION_PROMPT_OVERHEAD_TOKENS + batchSize * HINDSIGHT_CONSOLIDATION_TOKENS_PER_FACT;
15840
+ const retMaxCompletion = clamp(Math.floor(ret.windowTokens * 3 / 16), HINDSIGHT_RETAIN_MAX_COMPLETION_FLOOR, HINDSIGHT_RETAIN_MAX_COMPLETION_CEILING);
15841
+ const refl = resolveLaneContextWindow("reflect", llm);
15842
+ const reflCompletionReserve = clamp(Math.floor(refl.windowTokens * 3 / 16), HINDSIGHT_RETAIN_MAX_COMPLETION_FLOOR, HINDSIGHT_RETAIN_MAX_COMPLETION_CEILING);
15843
+ const reflUsable = usableContextTokens(refl.windowTokens);
15844
+ const reflMaxContext = Math.max(HINDSIGHT_REFLECT_MAX_CONTEXT_FLOOR, Math.floor((reflUsable - reflCompletionReserve) / 1000) * 1000);
15845
+ return {
15846
+ consolidation: {
15847
+ lane: "consolidation",
15848
+ windowTokens: cons.windowTokens,
15849
+ usableTokens: consUsable,
15850
+ windowSource: cons.windowSource,
15851
+ provider: cons.provider,
15852
+ batchSize,
15853
+ maxCompletionTokens: consMaxCompletion,
15854
+ estimatedPromptTokens: consPrompt,
15855
+ worstCaseTotalTokens: consPrompt + consMaxCompletion
15856
+ },
15857
+ reflect: {
15858
+ lane: "reflect",
15859
+ windowTokens: refl.windowTokens,
15860
+ usableTokens: reflUsable,
15861
+ windowSource: refl.windowSource,
15862
+ provider: refl.provider,
15863
+ maxContextTokens: reflMaxContext,
15864
+ completionReserveTokens: reflCompletionReserve,
15865
+ estimatedPromptTokens: reflMaxContext,
15866
+ worstCaseTotalTokens: reflMaxContext + reflCompletionReserve
15867
+ },
15868
+ retain: {
15869
+ lane: "retain",
15870
+ windowTokens: ret.windowTokens,
15871
+ usableTokens: usableContextTokens(ret.windowTokens),
15872
+ windowSource: ret.windowSource,
15873
+ provider: ret.provider,
15874
+ maxCompletionTokens: retMaxCompletion,
15875
+ estimatedPromptTokens: HINDSIGHT_RETAIN_PROMPT_ESTIMATE_TOKENS,
15876
+ worstCaseTotalTokens: HINDSIGHT_RETAIN_PROMPT_ESTIMATE_TOKENS + retMaxCompletion
15877
+ }
15878
+ };
15879
+ }
15880
+ function laneCheck(b) {
15881
+ return { ...b, completionTokens: b.maxCompletionTokens, completionLabel: "completion" };
15882
+ }
15883
+ function reflectLaneCheck(b) {
15884
+ return {
15885
+ ...b,
15886
+ completionTokens: b.completionReserveTokens,
15887
+ completionLabel: "completion reserve"
15888
+ };
15889
+ }
15890
+ function laneFailure(b) {
15891
+ if (b.worstCaseTotalTokens > b.usableTokens) {
15892
+ return `hindsight ${b.lane}: worst-case ${b.worstCaseTotalTokens} tokens ` + `(${b.estimatedPromptTokens} prompt + ${b.completionTokens} ${b.completionLabel}) ` + `exceeds the ${b.usableTokens} usable tokens of ` + `the declared ${b.windowTokens}-token context window ` + `(${Math.round(HINDSIGHT_CONTEXT_SAFETY_FRACTION * 100)}% safety band held back; ` + `source: ${b.windowSource}, provider: ${b.provider}).`;
15893
+ }
15894
+ if (b.lane === "retain" && b.maxCompletionTokens !== undefined && b.maxCompletionTokens <= HINDSIGHT_UPSTREAM_RETAIN_CHUNK_SIZE) {
15895
+ return `hindsight retain: derived max_completion_tokens ${b.maxCompletionTokens} is not ` + `greater than the upstream retain_chunk_size ${HINDSIGHT_UPSTREAM_RETAIN_CHUNK_SIZE}; ` + `the hindsight container refuses to boot with this combination.`;
15896
+ }
15897
+ return;
15898
+ }
15899
+ function assertHindsightContextBudgetFits(budget) {
15900
+ for (const lane of [
15901
+ laneCheck(budget.consolidation),
15902
+ laneCheck(budget.retain),
15903
+ reflectLaneCheck(budget.reflect)
15904
+ ]) {
15905
+ const failure = laneFailure(lane);
15906
+ if (failure) {
15907
+ throw new HindsightContextBudgetError(lane.lane, `${failure}
15908
+ ` + `A local backend does NOT error on context overflow \u2014 llama.cpp context-shift ` + `silently drops the system prompt and returns HTTP 200 with conversational text ` + `instead of the required JSON, so this must be caught here.
15909
+ ` + `Fix: raise \`hindsight.llm.context_window\` (or \`hindsight.llm.${lane.lane}.context_window\`) ` + `to the backend's real window, or point the lane at a larger-window model.`);
15910
+ }
15911
+ }
15912
+ }
15913
+ function resolveCheckedHindsightContextBudget(llm) {
15914
+ const budget = resolveHindsightContextBudget(llm);
15915
+ assertHindsightContextBudgetFits(budget);
15916
+ return budget;
15917
+ }
15918
+ var HINDSIGHT_CLAUDE_CONTEXT_WINDOW = 200000, HINDSIGHT_CONSERVATIVE_CONTEXT_WINDOW = 32768, HINDSIGHT_CONTEXT_SAFETY_FRACTION = 0.2, HINDSIGHT_CONSOLIDATION_PROMPT_OVERHEAD_TOKENS = 2270, HINDSIGHT_CONSOLIDATION_TOKENS_PER_FACT = 2500, HINDSIGHT_RETAIN_PROMPT_ESTIMATE_TOKENS = 8000, HINDSIGHT_CONSOLIDATION_BATCH_SIZE_CEILING = 12, HINDSIGHT_CONSOLIDATION_BATCH_SIZE_FLOOR = 1, HINDSIGHT_CONSOLIDATION_MAX_COMPLETION_CEILING = 16384, HINDSIGHT_RETAIN_MAX_COMPLETION_CEILING = 16384, HINDSIGHT_UPSTREAM_RETAIN_CHUNK_SIZE = 3000, HINDSIGHT_CONSOLIDATION_MAX_COMPLETION_FLOOR = 1024, HINDSIGHT_RETAIN_MAX_COMPLETION_FLOOR, HINDSIGHT_REFLECT_MAX_CONTEXT_FLOOR = 2048, HindsightContextBudgetError;
15919
+ var init_hindsight_context_budget = __esm(() => {
15920
+ HINDSIGHT_RETAIN_MAX_COMPLETION_FLOOR = HINDSIGHT_UPSTREAM_RETAIN_CHUNK_SIZE + 72;
15921
+ HindsightContextBudgetError = class HindsightContextBudgetError extends Error {
15922
+ lane;
15923
+ constructor(lane, message) {
15924
+ super(message);
15925
+ this.name = "HindsightContextBudgetError";
15926
+ this.lane = lane;
15927
+ }
15928
+ };
15929
+ });
15930
+
15734
15931
  // src/util/atomic.ts
15735
15932
  var exports_atomic = {};
15736
15933
  __export(exports_atomic, {
@@ -15854,8 +16051,9 @@ function hindsightInteractiveLlmTimeoutSeconds() {
15854
16051
  function hindsightConsolidationLlmTimeoutSeconds() {
15855
16052
  return clientBudgetSeconds(LITELLM_TIMEOUT_TIERS.consolidation);
15856
16053
  }
15857
- function hindsightLlmBudgetEnv() {
15858
- const maxCompletionTokens = HINDSIGHT_DEFAULT_RETAIN_MAX_COMPLETION_TOKENS;
16054
+ function hindsightLlmBudgetEnv(llm) {
16055
+ const contextBudget = resolveCheckedHindsightContextBudget(llm);
16056
+ const maxCompletionTokens = Math.min(HINDSIGHT_DEFAULT_RETAIN_MAX_COMPLETION_TOKENS, contextBudget.retain.maxCompletionTokens);
15859
16057
  const interactiveS = hindsightInteractiveLlmTimeoutSeconds();
15860
16058
  const retainS = hindsightRetainClientTimeoutSeconds();
15861
16059
  const consolidationS = hindsightConsolidationLlmTimeoutSeconds();
@@ -15885,7 +16083,16 @@ function hindsightLlmBudgetEnv() {
15885
16083
  ["HINDSIGHT_API_LLM_TIMEOUT", String(interactiveS)],
15886
16084
  ["HINDSIGHT_API_REFLECT_LLM_TIMEOUT", String(interactiveS)],
15887
16085
  ["HINDSIGHT_API_RETAIN_LLM_TIMEOUT", String(retainS)],
15888
- ["HINDSIGHT_API_CONSOLIDATION_LLM_TIMEOUT", String(consolidationS)]
16086
+ ["HINDSIGHT_API_CONSOLIDATION_LLM_TIMEOUT", String(consolidationS)],
16087
+ ["HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE", String(contextBudget.consolidation.batchSize)],
16088
+ [
16089
+ "HINDSIGHT_API_CONSOLIDATION_MAX_COMPLETION_TOKENS",
16090
+ String(contextBudget.consolidation.maxCompletionTokens)
16091
+ ],
16092
+ [
16093
+ "HINDSIGHT_API_REFLECT_MAX_CONTEXT_TOKENS",
16094
+ String(contextBudget.reflect.maxContextTokens)
16095
+ ]
15889
16096
  ];
15890
16097
  }
15891
16098
  function isPortFree(port) {
@@ -16014,15 +16221,6 @@ function resolveHindsightLlm(llm, litellm) {
16014
16221
  const model = llm?.model?.trim() || (litellm ? litellm.model ?? HINDSIGHT_DEFAULT_LITELLM_MODEL : HINDSIGHT_DEFAULT_MODEL);
16015
16222
  return { provider, model };
16016
16223
  }
16017
- function isLoopbackHttpUrl(url) {
16018
- try {
16019
- const u = new URL(url.includes("://") ? url : `http://${url}`);
16020
- const h = (u.hostname || "").toLowerCase().replace(/^\[|\]$/g, "");
16021
- return h === "localhost" || h === "127.0.0.1" || h === "::1";
16022
- } catch {
16023
- return false;
16024
- }
16025
- }
16026
16224
  function collectHindsightLlmBaseUrls(llm, litellm) {
16027
16225
  const out = [];
16028
16226
  const seen = new Set;
@@ -16045,40 +16243,6 @@ function hindsightNeedsHostNetwork(llm, litellm) {
16045
16243
  return true;
16046
16244
  return collectHindsightLlmBaseUrls(llm).some(isLoopbackHttpUrl);
16047
16245
  }
16048
- function isSelfHostedHttpUrl(url) {
16049
- let host;
16050
- try {
16051
- const u = new URL(url.includes("://") ? url : `http://${url}`);
16052
- host = (u.hostname || "").toLowerCase().replace(/^\[|\]$/g, "");
16053
- } catch {
16054
- return false;
16055
- }
16056
- if (!host)
16057
- return false;
16058
- if (isLoopbackHttpUrl(url))
16059
- return true;
16060
- if (host === "host.docker.internal" || host === "gateway.docker.internal")
16061
- return true;
16062
- if (host.endsWith(".local") || host.endsWith(".internal"))
16063
- return true;
16064
- if (/^f[cd][0-9a-f]{2}:/.test(host) || /^fe[89ab][0-9a-f]:/.test(host))
16065
- return true;
16066
- const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
16067
- if (!v4)
16068
- return false;
16069
- const [a, b] = [Number(v4[1]), Number(v4[2])];
16070
- if (a === 10)
16071
- return true;
16072
- if (a === 127)
16073
- return true;
16074
- if (a === 192 && b === 168)
16075
- return true;
16076
- if (a === 172 && b >= 16 && b <= 31)
16077
- return true;
16078
- if (a === 169 && b === 254)
16079
- return true;
16080
- return false;
16081
- }
16082
16246
  function hindsightLocalLlmEnabled(llm, litellm, override) {
16083
16247
  if (override !== undefined)
16084
16248
  return override;
@@ -16140,13 +16304,11 @@ function startHindsight(ports, litellm, imageTag, llm, mirrorDir, gpu, perf) {
16140
16304
  `HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
16141
16305
  "-e",
16142
16306
  `HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
16143
- ...hindsightLlmBudgetEnv().flatMap(([k, v]) => ["-e", `${k}=${v}`]),
16144
- "-e",
16145
- `HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
16307
+ ...hindsightLlmBudgetEnv(llm).flatMap(([k, v]) => ["-e", `${k}=${v}`]),
16146
16308
  "-e",
16147
16309
  `HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
16148
16310
  "-e",
16149
- `HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
16311
+ `HINDSIGHT_API_WORKER_CONSOLIDATION_SLOT_LIMIT=${HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT}`,
16150
16312
  "-e",
16151
16313
  `HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`,
16152
16314
  "-e",
@@ -16375,10 +16537,9 @@ function generateHindsightComposeSnippet(llm, mirrorDir, litellm, gpu, perf) {
16375
16537
  ` - HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT}`,
16376
16538
  ` - HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
16377
16539
  ` - HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
16378
- ...hindsightLlmBudgetEnv().map(([k, v]) => ` - ${k}=${v}`),
16379
- ` - HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
16540
+ ...hindsightLlmBudgetEnv(llm).map(([k, v]) => ` - ${k}=${v}`),
16380
16541
  ` - HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
16381
- ` - HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
16542
+ ` - HINDSIGHT_API_WORKER_CONSOLIDATION_SLOT_LIMIT=${HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT}`,
16382
16543
  ` - HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`,
16383
16544
  ` - HINDSIGHT_API_WORKER_ID=${HINDSIGHT_DEFAULT_WORKER_ID}`,
16384
16545
  ...hindsightPerfEnvPairs(llm, litellm, gpu, perf).map(([k, v]) => ` - ${k}=${v}`),
@@ -16481,7 +16642,7 @@ async function ensureHindsightConsumer(configPath, account, uid = HINDSIGHT_DEFA
16481
16642
  atomicWriteFileSync2(configPath, tail, mode);
16482
16643
  return { added: true, reason: "added" };
16483
16644
  }
16484
- var HINDSIGHT_DEFAULT_API_PORT = 18888, HINDSIGHT_DEFAULT_UI_PORT = 9999, HINDSIGHT_DEFAULT_MCP_URL, HINDSIGHT_DEFAULT_API_BASE_URL, HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000, HINDSIGHT_CONSUMER_NAME = "hindsight", HINDSIGHT_DEFAULT_WORKER_ID = "switchroom-hindsight", HINDSIGHT_DATA_VOLUME = "switchroom-hindsight-data", HINDSIGHT_DEFAULT_UID = 11000, HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight", HINDSIGHT_IMAGE, HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-5", HINDSIGHT_DEFAULT_LITELLM_MODEL = "openrouter/google/gemini-3.1-flash-lite", HINDSIGHT_DEFAULT_MCP_STATELESS = true, HINDSIGHT_BROKER_SOCK_VOLUME, HINDSIGHT_CREDS_MIRROR_VOLUME, HINDSIGHT_CRED_DIR = "/run/claude-creds", HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true", HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150, HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8, HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600, HINDSIGHT_RETAIN_CHUNK_SIZE = 3000, HINDSIGHT_DEFAULT_RETAIN_MAX_COMPLETION_TOKENS = 16384, HINDSIGHT_RETAIN_MIN_TOKENS_PER_SECOND = 80.6, HINDSIGHT_RETAIN_CLIENT_DEADLINE_S, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 12, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 2, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 100, HINDSIGHT_DEFAULT_MEM_LIMIT = "8g", HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g", HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000, HINDSIGHT_DEFAULT_SHM_SIZE = "2g", HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)', HINDSIGHT_HEALTHCHECK_CMD, DOCKER_PROBE_TIMEOUT_MS, defaultDockerProbe = (args) => {
16645
+ var HINDSIGHT_DEFAULT_API_PORT = 18888, HINDSIGHT_DEFAULT_UI_PORT = 9999, HINDSIGHT_DEFAULT_MCP_URL, HINDSIGHT_DEFAULT_API_BASE_URL, HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000, HINDSIGHT_CONSUMER_NAME = "hindsight", HINDSIGHT_DEFAULT_WORKER_ID = "switchroom-hindsight", HINDSIGHT_DATA_VOLUME = "switchroom-hindsight-data", HINDSIGHT_DEFAULT_UID = 11000, HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight", HINDSIGHT_IMAGE, HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-5", HINDSIGHT_DEFAULT_LITELLM_MODEL = "openrouter/google/gemini-3.1-flash-lite", HINDSIGHT_DEFAULT_MCP_STATELESS = true, HINDSIGHT_BROKER_SOCK_VOLUME, HINDSIGHT_CREDS_MIRROR_VOLUME, HINDSIGHT_CRED_DIR = "/run/claude-creds", HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true", HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150, HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8, HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600, HINDSIGHT_RETAIN_CHUNK_SIZE = 3000, HINDSIGHT_DEFAULT_RETAIN_MAX_COMPLETION_TOKENS = 16384, HINDSIGHT_RETAIN_MIN_TOKENS_PER_SECOND = 80.6, HINDSIGHT_RETAIN_CLIENT_DEADLINE_S, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 500, HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT = 6, HINDSIGHT_DEFAULT_MEM_LIMIT = "8g", HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g", HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000, HINDSIGHT_DEFAULT_SHM_SIZE = "2g", HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)', HINDSIGHT_HEALTHCHECK_CMD, DOCKER_PROBE_TIMEOUT_MS, defaultDockerProbe = (args) => {
16485
16646
  try {
16486
16647
  return execFileSync("docker", args, {
16487
16648
  stdio: "pipe",
@@ -16499,6 +16660,8 @@ var init_hindsight = __esm(() => {
16499
16660
  init_host_capabilities();
16500
16661
  init_hindsight_perf_defaults();
16501
16662
  init_hindsight_pg_defaults();
16663
+ init_hindsight_context_budget();
16664
+ init_hindsight_perf_defaults();
16502
16665
  HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
16503
16666
  HINDSIGHT_DEFAULT_API_BASE_URL = HINDSIGHT_DEFAULT_MCP_URL.replace(/\/mcp\/?$/, "");
16504
16667
  HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
@@ -28932,7 +29095,7 @@ function buildSettingsHooksBlock(p) {
28932
29095
  ` + ` - Question with a short answer \u2192 just reply with the answer.
28933
29096
  ` + " - Complex tool-driven work \u2192 go straight to the tools (the " + "compose-area preview is the ambient liveness signal), then reply " + 'once with the answer or a genuine mid-work pivot ("halfway ' + 'through \u2014 found an unexpected issue, want me to continue?"). Not ' + `"still working".
28934
29097
 
28935
- ` + "NARRATE YOUR INTENT (your reasoning is NOT shown to the user). The " + "user cannot see your thinking \u2014 only your tool activity and your " + "replies. So before you fire a burst of tools or work silently for a " + "while, first write ONE short plain-language line of intent as " + "ordinary working text (NOT a reply tool call): name what you are " + 'about to do and why \u2014 "Now checking the gateway logs to find why ' + 'the turn stalled". The framework mirrors that line into the live ' + "compose-area preview the user watches; because it is plain text and " + "not a reply, it is never a chat message and never pings. One intent " + "line before a silent stretch, in plain words, never raw tool names " + '("calling Bash", "Read(x)") and never a debug dump. And when you run ' + "a Bash command, always give it a plain-English `description` naming " + "the goal \u2014 that description, not the raw command, is what the user " + "sees. This intent narration is the ONE thing to keep saying; it is " + "NOT the placeholder ack banned above (that is a chat reply, which " + `you still skip).
29098
+ ` + "LABEL THE WORK AS YOU DO IT. The user watches a live preview built " + "from your tool activity, so what you put in a tool call is what they " + "read. Two output requirements. FIRST: every Bash and tool call gets a " + 'plain-English `description` naming the goal \u2014 "Checking the gateway ' + 'logs to find why the turn stalled", never raw tool names ("calling ' + 'Bash", "Read(x)") and never a debug dump. That description, not the ' + "raw command, is what the user sees. SECOND: before a long stretch of " + "work, post ONE short status line saying what you are starting \u2014 one " + "line per stretch, in plain words. Emit it as the `description` on the " + "first tool call of the stretch, or as a status line through the " + "progress tool where you have one; do NOT leave it sitting as unsent " + "transcript text (see the reply rule below). This labelling is the ONE " + "thing to keep doing; it is NOT the placeholder ack banned above (that " + `is a chat reply, which you still skip).
28936
29099
 
28937
29100
  ` + 'Do NOT send a trailing confirmation after your answer \u2014 no "Done.", ' + '"Sent.", "Hope that helps." as a separate message once you have ' + "already replied. Your answer is the last thing the user should " + `see; a follow-up "Done." is dead-air clutter (and the user's ` + `device already pinged on the answer). Stop after the answer.
28938
29101
 
@@ -28942,7 +29105,7 @@ function buildSettingsHooksBlock(p) {
28942
29105
 
28943
29106
  ` + "TURN-END SHAPE: the human reads your turn-end on a phone. Build it " + "for a thumb, not a terminal. LEAD WITH THE ONE DECISION OR ANSWER " + "in the first line \u2014 the single thing the user has to know. A routine " + "turn-end is a FEW SHORT LINES, not a multi-section report; if you " + "find yourself writing more than ~6 lines or stacking labelled " + "sections, you are dumping, not answering. ONE IDEA PER MESSAGE: " + "split genuinely separate topics into separate replies rather than " + "piling sections into one bubble. Push detail BELOW the answer or " + 'leave it out \u2014 offer "want the detail?" instead of pre-emptively ' + "dumping logs, full file contents, or a section per thread you " + "touched. RESERVE the long multi-section structured message for when " + 'the operator EXPLICITLY asked to go deep ("full breakdown", "walk me ' + 'through it", "show everything"). Default to short; expand on ' + "request. A wall of text the user has to re-read to find the one fact " + `that matters is the failure this exists to prevent.
28944
29107
 
28945
- ` + 'CRITICAL: "answer" means a call to the reply tool ' + "(mcp__switchroom-telegram__reply). " + "Your terminal/transcript text is NEVER delivered to Telegram \u2014 the " + "user sees only what you send through the reply tool. After a long " + "tool sequence (scheduling, multi-step research, sub-agent handback), " + "do not let your closing narration stand as the answer: end the turn " + "by passing that narration to the reply tool. No reply tool call = the " + "user got nothing, however much text you wrote. Call the reply tool as " + "your FIRST action when you have the answer \u2014 do not write it out as " + "transcript text first and call reply afterward: a framework backstop " + "flushes unsent text after a delay and then your real reply lands late " + "and out of order.</turn-pacing>";
29108
+ ` + 'CRITICAL: "answer" means a call to the reply tool ' + "(mcp__switchroom-telegram__reply). " + "Your terminal/transcript text is NEVER delivered to Telegram \u2014 the " + "user sees only what you send through the reply tool. After a long " + "tool sequence (scheduling, multi-step research, sub-agent handback), " + "do not let your closing narration stand as the answer: end the turn " + "by passing that narration to the reply tool. No reply tool call = the " + "user got nothing, however much text you wrote. Call the reply tool as " + "your FIRST action when you have the answer \u2014 do not write it out as " + "transcript text first and call reply afterward: a framework backstop " + "flushes unsent text after a delay and then your real reply lands late " + "and out of order. This is why the work-labelling above rides on tool " + "`description`s rather than loose prose: a tool description reaches the " + "preview immediately and is never subject to that flush, so there is " + "exactly one rule \u2014 user-facing text goes through a tool call, always." + "</turn-pacing>";
28946
29109
  const switchroomUserPromptSubmit = [
28947
29110
  ...useHotReloadStable ? [
28948
29111
  {
@@ -30000,22 +30163,19 @@ messaging a capable colleague \u2014 not a tool emitting output. Five beats:
30000
30163
  compose the full answer. This holds even for a pure-thinking
30001
30164
  answer: if it will run to a paragraph, ack first. It is the line
30002
30165
  between a colleague and a black box.
30003
- 2. **Then go quiet and work \u2014 but leave a trail of intent.** Heads-down
30166
+ 2. **Then go quiet and work \u2014 but label what you are doing.** Heads-down
30004
30167
  is correct: do NOT narrate every tool call, and a typing indicator
30005
- runs automatically (you do not maintain it). One thing you SHOULD do,
30006
- because your private reasoning is never shown to the user: before a
30007
- burst of tool calls or a long silent stretch, drop ONE short
30008
- plain-language line of intent in your own working text \u2014 *what* you
30009
- are about to do and *why* ("Now checking the gateway logs to find why
30010
- the turn stalled"). Write it as ordinary text, NOT a \`reply\` call, so
30011
- it never becomes a chat message and never pings \u2014 the framework
30012
- mirrors that line into the live preview the user watches while you
30013
- work. One line per stretch, plain words, never raw tool names
30014
- ("calling Bash") or debug dumps. It is the difference between the user
30015
- seeing "checking the logs to find the stall" and seeing a black box.
30016
- When a step is a \`Bash\` command, always give it a plain-English
30017
- \`description\` naming the goal \u2014 that description, not the raw command,
30018
- is what the user sees.
30168
+ runs automatically (you do not maintain it). What you SHOULD do is
30169
+ make the work legible through the tool calls themselves: always give a
30170
+ \`Bash\` (or any tool) call a plain-English \`description\` naming the
30171
+ goal \u2014 "Checking the gateway logs to find why the turn stalled". That
30172
+ description, not the raw command, is what the user sees in the live
30173
+ preview. Before a long silent stretch, put one short status line there
30174
+ too, saying what you are starting. One line per stretch, plain words,
30175
+ never raw tool names ("calling Bash") or debug dumps. It is the
30176
+ difference between the user seeing "checking the logs to find the
30177
+ stall" and seeing a black box. Keep it in the tool call \u2014 loose
30178
+ transcript prose is not delivered and can be flushed late (see beat 5).
30019
30179
  3. **Surface meaningful progress** at genuine inflection points \u2014 a
30020
30180
  hard step finished, a blocker, a pivot, dispatching a sub-agent, a
30021
30181
  notably slow wait, a finding worth knowing now. One short \`reply\`,
@@ -19434,17 +19434,19 @@ var HindsightPerOpLlmSchema = exports_external.object({
19434
19434
  model: exports_external.string().min(1).optional().describe("Per-op model (upstream `HINDSIGHT_API_<OP>_LLM_MODEL`). Absent → " + "inherit the global `hindsight.llm.model`."),
19435
19435
  provider: exports_external.string().min(1).optional().describe("Per-op provider (upstream `HINDSIGHT_API_<OP>_LLM_PROVIDER`). " + "Absent → inherit the global `hindsight.llm.provider`."),
19436
19436
  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."),
19437
- 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.")
19437
+ 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."),
19438
+ 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.")
19438
19439
  }).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.");
19439
19440
  var HindsightConfigSchema = exports_external.object({
19440
19441
  llm: exports_external.object({
19441
19442
  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."),
19442
19443
  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."),
19444
+ 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."),
19443
19445
  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."),
19444
19446
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
19445
19447
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
19446
19448
  }).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."),
19447
- 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).")
19449
+ 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, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT), 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), 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).")
19448
19450
  });
19449
19451
  var MicrosoftWorkspaceConfigSchema = exports_external.object({
19450
19452
  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)."),
@@ -21121,7 +21123,7 @@ function allocateAgentUid(name) {
21121
21123
  }
21122
21124
 
21123
21125
  // src/build-info.ts
21124
- var VERSION = "0.19.23";
21126
+ var VERSION = "0.19.24";
21125
21127
 
21126
21128
  // src/cli/agent-config.ts
21127
21129
  import { join as join2 } from "node:path";
@@ -21224,6 +21226,10 @@ var HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT = 4;
21224
21226
  var HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT = 1;
21225
21227
  var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT = 1;
21226
21228
  var HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16 = "true";
21229
+ var HINDSIGHT_DEFAULT_RERANKER_LOCAL_BATCH_SIZE = 128;
21230
+ var HINDSIGHT_DEFAULT_LLM_STRICT_SCHEMA = "true";
21231
+ var HINDSIGHT_DEFAULT_LLM_MAX_RETRIES = 2;
21232
+ var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 2;
21227
21233
  var HINDSIGHT_PERF_DEFAULTS_UNGATED = [
21228
21234
  [
21229
21235
  "HINDSIGHT_API_RECALL_MAX_CANDIDATES_PER_SOURCE",
@@ -21237,10 +21243,18 @@ var HINDSIGHT_PERF_DEFAULTS_UNGATED = [
21237
21243
  "HINDSIGHT_API_LINK_EXPANSION_TIMEOUT",
21238
21244
  String(HINDSIGHT_DEFAULT_LINK_EXPANSION_TIMEOUT_S)
21239
21245
  ],
21240
- ["HINDSIGHT_API_LLM_REASONING_EFFORT", HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT]
21246
+ ["HINDSIGHT_API_LLM_REASONING_EFFORT", HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT],
21247
+ [
21248
+ "HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM",
21249
+ String(HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM)
21250
+ ]
21241
21251
  ];
21242
21252
  var HINDSIGHT_PERF_DEFAULTS_GPU = [
21243
- ["HINDSIGHT_API_RERANKER_LOCAL_FP16", HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16]
21253
+ ["HINDSIGHT_API_RERANKER_LOCAL_FP16", HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16],
21254
+ [
21255
+ "HINDSIGHT_API_RERANKER_LOCAL_BATCH_SIZE",
21256
+ String(HINDSIGHT_DEFAULT_RERANKER_LOCAL_BATCH_SIZE)
21257
+ ]
21244
21258
  ];
21245
21259
  var HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM = [
21246
21260
  ["HINDSIGHT_API_LLM_MAX_CONCURRENT", String(HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT)],
@@ -21251,13 +21265,21 @@ var HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM = [
21251
21265
  [
21252
21266
  "HINDSIGHT_API_CONSOLIDATION_LLM_MAX_CONCURRENT",
21253
21267
  String(HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT)
21254
- ]
21268
+ ],
21269
+ ["HINDSIGHT_API_LLM_STRICT_SCHEMA", HINDSIGHT_DEFAULT_LLM_STRICT_SCHEMA],
21270
+ ["HINDSIGHT_API_LLM_MAX_RETRIES", String(HINDSIGHT_DEFAULT_LLM_MAX_RETRIES)]
21255
21271
  ];
21272
+ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
21273
+ "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY"
21274
+ ]);
21256
21275
  var HINDSIGHT_PERF_ENV_KEYS = new Set([
21257
- ...HINDSIGHT_PERF_DEFAULTS_UNGATED,
21258
- ...HINDSIGHT_PERF_DEFAULTS_GPU,
21259
- ...HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM
21260
- ].map(([k]) => k));
21276
+ ...[
21277
+ ...HINDSIGHT_PERF_DEFAULTS_UNGATED,
21278
+ ...HINDSIGHT_PERF_DEFAULTS_GPU,
21279
+ ...HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM
21280
+ ].map(([k]) => k),
21281
+ ...HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS
21282
+ ]);
21261
21283
 
21262
21284
  // src/setup/hindsight-pg-defaults.ts
21263
21285
  var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION = 8 * 1024;
@@ -21280,6 +21302,10 @@ var HINDSIGHT_PG_DEFAULTS = [
21280
21302
  ];
21281
21303
  var HINDSIGHT_PG_ENV_KEYS = new Set(HINDSIGHT_PG_DEFAULTS.map(([k]) => k));
21282
21304
 
21305
+ // src/setup/hindsight-context-budget.ts
21306
+ var HINDSIGHT_UPSTREAM_RETAIN_CHUNK_SIZE = 3000;
21307
+ var HINDSIGHT_RETAIN_MAX_COMPLETION_FLOOR = HINDSIGHT_UPSTREAM_RETAIN_CHUNK_SIZE + 72;
21308
+
21283
21309
  // src/setup/hindsight.ts
21284
21310
  var HINDSIGHT_DEFAULT_API_PORT = 18888;
21285
21311
  var HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;