switchroom 0.7.15 → 0.10.0
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/README.md +51 -59
- package/bin/run-hook.sh +27 -11
- package/bin/timezone-hook.sh +9 -7
- package/dist/agent-scheduler/index.js +410 -133
- package/dist/auth-broker/index.js +13932 -0
- package/dist/cli/switchroom.js +26937 -5601
- package/dist/host-control/main.js +12702 -0
- package/dist/vault/approvals/kernel-server.js +467 -184
- package/dist/vault/broker/server.js +1430 -724
- package/examples/minimal.yaml +63 -0
- package/examples/personal-google-workspace-mcp/.env.example +34 -0
- package/examples/personal-google-workspace-mcp/README.md +194 -0
- package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
- package/examples/switchroom.yaml +220 -0
- package/package.json +7 -4
- package/profiles/_base/settings.json.hbs +20 -5
- package/profiles/_base/start.sh.hbs +16 -3
- package/profiles/_shared/agent-self-service.md.hbs +126 -0
- package/profiles/_shared/telegram-style.md.hbs +20 -90
- package/profiles/_shared/vault-protocol.md.hbs +68 -0
- package/profiles/default/CLAUDE.md +50 -96
- package/profiles/default/CLAUDE.md.hbs +36 -6
- package/profiles/default/workspace/SOUL.md.hbs +12 -5
- package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
- package/skills/buildkite-agent-runtime/SKILL.md +44 -11
- package/skills/buildkite-api/SKILL.md +31 -8
- package/skills/buildkite-cli/SKILL.md +27 -9
- package/skills/buildkite-migration/SKILL.md +22 -9
- package/skills/buildkite-pipelines/SKILL.md +26 -9
- package/skills/buildkite-secure-delivery/SKILL.md +23 -9
- package/skills/buildkite-test-engine/SKILL.md +25 -8
- package/skills/docx/SKILL.md +1 -1
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
- package/skills/file-bug/SKILL.md +34 -6
- package/skills/humanizer/SKILL.md +15 -0
- package/skills/humanizer-calibrate/SKILL.md +7 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/pdf/SKILL.md +1 -1
- package/skills/pptx/SKILL.md +1 -1
- package/skills/skill-creator/SKILL.md +21 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
- package/skills/switchroom-cli/SKILL.md +63 -64
- package/skills/switchroom-health/SKILL.md +23 -10
- package/skills/switchroom-install/SKILL.md +3 -3
- package/skills/switchroom-manage/SKILL.md +26 -19
- package/skills/switchroom-runtime/SKILL.md +191 -0
- package/skills/switchroom-status/SKILL.md +27 -2
- package/skills/telegram-test-harness/SKILL.md +3 -0
- package/skills/token-helpers/SKILL.md +24 -1
- package/skills/webapp-testing/SKILL.md +31 -1
- package/skills/xlsx/SKILL.md +1 -1
- package/telegram-plugin/admin-commands/index.ts +7 -5
- package/telegram-plugin/analytics-posthog.ts +191 -0
- package/telegram-plugin/bridge/bridge.ts +69 -0
- package/telegram-plugin/bridge/ipc-client.ts +4 -1
- package/telegram-plugin/dist/bridge/bridge.js +194 -119
- package/telegram-plugin/dist/gateway/gateway.js +23611 -19671
- package/telegram-plugin/dist/server.js +245 -189
- package/telegram-plugin/first-paint.ts +3 -24
- package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
- package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
- package/telegram-plugin/gateway/auth-command.ts +794 -0
- package/telegram-plugin/gateway/auth-line.ts +123 -0
- package/telegram-plugin/gateway/boot-card.ts +169 -40
- package/telegram-plugin/gateway/boot-issue-cache.ts +308 -0
- package/telegram-plugin/gateway/boot-probes.ts +166 -123
- package/telegram-plugin/gateway/boot-reason.ts +41 -7
- package/telegram-plugin/gateway/boot-version.ts +66 -0
- package/telegram-plugin/gateway/gateway.ts +3499 -1885
- package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +18 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +106 -0
- package/telegram-plugin/gateway/quarantine.ts +69 -0
- package/telegram-plugin/gateway/quota-cache.ts +9 -4
- package/telegram-plugin/gateway/reaction-trigger.ts +401 -0
- package/telegram-plugin/gateway/recent-denials.test.ts +103 -0
- package/telegram-plugin/gateway/recent-denials.ts +77 -0
- package/telegram-plugin/gateway/startup-network-retry.ts +109 -31
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +125 -0
- package/telegram-plugin/history.ts +91 -0
- package/telegram-plugin/hooks/hooks.json +10 -0
- package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +130 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -2
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +22 -2
- package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
- package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
- package/telegram-plugin/inbound-classifier.ts +50 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +136 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/package.json +4 -2
- package/telegram-plugin/permission-rule.ts +51 -0
- package/telegram-plugin/permission-title.ts +56 -0
- package/telegram-plugin/quota-check.ts +19 -41
- package/telegram-plugin/registry/reaper.ts +223 -0
- package/telegram-plugin/retry-api-call.ts +80 -0
- package/telegram-plugin/runtime-metrics.ts +177 -0
- package/telegram-plugin/scripts/build.mjs +0 -1
- package/telegram-plugin/secret-detect/index.ts +24 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +64 -12
- package/telegram-plugin/secret-detect/vault-error.ts +78 -11
- package/telegram-plugin/secret-detect/vault-write.ts +14 -2
- package/telegram-plugin/server.js +41795 -0
- package/telegram-plugin/session-tail.ts +6 -1
- package/telegram-plugin/shared/bot-runtime.ts +5 -4
- package/telegram-plugin/silence-poke.ts +420 -0
- package/telegram-plugin/silent-end.ts +174 -0
- package/telegram-plugin/stream-controller.ts +13 -0
- package/telegram-plugin/stream-reply-handler.ts +7 -0
- package/telegram-plugin/subagent-watcher.ts +213 -4
- package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
- package/telegram-plugin/tests/boot-card-issue-dedup.test.ts +247 -0
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +182 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +65 -2
- package/telegram-plugin/tests/boot-card-render.test.ts +146 -0
- package/telegram-plugin/tests/boot-card-silent-on-operator.test.ts +103 -0
- package/telegram-plugin/tests/boot-probes.test.ts +216 -10
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/finalize-callback.test.ts +190 -0
- package/telegram-plugin/tests/gateway-message-validator.test.ts +26 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +12 -3
- package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +104 -0
- package/telegram-plugin/tests/history-reaper.test.ts +378 -0
- package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
- package/telegram-plugin/tests/inbound-classifier.test.ts +76 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +267 -0
- package/telegram-plugin/tests/issues-card.test.ts +49 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +132 -0
- package/telegram-plugin/tests/permission-rule.test.ts +80 -1
- package/telegram-plugin/tests/permission-title.test.ts +31 -0
- package/telegram-plugin/tests/quota-check.test.ts +5 -35
- package/telegram-plugin/tests/races.test.ts +179 -0
- package/telegram-plugin/tests/reaction-trigger-flow.test.ts +353 -0
- package/telegram-plugin/tests/reaction-trigger.test.ts +397 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +152 -1
- package/telegram-plugin/tests/runtime-metrics.test.ts +145 -0
- package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +155 -0
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +133 -0
- package/telegram-plugin/tests/secret-detect-false-positives.test.ts +137 -0
- package/telegram-plugin/tests/silence-poke.test.ts +493 -0
- package/telegram-plugin/tests/silent-end.test.ts +206 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +107 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +224 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +316 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +263 -0
- package/telegram-plugin/tests/turn-signal-tracker.test.ts +81 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +256 -0
- package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +73 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +226 -0
- package/telegram-plugin/tests/vault-grant-union.test.ts +130 -0
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +140 -0
- package/telegram-plugin/tests/vault-posture-quarantine.test.ts +104 -0
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +114 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +106 -0
- package/telegram-plugin/turn-signal-tracker.ts +100 -24
- package/telegram-plugin/uat/SETUP.md +210 -35
- package/telegram-plugin/uat/assertions.ts +264 -37
- package/telegram-plugin/uat/driver-info.ts +57 -0
- package/telegram-plugin/uat/driver.ts +590 -51
- package/telegram-plugin/uat/harness.ts +140 -94
- package/telegram-plugin/uat/load-env.test.ts +72 -0
- package/telegram-plugin/uat/load-env.ts +48 -0
- package/telegram-plugin/uat/login.ts +96 -53
- package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
- package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
- package/telegram-plugin/uat/runners/report.ts +150 -0
- package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
- package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
- package/telegram-plugin/uat/runners/scorer.ts +106 -0
- package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
- package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
- package/telegram-plugin/uat/scenarios/ask-user-button-tap-dm.test.ts +141 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +255 -0
- package/telegram-plugin/uat/scenarios/fuzz-human-style-dm.test.ts +275 -0
- package/telegram-plugin/uat/scenarios/fuzz-random-prompts-dm.test.ts +146 -0
- package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +486 -0
- package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +100 -0
- package/telegram-plugin/uat/scenarios/jtbd-soft-commit-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-status-query-dm.test.ts +49 -0
- package/telegram-plugin/uat/scenarios/location-inbound-dm.test.ts +65 -0
- package/telegram-plugin/uat/scenarios/midturn-silent-dm.test.ts +175 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +142 -0
- package/telegram-plugin/uat/scenarios/reactions-trigger-turn-dm.test.ts +96 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-deletes-original-dm.test.ts +123 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-no-false-positive-dm.test.ts +87 -0
- package/telegram-plugin/uat/scenarios/silence-poke-soft-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +95 -0
- package/telegram-plugin/uat/scenarios/smoke-dm-reply.test.ts +57 -0
- package/telegram-plugin/uat/scenarios/subagent-watcher-no-rerun-dm.test.ts +135 -0
- package/telegram-plugin/uat/scenarios/vault-approval-posture-telegram-id-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/vault-audit-allow-dm.test.ts +108 -0
- package/telegram-plugin/uat/scenarios/vault-grant-auto-resume-dm.test.ts +121 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-concurrent-dm.test.ts +161 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-end-to-end-dm.test.ts +158 -0
- package/telegram-plugin/uat/scenarios/voice-inbound-dm.test.ts +65 -0
- package/telegram-plugin/vault-approval-posture.ts +42 -0
- package/telegram-plugin/welcome-text.ts +1 -0
- package/telegram-plugin/active-pins-sweep.ts +0 -204
- package/telegram-plugin/active-pins.ts +0 -146
- package/telegram-plugin/auth-dashboard.ts +0 -1104
- package/telegram-plugin/auth-slot-parser.ts +0 -497
- package/telegram-plugin/card-event-log.ts +0 -138
- package/telegram-plugin/dist/foreman/foreman.js +0 -31106
- package/telegram-plugin/docs/multi-agent-card-design.md +0 -847
- package/telegram-plugin/docs/pinned-progress-card-reliability.md +0 -144
- package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
- package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
- package/telegram-plugin/foreman/foreman.ts +0 -1165
- package/telegram-plugin/foreman/setup-flow.ts +0 -345
- package/telegram-plugin/foreman/setup-state.ts +0 -239
- package/telegram-plugin/foreman/state.ts +0 -203
- package/telegram-plugin/pin-event-log.ts +0 -76
- package/telegram-plugin/progress-card-driver.ts +0 -2886
- package/telegram-plugin/progress-card-pin-manager.ts +0 -589
- package/telegram-plugin/progress-card-pin-watchdog.ts +0 -98
- package/telegram-plugin/progress-card.ts +0 -1409
- package/telegram-plugin/tests/HARNESS.md +0 -340
- package/telegram-plugin/tests/_progress-card-harness.ts +0 -109
- package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +0 -211
- package/telegram-plugin/tests/active-pins-sweep.test.ts +0 -309
- package/telegram-plugin/tests/active-pins.test.ts +0 -187
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
- package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
- package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
- package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +0 -201
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
- package/telegram-plugin/tests/card-event-log.test.ts +0 -145
- package/telegram-plugin/tests/first-paint.test.ts +0 -257
- package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
- package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
- package/telegram-plugin/tests/foreman-state.test.ts +0 -164
- package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
- package/telegram-plugin/tests/harness-ordering-invariants.test.ts +0 -243
- package/telegram-plugin/tests/pin-event-log.test.ts +0 -124
- package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +0 -73
- package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +0 -272
- package/telegram-plugin/tests/progress-card-cross-turn.test.ts +0 -258
- package/telegram-plugin/tests/progress-card-delay-842.test.ts +0 -160
- package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +0 -81
- package/telegram-plugin/tests/progress-card-draft-flag.test.ts +0 -80
- package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +0 -215
- package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +0 -123
- package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +0 -76
- package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +0 -62
- package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +0 -84
- package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +0 -139
- package/telegram-plugin/tests/progress-card-pin-manager.test.ts +0 -773
- package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +0 -66
- package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +0 -64
- package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +0 -190
- package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +0 -146
- package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +0 -123
- package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +0 -82
- package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +0 -114
- package/telegram-plugin/tests/real-gateway-harness.ts +0 -699
- package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +0 -313
- package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +0 -299
- package/telegram-plugin/tests/real-gateway-spec.test.ts +0 -487
- package/telegram-plugin/tests/real-gateway.smoke.test.ts +0 -101
- package/telegram-plugin/tests/setup-flow.test.ts +0 -510
- package/telegram-plugin/tests/setup-state.test.ts +0 -146
- package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +0 -116
- package/telegram-plugin/tests/turn-end-regressions.test.ts +0 -489
- package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +0 -218
- package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +0 -131
- package/telegram-plugin/tests/two-zone-bg-detection.test.ts +0 -120
- package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +0 -116
- package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +0 -87
- package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +0 -211
- package/telegram-plugin/tests/two-zone-card-cap.test.ts +0 -62
- package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +0 -110
- package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +0 -128
- package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +0 -58
- package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +0 -133
- package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +0 -155
- package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +0 -117
- package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +0 -187
- package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +0 -149
- package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +0 -114
- package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +0 -105
- package/telegram-plugin/tests/waiting-ux-harness.ts +0 -381
- package/telegram-plugin/tests/waiting-ux.e2e.test.ts +0 -233
- package/telegram-plugin/turn-flush-prose-recovery.ts +0 -40
- package/telegram-plugin/two-zone-card.ts +0 -269
- package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +0 -61
|
@@ -17,6 +17,45 @@ import { basename } from "node:path";
|
|
|
17
17
|
const COMMAND_TITLE_MAX = 40;
|
|
18
18
|
const PATH_TITLE_MAX = 40;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Human-friendly descriptions for switchroom-managed MCP tools. The
|
|
22
|
+
* raw `mcp__<server>__<tool>` name is operator-unfriendly — they shouldn't
|
|
23
|
+
* have to decode the namespace to understand what the agent is asking
|
|
24
|
+
* to do. Use this map to turn the code-level identifier into a verb
|
|
25
|
+
* phrase ("Read its own merged config" instead of
|
|
26
|
+
* "mcp__agent-config__config_get") for the approval card.
|
|
27
|
+
*
|
|
28
|
+
* Note: post-#1215 these tools are pre-allowed in scaffolded
|
|
29
|
+
* settings.permissions.allow, so the card should fire rarely.
|
|
30
|
+
* This map is for the fallback path — agents the operator
|
|
31
|
+
* narrowed the allowlist on, or tools added in future PRs that
|
|
32
|
+
* haven't shipped the allowlist bump yet.
|
|
33
|
+
*/
|
|
34
|
+
const MCP_TOOL_DESCRIPTIONS: Record<string, string> = {
|
|
35
|
+
// agent-config — every agent's self-service surface (#1163, #1215)
|
|
36
|
+
"mcp__agent-config__config_get": "Read its own merged config",
|
|
37
|
+
"mcp__agent-config__cron_list": "List its own scheduled tasks",
|
|
38
|
+
"mcp__agent-config__skill_list": "List its own installed skills",
|
|
39
|
+
"mcp__agent-config__audit_tail": "Read its own recent tool-call audit log",
|
|
40
|
+
"mcp__agent-config__peers_list": "List the other agents on this instance",
|
|
41
|
+
"mcp__agent-config__schedule_add": "Add a scheduled task to its own cron",
|
|
42
|
+
"mcp__agent-config__schedule_remove": "Remove one of its own scheduled tasks",
|
|
43
|
+
"mcp__agent-config__skill_install": "Install a bundled skill onto itself",
|
|
44
|
+
"mcp__agent-config__skill_remove": "Remove one of its own installed skills",
|
|
45
|
+
// hostd — admin-flagged agents' fleet-management surface (#1175, #1215)
|
|
46
|
+
"mcp__hostd__agent_restart": "Restart an agent in the fleet",
|
|
47
|
+
"mcp__hostd__agent_start": "Start a stopped agent in the fleet",
|
|
48
|
+
"mcp__hostd__agent_stop": "Stop a running agent in the fleet",
|
|
49
|
+
"mcp__hostd__agent_logs": "Read another agent's container logs",
|
|
50
|
+
"mcp__hostd__agent_exec": "Run a read-only inspection inside another agent",
|
|
51
|
+
"mcp__hostd__update_check": "Check what a fleet-wide update would do",
|
|
52
|
+
"mcp__hostd__update_apply": "Apply a fleet-wide update (pull + recreate)",
|
|
53
|
+
// hindsight — memory
|
|
54
|
+
"mcp__hindsight__recall": "Recall relevant memories",
|
|
55
|
+
"mcp__hindsight__retain": "Retain a memory",
|
|
56
|
+
"mcp__hindsight__reflect": "Reflect across its memory bank",
|
|
57
|
+
};
|
|
58
|
+
|
|
20
59
|
/**
|
|
21
60
|
* Build a title fragment for a permission prompt. Returns the toolName
|
|
22
61
|
* for any tool we don't recognise — the helper is intentionally
|
|
@@ -27,6 +66,23 @@ export function summarizeToolForTitle(
|
|
|
27
66
|
toolName: string,
|
|
28
67
|
inputPreview: string | undefined,
|
|
29
68
|
): string {
|
|
69
|
+
// MCP tools: `mcp__<server>__<verb>`. Prefer a curated human
|
|
70
|
+
// description (so the card reads "Read its own merged config"
|
|
71
|
+
// instead of "mcp__agent-config__config_get"). Fall through to a
|
|
72
|
+
// generic `<server>: <verb-with-spaces>` shape for unknown MCP
|
|
73
|
+
// tools and finally to the raw name when even that fails.
|
|
74
|
+
if (toolName.startsWith("mcp__")) {
|
|
75
|
+
const curated = MCP_TOOL_DESCRIPTIONS[toolName];
|
|
76
|
+
if (curated) return curated;
|
|
77
|
+
const parts = toolName.split("__");
|
|
78
|
+
if (parts.length >= 3) {
|
|
79
|
+
const server = parts[1]!;
|
|
80
|
+
const verb = parts.slice(2).join("__").replace(/_/g, " ");
|
|
81
|
+
return `${server}: ${verb}`;
|
|
82
|
+
}
|
|
83
|
+
return toolName;
|
|
84
|
+
}
|
|
85
|
+
|
|
30
86
|
const input = parseInput(inputPreview);
|
|
31
87
|
if (!input) return toolName;
|
|
32
88
|
|
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
|
|
18
18
|
import { readFileSync, existsSync } from "fs";
|
|
19
19
|
import { join } from "path";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
|
|
21
|
+
// RFC H: per-account quota state moved to switchroom-auth-broker
|
|
22
|
+
// (state/auth-broker/quota.json). The gateway's in-process cache
|
|
23
|
+
// below is still useful for sub-second formatting, but the disk-
|
|
24
|
+
// persistence layer that account-quota-store provided is gone —
|
|
25
|
+
// the broker owns the canonical store and exposes it via
|
|
26
|
+
// `list-state`. Disk hydrate / disk persist below are no-ops.
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* OAuth beta flag — proves the request is coming from a subscription client.
|
|
@@ -350,20 +352,10 @@ export async function fetchAccountQuota(
|
|
|
350
352
|
timeoutMs: opts.timeoutMs,
|
|
351
353
|
});
|
|
352
354
|
accountQuotaCache.set(label, { fetchedAt: now, result });
|
|
353
|
-
//
|
|
354
|
-
// re-hydrate
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
try {
|
|
358
|
-
writeAccountQuota(
|
|
359
|
-
label,
|
|
360
|
-
snapshotFromQuotaUtilization(result.data, new Date(now)),
|
|
361
|
-
opts.home,
|
|
362
|
-
);
|
|
363
|
-
} catch {
|
|
364
|
-
/* best-effort */
|
|
365
|
-
}
|
|
366
|
-
}
|
|
355
|
+
// Note: pre-RFC-H this also persisted to disk via writeAccountQuota
|
|
356
|
+
// (#708) so a gateway restart could re-hydrate without an API call.
|
|
357
|
+
// Post-RFC-H the broker holds canonical quota state and answers
|
|
358
|
+
// via `list-state`, so the gateway's in-process cache is enough.
|
|
367
359
|
return result;
|
|
368
360
|
}
|
|
369
361
|
|
|
@@ -381,29 +373,15 @@ export async function fetchAccountQuota(
|
|
|
381
373
|
* prefetch will replace it on the next tap.
|
|
382
374
|
*/
|
|
383
375
|
export function hydrateAccountQuotaCacheFromDisk(
|
|
384
|
-
|
|
385
|
-
|
|
376
|
+
_labels: ReadonlyArray<string>,
|
|
377
|
+
_home?: string,
|
|
386
378
|
): void {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const result: QuotaResult = {
|
|
394
|
-
ok: true,
|
|
395
|
-
data: {
|
|
396
|
-
fiveHourUtilizationPct: snap.fiveHourPct ?? 0,
|
|
397
|
-
sevenDayUtilizationPct: snap.sevenDayPct ?? 0,
|
|
398
|
-
fiveHourResetAt: snap.fiveHourResetAt ? new Date(snap.fiveHourResetAt) : null,
|
|
399
|
-
sevenDayResetAt: snap.sevenDayResetAt ? new Date(snap.sevenDayResetAt) : null,
|
|
400
|
-
representativeClaim: null,
|
|
401
|
-
overageStatus: null,
|
|
402
|
-
overageDisabledReason: null,
|
|
403
|
-
},
|
|
404
|
-
};
|
|
405
|
-
accountQuotaCache.set(label, { fetchedAt, result });
|
|
406
|
-
}
|
|
379
|
+
// No-op post-RFC-H. The disk-snapshot store this function used to
|
|
380
|
+
// re-hydrate from (per-account quota.json files under
|
|
381
|
+
// ~/.switchroom/accounts/<label>/) is gone — switchroom-auth-broker
|
|
382
|
+
// now owns canonical quota state. Boot-time hydration is the
|
|
383
|
+
// broker's `list-state` call instead. Signature preserved so
|
|
384
|
+
// existing call sites continue to compile while we phase them out.
|
|
407
385
|
}
|
|
408
386
|
|
|
409
387
|
/** Test/utility helper — wipe the per-account quota cache. The
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry reaper — prunes long-lived `subagents` and `turns` rows.
|
|
3
|
+
*
|
|
4
|
+
* Issue #1073. The init-time prune in `history.ts` only sweeps the `messages`
|
|
5
|
+
* table. `subagents` and `turns` in `registry.db` grew unbounded — a
|
|
6
|
+
* long-running agent accumulates a row per `Agent()` call and per turn
|
|
7
|
+
* forever. The SQLite WAL also grew without bound because no path issued
|
|
8
|
+
* a checkpoint.
|
|
9
|
+
*
|
|
10
|
+
* This module adds:
|
|
11
|
+
*
|
|
12
|
+
* - `pruneSubagentsOlderThan(db, cutoffMs, batchLimit)` — batch DELETE on
|
|
13
|
+
* `subagents` where `COALESCE(ended_at, last_activity_at, started_at)`
|
|
14
|
+
* is older than the cutoff. Batched so a huge backlog can't lock the
|
|
15
|
+
* DB for minutes; stops when a batch deletes 0 rows.
|
|
16
|
+
* - `pruneTurnsOlderThan(db, cutoffMs, batchLimit)` — same shape for
|
|
17
|
+
* `turns`, using `COALESCE(ended_at, started_at)`.
|
|
18
|
+
* - `runRegistryReaper(db, opts)` — one-shot orchestrator that runs both
|
|
19
|
+
* prunes, issues `PRAGMA wal_checkpoint(TRUNCATE)`, and returns counts.
|
|
20
|
+
*
|
|
21
|
+
* Timestamp model
|
|
22
|
+
* `subagents.started_at` / `last_activity_at` / `ended_at` and
|
|
23
|
+
* `turns.started_at` / `ended_at` are all unix MILLISECONDS (see
|
|
24
|
+
* subagents-schema.ts and turns-schema.ts), distinct from
|
|
25
|
+
* `messages.ts` which is unix SECONDS. Callers pass `cutoffMs`
|
|
26
|
+
* directly — no conversion is done here.
|
|
27
|
+
*
|
|
28
|
+
* Retention selection
|
|
29
|
+
* Default retention window is 14 days. Resolved by the gateway from:
|
|
30
|
+
* 1. `process.env.HISTORY_RETENTION_DAYS` (integer days)
|
|
31
|
+
* 2. `access.json:historyRetentionDays` (legacy: shared with the
|
|
32
|
+
* messages-table init prune)
|
|
33
|
+
* 3. fallback constant `DEFAULT_RETENTION_DAYS = 14`
|
|
34
|
+
*
|
|
35
|
+
* Concurrency
|
|
36
|
+
* bun:sqlite holds the DB connection in WAL mode. Reader/writer
|
|
37
|
+
* concurrency is fine. The batch DELETE statements use short
|
|
38
|
+
* transactions so the gateway's record paths (recordSubagentStart,
|
|
39
|
+
* bumpSubagentActivity, recordTurnStart, recordTurnEnd) never block
|
|
40
|
+
* for more than a single batch's worth of work.
|
|
41
|
+
*
|
|
42
|
+
* Hard rules
|
|
43
|
+
* - Never touch the `messages` table from here. That table has its
|
|
44
|
+
* own (separately-configured) retention policy.
|
|
45
|
+
* - Bound the loop: every prune call must have a max-iteration safety
|
|
46
|
+
* so a runaway clock or schema-corruption bug can't spin forever.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
type SqliteDatabase = {
|
|
50
|
+
exec(sql: string): void
|
|
51
|
+
prepare(sql: string): {
|
|
52
|
+
run(...params: unknown[]): unknown
|
|
53
|
+
all(...params: unknown[]): unknown[]
|
|
54
|
+
get(...params: unknown[]): unknown
|
|
55
|
+
}
|
|
56
|
+
transaction(fn: (...args: unknown[]) => unknown): (...args: unknown[]) => unknown
|
|
57
|
+
close(): void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Default retention window for subagents + turns. */
|
|
61
|
+
export const DEFAULT_RETENTION_DAYS = 14
|
|
62
|
+
|
|
63
|
+
/** Default batch size — empirically a good ceiling for SQLite write
|
|
64
|
+
* transactions on a busy WAL. Tuneable by callers via `batchLimit`. */
|
|
65
|
+
export const DEFAULT_BATCH_LIMIT = 5000
|
|
66
|
+
|
|
67
|
+
/** Defence-in-depth ceiling on the batch-delete loop. At
|
|
68
|
+
* DEFAULT_BATCH_LIMIT this caps a single prune call at 5 million rows,
|
|
69
|
+
* far more than any healthy agent registry will ever hold. */
|
|
70
|
+
const MAX_BATCH_ITERATIONS = 1000
|
|
71
|
+
|
|
72
|
+
export interface PruneResult {
|
|
73
|
+
/** Total rows deleted across all batches. */
|
|
74
|
+
deleted: number
|
|
75
|
+
/** Number of batch iterations executed. */
|
|
76
|
+
batches: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Delete `subagents` rows whose latest-known activity is older than
|
|
81
|
+
* `cutoffMs`. Activity is `COALESCE(ended_at, last_activity_at,
|
|
82
|
+
* started_at)` — a row gets the most generous timestamp available,
|
|
83
|
+
* so a still-running row that simply hasn't pinged liveness in 14d
|
|
84
|
+
* is NOT pruned if its `last_activity_at` is recent.
|
|
85
|
+
*
|
|
86
|
+
* Batched: deletes up to `batchLimit` rows per iteration, looping until
|
|
87
|
+
* a batch returns 0. Bounded by MAX_BATCH_ITERATIONS.
|
|
88
|
+
*/
|
|
89
|
+
export function pruneSubagentsOlderThan(
|
|
90
|
+
db: SqliteDatabase,
|
|
91
|
+
cutoffMs: number,
|
|
92
|
+
batchLimit: number = DEFAULT_BATCH_LIMIT,
|
|
93
|
+
): PruneResult {
|
|
94
|
+
// SQLite's DELETE ... LIMIT requires the SQLITE_ENABLE_UPDATE_DELETE_LIMIT
|
|
95
|
+
// compile flag. bun:sqlite ships with it OFF, so a literal LIMIT clause
|
|
96
|
+
// on DELETE fails parsing. Wrap with a sub-SELECT on rowid — that works
|
|
97
|
+
// on every SQLite build and behaves identically. The same pattern is
|
|
98
|
+
// used by pruneTurnsOlderThan below.
|
|
99
|
+
const stmt = db.prepare(`
|
|
100
|
+
DELETE FROM subagents
|
|
101
|
+
WHERE rowid IN (
|
|
102
|
+
SELECT rowid FROM subagents
|
|
103
|
+
WHERE COALESCE(ended_at, last_activity_at, started_at) < ?
|
|
104
|
+
LIMIT ?
|
|
105
|
+
)
|
|
106
|
+
`)
|
|
107
|
+
let total = 0
|
|
108
|
+
let batches = 0
|
|
109
|
+
for (let i = 0; i < MAX_BATCH_ITERATIONS; i++) {
|
|
110
|
+
const result = stmt.run(cutoffMs, batchLimit) as { changes: number }
|
|
111
|
+
batches += 1
|
|
112
|
+
const n = result.changes ?? 0
|
|
113
|
+
total += n
|
|
114
|
+
if (n === 0) break
|
|
115
|
+
}
|
|
116
|
+
return { deleted: total, batches }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Delete `turns` rows whose latest-known activity is older than `cutoffMs`.
|
|
121
|
+
* Activity is `COALESCE(ended_at, started_at)` — an open turn (ended_at
|
|
122
|
+
* NULL) is preserved if its `started_at` is recent. Batched like
|
|
123
|
+
* pruneSubagentsOlderThan.
|
|
124
|
+
*/
|
|
125
|
+
export function pruneTurnsOlderThan(
|
|
126
|
+
db: SqliteDatabase,
|
|
127
|
+
cutoffMs: number,
|
|
128
|
+
batchLimit: number = DEFAULT_BATCH_LIMIT,
|
|
129
|
+
): PruneResult {
|
|
130
|
+
const stmt = db.prepare(`
|
|
131
|
+
DELETE FROM turns
|
|
132
|
+
WHERE rowid IN (
|
|
133
|
+
SELECT rowid FROM turns
|
|
134
|
+
WHERE COALESCE(ended_at, started_at) < ?
|
|
135
|
+
LIMIT ?
|
|
136
|
+
)
|
|
137
|
+
`)
|
|
138
|
+
let total = 0
|
|
139
|
+
let batches = 0
|
|
140
|
+
for (let i = 0; i < MAX_BATCH_ITERATIONS; i++) {
|
|
141
|
+
const result = stmt.run(cutoffMs, batchLimit) as { changes: number }
|
|
142
|
+
batches += 1
|
|
143
|
+
const n = result.changes ?? 0
|
|
144
|
+
total += n
|
|
145
|
+
if (n === 0) break
|
|
146
|
+
}
|
|
147
|
+
return { deleted: total, batches }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface RegistryReaperResult {
|
|
151
|
+
subagents: PruneResult
|
|
152
|
+
turns: PruneResult
|
|
153
|
+
/** True if the WAL checkpoint ran without throwing. The result of the
|
|
154
|
+
* pragma is logged but not propagated (it can legitimately report
|
|
155
|
+
* "busy" under reader pressure — not a failure). */
|
|
156
|
+
walCheckpointed: boolean
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface RegistryReaperOpts {
|
|
160
|
+
/** Retention window in days. */
|
|
161
|
+
retentionDays?: number
|
|
162
|
+
/** Override "now" for tests. Defaults to Date.now(). */
|
|
163
|
+
now?: number
|
|
164
|
+
/** Override batch size (mostly for tests). */
|
|
165
|
+
batchLimit?: number
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Run the full registry reaper: prune subagents, prune turns, checkpoint
|
|
170
|
+
* the WAL. Caller logs the result.
|
|
171
|
+
*/
|
|
172
|
+
export function runRegistryReaper(
|
|
173
|
+
db: SqliteDatabase,
|
|
174
|
+
opts: RegistryReaperOpts = {},
|
|
175
|
+
): RegistryReaperResult {
|
|
176
|
+
const retentionDays = opts.retentionDays ?? DEFAULT_RETENTION_DAYS
|
|
177
|
+
const now = opts.now ?? Date.now()
|
|
178
|
+
const batchLimit = opts.batchLimit ?? DEFAULT_BATCH_LIMIT
|
|
179
|
+
const cutoffMs = now - retentionDays * 86_400_000
|
|
180
|
+
|
|
181
|
+
const subagents = pruneSubagentsOlderThan(db, cutoffMs, batchLimit)
|
|
182
|
+
const turns = pruneTurnsOlderThan(db, cutoffMs, batchLimit)
|
|
183
|
+
|
|
184
|
+
// WAL checkpoint releases the .db-wal file's pages back to the main DB
|
|
185
|
+
// and truncates the WAL to zero bytes. TRUNCATE mode does both;
|
|
186
|
+
// PASSIVE/FULL leave WAL pages behind. Wrap in try/catch — the
|
|
187
|
+
// checkpoint can return SQLITE_BUSY under concurrent reads, which
|
|
188
|
+
// bun:sqlite surfaces as a thrown error. That's a transient,
|
|
189
|
+
// non-fatal condition: the next reaper tick will retry.
|
|
190
|
+
let walCheckpointed = false
|
|
191
|
+
try {
|
|
192
|
+
db.prepare('PRAGMA wal_checkpoint(TRUNCATE)').run()
|
|
193
|
+
walCheckpointed = true
|
|
194
|
+
} catch {
|
|
195
|
+
walCheckpointed = false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { subagents, turns, walCheckpointed }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Resolve the retention window in days from environment + access-file
|
|
203
|
+
* sources. Order: env `HISTORY_RETENTION_DAYS` → `accessRetentionDays`
|
|
204
|
+
* (caller passes whatever's in access.json) → `DEFAULT_RETENTION_DAYS`.
|
|
205
|
+
*
|
|
206
|
+
* Returns a clamped positive integer. Invalid env values (non-numeric,
|
|
207
|
+
* <= 0, NaN) fall through to the access value, then to the default.
|
|
208
|
+
*/
|
|
209
|
+
export function resolveRetentionDays(accessRetentionDays?: number): number {
|
|
210
|
+
const envRaw = process.env.HISTORY_RETENTION_DAYS
|
|
211
|
+
if (envRaw != null && envRaw !== '') {
|
|
212
|
+
const n = Number.parseInt(envRaw, 10)
|
|
213
|
+
if (Number.isFinite(n) && n > 0) return n
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
typeof accessRetentionDays === 'number'
|
|
217
|
+
&& Number.isFinite(accessRetentionDays)
|
|
218
|
+
&& accessRetentionDays > 0
|
|
219
|
+
) {
|
|
220
|
+
return accessRetentionDays
|
|
221
|
+
}
|
|
222
|
+
return DEFAULT_RETENTION_DAYS
|
|
223
|
+
}
|
|
@@ -170,3 +170,83 @@ export function createRetryApiCall(
|
|
|
170
170
|
throw giveUpErr
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Compose a swallowing wrapper around a `retryApiCall` instance.
|
|
176
|
+
*
|
|
177
|
+
* Use this for **fire-and-forget** Telegram API callsites — boot/issues/
|
|
178
|
+
* subagent cards, "agent restarting" notices, reactions on stale targets,
|
|
179
|
+
* anything where the caller previously had `.catch(() => {})`. The wrapper
|
|
180
|
+
* resolves to `undefined` on the cases retryApiCall throws (THREAD_NOT_FOUND,
|
|
181
|
+
* give-up after network retries, GrammyError 403/400-not-chat, …) and logs
|
|
182
|
+
* a one-line note to `log` so the failure is at least visible in stderr.
|
|
183
|
+
*
|
|
184
|
+
* Why not just `.catch(() => {})` at the callsite? Two reasons:
|
|
185
|
+
*
|
|
186
|
+
* 1. We want THREAD_NOT_FOUND specifically to NOT crash but to be
|
|
187
|
+
* *visible* — `.catch(() => {})` silently swallows everything, which
|
|
188
|
+
* hid #1075 for months. The log here surfaces it.
|
|
189
|
+
* 2. Callers shouldn't have to remember to wrap each raw `bot.api.*`
|
|
190
|
+
* with the retry policy AND the swallow — this is one function.
|
|
191
|
+
*
|
|
192
|
+
* For callsites that legitimately need to inspect failure (e.g. drop
|
|
193
|
+
* thread_id and retry on main chat), use `retryApiCall` directly and
|
|
194
|
+
* handle `THREAD_NOT_FOUND` explicitly — see `gateway.ts:2806` for the
|
|
195
|
+
* canonical pattern (the reply chunk loop).
|
|
196
|
+
*/
|
|
197
|
+
export function createSwallowingRetryApiCall(
|
|
198
|
+
retry: <T>(fn: () => Promise<T>, opts?: RetryCallOpts) => Promise<T>,
|
|
199
|
+
log?: (line: string) => void,
|
|
200
|
+
): <T>(fn: () => Promise<T>, opts?: RetryCallOpts) => Promise<T | undefined> {
|
|
201
|
+
return async function swallow<T>(
|
|
202
|
+
fn: () => Promise<T>,
|
|
203
|
+
opts?: RetryCallOpts,
|
|
204
|
+
): Promise<T | undefined> {
|
|
205
|
+
try {
|
|
206
|
+
return await retry(fn, opts)
|
|
207
|
+
} catch (err) {
|
|
208
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
209
|
+
const verb = opts?.verb ?? 'api-call'
|
|
210
|
+
log?.(`telegram gateway: ${verb} swallowed: ${msg}\n`)
|
|
211
|
+
return undefined
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Helper for callsites that pass `message_thread_id` and want to fall
|
|
218
|
+
* back to the main chat when the thread is deleted.
|
|
219
|
+
*
|
|
220
|
+
* The caller provides a `send` closure that takes `threadId?: number` and
|
|
221
|
+
* builds its own request. On THREAD_NOT_FOUND, `send(undefined)` is invoked
|
|
222
|
+
* once more — the wrapper drops the thread id and re-tries; everything
|
|
223
|
+
* else falls through to the underlying retry policy.
|
|
224
|
+
*
|
|
225
|
+
* Returns the final API response (typed as `T` — fallback resolved, or
|
|
226
|
+
* threadId-bearing call resolved). On non-thread errors, propagates as
|
|
227
|
+
* `retry` does.
|
|
228
|
+
*/
|
|
229
|
+
export async function retryWithThreadFallback<T>(
|
|
230
|
+
retry: <U>(fn: () => Promise<U>, opts?: RetryCallOpts) => Promise<U>,
|
|
231
|
+
send: (threadId: number | undefined) => Promise<T>,
|
|
232
|
+
opts: { threadId: number | undefined; chat_id: string; verb?: string },
|
|
233
|
+
): Promise<T> {
|
|
234
|
+
try {
|
|
235
|
+
return await retry(() => send(opts.threadId), {
|
|
236
|
+
...(opts.threadId != null ? { threadId: opts.threadId } : {}),
|
|
237
|
+
chat_id: opts.chat_id,
|
|
238
|
+
...(opts.verb != null ? { verb: opts.verb } : {}),
|
|
239
|
+
})
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (err instanceof Error && err.message === 'THREAD_NOT_FOUND') {
|
|
242
|
+
// Drop the thread id and retry once on the main chat. Don't pass
|
|
243
|
+
// threadId in opts so a *second* thread-not-found (shouldn't
|
|
244
|
+
// happen) just propagates as a normal error.
|
|
245
|
+
return await retry(() => send(undefined), {
|
|
246
|
+
chat_id: opts.chat_id,
|
|
247
|
+
...(opts.verb != null ? { verb: opts.verb } : {}),
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
throw err
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime-metrics.ts — high-value gateway events fanned out to PostHog
|
|
3
|
+
* AND a local JSONL file.
|
|
4
|
+
*
|
|
5
|
+
* Why both sinks:
|
|
6
|
+
* - PostHog gets the events for dashboards, funnels, error correlation,
|
|
7
|
+
* fleet-wide KPI tracking. This is the source of truth for the
|
|
8
|
+
* conversational-turn-UX redesign KPIs (see docs/posthog.md).
|
|
9
|
+
* - JSONL is preserved as a per-agent debug breadcrumb so the agent's
|
|
10
|
+
* own context (or an operator on the host) can read what happened
|
|
11
|
+
* without round-tripping to PostHog. Same file the silence-poke
|
|
12
|
+
* subsystem (next PR) will append to.
|
|
13
|
+
*
|
|
14
|
+
* Distinct from `streaming-metrics.ts` — that module is the noisy
|
|
15
|
+
* gated-by-env stderr stream used for one-off streaming-perf analysis.
|
|
16
|
+
* Runtime metrics are always-on, narrow, and KPI-shaped.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, mkdirSync, appendFileSync } from 'node:fs'
|
|
20
|
+
import { dirname, join } from 'node:path'
|
|
21
|
+
import { captureEvent } from './analytics-posthog.js'
|
|
22
|
+
|
|
23
|
+
export type RuntimeMetricEvent =
|
|
24
|
+
/**
|
|
25
|
+
* A user-sent message that matches a status-query pattern
|
|
26
|
+
* ("status?", "still there?", etc). Primary lagging KPI for the
|
|
27
|
+
* conversational turn UX — every fire is a JTBD failure.
|
|
28
|
+
*/
|
|
29
|
+
| {
|
|
30
|
+
kind: 'inbound_status_query'
|
|
31
|
+
chat_id: string
|
|
32
|
+
message_id: number | null
|
|
33
|
+
thread_id: number | null
|
|
34
|
+
text_length: number
|
|
35
|
+
prior_turn_in_flight: boolean
|
|
36
|
+
seconds_since_turn_start: number | null
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A fresh turn began (user message arrived, ack reaction fired).
|
|
40
|
+
* Pairs with `turn_ended` for duration / TTFO computation.
|
|
41
|
+
*/
|
|
42
|
+
| {
|
|
43
|
+
kind: 'turn_started'
|
|
44
|
+
chat_id: string
|
|
45
|
+
message_id: number | null
|
|
46
|
+
thread_id: number | null
|
|
47
|
+
inbound_classified_as_status_query: boolean
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A turn completed (terminal reply or silent close). Carries the
|
|
51
|
+
* gap distribution + TTFO so the dashboard can compute outbound
|
|
52
|
+
* silence p95 without per-event reconstruction.
|
|
53
|
+
*/
|
|
54
|
+
| {
|
|
55
|
+
kind: 'turn_ended'
|
|
56
|
+
chat_id: string
|
|
57
|
+
thread_id: number | null
|
|
58
|
+
duration_ms: number
|
|
59
|
+
ttfo_ms: number | null
|
|
60
|
+
outbound_count: number
|
|
61
|
+
longest_silent_gap_ms: number
|
|
62
|
+
ended_via: 'reply' | 'stream_reply_done' | 'silent' | 'forced' | 'framework_fallback'
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Framework safety-net: a silence-poke was armed at 75s (soft) or
|
|
66
|
+
* 180s (firm). The system-reminder appended to the next tool result
|
|
67
|
+
* nudges the model to send an update. Doubles as a design-health
|
|
68
|
+
* signal — if these fire frequently, the conversational-pacing
|
|
69
|
+
* prompt isn't doing its job.
|
|
70
|
+
*/
|
|
71
|
+
| {
|
|
72
|
+
kind: 'silence_poke_fired'
|
|
73
|
+
key: string
|
|
74
|
+
level: 'soft' | 'firm'
|
|
75
|
+
silence_ms: number
|
|
76
|
+
subagent_wait: boolean
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The model sent an outbound message within the success window
|
|
80
|
+
* (default 15s) after a poke fired. Pair with `silence_poke_fired`
|
|
81
|
+
* to compute success rate — the design target is >80%.
|
|
82
|
+
*/
|
|
83
|
+
| {
|
|
84
|
+
kind: 'silence_poke_succeeded'
|
|
85
|
+
key: string
|
|
86
|
+
level: 'soft' | 'firm'
|
|
87
|
+
latency_ms: number
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Last-resort: 5 minutes silent, the framework itself sent a
|
|
91
|
+
* user-visible "still working… / still thinking…" message. Should
|
|
92
|
+
* be rare (target <5 per 1000 turns); a high rate means the model
|
|
93
|
+
* is genuinely stuck or the soft/firm pokes aren't being honoured.
|
|
94
|
+
*/
|
|
95
|
+
| {
|
|
96
|
+
kind: 'silence_fallback_sent'
|
|
97
|
+
key: string
|
|
98
|
+
fallback_kind: 'working' | 'thinking'
|
|
99
|
+
silence_ms: number
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The JSONL sink lives under the runtime state dir so it's per-agent
|
|
104
|
+
* and survives container restarts (the dir is bind-mounted from the
|
|
105
|
+
* host). Path can be overridden for tests via SWITCHROOM_RUNTIME_METRICS_PATH.
|
|
106
|
+
*/
|
|
107
|
+
function resolveJsonlPath(): string {
|
|
108
|
+
const override = process.env.SWITCHROOM_RUNTIME_METRICS_PATH
|
|
109
|
+
if (override && override.trim() !== '') return override.trim()
|
|
110
|
+
const base = process.env.SWITCHROOM_RUNTIME_STATE_DIR ?? '/state/agent'
|
|
111
|
+
return join(base, 'runtime-metrics.jsonl')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function appendJsonl(line: string): void {
|
|
115
|
+
const path = resolveJsonlPath()
|
|
116
|
+
try {
|
|
117
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
118
|
+
appendFileSync(path, line + '\n', 'utf-8')
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// JSONL is a local debug aid; failing to write must not break
|
|
121
|
+
// the gateway. Surface to stderr so it's at least visible in
|
|
122
|
+
// the plugin log.
|
|
123
|
+
process.stderr.write(`runtime-metrics: jsonl write failed: ${(err as Error).message}\n`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Whether to write JSONL at all. Defaults to ON (the user asked for it
|
|
129
|
+
* to stay as a local debugging side-channel). Operator can opt-out with
|
|
130
|
+
* SWITCHROOM_RUNTIME_METRICS_JSONL_DISABLED=1 if disk pressure is a
|
|
131
|
+
* concern.
|
|
132
|
+
*/
|
|
133
|
+
function jsonlEnabled(): boolean {
|
|
134
|
+
const v = process.env.SWITCHROOM_RUNTIME_METRICS_JSONL_DISABLED
|
|
135
|
+
return !(v === '1' || v === 'true')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Emit one runtime metric event. Fans out to:
|
|
140
|
+
* 1. JSONL file (unless disabled)
|
|
141
|
+
* 2. PostHog (unless SWITCHROOM_TELEMETRY_DISABLED=1)
|
|
142
|
+
*
|
|
143
|
+
* Never throws. Each sink fails independently — a broken sink does not
|
|
144
|
+
* block the other.
|
|
145
|
+
*/
|
|
146
|
+
export function emitRuntimeMetric(event: RuntimeMetricEvent): void {
|
|
147
|
+
const wrapped = { ts: Date.now(), ...event }
|
|
148
|
+
if (jsonlEnabled()) {
|
|
149
|
+
try {
|
|
150
|
+
appendJsonl(JSON.stringify(wrapped))
|
|
151
|
+
} catch {
|
|
152
|
+
// already guarded inside appendJsonl
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// captureEvent is async + internally guarded; void-fire to avoid blocking
|
|
156
|
+
// the caller. PostHog batches, so this is cheap.
|
|
157
|
+
void captureEvent(event.kind, { ...event, ts: wrapped.ts })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Exposed for tests — pin the JSONL path to a temp file. */
|
|
161
|
+
export function __setRuntimeMetricsPathForTests(path: string | null): void {
|
|
162
|
+
if (path == null) {
|
|
163
|
+
delete process.env.SWITCHROOM_RUNTIME_METRICS_PATH
|
|
164
|
+
} else {
|
|
165
|
+
process.env.SWITCHROOM_RUNTIME_METRICS_PATH = path
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Exposed for tests — read back the current resolved path. */
|
|
170
|
+
export function __getRuntimeMetricsPathForTests(): string {
|
|
171
|
+
return resolveJsonlPath()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Exposed for tests — JSONL gate helper. */
|
|
175
|
+
export function __isJsonlEnabledForTests(): boolean {
|
|
176
|
+
return jsonlEnabled()
|
|
177
|
+
}
|
|
@@ -24,7 +24,6 @@ const entries = [
|
|
|
24
24
|
{ src: "server.ts", out: "server.js", label: "server (legacy + dual-mode shim)" },
|
|
25
25
|
{ src: "gateway/gateway.ts", out: "gateway/gateway.js", label: "gateway (persistent service)" },
|
|
26
26
|
{ src: "bridge/bridge.ts", out: "bridge/bridge.js", label: "bridge (MCP proxy)" },
|
|
27
|
-
{ src: "foreman/foreman.ts", out: "foreman/foreman.js", label: "foreman (admin bot)" },
|
|
28
27
|
];
|
|
29
28
|
|
|
30
29
|
for (const { src, out, label } of entries) {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { ALL_PATTERNS } from './patterns.js'
|
|
27
27
|
import { scanKeyValue, type RawHit } from './kv-scanner.js'
|
|
28
|
+
import { shannonEntropy } from './entropy.js'
|
|
28
29
|
import { chunk } from './chunker.js'
|
|
29
30
|
import { isSuppressed } from './suppressor.js'
|
|
30
31
|
import { deriveSlug } from './slug.js'
|
|
@@ -79,6 +80,29 @@ export function detectSecrets(text: string): Detection[] {
|
|
|
79
80
|
const globalEnd = globalStart + cap.length
|
|
80
81
|
// For env_key_value (captureIndex=3), the LHS is group 1.
|
|
81
82
|
const keyName = p.rule_id === 'env_key_value' ? m[1] : undefined
|
|
83
|
+
// 2026-05-12: shape gate on env_key_value — the pattern matches
|
|
84
|
+
// any value after an ALLCAPS *_KEY/_TOKEN/_SECRET/_PASSWORD
|
|
85
|
+
// identifier, which previously fired on casual chat like
|
|
86
|
+
// "MY_TOKEN=hello" or "OPENAI_API_KEY=sk-yourkey" (placeholder
|
|
87
|
+
// values, code-shaped human language). Operator UAT reproduced
|
|
88
|
+
// this on 2026-05-12 — the redaction pipeline was deleting the
|
|
89
|
+
// operator's *question* and staging a card asking them to save
|
|
90
|
+
// the literal word "hello" as a vault entry.
|
|
91
|
+
//
|
|
92
|
+
// Mirror the kv_entropy gate from kv-scanner.ts: require
|
|
93
|
+
// BOTH a length floor (cuts short placeholders) AND a Shannon
|
|
94
|
+
// entropy floor (cuts low-randomness words like "hello",
|
|
95
|
+
// "yourkey", "foo"). Threshold is slightly looser than
|
|
96
|
+
// kv_entropy's 4.0 because the LHS structure already gives us
|
|
97
|
+
// higher confidence that this IS an env declaration.
|
|
98
|
+
// See tests/secret-detect-false-positives.test.ts for the
|
|
99
|
+
// pinned cases.
|
|
100
|
+
if (p.rule_id === 'env_key_value') {
|
|
101
|
+
const ENV_KV_MIN_LEN = 12
|
|
102
|
+
const ENV_KV_MIN_ENTROPY = 3.5
|
|
103
|
+
if (cap.length < ENV_KV_MIN_LEN) continue
|
|
104
|
+
if (shannonEntropy(cap) < ENV_KV_MIN_ENTROPY) continue
|
|
105
|
+
}
|
|
82
106
|
raw.push({
|
|
83
107
|
rule_id: p.rule_id,
|
|
84
108
|
start: globalStart,
|