switchroom 0.18.11 → 0.18.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-scheduler/index.js +37 -5
- package/dist/auth-broker/index.js +116 -78
- package/dist/cli/hindsight-mental-model-pretool.mjs +39 -0
- package/dist/cli/ms-365-write-pretool.mjs +31 -8
- package/dist/cli/notion-write-pretool.mjs +38 -6
- package/dist/cli/skill-validate-pretool.mjs +144 -2847
- package/dist/cli/switchroom.js +3529 -4543
- package/dist/cli/ui/index.html +163 -17
- package/dist/host-control/main.js +605 -2847
- package/dist/vault/approvals/kernel-server.js +120 -13
- package/dist/vault/broker/server.js +259 -157
- package/package.json +3 -4
- package/profiles/_base/start.sh.hbs +65 -0
- package/profiles/_shared/vault-protocol.md.hbs +3 -1
- package/profiles/coding/CLAUDE.md.hbs +1 -1
- package/profiles/default/CLAUDE.md.hbs +2 -2
- package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
- package/profiles/health-coach/CLAUDE.md.hbs +1 -1
- package/telegram-plugin/bridge/bridge.ts +37 -0
- package/telegram-plugin/bridge/inbound-dedup.ts +101 -0
- package/telegram-plugin/dist/bridge/bridge.js +122 -4
- package/telegram-plugin/dist/gateway/gateway.js +4213 -3288
- package/telegram-plugin/dist/server.js +139 -5
- package/telegram-plugin/flood-circuit-breaker.ts +493 -21
- package/telegram-plugin/format.ts +19 -0
- package/telegram-plugin/gateway/approval-hold.ts +602 -0
- package/telegram-plugin/gateway/auth-command.ts +92 -2
- package/telegram-plugin/gateway/auth-loopback-relay.ts +670 -0
- package/telegram-plugin/gateway/boot-card.ts +12 -5
- package/telegram-plugin/gateway/callback-query-handlers.ts +88 -1
- package/telegram-plugin/gateway/config-approval-handler.ts +6 -1
- package/telegram-plugin/gateway/disconnect-flush.ts +19 -0
- package/telegram-plugin/gateway/dm-pin-sweep.test.ts +251 -0
- package/telegram-plugin/gateway/dm-pin-sweep.ts +178 -0
- package/telegram-plugin/gateway/gateway.ts +1695 -230
- package/telegram-plugin/gateway/hostd-dispatch.ts +23 -0
- package/telegram-plugin/gateway/idle-clear.ts +90 -6
- package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +26 -5
- package/telegram-plugin/gateway/inject-handler.ts +8 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +46 -3
- package/telegram-plugin/gateway/ipc-server.ts +43 -0
- package/telegram-plugin/gateway/mental-model-propose-resolve.ts +145 -37
- package/telegram-plugin/gateway/model-command.ts +9 -3
- package/telegram-plugin/gateway/pending-session-command.ts +13 -1
- package/telegram-plugin/gateway/permission-ttl-sweep.ts +66 -0
- package/telegram-plugin/gateway/pre-approval-check.ts +74 -0
- package/telegram-plugin/gateway/queued-card-store.ts +217 -0
- package/telegram-plugin/gateway/session-model-file.ts +26 -1
- package/telegram-plugin/gateway/turn-end-gate-backstop.ts +59 -0
- package/telegram-plugin/gateway/turn-end-gate.ts +95 -0
- package/telegram-plugin/gateway/turn-typing-loop.ts +10 -2
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +13 -0
- package/telegram-plugin/history.ts +51 -0
- package/telegram-plugin/hooks/dispatch-claim-scan.mjs +259 -0
- package/telegram-plugin/hooks/dispatch-claim-stop.mjs +129 -0
- package/telegram-plugin/hooks/hooks.json +9 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +303 -2
- package/telegram-plugin/model-unavailable.ts +41 -11
- package/telegram-plugin/operator-events.ts +23 -0
- package/telegram-plugin/outbound-field-redact.ts +69 -0
- package/telegram-plugin/package.json +0 -1
- package/telegram-plugin/permission-rule.ts +1 -0
- package/telegram-plugin/permission-title.ts +1 -0
- package/telegram-plugin/render/render.ts +32 -14
- package/telegram-plugin/retry-api-call.ts +212 -2
- package/telegram-plugin/scoped-approval.ts +11 -2
- package/telegram-plugin/secret-detect/chunker.ts +18 -4
- package/telegram-plugin/secret-detect/index.ts +12 -56
- package/telegram-plugin/send-gate-degraded.test.ts +574 -0
- package/telegram-plugin/send-gate-observability.test.ts +470 -0
- package/telegram-plugin/send-gate-observability.ts +355 -0
- package/telegram-plugin/send-gate.test.ts +717 -0
- package/telegram-plugin/send-gate.ts +1056 -0
- package/telegram-plugin/session-tail.ts +82 -7
- package/telegram-plugin/shared/bot-runtime.ts +17 -5
- package/telegram-plugin/shared/gw-trace-gate.ts +105 -0
- package/telegram-plugin/status-pin-driver.ts +52 -7
- package/telegram-plugin/status-pin.ts +81 -0
- package/telegram-plugin/subagent-watcher.ts +173 -18
- package/telegram-plugin/tests/activity-card-wiring.test.ts +18 -5
- package/telegram-plugin/tests/approval-hold-harness.ts +425 -0
- package/telegram-plugin/tests/approval-hold-outcome.test.ts +327 -0
- package/telegram-plugin/tests/approval-hold-record.test.ts +531 -0
- package/telegram-plugin/tests/approval-hold-redeliver.test.ts +602 -0
- package/telegram-plugin/tests/auth-loopback-relay.test.ts +533 -0
- package/telegram-plugin/tests/boot-card-flood-suppress.test.ts +53 -7
- package/telegram-plugin/tests/busy-key-reaper.test.ts +1 -0
- package/telegram-plugin/tests/callback-query-handlers.test.ts +65 -0
- package/telegram-plugin/tests/dispatch-claim-scan.test.ts +250 -0
- package/telegram-plugin/tests/flood-breaker-blindness.test.ts +213 -0
- package/telegram-plugin/tests/flood-windows-persistence.test.ts +224 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +29 -1
- package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +66 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +57 -0
- package/telegram-plugin/tests/gw-trace-gate.test.ts +105 -0
- package/telegram-plugin/tests/history.test.ts +115 -0
- package/telegram-plugin/tests/idle-clear.test.ts +233 -3
- package/telegram-plugin/tests/inbound-dedup.test.ts +93 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +5 -1
- package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +448 -0
- package/telegram-plugin/tests/ipc-server-check-pre-approved.test.ts +194 -0
- package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +123 -0
- package/telegram-plugin/tests/missed-approvals-wiring.test.ts +1 -1
- package/telegram-plugin/tests/model-command.test.ts +14 -0
- package/telegram-plugin/tests/operator-events-session-tail.test.ts +74 -0
- package/telegram-plugin/tests/outbound-field-redact.test.ts +107 -0
- package/telegram-plugin/tests/pending-session-command.test.ts +21 -0
- package/telegram-plugin/tests/permission-card-routing.test.ts +30 -5
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +8 -7
- package/telegram-plugin/tests/permission-rearm-wiring.test.ts +1 -1
- package/telegram-plugin/tests/pre-approval-check.test.ts +148 -0
- package/telegram-plugin/tests/queued-card-store.test.ts +232 -0
- package/telegram-plugin/tests/reaction-flush-turn-gated.test.ts +100 -0
- package/telegram-plugin/tests/reaction-gate-routing.test.ts +173 -0
- package/telegram-plugin/tests/render/render.test.ts +88 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +398 -0
- package/telegram-plugin/tests/scoped-approval.test.ts +27 -0
- package/telegram-plugin/tests/secret-detect-chunk-overlap.test.ts +65 -0
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +5 -4
- package/telegram-plugin/tests/session-model-file.test.ts +50 -0
- package/telegram-plugin/tests/session-tail-sidecar-reap.test.ts +268 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +35 -14
- package/telegram-plugin/tests/status-pin.test.ts +275 -1
- package/telegram-plugin/tests/subagent-watcher-deferral-log-ratelimit.test.ts +316 -0
- package/telegram-plugin/tests/subagent-watcher-fd-leak.test.ts +275 -0
- package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +92 -0
- package/telegram-plugin/tests/turn-end-gate.test.ts +137 -0
- package/telegram-plugin/tests/typing-emitter.test.ts +586 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +20 -0
- package/telegram-plugin/tests/worktree-watch-cwds.test.ts +215 -1
- package/telegram-plugin/typing-emitter.ts +224 -0
- package/telegram-plugin/uat/scenarios/jtbd-feel-like-a-colleague-dm.test.ts +136 -0
- package/telegram-plugin/welcome-text.ts +42 -0
- package/telegram-plugin/worktree-watch-cwds.ts +194 -5
- package/vendor/hindsight-memory/scripts/drain_pending.py +22 -6
- package/vendor/hindsight-memory/scripts/lib/client.py +12 -5
- package/vendor/hindsight-memory/scripts/lib/directives.py +38 -3
- package/vendor/hindsight-memory/scripts/lib/pending.py +36 -9
- package/vendor/hindsight-memory/scripts/session_end.py +14 -3
- package/vendor/hindsight-memory/scripts/session_start.py +21 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +38 -0
- package/vendor/hindsight-memory/tests/test_drain_pending.py +68 -0
- package/vendor/hindsight-memory/tests/test_pending.py +44 -0
- package/vendor/hindsight-memory/tests/test_session_end_pending.py +38 -0
- package/vendor/hindsight-memory/tests/test_session_start_drain.py +155 -0
- package/telegram-plugin/channel-envelope-safety.test.ts +0 -56
- package/telegram-plugin/channel-envelope-safety.ts +0 -56
- package/telegram-plugin/secret-detect/secretlint-source.ts +0 -95
- package/telegram-plugin/tests/secret-detect-secretlint.test.ts +0 -105
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.13",
|
|
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": {
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"build": "node scripts/build.mjs",
|
|
25
25
|
"build:cli": "node scripts/build.mjs && bun build --compile --target=bun-linux-x64 --minify bin/switchroom.ts --outfile switchroom-linux-amd64",
|
|
26
26
|
"pretest": "npm run build",
|
|
27
|
-
"test": "vitest run && bun test 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/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/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/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.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/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
27
|
+
"test": "vitest run && bun test 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/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/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.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/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
28
28
|
"test:vitest": "vitest run",
|
|
29
|
-
"test:bun": "bun test 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/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",
|
|
29
|
+
"test:bun": "bun test 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
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",
|
|
32
32
|
"lint:tsc": "tsc --noEmit",
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
44
44
|
"@secretlint/core": "^12.2.0",
|
|
45
45
|
"@secretlint/secretlint-rule-preset-recommend": "^12.2.0",
|
|
46
|
-
"@secretlint/types": "^12.2.0",
|
|
47
46
|
"@xterm/headless": "^6.0.0",
|
|
48
47
|
"bcryptjs": "^3.0.3",
|
|
49
48
|
"chalk": "^5.4.0",
|
|
@@ -165,8 +165,70 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
|
|
|
165
165
|
# The sidecar's own structured logging is written
|
|
166
166
|
# directly to its log file; this wrapper only handles process
|
|
167
167
|
# lifecycle. Ampersand-backgrounded by callers below.
|
|
168
|
+
# Size-based log rotation for a supervised sidecar's logfile (#3025).
|
|
169
|
+
# The gateway sidecar appends per-poll trace lines and can run for
|
|
170
|
+
# WEEKS without the supervise loop regaining control (it only loops on
|
|
171
|
+
# child exit), so rotate-at-restart alone let gateway-supervisor.log
|
|
172
|
+
# grow to 555MB on clerk / 162MB on klanker and fill the host disk.
|
|
173
|
+
# This background checker runs independently of the child's lifecycle.
|
|
174
|
+
#
|
|
175
|
+
# copytruncate (cp + `: >`), NOT `mv`: the supervised child holds an
|
|
176
|
+
# open fd on $_logfile (its stdout/stderr is redirected there with
|
|
177
|
+
# `>> "$_logfile"`). A plain `mv` would leave the child writing to the
|
|
178
|
+
# now-renamed inode, so the live log would silently stop growing and
|
|
179
|
+
# the rotated file would keep filling. copytruncate preserves the fd —
|
|
180
|
+
# the child keeps appending to the same (now-empty) inode. Worst-case
|
|
181
|
+
# race is a few log lines written between the cp and the truncate being
|
|
182
|
+
# lost; acceptable for a debug trace log. Keeps at most ~2×cap on disk
|
|
183
|
+
# (live + one .1 generation). Cap + interval are env-overridable.
|
|
184
|
+
_switchroom_log_rotator() {
|
|
185
|
+
local _logfile="$1"
|
|
186
|
+
local _max="${SWITCHROOM_SIDECAR_LOG_MAX_BYTES:-52428800}" # 50 MiB default
|
|
187
|
+
local _interval="${SWITCHROOM_SIDECAR_LOG_ROTATE_INTERVAL_SEC:-300}" # 5 min
|
|
188
|
+
# Validate BOTH env overrides up front; garbage falls back to the
|
|
189
|
+
# default. Unvalidated, a non-numeric interval makes `sleep` fail
|
|
190
|
+
# instantly and the `while true` loop hot-spins a CPU core for the
|
|
191
|
+
# container lifetime; a non-numeric cap makes the -gt test below
|
|
192
|
+
# error (a shell error line to stdout) every cycle forever. A zero
|
|
193
|
+
# interval would also hot-spin, so it falls back too.
|
|
194
|
+
case "$_max" in ''|*[!0-9]*) _max=52428800;; esac
|
|
195
|
+
case "$_interval" in ''|0|*[!0-9]*) _interval=300;; esac
|
|
196
|
+
# A cap of 0 disables rotation entirely (operator escape hatch).
|
|
197
|
+
# (Negative values contain '-' → non-numeric per the guard above →
|
|
198
|
+
# default cap, i.e. rotation stays on.)
|
|
199
|
+
[ "$_max" -eq 0 ] && return 0
|
|
200
|
+
while true; do
|
|
201
|
+
sleep "$_interval"
|
|
202
|
+
[ -f "$_logfile" ] || continue
|
|
203
|
+
local _size
|
|
204
|
+
_size=$(wc -c < "$_logfile" 2>/dev/null || echo 0)
|
|
205
|
+
# `wc -c < file` yields a bare integer; guard against empty/non-numeric.
|
|
206
|
+
case "$_size" in ''|*[!0-9]*) continue;; esac
|
|
207
|
+
if [ "$_size" -gt "$_max" ]; then
|
|
208
|
+
if cp "$_logfile" "$_logfile.1" 2>/dev/null; then
|
|
209
|
+
: > "$_logfile"
|
|
210
|
+
echo "[supervise] rotated $_logfile (${_size} bytes > ${_max} cap) → ${_logfile}.1" >> "$_logfile"
|
|
211
|
+
else
|
|
212
|
+
# cp failed (e.g. ENOSPC — exactly when rotation matters most).
|
|
213
|
+
# Fall back to truncate-without-backup: dropping the .1 backup
|
|
214
|
+
# beats a rotator that can never free space (#3025 review).
|
|
215
|
+
: > "$_logfile"
|
|
216
|
+
echo "[supervise] rotated $_logfile (${_size} bytes > ${_max} cap) WITHOUT backup — cp to ${_logfile}.1 failed (disk full?)" >> "$_logfile"
|
|
217
|
+
fi
|
|
218
|
+
fi
|
|
219
|
+
done
|
|
220
|
+
}
|
|
221
|
+
|
|
168
222
|
_switchroom_supervise() {
|
|
169
223
|
local _name="$1"; local _logfile="$2"; shift 2
|
|
224
|
+
# Start the size-based rotator for this sidecar's log (#3025). One
|
|
225
|
+
# per supervised sidecar; lives for the container lifetime alongside
|
|
226
|
+
# the respawn loop below.
|
|
227
|
+
_switchroom_log_rotator "$_logfile" &
|
|
228
|
+
# Record the rotator's pid so the oneshot-ok return path below can
|
|
229
|
+
# reap it — otherwise every completed one-shot sidecar leaks a
|
|
230
|
+
# background sleep loop for the container lifetime (#3025 review).
|
|
231
|
+
local _rotator_pid=$!
|
|
170
232
|
# Optional `--oneshot-ok` flag (must directly follow the logfile):
|
|
171
233
|
# marks a sidecar that can LEGITIMATELY complete. For such a sidecar
|
|
172
234
|
# a clean exit 0 means "job done" → stop supervising, no respawn.
|
|
@@ -211,6 +273,9 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
|
|
|
211
273
|
# below — a crash always self-heals.
|
|
212
274
|
if [ $_oneshot_ok -eq 1 ] && [ $_exit -eq 0 ]; then
|
|
213
275
|
echo "[supervise] $_name completed cleanly (exit 0) — one-shot-ok, not respawning." >> "$_logfile"
|
|
276
|
+
# The sidecar is done — its log stops growing, so reap the
|
|
277
|
+
# rotator instead of leaking its sleep loop forever (#3025).
|
|
278
|
+
kill "$_rotator_pid" 2>/dev/null || true
|
|
214
279
|
return 0
|
|
215
280
|
fi
|
|
216
281
|
# A run that stayed up for at least the backoff cap means the
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
Rendered automatically by `switchroom apply` / `agent reconcile` from
|
|
4
4
|
profiles/_shared/vault-protocol.md.hbs. Edit there — hand-edits to
|
|
5
5
|
this section will be overwritten on next reconcile. To add agent-
|
|
6
|
-
specific guidance,
|
|
6
|
+
specific guidance, edit CLAUDE.md below the "Yours (preserved across
|
|
7
|
+
apply)" marker line — that section is preserved verbatim across every
|
|
8
|
+
apply.
|
|
7
9
|
-->
|
|
8
10
|
|
|
9
11
|
## Vault & secrets
|
|
@@ -43,7 +43,7 @@ Claude Code's file-based auto-memory is disabled. Use **Hindsight** MCP tools:
|
|
|
43
43
|
- `mcp__hindsight__recall` — search past memories. Auto-fires on every message.
|
|
44
44
|
- `mcp__hindsight__retain` — store important facts. Auto-retains every turn (chunked, a small ~3-turn window each time), so it's prompt and cheap.
|
|
45
45
|
- `mcp__hindsight__reflect` — synthesize across memories for complex queries.
|
|
46
|
-
- `
|
|
46
|
+
- `mcp__switchroom-telegram__mental_model_propose` — propose a mental model: a standing semantic summary refreshed over the bank (e.g. "codebase architecture"). Mental-model writes are operator-approved: this posts an approval card and persists on approval. Don't call `mcp__hindsight__create_mental_model`/`update_mental_model` directly (they're denied and redirected here).
|
|
47
47
|
|
|
48
48
|
Save proactively: architecture decisions, codebase patterns, conventions, known issues, user preferences. Your session may be compacted at any time — only Hindsight memories survive.
|
|
49
49
|
|
|
@@ -45,14 +45,14 @@ Hindsight is a memory bank with semantic search, knowledge graph, entity resolut
|
|
|
45
45
|
### Mental Models
|
|
46
46
|
A mental model is a pre-computed semantic summary backed by reflection over the bank — a way to maintain a standing answer to a recurring question, semantically populated and refreshed.
|
|
47
47
|
|
|
48
|
-
- `
|
|
48
|
+
Creating, updating, or refreshing a mental model is **operator-approved** — don't call `create_mental_model` / `update_mental_model` / `refresh_mental_model` / `delete_mental_model` directly (a direct call is denied and redirected). Use `mcp__switchroom-telegram__mental_model_propose(name, source_query)` when you need a recurring synthesis: it posts an approval card and persists the model on approval. When the user shares a fact about themselves (preferences, background, goals), don't write a file and don't propose a model — just **retain** the fact. You do NOT need to build or maintain a per-agent "user profile": who the user is lives in dedicated per-user profile banks that the operator curates out-of-band, and recall surfaces it automatically.
|
|
49
49
|
|
|
50
50
|
### Directives (replaces feedback rules)
|
|
51
51
|
Hard rules the agent must follow during reflect — guardrails that are always applied.
|
|
52
52
|
|
|
53
53
|
- `mcp__hindsight__create_directive(text)` — e.g., `create_directive("Always prefer TypeScript over JavaScript for this user's projects")`. When the user gives you a correction or "always do X" rule, create a directive instead of writing a feedback `.md` file.
|
|
54
54
|
|
|
55
|
-
(
|
|
55
|
+
(Read-only inspection tools like `list_memories`, `list_mental_models`, `get_mental_model`, `list_directives` are available under the `mcp__hindsight__*` namespace if you ever need them, but you rarely should — Hindsight's own auto-recall surfaces what matters and the operator handles bank curation out-of-band. The mental-model WRITE tools — `create`/`update`/`refresh`/`delete_mental_model` — are gated: propose via `mcp__switchroom-telegram__mental_model_propose`, don't call them directly.)
|
|
56
56
|
|
|
57
57
|
### What to retain — and what NOT to retain
|
|
58
58
|
|
|
@@ -41,7 +41,7 @@ Claude Code's file-based auto-memory is disabled. Use **Hindsight** MCP tools:
|
|
|
41
41
|
|
|
42
42
|
- `mcp__hindsight__recall` — search past memories. Auto-fires every message.
|
|
43
43
|
- `mcp__hindsight__retain` — store important facts. Auto-retains every turn (chunked, a small ~3-turn window each time), so it's prompt and cheap.
|
|
44
|
-
- `
|
|
44
|
+
- `mcp__switchroom-telegram__mental_model_propose` — propose a mental model: a standing semantic summary refreshed over the bank (e.g. "contacts", "active projects", "user preferences"). Mental-model writes are operator-approved: this posts an approval card and persists on approval. Don't call `mcp__hindsight__create_mental_model`/`update_mental_model` directly (they're denied and redirected here).
|
|
45
45
|
|
|
46
46
|
Save proactively: contacts and their roles, scheduling preferences, project status, decisions with rationale, communication templates. Only Hindsight memories survive compaction.
|
|
47
47
|
|
|
@@ -35,7 +35,7 @@ Claude Code's file-based auto-memory is disabled. Use **Hindsight** MCP tools:
|
|
|
35
35
|
|
|
36
36
|
- `mcp__hindsight__recall` — search past memories. Auto-fires on every message.
|
|
37
37
|
- `mcp__hindsight__retain` — store important facts. Auto-retains every turn (chunked, a small ~3-turn window each time), so it's prompt and cheap.
|
|
38
|
-
- `
|
|
38
|
+
- `mcp__switchroom-telegram__mental_model_propose` — propose a mental model (e.g. a "fitness profile" — a standing semantic summary refreshed over the bank). Mental-model writes are operator-approved: this posts an approval card and persists on approval. Don't call `mcp__hindsight__create_mental_model`/`update_mental_model` directly (they're denied and redirected here).
|
|
39
39
|
|
|
40
40
|
Save proactively: workout logs, goals, PRs, preferences, patterns (e.g. "poor sleep on Sundays"), injuries/limitations. Only Hindsight memories survive session compaction.
|
|
41
41
|
|
|
@@ -32,6 +32,7 @@ import type { InboundMessage, PermissionEvent, StatusEvent } from '../gateway/ip
|
|
|
32
32
|
import { matchesAllowRule } from '../permission-rule.js'
|
|
33
33
|
import { createOutstandingPermissionLedger } from './permission-ledger.js'
|
|
34
34
|
import { appendCrashBreadcrumb } from './crash-breadcrumb.js'
|
|
35
|
+
import { InboundDedup, shouldDedupInbound, dedupChatKey } from './inbound-dedup.js'
|
|
35
36
|
|
|
36
37
|
installPluginLogger()
|
|
37
38
|
|
|
@@ -127,6 +128,7 @@ const TOOL_SCHEMAS = [
|
|
|
127
128
|
callback_data: { type: 'string', description: 'Opaque tag delivered back to the agent on tap. Max 58 chars (gateway prepends an `agent:` prefix to the 64-byte Telegram limit). Mutually exclusive with url.' },
|
|
128
129
|
ack_text: { type: 'string', description: 'Toast text shown to the user the instant they tap this button (#710). Default "✓ received". Max ~200 chars (Telegram answerCallbackQuery limit). Has no effect on URL buttons.' },
|
|
129
130
|
single_use: { type: 'boolean', description: 'When true (default) tapping any single_use button on the message removes the entire keyboard so the user can\'t double-fire. Set false on buttons that should stay tappable (e.g. a "Refresh" button). If ANY button on the message has single_use:false the keyboard is preserved on tap.' },
|
|
131
|
+
inline_keyboard_confirm: { type: 'boolean', description: 'Per-message override for the "✅ You chose: <label> · HH:MM" body annotation on tap (#789). true forces the annotation even if the agent default is off; false skips it even if the agent default is on; omit to use the agent default. Only applies to single-use keyboards and requires the agent parseMode to be the default html. Has no effect on URL buttons. Note: annotation rebuilds the body from the message\'s plain text, so formatting entities on the original message are lost.' },
|
|
130
132
|
},
|
|
131
133
|
required: ['text'],
|
|
132
134
|
},
|
|
@@ -563,6 +565,18 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
563
565
|
}
|
|
564
566
|
}
|
|
565
567
|
|
|
568
|
+
// #2094 finding 4 — turn-complete reset. A successful reply / stream_reply
|
|
569
|
+
// is the bridge's turn-complete proxy: the agent delivered its answer for
|
|
570
|
+
// this chat, so the per-chat inbound-dedup set can be cleared (thread-
|
|
571
|
+
// agnostic — the reply may omit message_thread_id). Bounds memory and lets
|
|
572
|
+
// a genuinely NEW turn re-use ids the sweep might legitimately resend.
|
|
573
|
+
if (inboundDedupEnabled && (tool === 'reply' || tool === 'stream_reply')) {
|
|
574
|
+
const replyChatId = args.chat_id
|
|
575
|
+
if (typeof replyChatId === 'string' && replyChatId.length > 0) {
|
|
576
|
+
inboundDedup.clearChat(replyChatId)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
566
580
|
// The gateway returns the same shape as the legacy server.ts handlers:
|
|
567
581
|
// { content: [{ type: 'text', text: '...' }] }
|
|
568
582
|
if (result.result && typeof result.result === 'object' && 'content' in (result.result as object)) {
|
|
@@ -709,7 +723,30 @@ mcp.setNotificationHandler(
|
|
|
709
723
|
|
|
710
724
|
let ipc: IpcClientHandle | null = null
|
|
711
725
|
|
|
726
|
+
// #2094 finding 4 — bridge-side messageId dedup. Guards against the
|
|
727
|
+
// stranded-message sweep re-delivering a genuine user message that was
|
|
728
|
+
// already forwarded to claude (double-executing side-effectful commands,
|
|
729
|
+
// double 👀). Cleared per-chat on turn-complete (a successful reply /
|
|
730
|
+
// stream_reply, below) and capped defensively. Kill switch:
|
|
731
|
+
// SWITCHROOM_INBOUND_DEDUP=0.
|
|
732
|
+
const inboundDedup = new InboundDedup()
|
|
733
|
+
const inboundDedupEnabled = process.env.SWITCHROOM_INBOUND_DEDUP !== '0'
|
|
734
|
+
|
|
712
735
|
function onInbound(msg: InboundMessage): void {
|
|
736
|
+
// Drop a re-delivered duplicate of a plain user message before it reaches
|
|
737
|
+
// the claude session. Only genuine re-deliverable messages are eligible
|
|
738
|
+
// (see shouldDedupInbound) — synthetic / button / structured-event inbounds
|
|
739
|
+
// are always forwarded.
|
|
740
|
+
if (inboundDedupEnabled && shouldDedupInbound(msg)) {
|
|
741
|
+
const chatKey = dedupChatKey(msg.chatId, msg.threadId)
|
|
742
|
+
if (inboundDedup.checkAndRecord(chatKey, msg.messageId) === 'duplicate') {
|
|
743
|
+
process.stderr.write(
|
|
744
|
+
`telegram bridge: dropping duplicate inbound messageId=${msg.messageId} ` +
|
|
745
|
+
`chat=${chatKey} (already forwarded this turn — sweep re-delivery, #2094)\n`,
|
|
746
|
+
)
|
|
747
|
+
return
|
|
748
|
+
}
|
|
749
|
+
}
|
|
713
750
|
// Convert IPC InboundMessage → MCP channel notification
|
|
714
751
|
mcp.notification({
|
|
715
752
|
method: 'notifications/claude/channel',
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge-side inbound messageId dedup (#2094 finding 4).
|
|
3
|
+
*
|
|
4
|
+
* `onInbound` forwards every IPC inbound to the claude session with no dedup.
|
|
5
|
+
* A genuine user message that was delivered successfully but slow to ack can
|
|
6
|
+
* be RE-SENT by the offline/stranded-message sweep — and claude then processes
|
|
7
|
+
* it twice: a side-effectful slash command double-executes, a 👀 is painted
|
|
8
|
+
* twice, etc. #2093's idle-gating shrank the window but did not close it.
|
|
9
|
+
*
|
|
10
|
+
* Durable fix: a per-chat set of already-forwarded messageIds. A repeat of a
|
|
11
|
+
* messageId within the same turn is dropped. The set is cleared on
|
|
12
|
+
* turn-complete (the bridge's turn-complete proxy is a successful
|
|
13
|
+
* reply/stream_reply for that chat) so memory stays bounded, and it is capped
|
|
14
|
+
* defensively so a chat that never replies can't grow the set without limit.
|
|
15
|
+
*
|
|
16
|
+
* SCOPE — only PLAIN user messages are deduped (see `shouldDedupInbound`).
|
|
17
|
+
* Synthetic inbounds (meta.source: cron / resume / vault, messageId == a
|
|
18
|
+
* timestamp), button callbacks, and structured events (checklist task changes,
|
|
19
|
+
* reactions) are NEVER deduped: they either carry unique ids already or
|
|
20
|
+
* legitimately repeat the same messageId for genuinely distinct events.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Defensive cap on tracked ids per chat (FIFO-evicted). Bounds memory for a
|
|
24
|
+
* chat that produces many messages without ever hitting a turn-complete. */
|
|
25
|
+
export const INBOUND_DEDUP_MAX_PER_CHAT = 512
|
|
26
|
+
|
|
27
|
+
export interface DedupInboundLike {
|
|
28
|
+
messageId: number
|
|
29
|
+
meta: Record<string, string>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Only genuine, re-deliverable user messages are eligible for dedup. Excludes:
|
|
34
|
+
* - messageId <= 0 (no real Telegram id to key on),
|
|
35
|
+
* - meta.source set (synthetic: cron / resume / vault / secret_* — unique
|
|
36
|
+
* timestamp ids, and re-delivery is handled upstream),
|
|
37
|
+
* - meta.button_callback (a tap; repeats are the user's intent),
|
|
38
|
+
* - meta.kind (structured events like checklist_task_changed — the same
|
|
39
|
+
* checklist messageId legitimately repeats for distinct state changes),
|
|
40
|
+
* - meta.event (reaction-class inbounds).
|
|
41
|
+
*/
|
|
42
|
+
export function shouldDedupInbound(msg: DedupInboundLike): boolean {
|
|
43
|
+
if (!Number.isFinite(msg.messageId) || msg.messageId <= 0) return false
|
|
44
|
+
const m = msg.meta ?? {}
|
|
45
|
+
if (m.source != null) return false
|
|
46
|
+
if (m.button_callback != null) return false
|
|
47
|
+
if (m.kind != null) return false
|
|
48
|
+
if (m.event != null) return false
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Per-chat forwarded-messageId tracker. Keyed by chatKey (`chatId:threadId`).
|
|
54
|
+
*/
|
|
55
|
+
export class InboundDedup {
|
|
56
|
+
private readonly byChat = new Map<string, Set<number>>()
|
|
57
|
+
|
|
58
|
+
constructor(private readonly maxPerChat: number = INBOUND_DEDUP_MAX_PER_CHAT) {}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Record `messageId` as forwarded for `chatKey`. Returns 'fresh' the first
|
|
62
|
+
* time a messageId is seen (caller forwards it) and 'duplicate' on a repeat
|
|
63
|
+
* (caller drops it). FIFO-evicts the oldest id once the per-chat cap is hit.
|
|
64
|
+
*/
|
|
65
|
+
checkAndRecord(chatKey: string, messageId: number): 'fresh' | 'duplicate' {
|
|
66
|
+
let set = this.byChat.get(chatKey)
|
|
67
|
+
if (set == null) {
|
|
68
|
+
set = new Set<number>()
|
|
69
|
+
this.byChat.set(chatKey, set)
|
|
70
|
+
}
|
|
71
|
+
if (set.has(messageId)) return 'duplicate'
|
|
72
|
+
if (set.size >= this.maxPerChat) {
|
|
73
|
+
// Set iteration order is insertion order — drop the oldest.
|
|
74
|
+
const oldest = set.values().next().value
|
|
75
|
+
if (oldest !== undefined) set.delete(oldest)
|
|
76
|
+
}
|
|
77
|
+
set.add(messageId)
|
|
78
|
+
return 'fresh'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Clear every tracked id for a chat (all threads) — the turn-complete
|
|
82
|
+
* reset. Thread-agnostic so a reply that omits message_thread_id still
|
|
83
|
+
* clears the inbound's thread-scoped key. */
|
|
84
|
+
clearChat(chatId: string): void {
|
|
85
|
+
const prefix = `${chatId}:`
|
|
86
|
+
for (const key of this.byChat.keys()) {
|
|
87
|
+
if (key === chatId || key.startsWith(prefix)) this.byChat.delete(key)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Test/introspection helper: number of tracked ids for a chatKey. */
|
|
92
|
+
size(chatKey: string): number {
|
|
93
|
+
return this.byChat.get(chatKey)?.size ?? 0
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Canonical chat-key derivation (mirrors gateway/chat-key.ts): a null/0
|
|
98
|
+
* thread collapses to '_'. */
|
|
99
|
+
export function dedupChatKey(chatId: string, threadId?: number): string {
|
|
100
|
+
return `${chatId}:${threadId == null || threadId === 0 ? '_' : threadId}`
|
|
101
|
+
}
|
|
@@ -23035,6 +23035,33 @@ function getNestedObj(obj, key) {
|
|
|
23035
23035
|
var DEFAULT_OPERATOR_EVENT_COOLDOWN_MS = 5 * 60000;
|
|
23036
23036
|
var cooldownMap = new Map;
|
|
23037
23037
|
|
|
23038
|
+
// text-voice-scrub.ts
|
|
23039
|
+
var NULL = "\x00";
|
|
23040
|
+
var FENCE_PH = `${NULL}VS_FENCE`;
|
|
23041
|
+
var INLINE_PH = `${NULL}VS_INLINE`;
|
|
23042
|
+
var HTML_CODE_PH = `${NULL}VS_HTMLCODE`;
|
|
23043
|
+
var HTML_PRE_PH = `${NULL}VS_HTMLPRE`;
|
|
23044
|
+
var URL_PH = `${NULL}VS_URL`;
|
|
23045
|
+
|
|
23046
|
+
// model-unavailable.ts
|
|
23047
|
+
var transientUpstreamSignals = [
|
|
23048
|
+
"not your usage limit",
|
|
23049
|
+
"not your account",
|
|
23050
|
+
"not your account's",
|
|
23051
|
+
"temporarily limiting requests",
|
|
23052
|
+
"temporarily rate",
|
|
23053
|
+
"server is temporarily",
|
|
23054
|
+
"would exceed your account\u2019s rate limit",
|
|
23055
|
+
"would exceed your account's rate limit"
|
|
23056
|
+
];
|
|
23057
|
+
function isTransientUpstreamSignal(text) {
|
|
23058
|
+
if (typeof text !== "string" || text.length === 0)
|
|
23059
|
+
return false;
|
|
23060
|
+
const sample = text.length > 16384 ? text.slice(0, 16384) : text;
|
|
23061
|
+
const lower = sample.toLowerCase();
|
|
23062
|
+
return transientUpstreamSignals.some((s) => lower.includes(s));
|
|
23063
|
+
}
|
|
23064
|
+
|
|
23038
23065
|
// tool-label-sidecar.ts
|
|
23039
23066
|
import { existsSync as existsSync2, readFileSync, statSync as statSync2 } from "node:fs";
|
|
23040
23067
|
import { join as join2 } from "node:path";
|
|
@@ -23453,7 +23480,8 @@ function detectErrorInTranscriptLine(line) {
|
|
|
23453
23480
|
const status = typeof obj.apiErrorStatus === "number" ? obj.apiErrorStatus : null;
|
|
23454
23481
|
const errStr = typeof obj.error === "string" ? obj.error : "";
|
|
23455
23482
|
const text = extractAssistantText(obj);
|
|
23456
|
-
const kind2 = status === 429 ?
|
|
23483
|
+
const kind2 = status === 429 ? isTransientUpstreamSignal(`${text}
|
|
23484
|
+
${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: errStr, status, message: text });
|
|
23457
23485
|
return {
|
|
23458
23486
|
kind: kind2,
|
|
23459
23487
|
raw: obj,
|
|
@@ -23547,6 +23575,7 @@ function startSessionTail(config2) {
|
|
|
23547
23575
|
const onOperatorEvent = config2.onOperatorEvent;
|
|
23548
23576
|
log?.(`session-tail: projectsDir=${projectsDir}`);
|
|
23549
23577
|
const sidecars = new Map;
|
|
23578
|
+
const createSidecar = config2.createSidecar ?? createToolLabelSidecar;
|
|
23550
23579
|
const stateDirForSidecar = process.env.TELEGRAM_STATE_DIR ?? null;
|
|
23551
23580
|
function sessionIdForFile(file) {
|
|
23552
23581
|
if (!file)
|
|
@@ -23561,7 +23590,7 @@ function startSessionTail(config2) {
|
|
|
23561
23590
|
if (existing)
|
|
23562
23591
|
return existing;
|
|
23563
23592
|
try {
|
|
23564
|
-
const s =
|
|
23593
|
+
const s = createSidecar({ stateDir: stateDirForSidecar, sessionId });
|
|
23565
23594
|
sidecars.set(sessionId, s);
|
|
23566
23595
|
s.onLabel((toolUseId, label, toolName) => {
|
|
23567
23596
|
rawOnEvent({ kind: "tool_label", toolUseId, label, toolName });
|
|
@@ -23572,6 +23601,17 @@ function startSessionTail(config2) {
|
|
|
23572
23601
|
return null;
|
|
23573
23602
|
}
|
|
23574
23603
|
}
|
|
23604
|
+
function stopSidecar(sessionId) {
|
|
23605
|
+
if (!sessionId)
|
|
23606
|
+
return;
|
|
23607
|
+
const s = sidecars.get(sessionId);
|
|
23608
|
+
if (!s)
|
|
23609
|
+
return;
|
|
23610
|
+
try {
|
|
23611
|
+
s.stop();
|
|
23612
|
+
} catch {}
|
|
23613
|
+
sidecars.delete(sessionId);
|
|
23614
|
+
}
|
|
23575
23615
|
function decorate(ev, sessionId) {
|
|
23576
23616
|
if (!sessionId)
|
|
23577
23617
|
return ev;
|
|
@@ -23664,6 +23704,11 @@ function startSessionTail(config2) {
|
|
|
23664
23704
|
} catch {}
|
|
23665
23705
|
watcher = null;
|
|
23666
23706
|
}
|
|
23707
|
+
const rotatedAwaySid = sessionIdForFile(currentFile);
|
|
23708
|
+
const nextSid = sessionIdForFile(file);
|
|
23709
|
+
if (rotatedAwaySid != null && rotatedAwaySid !== nextSid) {
|
|
23710
|
+
stopSidecar(rotatedAwaySid);
|
|
23711
|
+
}
|
|
23667
23712
|
currentFile = file;
|
|
23668
23713
|
const prior = fileCursors.get(file);
|
|
23669
23714
|
if (prior != null) {
|
|
@@ -23696,7 +23741,7 @@ function startSessionTail(config2) {
|
|
|
23696
23741
|
const multiAgent = isMultiAgentEnabled();
|
|
23697
23742
|
const CAP_TOOL_USE_THRESHOLD = 30;
|
|
23698
23743
|
const subTails = new Map;
|
|
23699
|
-
const IDLE_FSWATCH_TTL_MS = 5 * 60 * 1000;
|
|
23744
|
+
const IDLE_FSWATCH_TTL_MS = Math.max(1000, config2.subTailIdleReapMs ?? 5 * 60 * 1000);
|
|
23700
23745
|
function readSub(t) {
|
|
23701
23746
|
if (stopped)
|
|
23702
23747
|
return;
|
|
@@ -23803,6 +23848,7 @@ function startSessionTail(config2) {
|
|
|
23803
23848
|
} catch {}
|
|
23804
23849
|
t.watcher = null;
|
|
23805
23850
|
}
|
|
23851
|
+
stopSidecar(sessionIdForFile(t.file));
|
|
23806
23852
|
subTails.delete(file);
|
|
23807
23853
|
log?.(`session-tail: reaped idle sub ${t.agentId} (${file})`);
|
|
23808
23854
|
}
|
|
@@ -24562,6 +24608,7 @@ var BROAD_ONLY_TOOLS = new Set([
|
|
|
24562
24608
|
"WebSearch",
|
|
24563
24609
|
"Task",
|
|
24564
24610
|
"Agent",
|
|
24611
|
+
"Workflow",
|
|
24565
24612
|
"TodoWrite",
|
|
24566
24613
|
"ExitPlanMode"
|
|
24567
24614
|
]);
|
|
@@ -24697,6 +24744,60 @@ function appendCrashBreadcrumb(logPath, kind, err, now = new Date) {
|
|
|
24697
24744
|
} catch {}
|
|
24698
24745
|
}
|
|
24699
24746
|
|
|
24747
|
+
// bridge/inbound-dedup.ts
|
|
24748
|
+
var INBOUND_DEDUP_MAX_PER_CHAT = 512;
|
|
24749
|
+
function shouldDedupInbound(msg) {
|
|
24750
|
+
if (!Number.isFinite(msg.messageId) || msg.messageId <= 0)
|
|
24751
|
+
return false;
|
|
24752
|
+
const m = msg.meta ?? {};
|
|
24753
|
+
if (m.source != null)
|
|
24754
|
+
return false;
|
|
24755
|
+
if (m.button_callback != null)
|
|
24756
|
+
return false;
|
|
24757
|
+
if (m.kind != null)
|
|
24758
|
+
return false;
|
|
24759
|
+
if (m.event != null)
|
|
24760
|
+
return false;
|
|
24761
|
+
return true;
|
|
24762
|
+
}
|
|
24763
|
+
|
|
24764
|
+
class InboundDedup {
|
|
24765
|
+
maxPerChat;
|
|
24766
|
+
byChat = new Map;
|
|
24767
|
+
constructor(maxPerChat = INBOUND_DEDUP_MAX_PER_CHAT) {
|
|
24768
|
+
this.maxPerChat = maxPerChat;
|
|
24769
|
+
}
|
|
24770
|
+
checkAndRecord(chatKey, messageId) {
|
|
24771
|
+
let set2 = this.byChat.get(chatKey);
|
|
24772
|
+
if (set2 == null) {
|
|
24773
|
+
set2 = new Set;
|
|
24774
|
+
this.byChat.set(chatKey, set2);
|
|
24775
|
+
}
|
|
24776
|
+
if (set2.has(messageId))
|
|
24777
|
+
return "duplicate";
|
|
24778
|
+
if (set2.size >= this.maxPerChat) {
|
|
24779
|
+
const oldest = set2.values().next().value;
|
|
24780
|
+
if (oldest !== undefined)
|
|
24781
|
+
set2.delete(oldest);
|
|
24782
|
+
}
|
|
24783
|
+
set2.add(messageId);
|
|
24784
|
+
return "fresh";
|
|
24785
|
+
}
|
|
24786
|
+
clearChat(chatId) {
|
|
24787
|
+
const prefix = `${chatId}:`;
|
|
24788
|
+
for (const key of this.byChat.keys()) {
|
|
24789
|
+
if (key === chatId || key.startsWith(prefix))
|
|
24790
|
+
this.byChat.delete(key);
|
|
24791
|
+
}
|
|
24792
|
+
}
|
|
24793
|
+
size(chatKey) {
|
|
24794
|
+
return this.byChat.get(chatKey)?.size ?? 0;
|
|
24795
|
+
}
|
|
24796
|
+
}
|
|
24797
|
+
function dedupChatKey(chatId, threadId) {
|
|
24798
|
+
return `${chatId}:${threadId == null || threadId === 0 ? "_" : threadId}`;
|
|
24799
|
+
}
|
|
24800
|
+
|
|
24700
24801
|
// bridge/bridge.ts
|
|
24701
24802
|
installPluginLogger();
|
|
24702
24803
|
var STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join4(homedir3(), ".claude", "channels", "telegram");
|
|
@@ -24764,7 +24865,8 @@ var TOOL_SCHEMAS = [
|
|
|
24764
24865
|
url: { type: "string", description: "Open this URL when tapped. Mutually exclusive with callback_data." },
|
|
24765
24866
|
callback_data: { type: "string", description: "Opaque tag delivered back to the agent on tap. Max 58 chars (gateway prepends an `agent:` prefix to the 64-byte Telegram limit). Mutually exclusive with url." },
|
|
24766
24867
|
ack_text: { type: "string", description: 'Toast text shown to the user the instant they tap this button (#710). Default "\u2713 received". Max ~200 chars (Telegram answerCallbackQuery limit). Has no effect on URL buttons.' },
|
|
24767
|
-
single_use: { type: "boolean", description: `When true (default) tapping any single_use button on the message removes the entire keyboard so the user can't double-fire. Set false on buttons that should stay tappable (e.g. a "Refresh" button). If ANY button on the message has single_use:false the keyboard is preserved on tap.` }
|
|
24868
|
+
single_use: { type: "boolean", description: `When true (default) tapping any single_use button on the message removes the entire keyboard so the user can't double-fire. Set false on buttons that should stay tappable (e.g. a "Refresh" button). If ANY button on the message has single_use:false the keyboard is preserved on tap.` },
|
|
24869
|
+
inline_keyboard_confirm: { type: "boolean", description: `Per-message override for the "\u2705 You chose: <label> \xB7 HH:MM" body annotation on tap (#789). true forces the annotation even if the agent default is off; false skips it even if the agent default is on; omit to use the agent default. Only applies to single-use keyboards and requires the agent parseMode to be the default html. Has no effect on URL buttons. Note: annotation rebuilds the body from the message's plain text, so formatting entities on the original message are lost.` }
|
|
24768
24870
|
},
|
|
24769
24871
|
required: ["text"]
|
|
24770
24872
|
}
|
|
@@ -25175,6 +25277,12 @@ mcp.setRequestHandler(CallToolRequestSchema2, async (req) => {
|
|
|
25175
25277
|
isError: true
|
|
25176
25278
|
};
|
|
25177
25279
|
}
|
|
25280
|
+
if (inboundDedupEnabled && (tool === "reply" || tool === "stream_reply")) {
|
|
25281
|
+
const replyChatId = args.chat_id;
|
|
25282
|
+
if (typeof replyChatId === "string" && replyChatId.length > 0) {
|
|
25283
|
+
inboundDedup.clearChat(replyChatId);
|
|
25284
|
+
}
|
|
25285
|
+
}
|
|
25178
25286
|
if (result.result && typeof result.result === "object" && "content" in result.result) {
|
|
25179
25287
|
return result.result;
|
|
25180
25288
|
}
|
|
@@ -25259,7 +25367,17 @@ mcp.setNotificationHandler(exports_external.object({
|
|
|
25259
25367
|
});
|
|
25260
25368
|
});
|
|
25261
25369
|
var ipc = null;
|
|
25370
|
+
var inboundDedup = new InboundDedup;
|
|
25371
|
+
var inboundDedupEnabled = process.env.SWITCHROOM_INBOUND_DEDUP !== "0";
|
|
25262
25372
|
function onInbound(msg) {
|
|
25373
|
+
if (inboundDedupEnabled && shouldDedupInbound(msg)) {
|
|
25374
|
+
const chatKey = dedupChatKey(msg.chatId, msg.threadId);
|
|
25375
|
+
if (inboundDedup.checkAndRecord(chatKey, msg.messageId) === "duplicate") {
|
|
25376
|
+
process.stderr.write(`telegram bridge: dropping duplicate inbound messageId=${msg.messageId} ` + `chat=${chatKey} (already forwarded this turn \u2014 sweep re-delivery, #2094)
|
|
25377
|
+
`);
|
|
25378
|
+
return;
|
|
25379
|
+
}
|
|
25380
|
+
}
|
|
25263
25381
|
mcp.notification({
|
|
25264
25382
|
method: "notifications/claude/channel",
|
|
25265
25383
|
params: {
|