switchroom 0.19.27 → 0.19.29
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.
- package/dist/agent-scheduler/index.js +16 -2
- package/dist/auth-broker/index.js +141 -8
- package/dist/cli/autoaccept-poll.js +225 -17
- package/dist/cli/notion-write-pretool.mjs +18 -2
- package/dist/cli/switchroom.js +864 -43
- package/dist/host-control/main.js +144 -9
- package/dist/vault/approvals/kernel-server.js +146 -9
- package/dist/vault/broker/server.js +146 -9
- package/package.json +3 -2
- package/profiles/_base/start.sh.hbs +79 -15
- package/telegram-plugin/dist/bridge/bridge.js +1 -0
- package/telegram-plugin/dist/gateway/gateway.js +585 -50
- package/telegram-plugin/dist/server.js +1 -0
- package/telegram-plugin/edit-flood-fuse.ts +230 -27
- package/telegram-plugin/gateway/callback-query-handlers.ts +6 -0
- package/telegram-plugin/gateway/gateway.ts +9 -2
- package/telegram-plugin/gateway/mcp-failure-hook.ts +74 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +202 -21
- package/telegram-plugin/mcp-credential-failure.ts +459 -0
- package/telegram-plugin/operator-events.ts +38 -0
- package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +58 -1
- package/telegram-plugin/tests/edit-flood-fuse-reply-reserve.test.ts +340 -0
- package/telegram-plugin/tests/finalize-callback-flood-policy.test.ts +298 -0
- package/telegram-plugin/tests/finalize-callback.test.ts +41 -8
- package/telegram-plugin/tests/mcp-credential-failure.test.ts +310 -0
- package/vendor/hindsight-memory/CHANGELOG.md +50 -0
- package/vendor/hindsight-memory/scripts/backfill_transcripts.py +1 -0
- package/vendor/hindsight-memory/scripts/drain_pending.py +437 -11
- package/vendor/hindsight-memory/scripts/lib/client.py +14 -0
- package/vendor/hindsight-memory/scripts/lib/config.py +91 -0
- package/vendor/hindsight-memory/scripts/lib/pending.py +199 -28
- package/vendor/hindsight-memory/scripts/reconcile_tail.py +1 -0
- package/vendor/hindsight-memory/scripts/retain.py +41 -1
- package/vendor/hindsight-memory/scripts/subagent_retain.py +1 -0
- package/vendor/hindsight-memory/scripts/tests/test_backfill.py +23 -1
- package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +5 -1
- package/vendor/hindsight-memory/scripts/tests/test_drain_circuit_breaker.py +401 -0
- package/vendor/hindsight-memory/scripts/tests/test_drain_serialisation.py +286 -0
- package/vendor/hindsight-memory/scripts/tests/test_observation_scopes.py +325 -0
- package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +817 -8
- package/vendor/hindsight-memory/scripts/tests/test_reconcile_durability.py +160 -1
- package/vendor/hindsight-memory/scripts/tests/test_subagent_retain.py +40 -2
- package/vendor/hindsight-memory/settings.json +1 -1
- package/vendor/hindsight-memory/tests/test_hooks.py +11 -2
package/dist/cli/switchroom.js
CHANGED
|
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
|
|
|
2120
2120
|
});
|
|
2121
2121
|
|
|
2122
2122
|
// src/build-info.ts
|
|
2123
|
-
var VERSION = "0.19.
|
|
2123
|
+
var VERSION = "0.19.29", COMMIT_SHA = "c618451c";
|
|
2124
2124
|
|
|
2125
2125
|
// src/cli/resolve-version.ts
|
|
2126
2126
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -13571,13 +13571,46 @@ var init_zod = __esm(() => {
|
|
|
13571
13571
|
init_external();
|
|
13572
13572
|
});
|
|
13573
13573
|
|
|
13574
|
+
// src/memory/observation-scopes.ts
|
|
13575
|
+
function isObservationScope(value) {
|
|
13576
|
+
return typeof value === "string" && OBSERVATION_SCOPES.includes(value);
|
|
13577
|
+
}
|
|
13578
|
+
function observationScopesHint() {
|
|
13579
|
+
return OBSERVATION_SCOPES.join(", ");
|
|
13580
|
+
}
|
|
13581
|
+
function classifyObservationScope(raw) {
|
|
13582
|
+
if (raw === undefined)
|
|
13583
|
+
return { kind: "unset" };
|
|
13584
|
+
const value = raw.trim();
|
|
13585
|
+
if (!value)
|
|
13586
|
+
return { kind: "unset" };
|
|
13587
|
+
if (!isObservationScope(value))
|
|
13588
|
+
return { kind: "invalid", raw };
|
|
13589
|
+
return { kind: "set", scope: value };
|
|
13590
|
+
}
|
|
13591
|
+
function resolveObservationScope(configured, env2 = process.env) {
|
|
13592
|
+
if (configured !== undefined)
|
|
13593
|
+
return classifyObservationScope(configured);
|
|
13594
|
+
return classifyObservationScope(env2[OBSERVATION_SCOPES_ENV]);
|
|
13595
|
+
}
|
|
13596
|
+
var OBSERVATION_SCOPES, OBSERVATION_SCOPES_ENV = "HINDSIGHT_OBSERVATION_SCOPES";
|
|
13597
|
+
var init_observation_scopes = __esm(() => {
|
|
13598
|
+
OBSERVATION_SCOPES = [
|
|
13599
|
+
"per_tag",
|
|
13600
|
+
"combined",
|
|
13601
|
+
"all_combinations",
|
|
13602
|
+
"shared"
|
|
13603
|
+
];
|
|
13604
|
+
});
|
|
13605
|
+
|
|
13574
13606
|
// src/config/schema.ts
|
|
13575
13607
|
function isValidTimezone(value) {
|
|
13576
13608
|
return TIMEZONE_REGEX.test(value);
|
|
13577
13609
|
}
|
|
13578
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
|
|
13610
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
|
|
13579
13611
|
var init_schema = __esm(() => {
|
|
13580
13612
|
init_zod();
|
|
13613
|
+
init_observation_scopes();
|
|
13581
13614
|
CodeRepoEntrySchema = exports_external.object({
|
|
13582
13615
|
name: exports_external.string().describe("Short alias used when claiming (e.g. 'switchroom')"),
|
|
13583
13616
|
source: exports_external.string().describe("Absolute or home-relative path to the repo (e.g. ~/code/switchroom)"),
|
|
@@ -13689,6 +13722,7 @@ var init_schema = __esm(() => {
|
|
|
13689
13722
|
allow: exports_external.array(exports_external.string()).default([]).describe("Allowed tools (use ['all'] for unrestricted)"),
|
|
13690
13723
|
deny: exports_external.array(exports_external.string()).default([]).describe("Denied tools (overrides allow)")
|
|
13691
13724
|
}).optional();
|
|
13725
|
+
ObservationScopesSchema = exports_external.enum(OBSERVATION_SCOPES).optional().describe("Per-row observation scope stamped on every memory this agent " + `retains. "shared" makes Hindsight's consolidation write the ` + "resulting observations into ONE global untagged scope instead of a " + "scope per tag \u2014 what several agents pooling one bank need so their " + "observations actually merge rather than sitting in parallel " + "per-tag silos. OMITTED BY DEFAULT: unset means the field never goes " + "on the wire and the engine's own default stands, which is the " + "shipped behaviour. Applies to every retain path (Stop hook, " + "sidechain, boot reconcile, queue drain, backfill, session-handoff " + "mirror) and is carried on the queued payload, so a retain that fails " + "now and drains later still lands in this scope. Accepted values: " + `${OBSERVATION_SCOPES.join(", ")}. ` + "Cascade: override (per-agent wins over default).");
|
|
13692
13726
|
AgentMemorySchema = exports_external.object({
|
|
13693
13727
|
collection: exports_external.string().describe("Hindsight collection name for this agent"),
|
|
13694
13728
|
auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
|
|
@@ -13723,6 +13757,7 @@ var init_schema = __esm(() => {
|
|
|
13723
13757
|
empathy: exports_external.number().int().min(1).max(5).optional().describe("How much the bank weights emotional/relational context (1-5; engine default 3).")
|
|
13724
13758
|
}).optional().describe("Personality traits (1-5 each) steering how this bank frames recall, " + "reflect, and observation synthesis \u2014 a coach leans empathy-high, a " + "lawyer/analyst leans skepticism/literalism-high. Maps to the engine's " + "flat `disposition_skepticism`/`_literalism`/`_empathy` fields. " + "Cascade: per-key merge (an agent overrides individual traits and " + "inherits the rest, matching `recall`)."),
|
|
13725
13759
|
directive_capture_nudge: exports_external.boolean().optional().describe("Deterministic directive-capture nudge (issue #2848 Stage B). When " + "on (switchroom default true \u2014 Stage A measured a ~55% miss rate on " + "durable corrections), the auto-recall hook regex-detects correction " + '/ standing-rule-shaped inbound ("always/never \u2026", "from now on \u2026", ' + `"stop doing \u2026", a stated preference, "that's wrong, it's \u2026") and ` + "appends a terse advisory to the turn's context telling the model to " + "persist the rule with mcp__hindsight__create_directive if it IS " + "durable. Detection is pure regex \u2014 the model does the judgment " + "in-session and calls create_directive itself (no model callsite, no " + "silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
|
|
13760
|
+
observation_scopes: ObservationScopesSchema,
|
|
13726
13761
|
recall: exports_external.object({
|
|
13727
13762
|
max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
|
|
13728
13763
|
cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
|
|
@@ -13966,7 +14001,10 @@ var init_schema = __esm(() => {
|
|
|
13966
14001
|
admin_key: exports_external.string().optional().describe("LiteLLM master/admin key used at apply time to provision the team + " + "virtual key. Supports a vault reference (e.g. " + "'vault:litellm/master-key') \u2014 resolution happens at apply time via " + "the vault-broker. Never injected into the agent container."),
|
|
13967
14002
|
team: exports_external.string().optional().describe("LiteLLM team alias the per-agent key is created under. Defaults to " + "'switchroom' (applied in code, not as a schema default)."),
|
|
13968
14003
|
small_fast_model: exports_external.string().optional().describe("Model id exported as ANTHROPIC_SMALL_FAST_MODEL for the claude CLI's " + "background/fast lane, e.g. 'claude-haiku-4-5-20251001'."),
|
|
13969
|
-
tags: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Extra key/value metadata tags attached to the provisioned LiteLLM " + "virtual key. Merged per-key across cascade layers (agent wins).")
|
|
14004
|
+
tags: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Extra key/value metadata tags attached to the provisioned LiteLLM " + "virtual key. Merged per-key across cascade layers (agent wins)."),
|
|
14005
|
+
max_budget: exports_external.number().positive().optional().describe("HARD spend cap in USD for this agent's virtual key over one " + "`budget_duration` window. LiteLLM refuses the request once the key's " + "tracked spend exceeds it, so a runaway loop costs at most this much " + "before it is stopped. Defaults to " + "DEFAULT_KEY_MAX_BUDGET_USD (see src/litellm/budget.ts) \u2014 deliberately " + "conservative; raise it per-agent rather than removing it. Set 0 or " + "omit `budget_duration` at your own risk: an uncapped key is only as " + "bounded as the upstream account balance."),
|
|
14006
|
+
soft_budget: exports_external.number().positive().optional().describe("ADVISORY spend threshold in USD. LiteLLM keeps serving past it and " + "raises a budget alert instead. Must be < max_budget. NOTE: LiteLLM " + "accepts soft_budget only on POST /key/generate (GenerateKeyRequest); " + "UpdateKeyRequest does NOT carry it, so changing this value only takes " + "effect on a key that is (re)generated, not on an existing one."),
|
|
14007
|
+
budget_duration: exports_external.string().regex(/^\d+(s|m|h|d|mo)$/, "budget_duration must be a LiteLLM duration like '30d', '24h', '1mo'").optional().describe("Rolling window the budget resets on, in LiteLLM duration syntax " + "('30d', '24h', '1mo'). Defaults to DEFAULT_KEY_BUDGET_DURATION. " + "WITHOUT a duration LiteLLM treats max_budget as a LIFETIME cap that " + "never resets \u2014 the key silently dies for good once it is hit.")
|
|
13970
14008
|
}).optional().describe("LiteLLM routing config \u2014 opt-in per-agent virtual-key auto-provisioning " + "+ routing env. Default OFF. See LiteLLMConfigSchema doc for the full flow.");
|
|
13971
14009
|
HindsightPerOpLlmSchema = exports_external.object({
|
|
13972
14010
|
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`."),
|
|
@@ -13985,7 +14023,7 @@ var init_schema = __esm(() => {
|
|
|
13985
14023
|
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."),
|
|
13986
14024
|
consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
|
|
13987
14025
|
}).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."),
|
|
13988
|
-
env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \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), 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).")
|
|
14026
|
+
env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, 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_MAX_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_MAX_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), 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).")
|
|
13989
14027
|
});
|
|
13990
14028
|
MicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
13991
14029
|
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)."),
|
|
@@ -14118,6 +14156,7 @@ var init_schema = __esm(() => {
|
|
|
14118
14156
|
file: exports_external.boolean().optional(),
|
|
14119
14157
|
isolation: exports_external.enum(["default", "strict"]).optional(),
|
|
14120
14158
|
directive_capture_nudge: exports_external.boolean().optional(),
|
|
14159
|
+
observation_scopes: ObservationScopesSchema,
|
|
14121
14160
|
recall: exports_external.object({
|
|
14122
14161
|
max_memories: exports_external.number().int().min(0).optional(),
|
|
14123
14162
|
cache_ttl_secs: exports_external.number().int().min(0).optional(),
|
|
@@ -15765,6 +15804,12 @@ var init_hindsight_pg_defaults = __esm(() => {
|
|
|
15765
15804
|
});
|
|
15766
15805
|
|
|
15767
15806
|
// src/setup/hindsight-perf-defaults.ts
|
|
15807
|
+
function hindsightConsolidationLlmMaxConcurrentDefault(globalMaxConcurrent = HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT, retainMaxConcurrent = HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT) {
|
|
15808
|
+
const globalCap = Number.isFinite(globalMaxConcurrent) && globalMaxConcurrent >= 1 ? Math.floor(globalMaxConcurrent) : HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT;
|
|
15809
|
+
const retainCap = Number.isFinite(retainMaxConcurrent) && retainMaxConcurrent >= 0 ? Math.floor(retainMaxConcurrent) : HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT;
|
|
15810
|
+
const headroomBound = Math.max(1, globalCap - 1);
|
|
15811
|
+
return Math.min(headroomBound, Math.max(1, globalCap - retainCap - 1));
|
|
15812
|
+
}
|
|
15768
15813
|
function resolveHindsightPerfOverrides(configEnv, processEnv = process.env) {
|
|
15769
15814
|
const out = new Map;
|
|
15770
15815
|
for (const key of HINDSIGHT_PERF_ENV_KEYS) {
|
|
@@ -15791,6 +15836,14 @@ function hindsightPerfEnv(caps, overrides = new Map) {
|
|
|
15791
15836
|
groups.push(HINDSIGHT_PERF_DEFAULTS_GPU);
|
|
15792
15837
|
if (caps.localLlm === true)
|
|
15793
15838
|
groups.push(HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM);
|
|
15839
|
+
const effectiveCap = (key, fallback) => {
|
|
15840
|
+
const raw = overrides.get(key);
|
|
15841
|
+
if (raw === undefined)
|
|
15842
|
+
return fallback;
|
|
15843
|
+
const parsed = Number.parseInt(raw, 10);
|
|
15844
|
+
return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
|
|
15845
|
+
};
|
|
15846
|
+
const derivedConsolidationCap = String(hindsightConsolidationLlmMaxConcurrentDefault(effectiveCap("HINDSIGHT_API_LLM_MAX_CONCURRENT", HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT), effectiveCap("HINDSIGHT_API_RETAIN_LLM_MAX_CONCURRENT", HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT)));
|
|
15794
15847
|
const out = [];
|
|
15795
15848
|
const emitted = new Set;
|
|
15796
15849
|
for (const group of groups) {
|
|
@@ -15798,7 +15851,8 @@ function hindsightPerfEnv(caps, overrides = new Map) {
|
|
|
15798
15851
|
if (emitted.has(key))
|
|
15799
15852
|
continue;
|
|
15800
15853
|
emitted.add(key);
|
|
15801
|
-
|
|
15854
|
+
const shipped = key === "HINDSIGHT_API_CONSOLIDATION_LLM_MAX_CONCURRENT" ? derivedConsolidationCap : value;
|
|
15855
|
+
out.push([key, overrides.get(key) ?? shipped]);
|
|
15802
15856
|
}
|
|
15803
15857
|
}
|
|
15804
15858
|
for (const key of [...overrides.keys()].sort()) {
|
|
@@ -15812,10 +15866,11 @@ function hindsightPerfEnv(caps, overrides = new Map) {
|
|
|
15812
15866
|
function findUnmanagedHindsightEnvKeys(configEnv) {
|
|
15813
15867
|
return Object.keys(configEnv ?? {}).filter((key) => !HINDSIGHT_PERF_ENV_KEYS.has(key) && !HINDSIGHT_PG_ENV_KEYS.has(key)).sort();
|
|
15814
15868
|
}
|
|
15815
|
-
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
|
|
15869
|
+
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, 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_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000, HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true", HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150, HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8, HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 500, HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT = 6, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1, HINDSIGHT_DEFAULT_RECENCY_DECAY_FUNCTION = "exponential", HINDSIGHT_DEFAULT_RECENCY_DECAY_HALFLIFE_DAYS = 30, HINDSIGHT_PERF_DEFAULTS_UNGATED, HINDSIGHT_PERF_DEFAULTS_GPU, HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM, HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS, HINDSIGHT_PERF_ENV_KEYS;
|
|
15816
15870
|
var init_hindsight_perf_defaults = __esm(() => {
|
|
15817
15871
|
init_hindsight_pg_defaults();
|
|
15818
15872
|
HINDSIGHT_DEFAULT_RECALL_MAX_CANDIDATES_PER_SOURCE = Math.ceil(HINDSIGHT_RERANKER_MAX_CANDIDATES_FOR_DERIVATION * 0.4);
|
|
15873
|
+
HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT = hindsightConsolidationLlmMaxConcurrentDefault();
|
|
15819
15874
|
HINDSIGHT_PERF_DEFAULTS_UNGATED = [
|
|
15820
15875
|
[
|
|
15821
15876
|
"HINDSIGHT_API_RECALL_MAX_CANDIDATES_PER_SOURCE",
|
|
@@ -15869,6 +15924,14 @@ var init_hindsight_perf_defaults = __esm(() => {
|
|
|
15869
15924
|
[
|
|
15870
15925
|
"HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND",
|
|
15871
15926
|
String(HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND)
|
|
15927
|
+
],
|
|
15928
|
+
[
|
|
15929
|
+
"HINDSIGHT_API_RECENCY_DECAY_FUNCTION",
|
|
15930
|
+
HINDSIGHT_DEFAULT_RECENCY_DECAY_FUNCTION
|
|
15931
|
+
],
|
|
15932
|
+
[
|
|
15933
|
+
"HINDSIGHT_API_RECENCY_DECAY_HALFLIFE_DAYS",
|
|
15934
|
+
String(HINDSIGHT_DEFAULT_RECENCY_DECAY_HALFLIFE_DAYS)
|
|
15872
15935
|
]
|
|
15873
15936
|
];
|
|
15874
15937
|
HINDSIGHT_PERF_DEFAULTS_GPU = [
|
|
@@ -15893,7 +15956,10 @@ var init_hindsight_perf_defaults = __esm(() => {
|
|
|
15893
15956
|
];
|
|
15894
15957
|
HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
|
|
15895
15958
|
"HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY",
|
|
15896
|
-
"HINDSIGHT_CE_DECISIVE_RELATIVE_GAP"
|
|
15959
|
+
"HINDSIGHT_CE_DECISIVE_RELATIVE_GAP",
|
|
15960
|
+
"HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS",
|
|
15961
|
+
"HINDSIGHT_API_WORKER_MAX_SLOTS",
|
|
15962
|
+
"HINDSIGHT_API_WORKER_RETAIN_MAX_SLOTS"
|
|
15897
15963
|
]);
|
|
15898
15964
|
HINDSIGHT_PERF_ENV_KEYS = new Set([
|
|
15899
15965
|
...[
|
|
@@ -16983,22 +17049,35 @@ function isHindsightEnabled(config) {
|
|
|
16983
17049
|
return false;
|
|
16984
17050
|
return config?.memory?.backend === "hindsight";
|
|
16985
17051
|
}
|
|
16986
|
-
function isUpgradableRetainMission(current) {
|
|
16987
|
-
if (current == null || current.trim() === "")
|
|
16988
|
-
return true;
|
|
16989
|
-
return SUPERSEDED_RETAIN_MISSIONS.includes(current);
|
|
16990
|
-
}
|
|
16991
17052
|
function decideRetainMissionUpgrade(configured, current) {
|
|
17053
|
+
return decideMissionUpgrade(configured, current, DEFAULT_RETAIN_MISSION, SUPERSEDED_RETAIN_MISSIONS);
|
|
17054
|
+
}
|
|
17055
|
+
function decideMissionUpgrade(configured, current, desired, shipped) {
|
|
16992
17056
|
if (configured)
|
|
16993
17057
|
return { action: "config", mission: configured };
|
|
16994
|
-
if (current ===
|
|
17058
|
+
if (current === desired)
|
|
16995
17059
|
return { action: "none" };
|
|
16996
|
-
if (
|
|
16997
|
-
return { action: "upgrade", mission:
|
|
17060
|
+
if (current == null || current.trim() === "" || shipped.includes(current)) {
|
|
17061
|
+
return { action: "upgrade", mission: desired };
|
|
16998
17062
|
}
|
|
16999
17063
|
return { action: "none" };
|
|
17000
17064
|
}
|
|
17065
|
+
function decideObservationsMissionUpgrade(configured, profileDefault, current) {
|
|
17066
|
+
const desired = profileDefault ?? DEFAULT_OBSERVATIONS_MISSION;
|
|
17067
|
+
const shipped = [
|
|
17068
|
+
...SUPERSEDED_OBSERVATIONS_MISSIONS,
|
|
17069
|
+
DEFAULT_OBSERVATIONS_MISSION,
|
|
17070
|
+
...Object.values(PROFILE_MEMORY_DEFAULTS).map((d) => d.observations_mission).filter((m) => m != null)
|
|
17071
|
+
].filter((m) => m !== desired);
|
|
17072
|
+
return decideMissionUpgrade(configured, current, desired, shipped);
|
|
17073
|
+
}
|
|
17001
17074
|
async function fetchBankRetainMission(apiUrl, bankId, opts) {
|
|
17075
|
+
return fetchBankMissionField(apiUrl, bankId, "retain_mission", opts);
|
|
17076
|
+
}
|
|
17077
|
+
async function fetchBankObservationsMission(apiUrl, bankId, opts) {
|
|
17078
|
+
return fetchBankMissionField(apiUrl, bankId, "observations_mission", opts);
|
|
17079
|
+
}
|
|
17080
|
+
async function fetchBankMissionField(apiUrl, bankId, field, opts) {
|
|
17002
17081
|
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
17003
17082
|
const timeoutMs = opts?.timeoutMs ?? 5000;
|
|
17004
17083
|
const base = apiUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
|
|
@@ -17015,10 +17094,10 @@ async function fetchBankRetainMission(apiUrl, bankId, opts) {
|
|
|
17015
17094
|
if (config == null || typeof config !== "object") {
|
|
17016
17095
|
return { ok: false, reason: "Unexpected shape" };
|
|
17017
17096
|
}
|
|
17018
|
-
if (!(
|
|
17097
|
+
if (!(field in config)) {
|
|
17019
17098
|
return { ok: false, reason: "Unexpected shape" };
|
|
17020
17099
|
}
|
|
17021
|
-
const mission = config
|
|
17100
|
+
const mission = config[field];
|
|
17022
17101
|
if (mission != null && typeof mission !== "string") {
|
|
17023
17102
|
return { ok: false, reason: "Unexpected shape" };
|
|
17024
17103
|
}
|
|
@@ -17570,7 +17649,7 @@ async function addMemoryTag(apiUrl, bankId, memoryId, tag, opts) {
|
|
|
17570
17649
|
return { ok: false, reason: String(err) };
|
|
17571
17650
|
}
|
|
17572
17651
|
}
|
|
17573
|
-
var HINDSIGHT_SHIM_CLI_PATH = "/usr/local/bin/switchroom", HINDSIGHT_SHIM_AGENT_HOME = "/state/agent/home", DEFAULT_RETAIN_MISSION, SUPERSEDED_RETAIN_MISSIONS, PROFILE_MEMORY_DEFAULTS, USER_PROFILE_SOURCE_QUERY = "What are the key facts, preferences, context, and communication style about the user I talk to? Summarize what matters for making the agent feel like it knows them.", DEMOTE_FROM_RECALL_TAG = "[demote-from-recall]";
|
|
17652
|
+
var HINDSIGHT_SHIM_CLI_PATH = "/usr/local/bin/switchroom", HINDSIGHT_SHIM_AGENT_HOME = "/state/agent/home", DEFAULT_RETAIN_MISSION, SUPERSEDED_RETAIN_MISSIONS, DEFAULT_OBSERVATIONS_MISSION, SUPERSEDED_OBSERVATIONS_MISSIONS, PROFILE_MEMORY_DEFAULTS, USER_PROFILE_SOURCE_QUERY = "What are the key facts, preferences, context, and communication style about the user I talk to? Summarize what matters for making the agent feel like it knows them.", DEMOTE_FROM_RECALL_TAG = "[demote-from-recall]";
|
|
17574
17653
|
var init_hindsight2 = __esm(() => {
|
|
17575
17654
|
init_users();
|
|
17576
17655
|
init_hindsight();
|
|
@@ -17599,6 +17678,16 @@ var init_hindsight2 = __esm(() => {
|
|
|
17599
17678
|
` + `- Hindsight's own errors, retries, backlogs, or internal state \u2014 the memory
|
|
17600
17679
|
` + ` system's self-reports are not memories.
|
|
17601
17680
|
` + `- Restatements of the user's current request or the task in progress.
|
|
17681
|
+
` + `- Volatile state written as a timeless assertion. A version, count, size,
|
|
17682
|
+
` + ` backlog, status, or any "X is running Y" / "X is at Y" / "X is currently Y"
|
|
17683
|
+
` + ` claim is true only at the instant it was said. Concretely, never produce a
|
|
17684
|
+
` + ` fact whose text resembles any of these: "Switchroom fleet is running image
|
|
17685
|
+
` + ` version v0.18.19", "The switchroom repo is at /path/to/fleet, version
|
|
17686
|
+
` + ` v0.19.5", "Bank overlord has 43155 pending consolidations", "The build is
|
|
17687
|
+
` + ` currently green". If the claim is worth keeping, put the date INSIDE the
|
|
17688
|
+
` + ` fact text ("As of 2026-07-19 the fleet was running v0.18.19"); if you
|
|
17689
|
+
` + ` cannot date it, drop it. An undated one is recalled forever as though it
|
|
17690
|
+
` + ` were still true, which is worse than not remembering it at all.
|
|
17602
17691
|
` + `- Transient state (unread counts, build status, what is running right now) unless
|
|
17603
17692
|
` + ` the fact is explicitly dated, in which case record it as a dated observation.
|
|
17604
17693
|
` + `- Greetings, acknowledgements, and routine operational chatter.
|
|
@@ -17620,18 +17709,108 @@ var init_hindsight2 = __esm(() => {
|
|
|
17620
17709
|
` + "- Transient state (unread counts, build status, what is running right now) " + "unless the fact is explicitly dated, in which case record it as a dated " + `observation.
|
|
17621
17710
|
` + `- Greetings, acknowledgements, and routine operational chatter.
|
|
17622
17711
|
|
|
17623
|
-
` + "If a candidate fact matches an exclusion, drop it rather than rewording " + "it. If nothing durable remains, return an empty facts list."
|
|
17712
|
+
` + "If a candidate fact matches an exclusion, drop it rather than rewording " + "it. If nothing durable remains, return an empty facts list.",
|
|
17713
|
+
`Extract durable facts that will still be true and useful weeks from now: user preferences and standing rules, ongoing projects and recurring commitments, technical and architectural decisions with their rationale, and people/tool relationships. A preference revealed by a request is durable \u2014 record the preference (what the user likes, wants, or always does), not the request itself.
|
|
17714
|
+
` + `
|
|
17715
|
+
` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
|
|
17716
|
+
` + `candidate a file path, a command/process/agent/session id, a temp directory, or
|
|
17717
|
+
` + `the location where some output was written? If yes, drop it \u2014 it is transcript
|
|
17718
|
+
` + `exhaust, not memory.
|
|
17719
|
+
` + `
|
|
17720
|
+
` + `NEVER extract:
|
|
17721
|
+
` + `- Tool results verbatim or paraphrased. Concretely, never produce a fact whose
|
|
17722
|
+
` + ` text resembles any of these: "File created successfully at /path/to/file",
|
|
17723
|
+
` + ` "A background command with ID bctz4yskm is running, and its output will be
|
|
17724
|
+
` + ` written to /tmp/...", "Async agent a745598ba84e71df1 was launched successfully
|
|
17725
|
+
` + ` and is running in the background", "User executed a Bash command to sleep for
|
|
17726
|
+
` + ` 200 seconds", "The assistant used grep to locate 'truncateSync' in src/foo.ts".
|
|
17727
|
+
` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
|
|
17728
|
+
` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
|
|
17729
|
+
` + ` assistant used X to query Y", "ran a search", "sent the message").
|
|
17730
|
+
` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
|
|
17731
|
+
` + ` running) \u2014 retain the outcome only once the task completes or a decision is made.
|
|
17732
|
+
` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
|
|
17733
|
+
` + `- Slash commands the user typed and their effects (e.g. "User issued /clear to
|
|
17734
|
+
` + ` reset assistant state").
|
|
17735
|
+
` + `- Hindsight's own errors, retries, backlogs, or internal state \u2014 the memory
|
|
17736
|
+
` + ` system's self-reports are not memories.
|
|
17737
|
+
` + `- Restatements of the user's current request or the task in progress.
|
|
17738
|
+
` + `- Transient state (unread counts, build status, what is running right now) unless
|
|
17739
|
+
` + ` the fact is explicitly dated, in which case record it as a dated observation.
|
|
17740
|
+
` + `- Greetings, acknowledgements, and routine operational chatter.
|
|
17741
|
+
` + `
|
|
17742
|
+
` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
|
|
17743
|
+
` + "nothing durable remains, return an empty facts list."
|
|
17744
|
+
];
|
|
17745
|
+
DEFAULT_OBSERVATIONS_MISSION = `Synthesise durable, standing knowledge about the people, projects, and systems this agent works with: preferences and standing rules, roles and relationships, skills and recurring patterns, technical and operational decisions with their rationale, and the state of long-running work once it lands.
|
|
17746
|
+
` + `
|
|
17747
|
+
` + `The test is durability, not notability: an observation must still be worth reading weeks from now. A single dated event belongs in an observation only when it establishes or changes a standing fact.
|
|
17748
|
+
` + `
|
|
17749
|
+
` + `Do NOT synthesise observations from:
|
|
17750
|
+
` + `- Transcript exhaust \u2014 tool calls and their results, file paths, temp or scratchpad directories, where some output was written, or narration of what an assistant did.
|
|
17751
|
+
` + `- Identifiers with no standing meaning: session, agent, request, batch or command IDs, UUIDs, hashes, error codes.
|
|
17752
|
+
` + `- In-flight process narration \u2014 a task started, paused, or still running. Record the outcome once it lands, not the running state.
|
|
17753
|
+
` + `- The memory system's own errors, retries, backlogs, or internal state.
|
|
17754
|
+
` + `- Transient state (what is running right now, unread counts, build status) unless the fact is explicitly dated, in which case record it as dated.
|
|
17755
|
+
` + `
|
|
17756
|
+
` + "If the new facts contain nothing durable, record nothing rather than synthesising a weak observation.";
|
|
17757
|
+
SUPERSEDED_OBSERVATIONS_MISSIONS = [
|
|
17758
|
+
"Synthesise the person's wellbeing patterns, motivations, and emotional " + "context \u2014 how habits, setbacks, and encouragement connect over time."
|
|
17624
17759
|
];
|
|
17625
17760
|
PROFILE_MEMORY_DEFAULTS = {
|
|
17626
17761
|
"health-coach": {
|
|
17627
17762
|
disposition: { skepticism: 2, literalism: 2, empathy: 5 },
|
|
17628
|
-
observations_mission:
|
|
17763
|
+
observations_mission: `You consolidate the memory of a health and fitness coach working with one person. This bank records how that person actually lives and trains.
|
|
17764
|
+
` + `
|
|
17765
|
+
` + `Synthesise into durable observations:
|
|
17766
|
+
` + `- Goals, targets, and the plan currently in force, with the reasoning behind each.
|
|
17767
|
+
` + `- Training, nutrition, sleep, and alcohol patterns as they hold over weeks \u2014 what the person reliably does, not what they did once.
|
|
17768
|
+
` + `- Constraints that shape the plan: injuries, medical guidance, schedule, equipment, foods and sessions they refuse.
|
|
17769
|
+
` + `- Motivations, and what actually helps or backfires when they slip.
|
|
17770
|
+
` + `- Trends in the numbers: direction and range over time, not any single reading.
|
|
17771
|
+
` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
|
|
17772
|
+
` + `
|
|
17773
|
+
` + `A single day's log, weight reading, or session is evidence, not an observation. Record one only when it establishes or changes a standing pattern, target, or constraint \u2014 then fold it into the observation for that pattern and say what changed and roughly when.
|
|
17774
|
+
` + `
|
|
17775
|
+
` + `Granularity: one observation per habit, target, constraint, or trend. Aggregate repeated daily evidence into the observation for that pattern rather than creating one per day, and keep training, nutrition, and sleep as separate observations.
|
|
17776
|
+
` + `
|
|
17777
|
+
` + "Word observations as the person's own pattern and framing, never as a verdict on them."
|
|
17629
17778
|
},
|
|
17630
17779
|
"executive-assistant": {
|
|
17631
|
-
disposition: { skepticism: 4, literalism: 4, empathy: 3 }
|
|
17780
|
+
disposition: { skepticism: 4, literalism: 4, empathy: 3 },
|
|
17781
|
+
observations_mission: `You consolidate the memory of an executive assistant working for one person. This bank records that person's commitments, people, and standing arrangements.
|
|
17782
|
+
` + `
|
|
17783
|
+
` + `Synthesise into durable observations:
|
|
17784
|
+
` + `- Standing rules and preferences: how they want things scheduled, written, and filed, and when they want to be interrupted.
|
|
17785
|
+
` + `- People and organisations, and the relationship: who they are, what they are involved in, how to reach them.
|
|
17786
|
+
` + `- Recurring commitments and routines, and the constraints around them.
|
|
17787
|
+
` + `- Obligations and their state: what was promised to whom, the deadline, and what is still outstanding.
|
|
17788
|
+
` + `- Decisions made and decisions deferred, with the reasoning and the trade accepted.
|
|
17789
|
+
` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
|
|
17790
|
+
` + `
|
|
17791
|
+
` + `Live commitment state IS durable knowledge here, not ephemeral chatter. An outstanding obligation, a travel window, an unanswered request, a "currently X" arrangement \u2014 these are precisely what this agent must recall later. Keep them, and embed the date inside the observation text so a later reader can judge staleness. Do not drop them as ephemeral.
|
|
17792
|
+
` + `
|
|
17793
|
+
` + `Granularity: one observation per person, arrangement, or obligation. Aggregate repeated mentions into that observation rather than creating siblings; never merge two different people or two different commitments into one.
|
|
17794
|
+
` + `
|
|
17795
|
+
` + "Do not synthesise from transcript exhaust: tool calls and their results, message or event identifiers, or narration of what the assistant did."
|
|
17632
17796
|
},
|
|
17633
17797
|
coding: {
|
|
17634
|
-
disposition: { skepticism: 4, literalism: 5, empathy: 2 }
|
|
17798
|
+
disposition: { skepticism: 4, literalism: 5, empathy: 2 },
|
|
17799
|
+
observations_mission: `You consolidate the memory of a software-engineering agent. This bank records real work on real codebases.
|
|
17800
|
+
` + `
|
|
17801
|
+
` + `Synthesise into durable observations:
|
|
17802
|
+
` + `- Architecture and design decisions, each with its rationale and the trade accepted.
|
|
17803
|
+
` + `- Root causes, with the evidence chain, and negative results \u2014 what was ruled out matters as much as what was found.
|
|
17804
|
+
` + `- How the repository works: build, test, and lint commands, conventions, CI gates, and where things live.
|
|
17805
|
+
` + `- Outcomes of code work: issue and PR numbers, what changed, whether it merged, what review found.
|
|
17806
|
+
` + `- The user's standing rules, preferences, and corrections to this agent's behaviour.
|
|
17807
|
+
` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
|
|
17808
|
+
` + `
|
|
17809
|
+
` + `Repository and service state IS durable knowledge here, not ephemeral chatter. Versions, a failing gate, an open PR, a measured number, a "currently X" claim \u2014 these are precisely what this agent must recall later. Keep them, and embed the date inside the observation text so a later reader can judge staleness. Do not drop them as ephemeral.
|
|
17810
|
+
` + `
|
|
17811
|
+
` + `Granularity: one observation per distinct decision, cause, convention, or work item. Aggregate repeated evidence about the same one into that observation rather than creating siblings; never merge two separate decisions into a single summary.
|
|
17812
|
+
` + `
|
|
17813
|
+
` + "Do not synthesise from transcript exhaust: tool calls and their results, scratch paths, session or request identifiers, or narration of what the agent did. Prefer specific and falsifiable \u2014 naming the file, the number, the commit, or the decision beats summarising the topic."
|
|
17635
17814
|
}
|
|
17636
17815
|
};
|
|
17637
17816
|
});
|
|
@@ -27998,6 +28177,21 @@ async function resolveRetainMissionPush(apiUrl, bankId, configured, label) {
|
|
|
27998
28177
|
}
|
|
27999
28178
|
return decision.mission;
|
|
28000
28179
|
}
|
|
28180
|
+
async function resolveObservationsMissionPush(apiUrl, bankId, configured, profileName, label) {
|
|
28181
|
+
if (configured)
|
|
28182
|
+
return configured;
|
|
28183
|
+
const profileDefault = PROFILE_MEMORY_DEFAULTS[profileName]?.observations_mission;
|
|
28184
|
+
const current = await fetchBankObservationsMission(apiUrl, bankId, { timeoutMs: 5000 });
|
|
28185
|
+
if (!current.ok) {
|
|
28186
|
+
console.warn(` ${source_default.yellow("\u26a0")} Could not read current observations_mission for ${label} (${current.reason}) \u2014 leaving it untouched`);
|
|
28187
|
+
return;
|
|
28188
|
+
}
|
|
28189
|
+
const decision = decideObservationsMissionUpgrade(configured, profileDefault, current.mission);
|
|
28190
|
+
if (decision.action === "upgrade") {
|
|
28191
|
+
console.log(` ${source_default.green("\u2713")} Seeding default observations_mission for ${label}`);
|
|
28192
|
+
}
|
|
28193
|
+
return decision.mission;
|
|
28194
|
+
}
|
|
28001
28195
|
function parseDurationToSeconds(d) {
|
|
28002
28196
|
if (!d)
|
|
28003
28197
|
return;
|
|
@@ -28710,6 +28904,7 @@ function buildWorkspaceContext(args) {
|
|
|
28710
28904
|
hindsightRecallTypes,
|
|
28711
28905
|
hindsightRecallSkipTrivial,
|
|
28712
28906
|
hindsightDirectiveCaptureNudge,
|
|
28907
|
+
hindsightObservationScopes,
|
|
28713
28908
|
hindsightTopicAliasesJson,
|
|
28714
28909
|
hindsightSenderBanksJson,
|
|
28715
28910
|
hindsightTopicFilterMode
|
|
@@ -28763,6 +28958,7 @@ function buildWorkspaceContext(args) {
|
|
|
28763
28958
|
hindsightRecallTypes,
|
|
28764
28959
|
hindsightRecallSkipTrivial,
|
|
28765
28960
|
hindsightDirectiveCaptureNudge,
|
|
28961
|
+
hindsightObservationScopesQ: hindsightObservationScopes ? shellSingleQuote(hindsightObservationScopes) : undefined,
|
|
28766
28962
|
hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
|
|
28767
28963
|
hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
|
|
28768
28964
|
hindsightTopicFilterMode,
|
|
@@ -29027,6 +29223,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
29027
29223
|
const hindsightRecallTypes = agentConfig.memory?.recall?.types?.length ? agentConfig.memory.recall.types.join(",") : undefined;
|
|
29028
29224
|
const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
|
|
29029
29225
|
const hindsightDirectiveCaptureNudge = agentConfig.memory?.directive_capture_nudge === undefined ? undefined : String(agentConfig.memory.directive_capture_nudge);
|
|
29226
|
+
const hindsightObservationScopes = agentConfig.memory?.observation_scopes;
|
|
29030
29227
|
const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
|
|
29031
29228
|
const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
|
|
29032
29229
|
const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
|
|
@@ -29062,6 +29259,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
29062
29259
|
hindsightRecallTypes,
|
|
29063
29260
|
hindsightRecallSkipTrivial,
|
|
29064
29261
|
hindsightDirectiveCaptureNudge,
|
|
29262
|
+
hindsightObservationScopes,
|
|
29065
29263
|
hindsightTopicAliasesJson,
|
|
29066
29264
|
hindsightSenderBanksJson,
|
|
29067
29265
|
hindsightTopicFilterMode
|
|
@@ -29504,6 +29702,11 @@ ${body}
|
|
|
29504
29702
|
if (seededRetainMission) {
|
|
29505
29703
|
missions.retain_mission = seededRetainMission;
|
|
29506
29704
|
}
|
|
29705
|
+
delete missions.observations_mission;
|
|
29706
|
+
const seededObservationsMission = await resolveObservationsMissionPush(apiUrl, hindsightBankId, agentConfig.memory?.observations_mission, agentConfig.extends ?? DEFAULT_PROFILE, formatAgentBankLabel(name, hindsightBankId));
|
|
29707
|
+
if (seededObservationsMission) {
|
|
29708
|
+
missions.observations_mission = seededObservationsMission;
|
|
29709
|
+
}
|
|
29507
29710
|
if (userBankMission) {
|
|
29508
29711
|
missions.bank_mission = userBankMission;
|
|
29509
29712
|
}
|
|
@@ -30001,6 +30204,7 @@ function reconcileAgentInner(name, agentConfigRaw, agentsDir, telegramConfig, sw
|
|
|
30001
30204
|
const hindsightRecallTypes = agentConfig.memory?.recall?.types?.length ? agentConfig.memory.recall.types.join(",") : undefined;
|
|
30002
30205
|
const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
|
|
30003
30206
|
const hindsightDirectiveCaptureNudge = agentConfig.memory?.directive_capture_nudge === undefined ? undefined : String(agentConfig.memory.directive_capture_nudge);
|
|
30207
|
+
const hindsightObservationScopes = agentConfig.memory?.observation_scopes;
|
|
30004
30208
|
const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
|
|
30005
30209
|
const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
|
|
30006
30210
|
const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
|
|
@@ -30049,6 +30253,7 @@ function reconcileAgentInner(name, agentConfigRaw, agentsDir, telegramConfig, sw
|
|
|
30049
30253
|
hindsightRecallTypes,
|
|
30050
30254
|
hindsightRecallSkipTrivial,
|
|
30051
30255
|
hindsightDirectiveCaptureNudge,
|
|
30256
|
+
hindsightObservationScopesQ: hindsightObservationScopes ? shellSingleQuote(hindsightObservationScopes) : undefined,
|
|
30052
30257
|
hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
|
|
30053
30258
|
hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
|
|
30054
30259
|
hindsightTopicFilterMode,
|
|
@@ -30471,6 +30676,7 @@ ${body}
|
|
|
30471
30676
|
hindsightRecallTypes,
|
|
30472
30677
|
hindsightRecallSkipTrivial,
|
|
30473
30678
|
hindsightDirectiveCaptureNudge,
|
|
30679
|
+
hindsightObservationScopes,
|
|
30474
30680
|
hindsightTopicAliasesJson,
|
|
30475
30681
|
hindsightSenderBanksJson,
|
|
30476
30682
|
hindsightTopicFilterMode
|
|
@@ -30574,6 +30780,10 @@ ${body}
|
|
|
30574
30780
|
const retainMission = await resolveRetainMissionPush(apiUrl, hindsightBankId, agentConfig.memory?.retain_mission, formatAgentBankLabel(name, hindsightBankId));
|
|
30575
30781
|
if (retainMission)
|
|
30576
30782
|
missions.retain_mission = retainMission;
|
|
30783
|
+
delete missions.observations_mission;
|
|
30784
|
+
const observationsMission = await resolveObservationsMissionPush(apiUrl, hindsightBankId, agentConfig.memory?.observations_mission, agentConfig.extends ?? DEFAULT_PROFILE, formatAgentBankLabel(name, hindsightBankId));
|
|
30785
|
+
if (observationsMission)
|
|
30786
|
+
missions.observations_mission = observationsMission;
|
|
30577
30787
|
if (Object.keys(missions).length > 0) {
|
|
30578
30788
|
await updateBankMissions(apiUrl, hindsightBankId, missions, { timeoutMs: 5000 }).then((result) => {
|
|
30579
30789
|
if (result.ok) {
|
|
@@ -45612,6 +45822,7 @@ var exports_provision = {};
|
|
|
45612
45822
|
__export(exports_provision, {
|
|
45613
45823
|
validateKey: () => validateKey,
|
|
45614
45824
|
updateKeyModels: () => updateKeyModels,
|
|
45825
|
+
updateKeyBudget: () => updateKeyBudget,
|
|
45615
45826
|
ensureTeam: () => ensureTeam,
|
|
45616
45827
|
ensureKey: () => ensureKey,
|
|
45617
45828
|
bindKeyToTeam: () => bindKeyToTeam,
|
|
@@ -45733,6 +45944,12 @@ async function generateKeyOnce(opts, fetchFn) {
|
|
|
45733
45944
|
if (opts.metadata && Object.keys(opts.metadata).length > 0) {
|
|
45734
45945
|
payload.metadata = opts.metadata;
|
|
45735
45946
|
}
|
|
45947
|
+
if (opts.budget) {
|
|
45948
|
+
payload.max_budget = opts.budget.maxBudget;
|
|
45949
|
+
payload.budget_duration = opts.budget.budgetDuration;
|
|
45950
|
+
if (opts.budget.softBudget != null)
|
|
45951
|
+
payload.soft_budget = opts.budget.softBudget;
|
|
45952
|
+
}
|
|
45736
45953
|
let resp;
|
|
45737
45954
|
try {
|
|
45738
45955
|
resp = await fetchFn(url, {
|
|
@@ -45918,6 +46135,27 @@ async function safeText(resp) {
|
|
|
45918
46135
|
return "";
|
|
45919
46136
|
}
|
|
45920
46137
|
}
|
|
46138
|
+
async function updateKeyBudget(opts, fetchFn = fetch) {
|
|
46139
|
+
const url = `${normalizeBase(opts.baseUrl)}/key/update`;
|
|
46140
|
+
let resp;
|
|
46141
|
+
try {
|
|
46142
|
+
resp = await fetchFn(url, {
|
|
46143
|
+
method: "POST",
|
|
46144
|
+
headers: authHeaders(opts.masterKey),
|
|
46145
|
+
body: JSON.stringify({
|
|
46146
|
+
key: opts.key,
|
|
46147
|
+
max_budget: opts.budget.maxBudget,
|
|
46148
|
+
budget_duration: opts.budget.budgetDuration
|
|
46149
|
+
})
|
|
46150
|
+
});
|
|
46151
|
+
} catch (err) {
|
|
46152
|
+
return { kind: "error", detail: err.message };
|
|
46153
|
+
}
|
|
46154
|
+
if (resp.ok)
|
|
46155
|
+
return { kind: "ok" };
|
|
46156
|
+
const body = await safeText(resp);
|
|
46157
|
+
return { kind: "error", detail: `HTTP ${resp.status}: ${body.slice(0, 200)}` };
|
|
46158
|
+
}
|
|
45921
46159
|
var LiteLLMProvisionError;
|
|
45922
46160
|
var init_provision = __esm(() => {
|
|
45923
46161
|
LiteLLMProvisionError = class LiteLLMProvisionError extends Error {
|
|
@@ -47312,6 +47550,176 @@ var init_doctor_webkite = __esm(() => {
|
|
|
47312
47550
|
init_loader();
|
|
47313
47551
|
});
|
|
47314
47552
|
|
|
47553
|
+
// src/openrouter/credit.ts
|
|
47554
|
+
function num(v) {
|
|
47555
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
47556
|
+
}
|
|
47557
|
+
function parseKeySnapshot(body) {
|
|
47558
|
+
if (body == null || typeof body !== "object")
|
|
47559
|
+
return null;
|
|
47560
|
+
const data = body.data;
|
|
47561
|
+
if (data == null || typeof data !== "object")
|
|
47562
|
+
return null;
|
|
47563
|
+
const d = data;
|
|
47564
|
+
if (num(d.usage) == null)
|
|
47565
|
+
return null;
|
|
47566
|
+
return {
|
|
47567
|
+
label: typeof d.label === "string" ? d.label : null,
|
|
47568
|
+
limit: num(d.limit),
|
|
47569
|
+
limitRemaining: num(d.limit_remaining),
|
|
47570
|
+
limitReset: typeof d.limit_reset === "string" ? d.limit_reset : null,
|
|
47571
|
+
usage: num(d.usage),
|
|
47572
|
+
isFreeTier: typeof d.is_free_tier === "boolean" ? d.is_free_tier : null
|
|
47573
|
+
};
|
|
47574
|
+
}
|
|
47575
|
+
function evaluateCredit(snapshot) {
|
|
47576
|
+
const usage = snapshot.usage ?? 0;
|
|
47577
|
+
if (snapshot.limit == null || snapshot.limitRemaining == null) {
|
|
47578
|
+
return {
|
|
47579
|
+
status: "warn",
|
|
47580
|
+
detail: `OpenRouter key has NO per-key credit limit set, so its remaining credit cannot be measured. ` + `GET /api/v1/key reports limit/limit_remaining as null when a key is uncapped, and the response ` + `carries no account-balance field \u2014 so there is nothing to threshold and this check CANNOT warn ` + `you before the account runs dry. (Key has spent $${usage.toFixed(2)} all-time.)`,
|
|
47581
|
+
fix: `Set a per-key credit limit at ${OPENROUTER_KEYS_URL} (vault key: \`${OPENROUTER_VAULT_KEY}\`). ` + `That is what makes proactive monitoring possible at all, and it bounds the damage if the proxy ` + `key ever leaks. This is a manual console action \u2014 OpenRouter exposes no API to set it.`,
|
|
47582
|
+
actionable: false
|
|
47583
|
+
};
|
|
47584
|
+
}
|
|
47585
|
+
const { limit, limitRemaining } = snapshot;
|
|
47586
|
+
const fraction = limit > 0 ? limitRemaining / limit : 0;
|
|
47587
|
+
const reset = snapshot.limitReset ? ` (resets: ${snapshot.limitReset})` : " (never resets)";
|
|
47588
|
+
const where = `$${limitRemaining.toFixed(2)} of $${limit.toFixed(2)} remaining${reset}`;
|
|
47589
|
+
if (limitRemaining <= 0) {
|
|
47590
|
+
return {
|
|
47591
|
+
status: "fail",
|
|
47592
|
+
detail: `OpenRouter key credit is EXHAUSTED \u2014 ${where}. Every request through it is answering HTTP 402.`,
|
|
47593
|
+
fix: `Top up at ${OPENROUTER_CREDITS_URL} or raise the key's limit at ${OPENROUTER_KEYS_URL} (vault key: \`${OPENROUTER_VAULT_KEY}\`).`,
|
|
47594
|
+
actionable: true
|
|
47595
|
+
};
|
|
47596
|
+
}
|
|
47597
|
+
if (fraction < FAIL_REMAINING_FRACTION || limitRemaining < FAIL_REMAINING_USD) {
|
|
47598
|
+
return {
|
|
47599
|
+
status: "fail",
|
|
47600
|
+
detail: `OpenRouter key credit is CRITICALLY low \u2014 ${where}. A single busy session can exhaust it.`,
|
|
47601
|
+
fix: `Top up at ${OPENROUTER_CREDITS_URL} or raise the key's limit at ${OPENROUTER_KEYS_URL} (vault key: \`${OPENROUTER_VAULT_KEY}\`).`,
|
|
47602
|
+
actionable: true
|
|
47603
|
+
};
|
|
47604
|
+
}
|
|
47605
|
+
if (fraction < WARN_REMAINING_FRACTION || limitRemaining < WARN_REMAINING_USD) {
|
|
47606
|
+
return {
|
|
47607
|
+
status: "warn",
|
|
47608
|
+
detail: `OpenRouter key credit is running low \u2014 ${where}.`,
|
|
47609
|
+
fix: `Top up at ${OPENROUTER_CREDITS_URL} before it reaches zero (vault key: \`${OPENROUTER_VAULT_KEY}\`).`,
|
|
47610
|
+
actionable: true
|
|
47611
|
+
};
|
|
47612
|
+
}
|
|
47613
|
+
return {
|
|
47614
|
+
status: "ok",
|
|
47615
|
+
detail: `OpenRouter key credit healthy \u2014 ${where}.`,
|
|
47616
|
+
actionable: false
|
|
47617
|
+
};
|
|
47618
|
+
}
|
|
47619
|
+
async function fetchKeySnapshot(apiKey, fetchFn, signal) {
|
|
47620
|
+
let resp;
|
|
47621
|
+
try {
|
|
47622
|
+
resp = await fetchFn(OPENROUTER_KEY_ENDPOINT, {
|
|
47623
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
47624
|
+
signal
|
|
47625
|
+
});
|
|
47626
|
+
} catch (err) {
|
|
47627
|
+
return { kind: "unreachable", detail: err.message };
|
|
47628
|
+
}
|
|
47629
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
47630
|
+
return { kind: "unauthorized", status: resp.status };
|
|
47631
|
+
}
|
|
47632
|
+
if (!resp.ok)
|
|
47633
|
+
return { kind: "unreachable", detail: `HTTP ${resp.status}` };
|
|
47634
|
+
let body;
|
|
47635
|
+
try {
|
|
47636
|
+
body = await resp.json();
|
|
47637
|
+
} catch {
|
|
47638
|
+
return { kind: "unexpected-shape" };
|
|
47639
|
+
}
|
|
47640
|
+
const snapshot = parseKeySnapshot(body);
|
|
47641
|
+
if (snapshot == null)
|
|
47642
|
+
return { kind: "unexpected-shape" };
|
|
47643
|
+
return { kind: "ok", snapshot };
|
|
47644
|
+
}
|
|
47645
|
+
function assessFetchFailure(result) {
|
|
47646
|
+
switch (result.kind) {
|
|
47647
|
+
case "unauthorized":
|
|
47648
|
+
return {
|
|
47649
|
+
status: "fail",
|
|
47650
|
+
detail: `OpenRouter rejected the credit query with HTTP ${result.status} \u2014 the key is invalid, revoked or disabled.`,
|
|
47651
|
+
fix: `Re-issue the key at ${OPENROUTER_KEYS_URL} and update the vault entry \`${OPENROUTER_VAULT_KEY}\`.`,
|
|
47652
|
+
actionable: true
|
|
47653
|
+
};
|
|
47654
|
+
case "unexpected-shape":
|
|
47655
|
+
return {
|
|
47656
|
+
status: "warn",
|
|
47657
|
+
detail: `OpenRouter's GET /api/v1/key returned an unrecognised body, so credit could not be read. ` + `Treated as UNKNOWN, not as healthy \u2014 the API shape may have changed.`,
|
|
47658
|
+
fix: `Re-check https://openrouter.ai/docs/api-reference/limits against src/openrouter/credit.ts.`,
|
|
47659
|
+
actionable: false
|
|
47660
|
+
};
|
|
47661
|
+
case "unreachable":
|
|
47662
|
+
return {
|
|
47663
|
+
status: "skip",
|
|
47664
|
+
detail: `Could not reach OpenRouter to check credit (${result.detail}).`,
|
|
47665
|
+
actionable: false
|
|
47666
|
+
};
|
|
47667
|
+
}
|
|
47668
|
+
}
|
|
47669
|
+
var WARN_REMAINING_FRACTION = 0.25, WARN_REMAINING_USD = 10, FAIL_REMAINING_FRACTION = 0.1, FAIL_REMAINING_USD = 3, OPENROUTER_VAULT_KEY = "openrouter/api-key", OPENROUTER_KEYS_URL = "https://openrouter.ai/settings/keys", OPENROUTER_CREDITS_URL = "https://openrouter.ai/credits", OPENROUTER_KEY_ENDPOINT = "https://openrouter.ai/api/v1/key";
|
|
47670
|
+
|
|
47671
|
+
// src/cli/doctor-openrouter-credit.ts
|
|
47672
|
+
async function runOpenRouterCreditChecks(deps) {
|
|
47673
|
+
if (!deps.enabled) {
|
|
47674
|
+
return [
|
|
47675
|
+
{
|
|
47676
|
+
name: CHECK_NAME2,
|
|
47677
|
+
status: "skip",
|
|
47678
|
+
detail: "No OpenRouter-backed model is configured in the LiteLLM proxy config."
|
|
47679
|
+
}
|
|
47680
|
+
];
|
|
47681
|
+
}
|
|
47682
|
+
let apiKey;
|
|
47683
|
+
try {
|
|
47684
|
+
apiKey = await deps.readSecret(OPENROUTER_VAULT_KEY);
|
|
47685
|
+
} catch (err) {
|
|
47686
|
+
return [
|
|
47687
|
+
{
|
|
47688
|
+
name: CHECK_NAME2,
|
|
47689
|
+
status: "skip",
|
|
47690
|
+
detail: `Could not read vault key \`${OPENROUTER_VAULT_KEY}\` (${err.message}).`
|
|
47691
|
+
}
|
|
47692
|
+
];
|
|
47693
|
+
}
|
|
47694
|
+
if (!apiKey) {
|
|
47695
|
+
return [
|
|
47696
|
+
{
|
|
47697
|
+
name: CHECK_NAME2,
|
|
47698
|
+
status: "warn",
|
|
47699
|
+
detail: `The proxy routes through OpenRouter but vault key \`${OPENROUTER_VAULT_KEY}\` is not ` + `readable here, so credit cannot be monitored.`,
|
|
47700
|
+
fix: `Grant read access to \`${OPENROUTER_VAULT_KEY}\`, or store it if it is missing.`
|
|
47701
|
+
}
|
|
47702
|
+
];
|
|
47703
|
+
}
|
|
47704
|
+
const fetched = await fetchKeySnapshot(apiKey, deps.fetchFn);
|
|
47705
|
+
const assessment = fetched.kind === "ok" ? evaluateCredit(fetched.snapshot) : assessFetchFailure(fetched);
|
|
47706
|
+
return [
|
|
47707
|
+
{
|
|
47708
|
+
name: CHECK_NAME2,
|
|
47709
|
+
status: assessment.status,
|
|
47710
|
+
detail: assessment.detail,
|
|
47711
|
+
...assessment.fix ? { fix: assessment.fix } : {}
|
|
47712
|
+
}
|
|
47713
|
+
];
|
|
47714
|
+
}
|
|
47715
|
+
function usesOpenRouter(litellmConfigText) {
|
|
47716
|
+
if (!litellmConfigText)
|
|
47717
|
+
return false;
|
|
47718
|
+
return litellmConfigText.includes("openrouter/") || litellmConfigText.includes("OPENROUTER_API_KEY");
|
|
47719
|
+
}
|
|
47720
|
+
var CHECK_NAME2 = "OpenRouter credit";
|
|
47721
|
+
var init_doctor_openrouter_credit = () => {};
|
|
47722
|
+
|
|
47315
47723
|
// src/cli/doctor-cron-session.ts
|
|
47316
47724
|
import { statSync as realStatSync } from "node:fs";
|
|
47317
47725
|
import { resolve as resolve38 } from "node:path";
|
|
@@ -47388,18 +47796,18 @@ function parseEpisode(raw) {
|
|
|
47388
47796
|
if (typeof raw !== "object" || raw === null)
|
|
47389
47797
|
return null;
|
|
47390
47798
|
const r = raw;
|
|
47391
|
-
const
|
|
47392
|
-
const firstTs =
|
|
47393
|
-
const untilTs =
|
|
47394
|
-
const peak =
|
|
47799
|
+
const num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
47800
|
+
const firstTs = num2(r.firstTs);
|
|
47801
|
+
const untilTs = num2(r.untilTs);
|
|
47802
|
+
const peak = num2(r.peakRetryAfterSec);
|
|
47395
47803
|
if (firstTs === null || untilTs === null || peak === null)
|
|
47396
47804
|
return null;
|
|
47397
47805
|
return {
|
|
47398
47806
|
firstTs,
|
|
47399
|
-
lastTs:
|
|
47807
|
+
lastTs: num2(r.lastTs) ?? firstTs,
|
|
47400
47808
|
peakRetryAfterSec: peak,
|
|
47401
47809
|
untilTs,
|
|
47402
|
-
count:
|
|
47810
|
+
count: num2(r.count) ?? 1
|
|
47403
47811
|
};
|
|
47404
47812
|
}
|
|
47405
47813
|
function readFlood429LedgerResult(path6, readFile2 = (p) => readFileSync63(p, "utf-8")) {
|
|
@@ -47571,6 +47979,25 @@ var init_doctor_flood_pressure = __esm(() => {
|
|
|
47571
47979
|
FIX2 = "A flood ban is server-side and cannot be cleared early \u2014 cut the outbound " + "rate that earns it. Check the gateway's `send-gate stats` and " + "`edit-flood-fuse` lines for the class doing the sending (progress cards and " + "the worker feed are the usual culprits), and see " + "`telegram-plugin/edit-flood-fuse.ts` for the per-class ceilings.";
|
|
47572
47980
|
});
|
|
47573
47981
|
|
|
47982
|
+
// src/litellm/budget.ts
|
|
47983
|
+
function resolveKeyBudget(config) {
|
|
47984
|
+
const raw = config?.max_budget;
|
|
47985
|
+
const maxBudget = raw === undefined ? DEFAULT_KEY_MAX_BUDGET_USD : raw;
|
|
47986
|
+
if (!Number.isFinite(maxBudget) || maxBudget <= 0)
|
|
47987
|
+
return null;
|
|
47988
|
+
const budgetDuration = config?.budget_duration && config.budget_duration.length > 0 ? config.budget_duration : DEFAULT_KEY_BUDGET_DURATION;
|
|
47989
|
+
const rawSoft = config?.soft_budget ?? DEFAULT_KEY_SOFT_BUDGET_USD;
|
|
47990
|
+
const softBudget = Number.isFinite(rawSoft) && rawSoft > 0 && rawSoft < maxBudget ? rawSoft : undefined;
|
|
47991
|
+
return { maxBudget, softBudget, budgetDuration };
|
|
47992
|
+
}
|
|
47993
|
+
function describeKeyBudget(budget) {
|
|
47994
|
+
if (budget == null)
|
|
47995
|
+
return "no spend cap (uncapped key)";
|
|
47996
|
+
const soft = budget.softBudget != null ? `, alert at $${budget.softBudget}` : "";
|
|
47997
|
+
return `cap $${budget.maxBudget}/${budget.budgetDuration}${soft}`;
|
|
47998
|
+
}
|
|
47999
|
+
var DEFAULT_KEY_MAX_BUDGET_USD = 25, DEFAULT_KEY_SOFT_BUDGET_USD = 15, DEFAULT_KEY_BUDGET_DURATION = "30d";
|
|
48000
|
+
|
|
47574
48001
|
// examples/switchroom.yaml
|
|
47575
48002
|
var switchroom_default = `# switchroom.yaml \u2014 Full example configuration
|
|
47576
48003
|
#
|
|
@@ -48725,7 +49152,7 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
|
|
|
48725
49152
|
let brokerKeyMaterialized = false;
|
|
48726
49153
|
const [
|
|
48727
49154
|
{ getViaBrokerStructured: getViaBrokerStructured2, putViaBroker: putViaBroker2 },
|
|
48728
|
-
{ ensureTeam: ensureTeam2, ensureKey: ensureKey2, validateKey: validateKey2, bindKeyToTeam: bindKeyToTeam2, updateKeyModels: updateKeyModels2 },
|
|
49155
|
+
{ ensureTeam: ensureTeam2, ensureKey: ensureKey2, validateKey: validateKey2, bindKeyToTeam: bindKeyToTeam2, updateKeyModels: updateKeyModels2, updateKeyBudget: updateKeyBudget2 },
|
|
48729
49156
|
{ addAgentSecret: addAgentSecret2 }
|
|
48730
49157
|
] = await Promise.all([
|
|
48731
49158
|
Promise.resolve().then(() => (init_client(), exports_client)),
|
|
@@ -48806,6 +49233,11 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
|
|
|
48806
49233
|
const baseUrl = litellm.base_url ?? topLevel?.base_url;
|
|
48807
49234
|
const team = litellm.team ?? topLevel?.team ?? "switchroom";
|
|
48808
49235
|
const adminKeyRef = litellm.admin_key ?? topLevel?.admin_key;
|
|
49236
|
+
const budget = resolveKeyBudget({
|
|
49237
|
+
max_budget: litellm.max_budget ?? topLevel?.max_budget,
|
|
49238
|
+
soft_budget: litellm.soft_budget ?? topLevel?.soft_budget,
|
|
49239
|
+
budget_duration: litellm.budget_duration ?? topLevel?.budget_duration
|
|
49240
|
+
});
|
|
48809
49241
|
if (!baseUrl || !adminKeyRef) {
|
|
48810
49242
|
failures.push({
|
|
48811
49243
|
agent: name,
|
|
@@ -48887,6 +49319,16 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
|
|
|
48887
49319
|
writeOut(source_default.gray(` ~ litellm/${name}: key already provisioned (skipped)
|
|
48888
49320
|
`));
|
|
48889
49321
|
}
|
|
49322
|
+
if (!driftReprovision && storedKey && masterKey && budget) {
|
|
49323
|
+
const cap = await updateKeyBudget2({ baseUrl, masterKey, key: storedKey, budget });
|
|
49324
|
+
if (cap.kind === "ok") {
|
|
49325
|
+
writeOut(source_default.gray(` ~ litellm/${name}: spend ${describeKeyBudget(budget)}${budget.softBudget != null ? " (soft budget applies to newly generated keys only)" : ""}
|
|
49326
|
+
`));
|
|
49327
|
+
} else {
|
|
49328
|
+
ctx.writeErr(source_default.yellow(` ! litellm/${name}: could NOT apply the spend cap (${cap.detail}) \u2014 ` + `this key remains UNCAPPED and can spend the whole upstream account balance. Manual: POST ${baseUrl}/key/update {"key":"<key>","max_budget":${budget.maxBudget},"budget_duration":"${budget.budgetDuration}"}.
|
|
49329
|
+
`));
|
|
49330
|
+
}
|
|
49331
|
+
}
|
|
48890
49332
|
if (!driftReprovision) {
|
|
48891
49333
|
if (configText !== null) {
|
|
48892
49334
|
const after = addAgentSecret2(configText, name, vaultKey);
|
|
@@ -48923,6 +49365,7 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
|
|
|
48923
49365
|
alias,
|
|
48924
49366
|
teamId,
|
|
48925
49367
|
metadata,
|
|
49368
|
+
...budget ? { budget } : {},
|
|
48926
49369
|
log: (m) => ctx.writeErr(source_default.gray(` ~ litellm/${name}: ${m}
|
|
48927
49370
|
`))
|
|
48928
49371
|
});
|
|
@@ -52352,6 +52795,7 @@ __export(exports_doctor, {
|
|
|
52352
52795
|
checkDepsCacheWritable: () => checkDepsCacheWritable,
|
|
52353
52796
|
checkDeployMounts: () => checkDeployMounts,
|
|
52354
52797
|
checkConfig: () => checkConfig,
|
|
52798
|
+
checkBankObservationsMissions: () => checkBankObservationsMissions,
|
|
52355
52799
|
checkBankIngestHealth: () => checkBankIngestHealth,
|
|
52356
52800
|
checkAgents: () => checkAgents,
|
|
52357
52801
|
buildPendingRetainsProbeScript: () => buildPendingRetainsProbeScript,
|
|
@@ -52372,6 +52816,20 @@ import {
|
|
|
52372
52816
|
} from "node:fs";
|
|
52373
52817
|
import { dirname as dirname31, join as join81, resolve as resolve44 } from "node:path";
|
|
52374
52818
|
import { createPublicKey, createPrivateKey } from "node:crypto";
|
|
52819
|
+
function readLitellmConfigText() {
|
|
52820
|
+
const candidates = [
|
|
52821
|
+
process.env.LITELLM_CONFIG_PATH,
|
|
52822
|
+
"/data/coolify/services/litellm/litellm-config.yaml",
|
|
52823
|
+
join81(process.cwd(), "docker", "litellm-proxy", "litellm-config.yaml")
|
|
52824
|
+
].filter((p) => typeof p === "string" && p.length > 0);
|
|
52825
|
+
for (const p of candidates) {
|
|
52826
|
+
try {
|
|
52827
|
+
if (existsSync77(p))
|
|
52828
|
+
return readFileSync71(p, "utf-8");
|
|
52829
|
+
} catch {}
|
|
52830
|
+
}
|
|
52831
|
+
return null;
|
|
52832
|
+
}
|
|
52375
52833
|
function findInNvm(bin) {
|
|
52376
52834
|
const nvmRoot = join81(process.env.HOME ?? "", ".nvm", "versions", "node");
|
|
52377
52835
|
if (!existsSync77(nvmRoot))
|
|
@@ -53041,6 +53499,58 @@ function probeAuthBrokerSocket(consumerName) {
|
|
|
53041
53499
|
return "unreachable";
|
|
53042
53500
|
return "missing";
|
|
53043
53501
|
}
|
|
53502
|
+
async function checkBankObservationsMissions(config, url, opts) {
|
|
53503
|
+
const banks = new Map;
|
|
53504
|
+
for (const [agentName, agentConfig] of Object.entries(config.agents)) {
|
|
53505
|
+
const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
|
|
53506
|
+
const bankId = resolved.memory?.collection ?? agentName;
|
|
53507
|
+
const existing = banks.get(bankId);
|
|
53508
|
+
if (existing)
|
|
53509
|
+
existing.agents.push(agentName);
|
|
53510
|
+
else
|
|
53511
|
+
banks.set(bankId, { agents: [agentName], config: resolved });
|
|
53512
|
+
}
|
|
53513
|
+
const inspected = await Promise.all([...banks].map(async ([bankId, entry]) => [
|
|
53514
|
+
bankId,
|
|
53515
|
+
entry,
|
|
53516
|
+
await fetchBankObservationsMission(url, bankId, { fetchImpl: opts?.fetchImpl })
|
|
53517
|
+
]));
|
|
53518
|
+
return inspected.map(([bankId, entry, read]) => {
|
|
53519
|
+
const label = `bank ${bankId} observations_mission` + (entry.agents[0] !== bankId ? ` (${entry.agents.join(", ")})` : "");
|
|
53520
|
+
const firstAgent = entry.agents[0];
|
|
53521
|
+
if (!read.ok) {
|
|
53522
|
+
return {
|
|
53523
|
+
name: label,
|
|
53524
|
+
status: "warn",
|
|
53525
|
+
detail: `could not read observations_mission: ${read.reason}`
|
|
53526
|
+
};
|
|
53527
|
+
}
|
|
53528
|
+
const profileDefault = PROFILE_MEMORY_DEFAULTS[entry.config.extends ?? DEFAULT_PROFILE]?.observations_mission;
|
|
53529
|
+
const decision = decideObservationsMissionUpgrade(entry.config.memory?.observations_mission, profileDefault, read.mission);
|
|
53530
|
+
if (decision.action === "upgrade") {
|
|
53531
|
+
return {
|
|
53532
|
+
name: label,
|
|
53533
|
+
status: "warn",
|
|
53534
|
+
detail: read.mission == null || read.mission.trim() === "" ? "unset \u2014 this bank consolidates under Hindsight's stock mission, not switchroom's" : "carrying a superseded switchroom default",
|
|
53535
|
+
fix: `Run: switchroom agent reconcile ${firstAgent}`
|
|
53536
|
+
};
|
|
53537
|
+
}
|
|
53538
|
+
if (decision.action === "config") {
|
|
53539
|
+
return read.mission === decision.mission ? { name: label, status: "ok", detail: "set from switchroom.yaml" } : {
|
|
53540
|
+
name: label,
|
|
53541
|
+
status: "warn",
|
|
53542
|
+
detail: "switchroom.yaml sets a different observations_mission than the bank carries",
|
|
53543
|
+
fix: `Run: switchroom agent reconcile ${firstAgent}`
|
|
53544
|
+
};
|
|
53545
|
+
}
|
|
53546
|
+
const isSwitchroomDefault = read.mission === (profileDefault ?? DEFAULT_OBSERVATIONS_MISSION);
|
|
53547
|
+
return {
|
|
53548
|
+
name: label,
|
|
53549
|
+
status: "ok",
|
|
53550
|
+
detail: isSwitchroomDefault ? profileDefault != null ? `switchroom ${entry.config.extends ?? DEFAULT_PROFILE}-profile default` : "switchroom fleet default" : "operator-authored (left untouched by the never-clobber rule)"
|
|
53551
|
+
};
|
|
53552
|
+
});
|
|
53553
|
+
}
|
|
53044
53554
|
async function checkBankIngestHealth(config, url, opts) {
|
|
53045
53555
|
const results = [];
|
|
53046
53556
|
const now = opts?.now ?? new Date;
|
|
@@ -53200,6 +53710,7 @@ async function checkHindsight(config) {
|
|
|
53200
53710
|
results.push(checkHnswPartialIndexes());
|
|
53201
53711
|
results.push(await checkHindsightHealthEndpoint(url));
|
|
53202
53712
|
results.push(...await checkBankIngestHealth(config, url, { includeConsolidationBacklog: true }));
|
|
53713
|
+
results.push(...await checkBankObservationsMissions(config, url));
|
|
53203
53714
|
const memoryAgentsDir = resolveAgentsDir(config);
|
|
53204
53715
|
for (const agentName of Object.keys(config.agents)) {
|
|
53205
53716
|
results.push(checkAgentRecallHealth(agentName, resolve44(memoryAgentsDir, agentName)));
|
|
@@ -53314,7 +53825,7 @@ function checkPendingRetainsQueues(config, opts) {
|
|
|
53314
53825
|
}
|
|
53315
53826
|
}
|
|
53316
53827
|
const skippedNote = unreachable.length > 0 ? `; skipped (unreachable): ${unreachable.join(", ")}` : "";
|
|
53317
|
-
const backlogFix = `Clear a backlog explicitly (the SessionStart drain cannot \u2014 its per-entry ` + `timeout is clamped to the hook budget, far below a real retain): ` + `\`docker exec switchroom-<agent> python3 ` + `/state/agent/.claude/plugins/hindsight-memory/scripts/drain_pending.py --backlog\`. ` + `Run \`--phase reconcile\` first \u2014 it costs nothing and typically clears most of ` + `the queue by confirming those documents already exist. PACE THE REST: the retain ` + `model pool is small and shared with live traffic, so drain ONE agent at a time at ` + `the default HINDSIGHT_DRAIN_CONCURRENCY=1, and keep HINDSIGHT_DRAIN_SLEEP_S set. ` + `Point HINDSIGHT_DRAIN_P95_CMD at the same figure the latency watchdog alarms on so ` + `the replay backs off instead of causing the alarm \u2014 on a LiteLLM deployment that is ` + `\`docker exec -i <postgres> psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At -c ` + `"select coalesce((select round(percentile_cont(0.95) within group (order by ` + `request_duration_ms)) from \\"LiteLLM_SpendLogs\\" where \\"startTime\\" > now() ` + `- interval '10 minutes' and metadata->>'user_api_key_alias' like 'hindsight-%' and ` + `status='success' and request_duration_ms is not null), -1)"\` (must print a ` + `millisecond integer on stdout; unset = no backoff). Fix the upstream first (bank ` + `health rows above), or every retry just ages entries toward .dead. Entries the ` + `drain retires are MOVED to \`.hindsight/pending-reconciled/\` (bounded, so they ` + `expire), never deleted \u2014 every retire rests on an HTTP 200, which is an ack and ` + `not proof, so the payload stays recoverable. If that archive cannot be written ` + `(disk full, permissions) the entry STAYS QUEUED and the drain says so on stderr; ` + `a failure to archive is never a reason to delete. FIX THE DISK ANYWAY \u2014 "stays ` + `queued" is bounded: entries pile up, the queue hits ` + `HINDSIGHT_PENDING_MAX_ENTRIES/MAX_BYTES, and enqueue then sheds the OLDEST ` + `entries to keep accepting the newest. While the disk is full their archive move ` + `fails too, so those oldest turns are removed outright (the ledger line reads ` + `\`reason=...+archive-failed\`). Under sustained ENOSPC "keep it queued" degrades ` + `to "keep the newest, drop the oldest".`;
|
|
53828
|
+
const backlogFix = `Clear a backlog explicitly (the SessionStart drain cannot \u2014 its per-entry ` + `timeout is clamped to the hook budget, far below a real retain): ` + `\`docker exec switchroom-<agent> python3 ` + `/state/agent/.claude/plugins/hindsight-memory/scripts/drain_pending.py --backlog\`. ` + `Run \`--phase reconcile\` first \u2014 it costs nothing and typically clears most of ` + `the queue by confirming those documents already exist. PACE THE REST: the retain ` + `model pool is small and shared with live traffic, so drain ONE agent at a time at ` + `the default HINDSIGHT_DRAIN_CONCURRENCY=1, and keep HINDSIGHT_DRAIN_SLEEP_S set. ` + `Point HINDSIGHT_DRAIN_P95_CMD at the same figure the latency watchdog alarms on so ` + `the replay backs off instead of causing the alarm \u2014 on a LiteLLM deployment that is ` + `\`docker exec -i <postgres> psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At -c ` + `"select coalesce((select round(percentile_cont(0.95) within group (order by ` + `request_duration_ms)) from \\"LiteLLM_SpendLogs\\" where \\"startTime\\" > now() ` + `- interval '10 minutes' and metadata->>'user_api_key_alias' like 'hindsight-%' and ` + `status='success' and request_duration_ms is not null), -1)"\` (must print a ` + `millisecond integer on stdout; unset = no backoff). ` + `SAFE ALONGSIDE THE \`hindsight-drain\` SIDECAR and a session booting underneath ` + `you: drain_pending.py takes an exclusive lock on ` + `\`$HOME/.hindsight/drain-pending.lock\` for the whole run, so whichever drain ` + `starts second prints "another drain holds ..." and exits without touching the ` + `queue. Do NOT wrap this in \`flock\` on that path yourself \u2014 you would hold the ` + `lock the script then asks for, and it would skip every time. Entries past the ` + `attempt ceiling (HINDSIGHT_DRAIN_ATTEMPT_CEILING, default 20) are PARKED and ` + `reported rather than retried forever; add \`--force\` once the upstream is fixed ` + `to replay them. ` + `Fix the upstream first (bank ` + `health rows above), or every retry just ages entries toward .dead. Entries the ` + `drain retires are MOVED to \`.hindsight/pending-reconciled/\` (bounded, so they ` + `expire), never deleted \u2014 every retire rests on an HTTP 200, which is an ack and ` + `not proof, so the payload stays recoverable. If that archive cannot be written ` + `(disk full, permissions) the entry STAYS QUEUED and the drain says so on stderr; ` + `a failure to archive is never a reason to delete. FIX THE DISK ANYWAY \u2014 "stays ` + `queued" is bounded: entries pile up, the queue hits ` + `HINDSIGHT_PENDING_MAX_ENTRIES/MAX_BYTES, and enqueue then sheds the OLDEST ` + `entries to keep accepting the newest. While the disk is full their archive move ` + `fails too, so those oldest turns are removed outright (the ledger line reads ` + `\`reason=...+archive-failed\`). Under sustained ENOSPC "keep it queued" degrades ` + `to "keep the newest, drop the oldest".`;
|
|
53318
53829
|
if (dead.length > 0 || dropped.length > 0 || evicted.length > 0) {
|
|
53319
53830
|
const parts = [];
|
|
53320
53831
|
if (dead.length > 0)
|
|
@@ -54506,6 +55017,25 @@ function registerDoctorCommand(program3) {
|
|
|
54506
55017
|
},
|
|
54507
55018
|
{ title: "MFF Skill", results: await checkMff(passphrase, vaultPath, config) },
|
|
54508
55019
|
{ title: "Webkite", results: runWebkiteChecks(config) },
|
|
55020
|
+
{
|
|
55021
|
+
title: "OpenRouter Credit",
|
|
55022
|
+
results: await runOpenRouterCreditChecks({
|
|
55023
|
+
enabled: usesOpenRouter(readLitellmConfigText()),
|
|
55024
|
+
readSecret: async (key) => {
|
|
55025
|
+
try {
|
|
55026
|
+
const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
|
|
55027
|
+
const result = await getViaBrokerStructured2(key);
|
|
55028
|
+
if (result.kind === "ok" && result.entry.kind === "string") {
|
|
55029
|
+
return result.entry.value;
|
|
55030
|
+
}
|
|
55031
|
+
return null;
|
|
55032
|
+
} catch {
|
|
55033
|
+
return null;
|
|
55034
|
+
}
|
|
55035
|
+
},
|
|
55036
|
+
fetchFn: (url, init) => fetch(url, { headers: init?.headers, signal: init?.signal ?? AbortSignal.timeout(1e4) })
|
|
55037
|
+
})
|
|
55038
|
+
},
|
|
54509
55039
|
{ title: "Cron Session", results: runCronSessionChecks(config) },
|
|
54510
55040
|
{
|
|
54511
55041
|
title: "Generated-surface drift (KEN-130)",
|
|
@@ -54573,6 +55103,7 @@ var init_doctor = __esm(() => {
|
|
|
54573
55103
|
init_scaffold();
|
|
54574
55104
|
init_manager();
|
|
54575
55105
|
init_accounts();
|
|
55106
|
+
init_schema();
|
|
54576
55107
|
init_manifest();
|
|
54577
55108
|
init_hindsight2();
|
|
54578
55109
|
init_hindsight();
|
|
@@ -54589,6 +55120,7 @@ var init_doctor = __esm(() => {
|
|
|
54589
55120
|
init_doctor_hostd();
|
|
54590
55121
|
init_doctor_drive();
|
|
54591
55122
|
init_doctor_webkite();
|
|
55123
|
+
init_doctor_openrouter_credit();
|
|
54592
55124
|
init_doctor_cron_session();
|
|
54593
55125
|
init_doctor_flood_pressure();
|
|
54594
55126
|
init_doctor_drift();
|
|
@@ -69707,8 +70239,10 @@ import { join as join19 } from "node:path";
|
|
|
69707
70239
|
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
69708
70240
|
|
|
69709
70241
|
// src/agents/handoff-summarizer.ts
|
|
70242
|
+
init_observation_scopes();
|
|
69710
70243
|
import { readFileSync as readFileSync19, writeFileSync as writeFileSync9, renameSync as renameSync7, mkdirSync as mkdirSync16, existsSync as existsSync23, statSync as statSync11, readdirSync as readdirSync10 } from "node:fs";
|
|
69711
70244
|
import { join as join18 } from "node:path";
|
|
70245
|
+
var HANDOFF_STATUS_MIRROR_SKIPPED = "mirror-skipped-invalid-scope";
|
|
69712
70246
|
var DEFAULT_MAX_TURNS = 50;
|
|
69713
70247
|
var TOPIC_MAX_CHARS = 117;
|
|
69714
70248
|
var TURN_TEXT_MAX_CHARS = 1200;
|
|
@@ -69867,8 +70401,8 @@ async function buildHandoff(opts) {
|
|
|
69867
70401
|
`);
|
|
69868
70402
|
return "write-error";
|
|
69869
70403
|
}
|
|
69870
|
-
await mirrorToHindsight(briefing, opts).catch(() =>
|
|
69871
|
-
return "ok";
|
|
70404
|
+
const mirrored = await mirrorToHindsight(briefing, opts).catch(() => true);
|
|
70405
|
+
return mirrored ? "ok" : HANDOFF_STATUS_MIRROR_SKIPPED;
|
|
69872
70406
|
}
|
|
69873
70407
|
function errMsg(err) {
|
|
69874
70408
|
if (err && typeof err === "object" && "message" in err) {
|
|
@@ -69880,15 +70414,22 @@ async function mirrorToHindsight(briefing, opts) {
|
|
|
69880
70414
|
const url = opts.hindsightUrl ?? process.env.HINDSIGHT_API_URL;
|
|
69881
70415
|
const bankId = opts.hindsightBankId ?? process.env.HINDSIGHT_BANK_ID ?? "default";
|
|
69882
70416
|
if (!url)
|
|
69883
|
-
return;
|
|
70417
|
+
return true;
|
|
69884
70418
|
const fetchFn = opts.fetch ?? fetch;
|
|
69885
70419
|
const endpoint = `${url.replace(/\/$/, "")}/v1/default/banks/${encodeURIComponent(bankId)}/memories`;
|
|
70420
|
+
const scope = resolveObservationScope(opts.observationScopes, opts.env ?? process.env);
|
|
70421
|
+
if (scope.kind === "invalid") {
|
|
70422
|
+
process.stderr.write(`handoff: hindsight mirror SKIPPED \u2014 observation scope ` + `${JSON.stringify(scope.raw)} is not valid ` + `(accepted: ${observationScopesHint()}). Fix ` + `memory.observation_scopes in switchroom.yaml (or the ` + `${OBSERVATION_SCOPES_ENV} export), then \`switchroom apply\` and ` + `restart the agent. The on-disk handoff sidecars were written ` + `normally; only the recallable Hindsight copy was skipped.
|
|
70423
|
+
`);
|
|
70424
|
+
return false;
|
|
70425
|
+
}
|
|
69886
70426
|
const body = {
|
|
69887
70427
|
items: [
|
|
69888
70428
|
{
|
|
69889
70429
|
content: briefing,
|
|
69890
70430
|
document_id: "session_handoff",
|
|
69891
|
-
tags: ["session_handoff", opts.agentName]
|
|
70431
|
+
tags: ["session_handoff", opts.agentName],
|
|
70432
|
+
...scope.kind === "set" ? { observation_scopes: scope.scope } : {}
|
|
69892
70433
|
}
|
|
69893
70434
|
],
|
|
69894
70435
|
async: true
|
|
@@ -69903,6 +70444,7 @@ async function mirrorToHindsight(briefing, opts) {
|
|
|
69903
70444
|
process.stderr.write(`handoff: hindsight mirror failed \u2014 ${errMsg(err)}
|
|
69904
70445
|
`);
|
|
69905
70446
|
}
|
|
70447
|
+
return true;
|
|
69906
70448
|
}
|
|
69907
70449
|
function findLatestSessionJsonl(claudeConfigDir) {
|
|
69908
70450
|
const projects = join18(claudeConfigDir, "projects");
|
|
@@ -91975,6 +92517,7 @@ Dependency manifest`));
|
|
|
91975
92517
|
init_helpers();
|
|
91976
92518
|
init_loader();
|
|
91977
92519
|
import { resolve as resolve48 } from "node:path";
|
|
92520
|
+
init_merge();
|
|
91978
92521
|
|
|
91979
92522
|
// src/agents/session-retention.ts
|
|
91980
92523
|
import {
|
|
@@ -92066,12 +92609,13 @@ function registerHandoffCommand(program3) {
|
|
|
92066
92609
|
let agentDir;
|
|
92067
92610
|
try {
|
|
92068
92611
|
const config = getConfig(program3);
|
|
92069
|
-
|
|
92070
|
-
if (!
|
|
92612
|
+
const rawAgent = config.agents[agentName];
|
|
92613
|
+
if (!rawAgent) {
|
|
92071
92614
|
process.stderr.write(`handoff: agent "${agentName}" not defined in switchroom.yaml
|
|
92072
92615
|
`);
|
|
92073
92616
|
return;
|
|
92074
92617
|
}
|
|
92618
|
+
agentConfig = resolveAgentConfig(config.defaults, config.profiles, rawAgent);
|
|
92075
92619
|
const agentsDir = resolveAgentsDir(config);
|
|
92076
92620
|
agentDir = resolve48(agentsDir, agentName);
|
|
92077
92621
|
} catch (err) {
|
|
@@ -92101,10 +92645,14 @@ function registerHandoffCommand(program3) {
|
|
|
92101
92645
|
jsonlPath: jsonl,
|
|
92102
92646
|
agentDir,
|
|
92103
92647
|
agentName,
|
|
92104
|
-
maxTurns: cappedMaxTurns
|
|
92648
|
+
maxTurns: cappedMaxTurns,
|
|
92649
|
+
observationScopes: agentConfig?.memory?.observation_scopes
|
|
92105
92650
|
});
|
|
92106
92651
|
process.stderr.write(`handoff: ${status}
|
|
92107
92652
|
`);
|
|
92653
|
+
if (status === HANDOFF_STATUS_MIRROR_SKIPPED) {
|
|
92654
|
+
process.exitCode = 1;
|
|
92655
|
+
}
|
|
92108
92656
|
const retention = pruneSessionJsonl(claudeConfigDir, {
|
|
92109
92657
|
maxCount: continuity?.session_retention_max_count ?? DEFAULT_SESSION_RETENTION_MAX_COUNT,
|
|
92110
92658
|
maxAgeDays: continuity?.session_retention_max_age_days ?? DEFAULT_SESSION_RETENTION_MAX_AGE_DAYS,
|
|
@@ -101885,25 +102433,25 @@ function syncIssue(deps, repo, job_spec, iss) {
|
|
|
101885
102433
|
}
|
|
101886
102434
|
return;
|
|
101887
102435
|
}
|
|
101888
|
-
const
|
|
101889
|
-
const upd = deps.run(["issue", "edit",
|
|
102436
|
+
const num2 = String(iss.gh_issue);
|
|
102437
|
+
const upd = deps.run(["issue", "edit", num2, "-R", repo, "--body", body]);
|
|
101890
102438
|
if (!upd.ok) {
|
|
101891
|
-
deps.log(`fleet-health: gh issue edit #${
|
|
102439
|
+
deps.log(`fleet-health: gh issue edit #${num2} failed: ${upd.stderr}`);
|
|
101892
102440
|
}
|
|
101893
102441
|
if (iss.status === "closed") {
|
|
101894
102442
|
const c = deps.run([
|
|
101895
102443
|
"issue",
|
|
101896
102444
|
"close",
|
|
101897
|
-
|
|
102445
|
+
num2,
|
|
101898
102446
|
"-R",
|
|
101899
102447
|
repo,
|
|
101900
102448
|
"--comment",
|
|
101901
102449
|
`Verified count-drop (frequency ${iss.frequency} \u2264 resolved threshold). Closed by the Fleet Health sensor.`
|
|
101902
102450
|
]);
|
|
101903
102451
|
if (c.ok)
|
|
101904
|
-
deps.log(`fleet-health: closed GH #${
|
|
102452
|
+
deps.log(`fleet-health: closed GH #${num2} (count-drop) for ${iss.dedup_key}`);
|
|
101905
102453
|
else
|
|
101906
|
-
deps.log(`fleet-health: gh issue close #${
|
|
102454
|
+
deps.log(`fleet-health: gh issue close #${num2} failed: ${c.stderr}`);
|
|
101907
102455
|
}
|
|
101908
102456
|
}
|
|
101909
102457
|
function syncLedgerIssues(ledger, repo, deps) {
|
|
@@ -103275,6 +103823,278 @@ async function notifyOperator(agentsDir, text, log) {
|
|
|
103275
103823
|
return agent !== null;
|
|
103276
103824
|
}
|
|
103277
103825
|
|
|
103826
|
+
// src/cli/openrouter-watch.ts
|
|
103827
|
+
init_source();
|
|
103828
|
+
import { existsSync as existsSync113, readdirSync as readdirSync46 } from "node:fs";
|
|
103829
|
+
import { homedir as homedir64 } from "node:os";
|
|
103830
|
+
import { join as join112, resolve as resolve65 } from "node:path";
|
|
103831
|
+
|
|
103832
|
+
// src/openrouter/install-cron.ts
|
|
103833
|
+
import { existsSync as existsSync112, mkdirSync as mkdirSync63, readFileSync as readFileSync99, renameSync as renameSync28, writeFileSync as writeFileSync47 } from "node:fs";
|
|
103834
|
+
import { dirname as dirname42 } from "node:path";
|
|
103835
|
+
var CRON_PATH2 = "/etc/cron.d/openrouter-watch";
|
|
103836
|
+
var CRON_SCHEDULE2 = "17 * * * *";
|
|
103837
|
+
var CRON_LOG_PATH2 = "/var/log/openrouter-watch.log";
|
|
103838
|
+
var CRON_LOCK_PATH2 = "/run/lock/openrouter-watch.lock";
|
|
103839
|
+
function renderCron2(opts) {
|
|
103840
|
+
return `# switchroom openrouter-watch \u2014 model-free OpenRouter credit watchdog.
|
|
103841
|
+
` + `# Managed by \`switchroom openrouter-watch --install-cron\`; edits are overwritten.
|
|
103842
|
+
` + `# Exit 0 = clean, 10 = credit signal firing (DM sent), 1 = the check could not complete.
|
|
103843
|
+
` + `SHELL=/bin/sh
|
|
103844
|
+
` + `PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
103845
|
+
` + `${CRON_SCHEDULE2} ${opts.user} /usr/bin/flock -n ${CRON_LOCK_PATH2} ` + `${opts.binary} openrouter-watch >> ${CRON_LOG_PATH2} 2>&1
|
|
103846
|
+
`;
|
|
103847
|
+
}
|
|
103848
|
+
function installCron2(opts) {
|
|
103849
|
+
const path10 = opts.path ?? CRON_PATH2;
|
|
103850
|
+
const content = renderCron2(opts);
|
|
103851
|
+
if (existsSync112(path10)) {
|
|
103852
|
+
try {
|
|
103853
|
+
if (readFileSync99(path10, "utf8") === content) {
|
|
103854
|
+
return { status: "unchanged", path: path10, content };
|
|
103855
|
+
}
|
|
103856
|
+
} catch {}
|
|
103857
|
+
}
|
|
103858
|
+
mkdirSync63(dirname42(path10), { recursive: true });
|
|
103859
|
+
const tmp = `${path10}.${process.pid}.tmp`;
|
|
103860
|
+
writeFileSync47(tmp, content, { mode: 420 });
|
|
103861
|
+
renameSync28(tmp, path10);
|
|
103862
|
+
return { status: "installed", path: path10, content };
|
|
103863
|
+
}
|
|
103864
|
+
// src/openrouter/state.ts
|
|
103865
|
+
import { mkdirSync as mkdirSync64, readFileSync as readFileSync100, renameSync as renameSync29, writeFileSync as writeFileSync48 } from "node:fs";
|
|
103866
|
+
import { homedir as homedir63 } from "node:os";
|
|
103867
|
+
import { dirname as dirname43, resolve as resolve64 } from "node:path";
|
|
103868
|
+
function defaultStatePath3(home2 = process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir63()) {
|
|
103869
|
+
return resolve64(home2, ".switchroom", "openrouter-watch", "state.json");
|
|
103870
|
+
}
|
|
103871
|
+
function loadState2(path10) {
|
|
103872
|
+
let parsed;
|
|
103873
|
+
try {
|
|
103874
|
+
parsed = JSON.parse(readFileSync100(path10, "utf8"));
|
|
103875
|
+
} catch {
|
|
103876
|
+
return {};
|
|
103877
|
+
}
|
|
103878
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
103879
|
+
return {};
|
|
103880
|
+
const s = parsed;
|
|
103881
|
+
if (s.v !== 1)
|
|
103882
|
+
return {};
|
|
103883
|
+
const out = {};
|
|
103884
|
+
if (typeof s.lastNotifiedAt === "number" && Number.isFinite(s.lastNotifiedAt)) {
|
|
103885
|
+
out.lastNotifiedAt = s.lastNotifiedAt;
|
|
103886
|
+
}
|
|
103887
|
+
if (typeof s.lastNotifiedStatus === "string")
|
|
103888
|
+
out.lastNotifiedStatus = s.lastNotifiedStatus;
|
|
103889
|
+
if (out.lastNotifiedAt === undefined)
|
|
103890
|
+
delete out.lastNotifiedStatus;
|
|
103891
|
+
return out;
|
|
103892
|
+
}
|
|
103893
|
+
function saveState2(path10, state) {
|
|
103894
|
+
mkdirSync64(dirname43(path10), { recursive: true, mode: 448 });
|
|
103895
|
+
const tmp = `${path10}.${process.pid}.tmp`;
|
|
103896
|
+
const payload = { v: 1, ...state };
|
|
103897
|
+
writeFileSync48(tmp, JSON.stringify(payload, null, 2) + `
|
|
103898
|
+
`, { mode: 384 });
|
|
103899
|
+
renameSync29(tmp, path10);
|
|
103900
|
+
}
|
|
103901
|
+
|
|
103902
|
+
// src/openrouter/watch.ts
|
|
103903
|
+
var RENOTIFY_MS2 = 6 * 60 * 60 * 1000;
|
|
103904
|
+
function decideNotification(assessment, state, now) {
|
|
103905
|
+
if (!assessment.actionable) {
|
|
103906
|
+
return { notice: null, nextState: {}, firing: false };
|
|
103907
|
+
}
|
|
103908
|
+
const escalated = state.lastNotifiedStatus != null && state.lastNotifiedStatus !== "fail" && assessment.status === "fail";
|
|
103909
|
+
const cooled = state.lastNotifiedAt == null || now - state.lastNotifiedAt >= RENOTIFY_MS2;
|
|
103910
|
+
if (!escalated && !cooled) {
|
|
103911
|
+
return { notice: null, nextState: state, firing: true };
|
|
103912
|
+
}
|
|
103913
|
+
const repeat = state.lastNotifiedAt != null && !escalated;
|
|
103914
|
+
return {
|
|
103915
|
+
notice: renderNotice(assessment, { repeat, escalated }),
|
|
103916
|
+
nextState: { lastNotifiedAt: now, lastNotifiedStatus: assessment.status },
|
|
103917
|
+
firing: true
|
|
103918
|
+
};
|
|
103919
|
+
}
|
|
103920
|
+
function renderNotice(assessment, opts) {
|
|
103921
|
+
const head = assessment.status === "fail" ? opts.escalated ? "\uD83D\uDEA8 OpenRouter credit \u2014 ESCALATED" : "\uD83D\uDEA8 OpenRouter credit" : "\u26a0\ufe0f OpenRouter credit";
|
|
103922
|
+
const suffix = opts.repeat && !opts.escalated ? " (still unresolved)" : "";
|
|
103923
|
+
return [
|
|
103924
|
+
`${head}${suffix}`,
|
|
103925
|
+
assessment.detail,
|
|
103926
|
+
assessment.fix ? `\u2192 ${assessment.fix}` : ""
|
|
103927
|
+
].filter((l) => l.length > 0).join(`
|
|
103928
|
+
`);
|
|
103929
|
+
}
|
|
103930
|
+
|
|
103931
|
+
// src/openrouter/run.ts
|
|
103932
|
+
async function tick2(deps) {
|
|
103933
|
+
const now = deps.now?.() ?? Date.now();
|
|
103934
|
+
let apiKey = null;
|
|
103935
|
+
try {
|
|
103936
|
+
apiKey = await deps.readSecret(OPENROUTER_VAULT_KEY);
|
|
103937
|
+
} catch (err2) {
|
|
103938
|
+
deps.log(`openrouter-watch: vault read failed for \`${OPENROUTER_VAULT_KEY}\`: ${err2.message}`);
|
|
103939
|
+
}
|
|
103940
|
+
if (!apiKey) {
|
|
103941
|
+
deps.log(`openrouter-watch: vault key \`${OPENROUTER_VAULT_KEY}\` is not readable \u2014 credit cannot be checked.`);
|
|
103942
|
+
return {
|
|
103943
|
+
assessment: {
|
|
103944
|
+
status: "skip",
|
|
103945
|
+
detail: `Vault key \`${OPENROUTER_VAULT_KEY}\` unreadable.`,
|
|
103946
|
+
actionable: false
|
|
103947
|
+
},
|
|
103948
|
+
notice: null,
|
|
103949
|
+
firing: false,
|
|
103950
|
+
delivered: false,
|
|
103951
|
+
exitCode: 1
|
|
103952
|
+
};
|
|
103953
|
+
}
|
|
103954
|
+
const fetched = await fetchKeySnapshot(apiKey, deps.fetchFn);
|
|
103955
|
+
const assessment = fetched.kind === "ok" ? evaluateCredit(fetched.snapshot) : assessFetchFailure(fetched);
|
|
103956
|
+
const prior = loadState2(deps.statePath);
|
|
103957
|
+
const decision = decideNotification(assessment, prior, now);
|
|
103958
|
+
let delivered = false;
|
|
103959
|
+
if (decision.notice && !deps.dryRun) {
|
|
103960
|
+
delivered = await deps.notify(decision.notice);
|
|
103961
|
+
}
|
|
103962
|
+
if (!deps.dryRun && (decision.notice === null || delivered)) {
|
|
103963
|
+
try {
|
|
103964
|
+
saveState2(deps.statePath, decision.nextState);
|
|
103965
|
+
} catch (err2) {
|
|
103966
|
+
deps.log(`openrouter-watch: could not persist state: ${err2.message}`);
|
|
103967
|
+
return { assessment, notice: decision.notice, firing: decision.firing, delivered, exitCode: 1 };
|
|
103968
|
+
}
|
|
103969
|
+
}
|
|
103970
|
+
if (decision.notice && !deps.dryRun && !delivered) {
|
|
103971
|
+
deps.log("openrouter-watch: alert reached no gateway \u2014 undelivered, will retry next tick");
|
|
103972
|
+
return { assessment, notice: decision.notice, firing: decision.firing, delivered, exitCode: 1 };
|
|
103973
|
+
}
|
|
103974
|
+
const cannotComplete = fetched.kind === "unreachable";
|
|
103975
|
+
const exitCode = cannotComplete ? 1 : decision.firing ? 10 : 0;
|
|
103976
|
+
return { assessment, notice: decision.notice, firing: decision.firing, delivered, exitCode };
|
|
103977
|
+
}
|
|
103978
|
+
// src/cli/openrouter-watch.ts
|
|
103979
|
+
function registerOpenRouterWatchCommand(program3) {
|
|
103980
|
+
program3.command("openrouter-watch").description("Model-free OpenRouter credit watchdog: warns before the key's credit runs out. " + "Alerts at most once per 6h, and escalates immediately.").option("--install-cron", `arm the watchdog: write ${CRON_PATH2} (${CRON_SCHEDULE2}, flock-guarded) and exit`, false).option("--cron-user <user>", "unix user the cron tick runs as (default: current user)").option("--dry-run", "evaluate and print; send no DM and write no state", false).option("--json", "emit machine-readable JSON", false).option("--state <path>", "state file path (default ~/.switchroom/openrouter-watch/state.json)").option("--agents-dir <path>", "agents scaffold dir (default ~/.switchroom/agents)").action(async (opts) => {
|
|
103981
|
+
if (opts.installCron) {
|
|
103982
|
+
process.exitCode = runInstallCron2(opts.cronUser);
|
|
103983
|
+
return;
|
|
103984
|
+
}
|
|
103985
|
+
const agentsDir = opts.agentsDir ?? process.env.SWITCHROOM_AGENTS_DIR ?? join112(process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir64(), ".switchroom", "agents");
|
|
103986
|
+
const statePath = opts.state ?? defaultStatePath3();
|
|
103987
|
+
const log = (m) => {
|
|
103988
|
+
process.stderr.write(`${m}
|
|
103989
|
+
`);
|
|
103990
|
+
};
|
|
103991
|
+
const result = await tick2({
|
|
103992
|
+
statePath,
|
|
103993
|
+
dryRun: Boolean(opts.dryRun),
|
|
103994
|
+
log,
|
|
103995
|
+
readSecret: async (key) => {
|
|
103996
|
+
const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
|
|
103997
|
+
const res = await getViaBrokerStructured2(key);
|
|
103998
|
+
if (res.kind === "ok" && res.entry.kind === "string")
|
|
103999
|
+
return res.entry.value;
|
|
104000
|
+
return null;
|
|
104001
|
+
},
|
|
104002
|
+
fetchFn: (url, init) => fetch(url, {
|
|
104003
|
+
headers: init?.headers,
|
|
104004
|
+
signal: init?.signal ?? AbortSignal.timeout(1e4)
|
|
104005
|
+
}),
|
|
104006
|
+
notify: (text) => notifyOperator2(agentsDir, text, log)
|
|
104007
|
+
});
|
|
104008
|
+
if (opts.json) {
|
|
104009
|
+
process.stdout.write(JSON.stringify({
|
|
104010
|
+
status: result.assessment.status,
|
|
104011
|
+
detail: result.assessment.detail,
|
|
104012
|
+
fix: result.assessment.fix ?? null,
|
|
104013
|
+
actionable: result.assessment.actionable,
|
|
104014
|
+
firing: result.firing,
|
|
104015
|
+
notified: result.notice,
|
|
104016
|
+
delivered: result.delivered,
|
|
104017
|
+
exitCode: result.exitCode,
|
|
104018
|
+
vaultKey: OPENROUTER_VAULT_KEY,
|
|
104019
|
+
thresholds: {
|
|
104020
|
+
warnRemainingFraction: WARN_REMAINING_FRACTION,
|
|
104021
|
+
warnRemainingUsd: WARN_REMAINING_USD,
|
|
104022
|
+
failRemainingFraction: FAIL_REMAINING_FRACTION,
|
|
104023
|
+
failRemainingUsd: FAIL_REMAINING_USD,
|
|
104024
|
+
renotifyMs: RENOTIFY_MS2
|
|
104025
|
+
}
|
|
104026
|
+
}, null, 2) + `
|
|
104027
|
+
`);
|
|
104028
|
+
} else {
|
|
104029
|
+
const mark = result.assessment.status === "fail" ? source_default.red("FAIL") : result.assessment.status === "warn" ? source_default.yellow("WARN") : result.assessment.status === "skip" ? source_default.gray("skip") : source_default.green("ok ");
|
|
104030
|
+
process.stdout.write(`${mark} ${result.assessment.detail}
|
|
104031
|
+
`);
|
|
104032
|
+
if (result.assessment.fix)
|
|
104033
|
+
process.stdout.write(` \u2192 ${result.assessment.fix}
|
|
104034
|
+
`);
|
|
104035
|
+
if (result.notice) {
|
|
104036
|
+
const header = opts.dryRun ? "would send" : result.delivered ? "sent" : "UNDELIVERED";
|
|
104037
|
+
process.stdout.write(`
|
|
104038
|
+
${source_default.bold(`operator DM ${header}:`)}
|
|
104039
|
+
`);
|
|
104040
|
+
process.stdout.write(result.notice.split(`
|
|
104041
|
+
`).map((l) => ` | ${l}`).join(`
|
|
104042
|
+
`) + `
|
|
104043
|
+
`);
|
|
104044
|
+
} else if (result.firing) {
|
|
104045
|
+
process.stdout.write(source_default.gray(` (firing, but already reported within the ${RENOTIFY_MS2 / 3600000}h window)
|
|
104046
|
+
`));
|
|
104047
|
+
}
|
|
104048
|
+
}
|
|
104049
|
+
process.exitCode = result.exitCode;
|
|
104050
|
+
});
|
|
104051
|
+
}
|
|
104052
|
+
function runInstallCron2(cronUser) {
|
|
104053
|
+
const user = cronUser ?? process.env.SUDO_USER ?? process.env.USER ?? process.env.LOGNAME;
|
|
104054
|
+
if (!user || user === "root") {
|
|
104055
|
+
process.stderr.write(source_default.red("openrouter-watch: refusing to install a cron for `root`.\n") + " The state file and the agents scaffold live under the OPERATOR's " + "~/.switchroom, so a root tick would keep its re-notify ledger somewhere " + `nothing else reads.
|
|
104056
|
+
` + " Re-run with `--cron-user <operator>`.\n");
|
|
104057
|
+
return 1;
|
|
104058
|
+
}
|
|
104059
|
+
const binary = process.env.SWITCHROOM_BINARY ?? "/usr/local/bin/switchroom";
|
|
104060
|
+
if (!binary.startsWith("/")) {
|
|
104061
|
+
process.stderr.write(source_default.red(`openrouter-watch: SWITCHROOM_BINARY must be an absolute path (got ${binary})
|
|
104062
|
+
`));
|
|
104063
|
+
return 1;
|
|
104064
|
+
}
|
|
104065
|
+
let res;
|
|
104066
|
+
try {
|
|
104067
|
+
res = installCron2({ user, binary });
|
|
104068
|
+
} catch (e) {
|
|
104069
|
+
process.stderr.write(source_default.red(`openrouter-watch: could not write ${CRON_PATH2}: ${e.message}
|
|
104070
|
+
`) + " This path needs root; re-run under sudo with `--cron-user <operator>`.\n");
|
|
104071
|
+
return 1;
|
|
104072
|
+
}
|
|
104073
|
+
const verb = res.status === "installed" ? "installed" : "already up to date";
|
|
104074
|
+
process.stdout.write(`${source_default.green("\u2713")} openrouter-watch cron ${verb} at ${res.path} ` + `(${CRON_SCHEDULE2}, as ${user})
|
|
104075
|
+
` + ` Verify with: switchroom openrouter-watch --dry-run
|
|
104076
|
+
`);
|
|
104077
|
+
return 0;
|
|
104078
|
+
}
|
|
104079
|
+
async function notifyOperator2(agentsDir, text, log) {
|
|
104080
|
+
const candidates = [];
|
|
104081
|
+
try {
|
|
104082
|
+
for (const name of readdirSync46(agentsDir).sort()) {
|
|
104083
|
+
const sock = resolve65(agentsDir, name, "telegram", "gateway.sock");
|
|
104084
|
+
if (existsSync113(sock))
|
|
104085
|
+
candidates.push({ agent: name, sock });
|
|
104086
|
+
}
|
|
104087
|
+
} catch (e) {
|
|
104088
|
+
log(`openrouter-watch: agents dir unreadable (${e.message})`);
|
|
104089
|
+
}
|
|
104090
|
+
if (candidates.length === 0) {
|
|
104091
|
+
log("openrouter-watch: no gateway socket found \u2014 alert undelivered, will retry next tick");
|
|
104092
|
+
return false;
|
|
104093
|
+
}
|
|
104094
|
+
const agent = await postOperatorNoticeViaGateways(candidates, text, log, 5000, "openrouter-watch", "");
|
|
104095
|
+
return agent !== null;
|
|
104096
|
+
}
|
|
104097
|
+
|
|
103278
104098
|
// src/cli/index.ts
|
|
103279
104099
|
init_posthog();
|
|
103280
104100
|
installGlobalErrorHandlers();
|
|
@@ -103334,6 +104154,7 @@ registerWebdCommand(program3);
|
|
|
103334
104154
|
registerHostCommand(program3);
|
|
103335
104155
|
registerFleetHealthCommand(program3);
|
|
103336
104156
|
registerHindsightWatchCommand(program3);
|
|
104157
|
+
registerOpenRouterWatchCommand(program3);
|
|
103337
104158
|
|
|
103338
104159
|
// bin/switchroom.ts
|
|
103339
104160
|
program3.parse();
|