switchroom 0.18.30 → 0.18.32
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 +4 -2
- package/dist/auth-broker/index.js +4 -2
- package/dist/cli/notion-write-pretool.mjs +4 -2
- package/dist/cli/switchroom.js +708 -255
- package/dist/host-control/main.js +5 -3
- package/dist/vault/approvals/kernel-server.js +4 -2
- package/dist/vault/broker/server.js +4 -2
- package/package.json +3 -2
- package/profiles/_base/start.sh.hbs +142 -7
- package/telegram-plugin/dist/gateway/gateway.js +25870 -25007
- package/telegram-plugin/gateway/backstop-delivery.ts +223 -23
- package/telegram-plugin/gateway/captured-answer-resume.ts +259 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +6 -44
- package/telegram-plugin/gateway/gateway-import-clean.test.ts +188 -0
- package/telegram-plugin/gateway/gateway.ts +5479 -7069
- package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +7 -15
- package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +35 -68
- package/telegram-plugin/gateway/obligation-ledger.ts +42 -0
- package/telegram-plugin/gateway/obligation-store.ts +37 -1
- package/telegram-plugin/gateway/outbound-send-path.ts +2012 -0
- package/telegram-plugin/gateway/turn-flush-suppression.ts +82 -0
- package/telegram-plugin/pending-user-notice.ts +59 -13
- package/telegram-plugin/subagent-watcher.ts +111 -28
- package/telegram-plugin/tests/backstop-delivery.test.ts +167 -0
- package/telegram-plugin/tests/backstop-readback-probe.test.ts +144 -0
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +16 -6
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +3 -3
- package/telegram-plugin/tests/captured-answer-resume.test.ts +358 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +29 -19
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +4 -1
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +18 -7
- package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +249 -0
- package/telegram-plugin/tests/gateway-bot-construction-deferral.test.ts +251 -0
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +5 -128
- package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +303 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +10 -3
- package/telegram-plugin/tests/inbound-delivery-cutover-flip.test.ts +54 -150
- package/telegram-plugin/tests/inbound-delivery-cutover-gate.test.ts +10 -14
- package/telegram-plugin/tests/inbound-delivery-dispatch-equivalence.test.ts +6 -7
- package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +0 -16
- package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +4 -4
- package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +69 -14
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +9 -3
- package/telegram-plugin/tests/obligation-ledger.test.ts +40 -0
- package/telegram-plugin/tests/obligation-store.test.ts +43 -0
- package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +2 -1
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +9 -6
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +5 -2
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -2
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +3 -2
- package/telegram-plugin/tests/send-reply-golden.test.ts +571 -0
- package/telegram-plugin/tests/subagent-watcher-resume-reregister.test.ts +291 -0
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +32 -0
- package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +8 -12
- package/telegram-plugin/tests/turn-flush-safety.test.ts +8 -6
- package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +112 -0
- package/telegram-plugin/tests/turn-flush-suppression.test.ts +90 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +2 -2
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +126 -0
- package/telegram-plugin/tool-activity-summary.ts +27 -3
- package/telegram-plugin/worker-activity-feed.ts +24 -0
- package/telegram-plugin/gateway/busy-key-reaper.ts +0 -113
- package/telegram-plugin/gateway/gate-parity-probe.ts +0 -102
- package/telegram-plugin/tests/busy-key-reaper.test.ts +0 -192
- package/telegram-plugin/tests/fixtures/cutover-killswitch-probe.ts +0 -75
- package/telegram-plugin/tests/gate-parity-probe.test.ts +0 -171
- package/telegram-plugin/tests/parallel-turns-deadlock-fix.test.ts +0 -217
|
@@ -19265,7 +19265,8 @@ var MemoryBackendConfigSchema = exports_external.object({
|
|
|
19265
19265
|
llm_model: exports_external.string().optional().describe("LiteLLM model name for Hindsight's LLM ops (retain/reflect/" + "consolidation) when the top-level `litellm.enabled` carve-out is " + "on. Defaults to a cheap OpenRouter model (routed via LiteLLM's " + "model-mapped path, not the Anthropic OAuth pass-through) to keep " + "background memory-op cost off the Claude subscription quota. Set " + "to a `claude-*` model name to route it back through the OAuth " + "pass-through instead. Has no effect when litellm is disabled."),
|
|
19266
19266
|
api_key: exports_external.string().optional().describe("API key or vault reference for embedding provider"),
|
|
19267
19267
|
docker_service: exports_external.boolean().default(true).describe("Whether to include Hindsight in docker-compose"),
|
|
19268
|
-
url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/.")
|
|
19268
|
+
url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/."),
|
|
19269
|
+
mcp_transport: exports_external.enum(["shim", "http"]).optional().describe("How agents reach Hindsight's MCP surface. Default (unset or " + "'shim'): the lazy-connect stdio proxy `switchroom " + "hindsight-mcp-shim`, which always registers at session start " + "even when the hindsight container is down and reconnects " + "per-call. 'http' is the escape hatch that restores the legacy " + "direct Streamable-HTTP entry (session-fatal if the backend is " + "down when the agent session boots).")
|
|
19269
19270
|
}).optional()
|
|
19270
19271
|
});
|
|
19271
19272
|
var VaultConfigSchema = exports_external.object({
|
|
@@ -19308,7 +19309,8 @@ var HostControlConfigSchema = exports_external.object({
|
|
|
19308
19309
|
auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
|
|
19309
19310
|
});
|
|
19310
19311
|
var WebServiceConfigSchema = exports_external.object({
|
|
19311
|
-
managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
|
|
19312
|
+
managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers."),
|
|
19313
|
+
port: exports_external.number().int().min(1).max(65535).default(8080).describe("Host loopback port the web server listens on (127.0.0.1:<port>). " + "Default: 8080. Set this when another service owns 8080 on the " + "host (e.g. UniFi OS Server) — `switchroom webd install` and the " + "`switchroom update` refresh-web step both honor it, so a custom " + "port survives updates instead of being silently reverted to " + "8080 by the regenerated compose file. Remember to point the " + "host-side consumers (cloudflared tunnel, `tailscale serve`) at " + "the same port.")
|
|
19312
19314
|
});
|
|
19313
19315
|
var FleetHealthConfigSchema = exports_external.object({
|
|
19314
19316
|
owner_agent: exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
@@ -26645,7 +26647,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:f
|
|
|
26645
26647
|
import { dirname as dirname4, join as join7 } from "node:path";
|
|
26646
26648
|
|
|
26647
26649
|
// src/build-info.ts
|
|
26648
|
-
var VERSION = "0.18.
|
|
26650
|
+
var VERSION = "0.18.32";
|
|
26649
26651
|
|
|
26650
26652
|
// src/cli/resolve-version.ts
|
|
26651
26653
|
function readPackageVersion() {
|
|
@@ -4685,7 +4685,8 @@ var init_schema = __esm(() => {
|
|
|
4685
4685
|
llm_model: exports_external.string().optional().describe("LiteLLM model name for Hindsight's LLM ops (retain/reflect/" + "consolidation) when the top-level `litellm.enabled` carve-out is " + "on. Defaults to a cheap OpenRouter model (routed via LiteLLM's " + "model-mapped path, not the Anthropic OAuth pass-through) to keep " + "background memory-op cost off the Claude subscription quota. Set " + "to a `claude-*` model name to route it back through the OAuth " + "pass-through instead. Has no effect when litellm is disabled."),
|
|
4686
4686
|
api_key: exports_external.string().optional().describe("API key or vault reference for embedding provider"),
|
|
4687
4687
|
docker_service: exports_external.boolean().default(true).describe("Whether to include Hindsight in docker-compose"),
|
|
4688
|
-
url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/.")
|
|
4688
|
+
url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/."),
|
|
4689
|
+
mcp_transport: exports_external.enum(["shim", "http"]).optional().describe("How agents reach Hindsight's MCP surface. Default (unset or " + "'shim'): the lazy-connect stdio proxy `switchroom " + "hindsight-mcp-shim`, which always registers at session start " + "even when the hindsight container is down and reconnects " + "per-call. 'http' is the escape hatch that restores the legacy " + "direct Streamable-HTTP entry (session-fatal if the backend is " + "down when the agent session boots).")
|
|
4689
4690
|
}).optional()
|
|
4690
4691
|
});
|
|
4691
4692
|
VaultConfigSchema = exports_external.object({
|
|
@@ -4728,7 +4729,8 @@ var init_schema = __esm(() => {
|
|
|
4728
4729
|
auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
|
|
4729
4730
|
});
|
|
4730
4731
|
WebServiceConfigSchema = exports_external.object({
|
|
4731
|
-
managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
|
|
4732
|
+
managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers."),
|
|
4733
|
+
port: exports_external.number().int().min(1).max(65535).default(8080).describe("Host loopback port the web server listens on (127.0.0.1:<port>). " + "Default: 8080. Set this when another service owns 8080 on the " + "host (e.g. UniFi OS Server) — `switchroom webd install` and the " + "`switchroom update` refresh-web step both honor it, so a custom " + "port survives updates instead of being silently reverted to " + "8080 by the regenerated compose file. Remember to point the " + "host-side consumers (cloudflared tunnel, `tailscale serve`) at " + "the same port.")
|
|
4732
4734
|
});
|
|
4733
4735
|
FleetHealthConfigSchema = exports_external.object({
|
|
4734
4736
|
owner_agent: exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
@@ -4685,7 +4685,8 @@ var init_schema = __esm(() => {
|
|
|
4685
4685
|
llm_model: exports_external.string().optional().describe("LiteLLM model name for Hindsight's LLM ops (retain/reflect/" + "consolidation) when the top-level `litellm.enabled` carve-out is " + "on. Defaults to a cheap OpenRouter model (routed via LiteLLM's " + "model-mapped path, not the Anthropic OAuth pass-through) to keep " + "background memory-op cost off the Claude subscription quota. Set " + "to a `claude-*` model name to route it back through the OAuth " + "pass-through instead. Has no effect when litellm is disabled."),
|
|
4686
4686
|
api_key: exports_external.string().optional().describe("API key or vault reference for embedding provider"),
|
|
4687
4687
|
docker_service: exports_external.boolean().default(true).describe("Whether to include Hindsight in docker-compose"),
|
|
4688
|
-
url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/.")
|
|
4688
|
+
url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/."),
|
|
4689
|
+
mcp_transport: exports_external.enum(["shim", "http"]).optional().describe("How agents reach Hindsight's MCP surface. Default (unset or " + "'shim'): the lazy-connect stdio proxy `switchroom " + "hindsight-mcp-shim`, which always registers at session start " + "even when the hindsight container is down and reconnects " + "per-call. 'http' is the escape hatch that restores the legacy " + "direct Streamable-HTTP entry (session-fatal if the backend is " + "down when the agent session boots).")
|
|
4689
4690
|
}).optional()
|
|
4690
4691
|
});
|
|
4691
4692
|
VaultConfigSchema = exports_external.object({
|
|
@@ -4728,7 +4729,8 @@ var init_schema = __esm(() => {
|
|
|
4728
4729
|
auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
|
|
4729
4730
|
});
|
|
4730
4731
|
WebServiceConfigSchema = exports_external.object({
|
|
4731
|
-
managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
|
|
4732
|
+
managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers."),
|
|
4733
|
+
port: exports_external.number().int().min(1).max(65535).default(8080).describe("Host loopback port the web server listens on (127.0.0.1:<port>). " + "Default: 8080. Set this when another service owns 8080 on the " + "host (e.g. UniFi OS Server) — `switchroom webd install` and the " + "`switchroom update` refresh-web step both honor it, so a custom " + "port survives updates instead of being silently reverted to " + "8080 by the regenerated compose file. Remember to point the " + "host-side consumers (cloudflared tunnel, `tailscale serve`) at " + "the same port.")
|
|
4732
4734
|
});
|
|
4733
4735
|
FleetHealthConfigSchema = exports_external.object({
|
|
4734
4736
|
owner_agent: exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.18.
|
|
4
|
+
"version": "0.18.32",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"test:vitest": "vitest run",
|
|
29
29
|
"test:bun": "bun test telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
30
30
|
"test:watch": "vitest",
|
|
31
|
-
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs",
|
|
31
|
+
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs",
|
|
32
32
|
"lint:tsc": "tsc --noEmit",
|
|
33
33
|
"lint:plugin-references": "node scripts/check-plugin-references.mjs",
|
|
34
34
|
"lint:bot-api-wrapping": "bash scripts/check-bot-api-wrapping.sh",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"lint:no-pii": "node scripts/check-no-pii-secrets.mjs",
|
|
37
37
|
"lint:web-subscription-honest": "node scripts/check-web-subscription-honest.mjs",
|
|
38
38
|
"lint:no-broadcast-delivery": "node scripts/check-no-broadcast-delivery.mjs",
|
|
39
|
+
"lint:gateway-line-ratchet": "node scripts/check-gateway-line-ratchet.mjs",
|
|
39
40
|
"prepublishOnly": "npm run build && npm run lint && npm test"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
@@ -602,16 +602,54 @@ export HINDSIGHT_SENDER_BANKS_JSON={{{hindsightSenderBanksJsonQ}}}
|
|
|
602
602
|
{{#if hindsightTopicFilterMode}}
|
|
603
603
|
export HINDSIGHT_TOPIC_FILTER_MODE={{hindsightTopicFilterMode}}
|
|
604
604
|
{{/if}}
|
|
605
|
-
#
|
|
606
|
-
#
|
|
605
|
+
# Give Claude Code a longer MCP server-startup budget (ms). The default can
|
|
606
|
+
# race a Hindsight restart window; the boot gate below usually absorbs it,
|
|
607
|
+
# but this keeps the in-session connect attempt itself patient too.
|
|
608
|
+
# Documented Claude Code env var (MCP server startup timeout).
|
|
609
|
+
export MCP_TIMEOUT="${MCP_TIMEOUT:-30000}"
|
|
610
|
+
# Wait for Hindsight MCP to be READY before launching Claude, otherwise the
|
|
611
|
+
# MCP server connection fails at startup with "1 MCP server failed" and the
|
|
612
|
+
# session boots toolless (incident 2026-07-18: clerk booted toolless despite
|
|
613
|
+
# a 30s bare-GET gate — the endpoint returned HTTP 200 mid-restart while a
|
|
614
|
+
# real MCP initialize still failed). So: probe with an actual JSON-RPC
|
|
615
|
+
# initialize POST and require a JSON-RPC result, not just HTTP 200.
|
|
616
|
+
# Bounded at 120s of real wall-clock time (each probe costs up to ~3s of
|
|
617
|
+
# curl timeout + 1s sleep, so an iteration count would overshoot — measure
|
|
618
|
+
# elapsed seconds against the deadline instead; HINDSIGHT_WAIT holds real
|
|
619
|
+
# elapsed seconds). On timeout we log a warning and boot anyway (degraded
|
|
620
|
+
# beats never-boots — the plugin reconnects once Hindsight recovers).
|
|
607
621
|
HINDSIGHT_WAIT=0
|
|
608
|
-
|
|
609
|
-
|
|
622
|
+
HINDSIGHT_READY=0
|
|
623
|
+
_hs_start=$(date +%s)
|
|
624
|
+
while [ $HINDSIGHT_WAIT -lt 120 ]; do
|
|
625
|
+
_hs_resp=$(curl -sf --max-time 3 -i \
|
|
626
|
+
-X POST {{{hindsightApiBaseUrlQ}}}/mcp/ \
|
|
627
|
+
-H "Content-Type: application/json" \
|
|
628
|
+
-H "Accept: application/json, text/event-stream" \
|
|
629
|
+
-d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"switchroom-boot-gate","version":"1.0"}}}' \
|
|
630
|
+
2>/dev/null)
|
|
631
|
+
# Match on serverInfo — only present in a successful initialize result;
|
|
632
|
+
# a JSON-RPC *error* body could legitimately contain the substring "result".
|
|
633
|
+
if printf '%s\n' "$_hs_resp" | grep -q '"serverInfo"'; then
|
|
634
|
+
HINDSIGHT_READY=1
|
|
635
|
+
# Best-effort cleanup of the probe's server-side MCP session (one would
|
|
636
|
+
# leak per boot otherwise). Header may be absent on stateless servers.
|
|
637
|
+
_hs_sid=$(printf '%s\n' "$_hs_resp" | tr -d '\r' | sed -n 's/^[Mm][Cc][Pp]-[Ss]ession-[Ii][Dd]: *//p' | head -n 1)
|
|
638
|
+
if [ -n "$_hs_sid" ]; then
|
|
639
|
+
curl -s -o /dev/null --max-time 2 -X DELETE {{{hindsightApiBaseUrlQ}}}/mcp/ \
|
|
640
|
+
-H "Mcp-Session-Id: $_hs_sid" 2>/dev/null || true
|
|
641
|
+
fi
|
|
642
|
+
HINDSIGHT_WAIT=$(( $(date +%s) - _hs_start ))
|
|
610
643
|
break
|
|
611
644
|
fi
|
|
612
645
|
sleep 1
|
|
613
|
-
HINDSIGHT_WAIT=$((
|
|
646
|
+
HINDSIGHT_WAIT=$(( $(date +%s) - _hs_start ))
|
|
614
647
|
done
|
|
648
|
+
if [ "$HINDSIGHT_READY" = "1" ]; then
|
|
649
|
+
echo "start.sh: hindsight boot gate — MCP initialize OK after ${HINDSIGHT_WAIT}s" >&2
|
|
650
|
+
else
|
|
651
|
+
echo "start.sh: WARNING hindsight boot gate TIMED OUT after ${HINDSIGHT_WAIT}s — MCP initialize never returned a JSON-RPC result; launching claude anyway (memory tools may be unavailable this session)" >&2
|
|
652
|
+
fi
|
|
615
653
|
{{/if}}
|
|
616
654
|
{{#if userEnvQuoted}}
|
|
617
655
|
# User-declared env vars from switchroom.yaml (agent + defaults merged).
|
|
@@ -1170,15 +1208,92 @@ fi
|
|
|
1170
1208
|
# which the gateway relays to the operator chat once at boot. The normal apply
|
|
1171
1209
|
# path is silent here — the gateway already acked the `/model` in chat.
|
|
1172
1210
|
#
|
|
1211
|
+
# --- Live configured-default resolution (Defect B / drift fix) ---
|
|
1212
|
+
#
|
|
1213
|
+
# The apply-time bake ({{{modelQ}}}) is only the LAST-DITCH fallback: editing
|
|
1214
|
+
# `model:` in switchroom.yaml and restarting must take effect WITHOUT a full
|
|
1215
|
+
# `switchroom apply`. The launcher asks the SAME resolver the gateway's
|
|
1216
|
+
# /status uses (`switchroom agent effective-model`, which applies
|
|
1217
|
+
# resolveMainModel server-side) against the live mounted yaml — one resolver,
|
|
1218
|
+
# never re-implemented in shell, so what-you-see and what-runs cannot split.
|
|
1219
|
+
#
|
|
1220
|
+
# Fallback chain, never silent past step 1:
|
|
1221
|
+
# 1. live read of $SWITCHROOM_CONFIG (one bounded retry — a host-side
|
|
1222
|
+
# in-place rewrite can present a torn file for a moment);
|
|
1223
|
+
# 2. `.configured-default-model` — the last-known-good live value this
|
|
1224
|
+
# launcher recorded on every prior boot (read BEFORE it is overwritten);
|
|
1225
|
+
# 3. the apply-time bake.
|
|
1226
|
+
# Steps 2/3 append to `.session-model-alert` so the gateway tells the
|
|
1227
|
+
# operator once at boot. No SWITCHROOM_CONFIG mount at all means there is
|
|
1228
|
+
# nothing to read live — the bake IS the truth there (stderr note only; an
|
|
1229
|
+
# operator alert every boot forever would be noise with no action).
|
|
1230
|
+
#
|
|
1231
|
+
# The yaml file bind mount re-resolves by PATH at container start, so a
|
|
1232
|
+
# host-side atomic-rename write is always picked up by the NEXT boot — the
|
|
1233
|
+
# path this block runs on. Only mid-life reads by long-running processes can
|
|
1234
|
+
# see a stale inode; that pre-existing edge is out of scope here (tracked as
|
|
1235
|
+
# a follow-up on the drift fix).
|
|
1236
|
+
#
|
|
1173
1237
|
# NB {{{modelQ}}} is already shell-single-quoted by the scaffold (it renders as
|
|
1174
1238
|
# a quoted token, e.g. 'claude-sonnet-5'), so it is assigned BARE here — never
|
|
1175
1239
|
# inside additional double quotes, which would embed the literal quote chars in
|
|
1176
1240
|
# the value and break `claude --model`.
|
|
1177
|
-
|
|
1241
|
+
_BAKED_MODEL={{{modelQ}}}
|
|
1242
|
+
_LKG_MODEL="$(cat "{{agentDir}}/.configured-default-model" 2>/dev/null | tr -d '[:space:]' || true)"
|
|
1243
|
+
_EFFECTIVE_MODEL=""
|
|
1244
|
+
if [ -n "$SWITCHROOM_CONFIG" ] && [ -f "$SWITCHROOM_CONFIG" ] && command -v switchroom >/dev/null 2>&1; then
|
|
1245
|
+
_lm_live=""
|
|
1246
|
+
for _lm_try in 1 2; do
|
|
1247
|
+
_lm_live="$(timeout 20 switchroom --config "$SWITCHROOM_CONFIG" agent effective-model "{{name}}" 2>/dev/null | tail -n 1 | tr -d '[:space:]')" || _lm_live=""
|
|
1248
|
+
# Shape gate — same regex as the .session-model carrier below (byte-kept
|
|
1249
|
+
# with MODEL_ARG_RE); the tr above already guarantees single-line.
|
|
1250
|
+
if [ -n "$_lm_live" ] && printf '%s' "$_lm_live" | grep -Eq '^[A-Za-z0-9][]A-Za-z0-9._[/-]{0,99}$'; then
|
|
1251
|
+
break
|
|
1252
|
+
fi
|
|
1253
|
+
_lm_live=""
|
|
1254
|
+
if [ "$_lm_try" -eq 1 ]; then sleep 1; fi
|
|
1255
|
+
done
|
|
1256
|
+
if [ -n "$_lm_live" ]; then
|
|
1257
|
+
# Routing class (Claude passthrough vs sr-* router root) is baked into
|
|
1258
|
+
# compose at render time, so a Claude<->sr-* class flip CANNOT be applied
|
|
1259
|
+
# by a bare restart — half-applying would run the new model against the
|
|
1260
|
+
# old routing. Detect the flip, keep the baked model, and tell the
|
|
1261
|
+
# operator to run a real apply.
|
|
1262
|
+
case "$_BAKED_MODEL" in sr-*) _lm_baked_class=router ;; *) _lm_baked_class=claude ;; esac
|
|
1263
|
+
case "$_lm_live" in sr-*) _lm_live_class=router ;; *) _lm_live_class=claude ;; esac
|
|
1264
|
+
if [ "$_lm_live_class" != "$_lm_baked_class" ]; then
|
|
1265
|
+
echo "session-model: configured model class changed ('$_BAKED_MODEL' -> '$_lm_live', $_lm_baked_class -> $_lm_live_class) — routing is render-time, NOT applying; run 'switchroom apply' then restart" >&2
|
|
1266
|
+
printf 'The configured model changed from `%s` to `%s`, which switches routing class — a restart alone cannot apply that. The agent booted on `%s`. Run `switchroom apply` on the host, then restart this agent.\n' "$_BAKED_MODEL" "$_lm_live" "$_BAKED_MODEL" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1267
|
+
_EFFECTIVE_MODEL="$_BAKED_MODEL"
|
|
1268
|
+
else
|
|
1269
|
+
_EFFECTIVE_MODEL="$_lm_live"
|
|
1270
|
+
if [ "$_lm_live" != "$_BAKED_MODEL" ]; then
|
|
1271
|
+
echo "session-model: live configured model '$_lm_live' differs from apply-time bake '$_BAKED_MODEL' — using the live value (switchroom.yaml is the source of truth)" >&2
|
|
1272
|
+
fi
|
|
1273
|
+
fi
|
|
1274
|
+
elif [ -n "$_LKG_MODEL" ] && printf '%s' "$_LKG_MODEL" | grep -Eq '^[A-Za-z0-9][]A-Za-z0-9._[/-]{0,99}$'; then
|
|
1275
|
+
# Live read failed (torn write mid-read, yaml schema error, CLI wedge).
|
|
1276
|
+
# Last-known-good beats the bake: it was live truth on a prior boot,
|
|
1277
|
+
# while the bake can predate any number of yaml edits. Loud either way —
|
|
1278
|
+
# a silent fallback here would resurrect Defect B as an intermittent flap.
|
|
1279
|
+
_EFFECTIVE_MODEL="$_LKG_MODEL"
|
|
1280
|
+
echo "session-model: live config read FAILED — using last-known-good configured model '$_LKG_MODEL' (apply-time bake: '$_BAKED_MODEL')" >&2
|
|
1281
|
+
printf 'switchroom.yaml could not be read at boot, so the agent booted on the last-known-good configured model `%s`. If you just changed `model:`, check the yaml for errors and restart the agent.\n' "$_LKG_MODEL" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1282
|
+
else
|
|
1283
|
+
_EFFECTIVE_MODEL="$_BAKED_MODEL"
|
|
1284
|
+
echo "session-model: live config read FAILED and no last-known-good value — using apply-time baked model '$_BAKED_MODEL'" >&2
|
|
1285
|
+
printf 'switchroom.yaml could not be read at boot and no prior boot recorded a model, so the agent booted on the apply-time default `%s`. If you just changed `model:`, check the yaml for errors and restart the agent.\n' "$_BAKED_MODEL" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1286
|
+
fi
|
|
1287
|
+
unset _lm_live _lm_try _lm_baked_class _lm_live_class
|
|
1288
|
+
else
|
|
1289
|
+
_EFFECTIVE_MODEL="$_BAKED_MODEL"
|
|
1290
|
+
fi
|
|
1291
|
+
unset _BAKED_MODEL _LKG_MODEL
|
|
1178
1292
|
# Record the RESOLVED configured default (raw, unquoted) every boot, before
|
|
1179
1293
|
# override resolution. The gateway copies this into the carrier's
|
|
1180
1294
|
# `configuredDefaultAtWrite`, so both sides of the invalidation compare below
|
|
1181
|
-
# come from the same resolver. Overwrite, not consumed
|
|
1295
|
+
# come from the same resolver. Overwrite, not consumed — and doubling as the
|
|
1296
|
+
# last-known-good live value the fallback chain above reads on the NEXT boot.
|
|
1182
1297
|
printf '%s\n' "$_EFFECTIVE_MODEL" > "{{agentDir}}/.configured-default-model" 2>/dev/null || true
|
|
1183
1298
|
|
|
1184
1299
|
# Rev-4 hygiene (#3184 review LOW-2): remove the files retired with the
|
|
@@ -1304,6 +1419,26 @@ if [ -f "{{agentDir}}/.session-model" ]; then
|
|
|
1304
1419
|
unset _smf _sm_model _sm_cfg _sm_attempts _SM_MAX_ATTEMPTS
|
|
1305
1420
|
fi
|
|
1306
1421
|
|
|
1422
|
+
# Configured-default LiteLLM guard. A proxy-only *configured* model with
|
|
1423
|
+
# LiteLLM unreachable at boot would exec against the /anthropic passthrough,
|
|
1424
|
+
# where `fable`/`claude-fable-5` is a retired codename and EVERY call 4xxs —
|
|
1425
|
+
# a total outage. The carrier block above already drops proxy-only OVERRIDES
|
|
1426
|
+
# on a down proxy; this extends the same guard to the configured default,
|
|
1427
|
+
# which the live-config read above makes a mainline path (pinning `model:
|
|
1428
|
+
# fable` in yaml is now the normal way to run Fable). Claude agents have a
|
|
1429
|
+
# safe same-class fallback (opus); sr-* configured defaults keep today's
|
|
1430
|
+
# loud-degraded behavior — there is no Claude fallback for them and the
|
|
1431
|
+
# repoint case below already leaves their routing to self-heal.
|
|
1432
|
+
case "$_EFFECTIVE_MODEL" in
|
|
1433
|
+
fable|claude-fable-5)
|
|
1434
|
+
if [ -z "$_LITELLM_OK" ]; then
|
|
1435
|
+
echo "session-model: configured model '$_EFFECTIVE_MODEL' is proxy-only but LiteLLM is unreachable at boot — booting 'opus' until the proxy recovers" >&2
|
|
1436
|
+
printf 'LiteLLM was unreachable at boot, so the configured model `%s` (proxy-only) could not be used — the agent booted on `opus` instead. Restart the agent once the proxy is back to return to `%s`.\n' "$_EFFECTIVE_MODEL" "$_EFFECTIVE_MODEL" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1437
|
+
_EFFECTIVE_MODEL="opus"
|
|
1438
|
+
fi
|
|
1439
|
+
;;
|
|
1440
|
+
esac
|
|
1441
|
+
|
|
1307
1442
|
# --- Session effort resolution (consume-once .session-effort carrier, #3186) ---
|
|
1308
1443
|
#
|
|
1309
1444
|
# The `/effort` sibling of the block above, session-scoped like /model
|