switchroom 0.15.45 → 0.16.5
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 +56 -15
- package/dist/auth-broker/index.js +383 -97
- package/dist/cli/autoaccept-poll.js +4842 -35
- package/dist/cli/drive-write-pretool.mjs +7 -4
- package/dist/cli/notion-write-pretool.mjs +35 -4
- package/dist/cli/self-improve-apply-guard-pretool.mjs +626 -0
- package/dist/cli/self-improve-stop.mjs +428 -0
- package/dist/cli/switchroom.js +2894 -841
- package/dist/host-control/main.js +2685 -207
- package/dist/vault/approvals/kernel-server.js +7453 -7413
- package/dist/vault/broker/server.js +11428 -11388
- package/examples/minimal.yaml +1 -0
- package/examples/switchroom.yaml +1 -0
- package/package.json +3 -3
- package/profiles/_base/start.sh.hbs +97 -1
- package/profiles/_shared/execution-discipline.md.hbs +18 -0
- package/profiles/default/CLAUDE.md.hbs +0 -19
- package/telegram-plugin/.claude-plugin/plugin.json +2 -2
- package/telegram-plugin/answer-stream-flag.ts +12 -49
- package/telegram-plugin/answer-stream.ts +5 -150
- package/telegram-plugin/auth-snapshot-format.ts +280 -48
- package/telegram-plugin/auto-fallback-fleet.ts +44 -1
- package/telegram-plugin/context-exhaustion.ts +12 -0
- package/telegram-plugin/demo-mask.ts +154 -0
- package/telegram-plugin/dist/bridge/bridge.js +55 -12
- package/telegram-plugin/dist/gateway/gateway.js +2938 -977
- package/telegram-plugin/dist/server.js +55 -12
- package/telegram-plugin/docs/waiting-ux-spec.md +2 -2
- package/telegram-plugin/draft-stream.ts +47 -410
- package/telegram-plugin/final-answer-detect.ts +17 -12
- package/telegram-plugin/fleet-fallback-resume.ts +131 -0
- package/telegram-plugin/format.ts +56 -19
- package/telegram-plugin/gateway/auth-add-flow.ts +332 -127
- package/telegram-plugin/gateway/auth-broker-client.ts +2 -2
- package/telegram-plugin/gateway/auth-command.ts +70 -14
- package/telegram-plugin/gateway/clean-shutdown-marker.ts +44 -0
- package/telegram-plugin/gateway/config-approval-handler.test.ts +91 -4
- package/telegram-plugin/gateway/config-approval-handler.ts +94 -13
- package/telegram-plugin/gateway/current-turn-map.ts +188 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +3 -1
- package/telegram-plugin/gateway/effort-command.ts +8 -3
- package/telegram-plugin/gateway/emission-authority.ts +369 -0
- package/telegram-plugin/gateway/feed-open-gate.ts +292 -0
- package/telegram-plugin/gateway/gateway.ts +1857 -292
- package/telegram-plugin/gateway/inject-handler.test.ts +2 -1
- package/telegram-plugin/gateway/model-command.ts +115 -4
- package/telegram-plugin/gateway/ms365-write-approval.test.ts +4 -4
- package/telegram-plugin/gateway/represent-guard.ts +72 -0
- package/telegram-plugin/gateway/status-surface-log.test.ts +5 -4
- package/telegram-plugin/gateway/status-surface-log.ts +14 -3
- package/telegram-plugin/history.ts +33 -11
- package/telegram-plugin/hooks/repo-context-pretool.mjs +26 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +5 -0
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +8 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +39 -15
- package/telegram-plugin/issues-card.ts +4 -0
- package/telegram-plugin/model-unavailable.ts +124 -0
- package/telegram-plugin/narrative-dedup.ts +69 -0
- package/telegram-plugin/over-ping-safety-net.ts +70 -4
- package/telegram-plugin/package.json +3 -3
- package/telegram-plugin/pending-work-progress.ts +12 -0
- package/telegram-plugin/permission-rule.ts +32 -5
- package/telegram-plugin/permission-title.ts +152 -9
- package/telegram-plugin/quota-check.ts +13 -0
- package/telegram-plugin/quota-watch.ts +135 -7
- package/telegram-plugin/registry/turns-schema.test.ts +24 -0
- package/telegram-plugin/registry/turns-schema.ts +9 -0
- package/telegram-plugin/runtime-metrics.ts +13 -0
- package/telegram-plugin/session-tail.ts +96 -11
- package/telegram-plugin/silence-poke.ts +170 -24
- package/telegram-plugin/slot-banner-driver.ts +3 -0
- package/telegram-plugin/status-no-truncate.ts +44 -0
- package/telegram-plugin/status-reactions.ts +20 -3
- package/telegram-plugin/stream-controller.ts +4 -23
- package/telegram-plugin/stream-reply-handler.ts +6 -24
- package/telegram-plugin/streaming-metrics.ts +91 -0
- package/telegram-plugin/subagent-watcher.ts +212 -66
- package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +47 -0
- package/telegram-plugin/tests/answer-stream-dedup.test.ts +9 -26
- package/telegram-plugin/tests/answer-stream-flag.test.ts +25 -58
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +41 -51
- package/telegram-plugin/tests/answer-stream.test.ts +2 -411
- package/telegram-plugin/tests/auth-add-flow.test.ts +488 -253
- package/telegram-plugin/tests/auth-command-format2.test.ts +71 -1
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +376 -6
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +120 -0
- package/telegram-plugin/tests/cross-turn-card-gate.test.ts +424 -0
- package/telegram-plugin/tests/demo-mask.test.ts +127 -0
- package/telegram-plugin/tests/draft-stream.test.ts +0 -827
- package/telegram-plugin/tests/emission-authority-card-drain-gate.test.ts +236 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +488 -0
- package/telegram-plugin/tests/emission-authority-open-gate.test.ts +179 -0
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +395 -0
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +177 -0
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +146 -0
- package/telegram-plugin/tests/feed-open-gate.test.ts +259 -0
- package/telegram-plugin/tests/feed-survival.test.ts +526 -0
- package/telegram-plugin/tests/fleet-fallback-resume.test.ts +197 -0
- package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +117 -0
- package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +4 -11
- package/telegram-plugin/tests/history.test.ts +60 -0
- package/telegram-plugin/tests/model-command.test.ts +134 -0
- package/telegram-plugin/tests/model-unavailable.test.ts +118 -0
- package/telegram-plugin/tests/narrative-dedup.test.ts +118 -0
- package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +285 -0
- package/telegram-plugin/tests/over-ping-final-answer-decoupling.test.ts +194 -0
- package/telegram-plugin/tests/over-ping-safety-net.test.ts +2 -2
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +373 -0
- package/telegram-plugin/tests/permission-card-origin-kill-switch.test.ts +42 -0
- package/telegram-plugin/tests/permission-rule.test.ts +17 -0
- package/telegram-plugin/tests/permission-title.test.ts +206 -17
- package/telegram-plugin/tests/quota-watch.test.ts +252 -9
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -1
- package/telegram-plugin/tests/repo-context-pretool.test.ts +62 -0
- package/telegram-plugin/tests/represent-guard.test.ts +162 -0
- package/telegram-plugin/tests/session-tail.test.ts +147 -3
- package/telegram-plugin/tests/silence-liveness-wiring.test.ts +18 -0
- package/telegram-plugin/tests/status-card-budget-parity.test.ts +72 -0
- package/telegram-plugin/tests/status-surface-log.test.ts +146 -0
- package/telegram-plugin/tests/subagent-watcher-clip-narrative.test.ts +58 -0
- package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +102 -0
- package/telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts +225 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +147 -0
- package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +597 -0
- package/telegram-plugin/tests/telegram-format.test.ts +101 -6
- package/telegram-plugin/tests/tool-activity-summary.test.ts +550 -15
- package/telegram-plugin/tests/tool-label-pretool.test.ts +73 -0
- package/telegram-plugin/tests/tool-label-sidecar.test.ts +44 -0
- package/telegram-plugin/tests/tool-labels.test.ts +67 -0
- package/telegram-plugin/tests/turn-liveness-floor.test.ts +196 -0
- package/telegram-plugin/tests/turn-liveness-invariant.test.ts +340 -0
- package/telegram-plugin/tests/welcome-text.test.ts +32 -3
- package/telegram-plugin/tests/worker-activity-feed.test.ts +470 -22
- package/telegram-plugin/tool-activity-summary.ts +375 -58
- package/telegram-plugin/turn-liveness-floor.ts +240 -0
- package/telegram-plugin/uat/assertions.ts +115 -0
- package/telegram-plugin/uat/driver.ts +68 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +119 -133
- package/telegram-plugin/uat/scenarios/jtbd-answer-pings.test.ts +94 -0
- package/telegram-plugin/uat/scenarios/jtbd-cross-turn-card-dm.test.ts +109 -0
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-thinkgap-dm.test.ts +478 -0
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-visibility-dm.test.ts +396 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-feed-open-dm.test.ts +202 -0
- package/telegram-plugin/uat/scenarios/jtbd-reply-is-last-dm.test.ts +202 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +93 -87
- package/telegram-plugin/welcome-text.ts +13 -1
- package/telegram-plugin/worker-activity-feed.ts +157 -82
- package/telegram-plugin/draft-transport.ts +0 -122
- package/telegram-plugin/tests/draft-retirement-wiring.test.ts +0 -82
- package/telegram-plugin/tests/draft-transport.test.ts +0 -211
package/examples/minimal.yaml
CHANGED
package/examples/switchroom.yaml
CHANGED
|
@@ -26,6 +26,7 @@ switchroom:
|
|
|
26
26
|
version: 1
|
|
27
27
|
agents_dir: ~/.switchroom/agents
|
|
28
28
|
skills_dir: ~/.switchroom/skills # shared skill pool (symlinked per agent)
|
|
29
|
+
# timezone: "Region/City" # agents' local time + cron evaluation; defaults to UTC if unset
|
|
29
30
|
|
|
30
31
|
telegram:
|
|
31
32
|
# Single-agent fallback ONLY. This is the bot the one shipped agent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.5",
|
|
4
4
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"build": "node scripts/build.mjs",
|
|
24
24
|
"build:cli": "node scripts/build.mjs && bun build --compile --target=bun-linux-x64 --minify bin/switchroom.ts --outfile switchroom-linux-amd64",
|
|
25
25
|
"pretest": "npm run build",
|
|
26
|
-
"test": "vitest run && bun test telegram-plugin/tests/history.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/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/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/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.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",
|
|
26
|
+
"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/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/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/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.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",
|
|
27
27
|
"test:vitest": "vitest run",
|
|
28
|
-
"test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.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/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/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.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/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/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts",
|
|
28
|
+
"test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.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/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/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/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/gateway/webhook-ingest-server.test.ts",
|
|
29
29
|
"test:watch": "vitest",
|
|
30
30
|
"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",
|
|
31
31
|
"lint:tsc": "tsc --noEmit",
|
|
@@ -67,6 +67,22 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
|
|
|
67
67
|
# lifecycle. Ampersand-backgrounded by callers below.
|
|
68
68
|
_switchroom_supervise() {
|
|
69
69
|
local _name="$1"; local _logfile="$2"; shift 2
|
|
70
|
+
# Optional `--oneshot-ok` flag (must directly follow the logfile):
|
|
71
|
+
# marks a sidecar that can LEGITIMATELY complete. For such a sidecar
|
|
72
|
+
# a clean exit 0 means "job done" → stop supervising, no respawn.
|
|
73
|
+
# This is the autoaccept sidecar's case: under
|
|
74
|
+
# SWITCHROOM_WEDGE_WATCHDOG=0 it dispatches the first-run prompts and
|
|
75
|
+
# exits 0 by design (and if the watch loop ever returns cleanly, that
|
|
76
|
+
# too is a completed job, not a crash). Without this flag the old
|
|
77
|
+
# supervisor respawned that clean exit every 60s forever — pure
|
|
78
|
+
# restart/log churn with zero productive work (klanker, #2471 wedge
|
|
79
|
+
# diagnosis: 192 status=0 respawns). A must-always-run sidecar (the
|
|
80
|
+
# gateway, the scheduler) must NOT pass this flag — a clean exit
|
|
81
|
+
# there IS abnormal and must still restart.
|
|
82
|
+
local _oneshot_ok=0
|
|
83
|
+
if [ "${1:-}" = "--oneshot-ok" ]; then
|
|
84
|
+
_oneshot_ok=1; shift
|
|
85
|
+
fi
|
|
70
86
|
local _cap=60
|
|
71
87
|
local _delay=1
|
|
72
88
|
local _attempt=0
|
|
@@ -88,6 +104,15 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
|
|
|
88
104
|
echo "[supervise] $_name exit 78 (EX_CONFIG) — quarantined, not restarting. Operator action required." >> "$_logfile"
|
|
89
105
|
return 0
|
|
90
106
|
fi
|
|
107
|
+
# Clean completion of an opt-in one-shot-capable sidecar: the job
|
|
108
|
+
# finished successfully, so do NOT respawn (respawning a
|
|
109
|
+
# legitimately-completed exit-0 is the churn this guard removes).
|
|
110
|
+
# A non-zero exit still falls through to the indefinite backoff
|
|
111
|
+
# below — a crash always self-heals.
|
|
112
|
+
if [ $_oneshot_ok -eq 1 ] && [ $_exit -eq 0 ]; then
|
|
113
|
+
echo "[supervise] $_name completed cleanly (exit 0) — one-shot-ok, not respawning." >> "$_logfile"
|
|
114
|
+
return 0
|
|
115
|
+
fi
|
|
91
116
|
# A run that stayed up for at least the backoff cap means the
|
|
92
117
|
# dependency was healthy and this exit is a fresh transient
|
|
93
118
|
# blip — reset backoff so recovery latency is minimal. Only
|
|
@@ -139,7 +164,13 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
|
|
|
139
164
|
# keeps a flaky run from busy-looping. Set SWITCHROOM_WEDGE_WATCHDOG=0
|
|
140
165
|
# to restore the legacy boot-only single-shot behaviour.
|
|
141
166
|
if [ -f /opt/switchroom/autoaccept-poll.js ] && command -v bun >/dev/null 2>&1; then
|
|
142
|
-
|
|
167
|
+
# --oneshot-ok: this sidecar can legitimately complete (exit 0) — the
|
|
168
|
+
# boot-only mode (SWITCHROOM_WEDGE_WATCHDOG=0) dispatches the first-run
|
|
169
|
+
# prompts then exits, and a clean watch-loop return is also a finished
|
|
170
|
+
# job, not a crash. Marking it one-shot-ok stops the supervisor from
|
|
171
|
+
# respawning that clean exit every 60s forever (the #2471-diagnosed
|
|
172
|
+
# churn). A genuine crash (non-zero) still backs off + retries.
|
|
173
|
+
_switchroom_supervise autoaccept /var/log/switchroom/autoaccept.log --oneshot-ok \
|
|
143
174
|
bun /opt/switchroom/autoaccept-poll.js "{{name}}" &
|
|
144
175
|
fi
|
|
145
176
|
|
|
@@ -287,6 +318,12 @@ export TELEGRAM_STATE_DIR="{{agentDir}}/telegram"
|
|
|
287
318
|
# basename(process.cwd()) inside the plugin because Claude Code spawns
|
|
288
319
|
# MCP servers with cwd = $HOME regardless of the parent's cwd.
|
|
289
320
|
export SWITCHROOM_AGENT_NAME="{{name}}"
|
|
321
|
+
# SWITCHROOM_AGENT_START_CWD is the agent's real start directory (the dir this
|
|
322
|
+
# script cd's into and where the agent's own CLAUDE.md lives). The
|
|
323
|
+
# repo-context-pretool hook reads it to suppress re-injection of the agent's
|
|
324
|
+
# own CLAUDE.md (already in the system prompt) while still injecting markers
|
|
325
|
+
# for worktree repos the agent navigates into mid-session.
|
|
326
|
+
export SWITCHROOM_AGENT_START_CWD="{{agentDir}}"
|
|
290
327
|
{{#if switchroomConfigPathQ}}
|
|
291
328
|
# SWITCHROOM_CONFIG lets the agent's own shell tool invoke `switchroom`
|
|
292
329
|
# without passing `--config` every time. The telegram-plugin MCP server
|
|
@@ -735,6 +772,65 @@ if command -v switchroom >/dev/null 2>&1; then
|
|
|
735
772
|
unset sr_wk_pair sr_wk_env sr_wk_key sr_wk_val
|
|
736
773
|
fi
|
|
737
774
|
|
|
775
|
+
# LiteLLM routing (opt-in, #litellm). When SWITCHROOM_LITELLM is set (compose
|
|
776
|
+
# env, gated on litellm.enabled && keyConfirmed), route the unmodified `claude`
|
|
777
|
+
# CLI through the operator's LiteLLM proxy at ANTHROPIC_BASE_URL: fetch the
|
|
778
|
+
# per-agent virtual key from the vault-broker and export it as the
|
|
779
|
+
# x-litellm-api-key Bearer header. The broker enforces the per-key ACL (the
|
|
780
|
+
# agent's standing secrets[] grant added by `switchroom apply`).
|
|
781
|
+
#
|
|
782
|
+
# Subscription-native by construction: claude still sends its Pro/Max OAuth
|
|
783
|
+
# `Authorization: Bearer`, which LiteLLM forwards UNCHANGED to Anthropic
|
|
784
|
+
# (forward_client_headers_to_llm_api). The proxy only meters and applies
|
|
785
|
+
# content-safety guardrails — it never alters the model or Claude's operation.
|
|
786
|
+
# See reference/invariants.md § "Operator-controlled gateway carve-out".
|
|
787
|
+
#
|
|
788
|
+
# FAIL-OPEN (operator decision 2026-06-28): if the key is missing OR the proxy
|
|
789
|
+
# is unreachable at boot, strip the routing env and fall back to the direct
|
|
790
|
+
# broker-OAuth Anthropic path, logging the reason LOUDLY. Availability wins over
|
|
791
|
+
# tracking/guardrails — a proxy outage must never take an agent dark. The
|
|
792
|
+
# tradeoff is an explicit, logged guardrail lapse for that session, not a silent
|
|
793
|
+
# one. Re-probed on every (re)boot, so recovery is automatic.
|
|
794
|
+
#
|
|
795
|
+
# Static tags (x-litellm-customer-id / x-litellm-tags) give per-AGENT
|
|
796
|
+
# attribution in LiteLLM. Per-SESSION is NOT achievable here: claude sends no
|
|
797
|
+
# session id on the wire and ANTHROPIC_CUSTOM_HEADERS is fixed per process — so
|
|
798
|
+
# per-session cost/issue tracking is handled out-of-band by correlating
|
|
799
|
+
# LiteLLM's request log against switchroom's turn ledger, never by mutating the
|
|
800
|
+
# claude protocol.
|
|
801
|
+
if [ -n "$SWITCHROOM_LITELLM" ] && command -v switchroom >/dev/null 2>&1; then
|
|
802
|
+
sr_ll_key="$(switchroom vault get "litellm/$SWITCHROOM_AGENT_NAME/api-key" 2>/dev/null || true)"
|
|
803
|
+
sr_ll_ok=""
|
|
804
|
+
if [ -z "$sr_ll_key" ]; then
|
|
805
|
+
echo "litellm: no virtual key for agent '$SWITCHROOM_AGENT_NAME' — falling back to direct OAuth (no tracking/guardrail)" >&2
|
|
806
|
+
elif command -v curl >/dev/null 2>&1 && [ -n "$ANTHROPIC_BASE_URL" ] \
|
|
807
|
+
&& ! curl -fsS -m 5 -o /dev/null "${ANTHROPIC_BASE_URL%/}/health/liveliness" 2>/dev/null; then
|
|
808
|
+
echo "litellm: proxy unreachable at $ANTHROPIC_BASE_URL — falling back to direct OAuth (no tracking/guardrail this session)" >&2
|
|
809
|
+
else
|
|
810
|
+
sr_ll_ok="1"
|
|
811
|
+
fi
|
|
812
|
+
if [ -n "$sr_ll_ok" ]; then
|
|
813
|
+
# Newline-separated Name: Value pairs (claude CLI ANTHROPIC_CUSTOM_HEADERS format).
|
|
814
|
+
# Tags: agent:<name> for per-agent spend tracking; profile:<profile> for
|
|
815
|
+
# fleet-level cost breakdown by role. Per-turn tags (cron vs telegram) are
|
|
816
|
+
# NOT achievable here — ANTHROPIC_CUSTOM_HEADERS is fixed per process and
|
|
817
|
+
# the claude CLI sends no session id. Use scheduler.jsonl ↔ /spend/logs
|
|
818
|
+
# time-window correlation for cron attribution instead.
|
|
819
|
+
export ANTHROPIC_CUSTOM_HEADERS="x-litellm-api-key: Bearer $sr_ll_key
|
|
820
|
+
x-litellm-customer-id: $SWITCHROOM_AGENT_NAME
|
|
821
|
+
x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:-default}"
|
|
822
|
+
# Let Claude Code enumerate models from the LiteLLM gateway so non-Claude
|
|
823
|
+
# models configured in the proxy appear in the /model picker and can be
|
|
824
|
+
# selected via --model. Without this, the CLI only knows its bundled list.
|
|
825
|
+
export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
|
|
826
|
+
else
|
|
827
|
+
# Fail-open: drop every routing var so the claude CLI talks to Anthropic
|
|
828
|
+
# directly on its OAuth credential (subscription path), unproxied.
|
|
829
|
+
unset ANTHROPIC_BASE_URL ANTHROPIC_SMALL_FAST_MODEL SWITCHROOM_LITELLM
|
|
830
|
+
fi
|
|
831
|
+
unset sr_ll_key sr_ll_ok
|
|
832
|
+
fi
|
|
833
|
+
|
|
738
834
|
{{#if useSwitchroomPlugin}}
|
|
739
835
|
if [ -n "$APPEND_PROMPT" ]; then
|
|
740
836
|
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## Grounding — check before you assert
|
|
2
|
+
|
|
3
|
+
Your memory and prior context are leads, not facts. Anything you state as true — about the code, a file, a system's state, a config value, a number, a status, or what you can and can't do — must come from something you actually checked this turn, not from recall, habit, or assumption. When you have a way to look, look.
|
|
4
|
+
|
|
5
|
+
- **Read the ground truth.** Mutable state — file and code contents, git status, config, versions, running services, processes, schedules, permissions — is read live with the right tool, not remembered. "I think it's X" is a lead to verify, not an answer.
|
|
6
|
+
- **Prefer finding over guessing.** If a fact exists somewhere you can reach — the codebase, a system, a doc, the web, a tool's output — go get it before answering. Favor the source over a plausible-sounding recollection.
|
|
7
|
+
- **Don't assert a limit you haven't tested.** Before saying you can't do something — "no access", "operator-only", "not editable", "no handle" — verify it live. A fabricated boundary is as wrong as a fabricated fact, and it stops work that was actually possible.
|
|
8
|
+
- **A weak or empty result isn't a conclusion.** Vary the query, path, command, or source before deciding something doesn't exist or isn't true.
|
|
9
|
+
- **Final answers carry evidence.** Tool output, a file you read, a check you ran, or a named blocker — not "it should work."
|
|
10
|
+
|
|
11
|
+
When you genuinely can't verify something this turn, say so plainly ("I haven't checked this —") instead of stating it as fact. A confident wrong answer costs far more than an honest "let me confirm."
|
|
12
|
+
|
|
13
|
+
## Execution Bias
|
|
14
|
+
|
|
15
|
+
How you should decide what to do next. These are procedural rules, not vibe.
|
|
16
|
+
|
|
17
|
+
- **Act in-turn.** If the request is actionable, do it this turn. Don't finish with a plan or promise when tools can move it forward.
|
|
18
|
+
- **Non-final turn:** use tools to advance, or ask the one clarifying question that unblocks safe progress. One question, not five.
|
|
@@ -31,25 +31,6 @@ You are operating in the **{{topicName}}** {{#if topicEmoji}}{{topicEmoji}} {{/i
|
|
|
31
31
|
- **Batch foreseeable approvals; don't drip surprises.** When you can already see that several actions will each need the user's approval, tell them up front which approvals are coming and why. Request independent ones together so they can decide once; for dependent ones (one's input comes from another), say what you're doing first and what approval comes next — a permission card should never arrive out of the blue.
|
|
32
32
|
- **A timed-out approval isn't a denial.** If a request came back denied only because the user was away (a timeout, not an explicit "no"), don't silently abandon it. When they're back, remind them it's still pending and re-offer it if they still want it.
|
|
33
33
|
|
|
34
|
-
## Execution Bias
|
|
35
|
-
|
|
36
|
-
How you should decide what to do next. These are procedural rules, not vibe.
|
|
37
|
-
|
|
38
|
-
- **Act in-turn.** If the request is actionable, do it this turn. Don't finish with a plan or promise when tools can move it forward.
|
|
39
|
-
- **Verify mutable facts before claiming them.** Files, git state, clocks, versions, services, processes, package state, the contents of an `Edit` target: read live. Memory and prior context are not verification sources. "I think the function is at line 200" is not an answer; `Grep`/`Read` is.
|
|
40
|
-
- **Final answer needs evidence.** Test/build/lint output, screenshot, inspection, tool output, or a named blocker. "It should work" is not a finalization.
|
|
41
|
-
- **Weak or empty tool result is not a conclusion.** Vary the query, path, command, or source before deciding the thing isn't there.
|
|
42
|
-
- **Non-final turn:** use tools to advance, or ask the one clarifying question that unblocks safe progress. One question, not five.
|
|
43
|
-
|
|
44
|
-
{{!--
|
|
45
|
-
Telegram-style guidance (the 5-beat pacing contract) lives in the
|
|
46
|
-
fleet invariants file at `~/.switchroom/fleet/switchroom-invariants.md`
|
|
47
|
-
and reaches the agent via Claude Code native CLAUDE.md discovery
|
|
48
|
-
(`--add-dir ~/.switchroom/fleet`). Do not re-include the
|
|
49
|
-
`{{> telegram-style}}` partial here — that would re-introduce the
|
|
50
|
-
session-level duplication the prompt redesign removed.
|
|
51
|
-
--}}
|
|
52
|
-
|
|
53
34
|
## Memory — Hindsight is your single backend
|
|
54
35
|
|
|
55
36
|
**Claude Code's built-in file-based auto-memory is disabled for this agent.** Don't try to write `.md` files under `.claude/projects/.../memory/` or maintain a `MEMORY.md` index — that whole system is off. There's exactly one memory backend: **Hindsight**.
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"name": "Ken Thompson",
|
|
7
7
|
"email": "me@kenthompson.com.au"
|
|
8
8
|
},
|
|
9
|
-
"homepage": "https://github.com/
|
|
10
|
-
"repository": "https://github.com/
|
|
9
|
+
"homepage": "https://github.com/switchroom/switchroom",
|
|
10
|
+
"repository": "https://github.com/switchroom/switchroom",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"keywords": [
|
|
13
13
|
"telegram",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Default **OFF** (flipped 2026-06-03, operator request) — see the rationale
|
|
5
5
|
* on the `ANSWER_STREAM_VISIBLE_ENABLED` gate in `gateway/gateway.ts`. When
|
|
6
|
-
* off, the answer lane
|
|
6
|
+
* off, the answer lane stays dormant (no draft, no visible preview) and the
|
|
7
7
|
* reply tool is the single canonical formatted message — no unformatted
|
|
8
8
|
* preliminary that flashes and gets deleted. Opt back IN per agent with
|
|
9
9
|
* `SWITCHROOM_VISIBLE_ANSWER_STREAM=1` (also accepts true/on/yes).
|
|
@@ -17,25 +17,6 @@ export function parseVisibleAnswerStreamEnabled(raw: string | undefined): boolea
|
|
|
17
17
|
return v === '1' || v === 'true' || v === 'on' || v === 'yes'
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Draft-answer-lane retirement (2026-06-05). The compose-box draft transport
|
|
22
|
-
* (`sendMessageDraft`) is invisible to the mtcute UAT harness, so the live
|
|
23
|
-
* answer-stream surface couldn't be tested. Retired by DEFAULT: the answer lane
|
|
24
|
-
* now opens a real, observable edit-in-place message instead of the compose-box
|
|
25
|
-
* draft (and the onMetric silence-liveness reset from #2169 now fires on visible
|
|
26
|
-
* sends in BOTH DMs and supergroups, not just DM drafts). Kill switch
|
|
27
|
-
* `SWITCHROOM_DRAFT_ANSWER_LANE=0` (also false/off/no) restores the legacy
|
|
28
|
-
* invisible draft.
|
|
29
|
-
*
|
|
30
|
-
* Returns true when the draft lane is RETIRED (the default — env unset or any
|
|
31
|
-
* truthy value); false only for an explicit disable of the retirement.
|
|
32
|
-
*/
|
|
33
|
-
export function parseDraftLaneRetiredEnabled(raw: string | undefined): boolean {
|
|
34
|
-
if (raw == null) return true
|
|
35
|
-
const v = raw.trim().toLowerCase()
|
|
36
|
-
return !(v === '0' || v === 'false' || v === 'off' || v === 'no')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
20
|
/**
|
|
40
21
|
* `minInitialChars` sentinel meaning "never open a visible chat-timeline
|
|
41
22
|
* preview" — mirrors the `Number.MAX_SAFE_INTEGER` gate the createAnswerStream
|
|
@@ -43,63 +24,45 @@ export function parseDraftLaneRetiredEnabled(raw: string | undefined): boolean {
|
|
|
43
24
|
*/
|
|
44
25
|
export const ANSWER_LANE_NEVER_OPENS = Number.MAX_SAFE_INTEGER
|
|
45
26
|
|
|
46
|
-
|
|
27
|
+
/** The answer-lane rendering state. 'draft' is removed — lane is either
|
|
28
|
+
* visible (opt-in) or dormant (the default: reply tool is the only message). */
|
|
29
|
+
export type AnswerLaneState = 'visible' | 'dormant'
|
|
47
30
|
|
|
48
31
|
export interface AnswerLaneConfig {
|
|
49
32
|
/** `minInitialChars` for createAnswerStream: `1` opens a visible preview on
|
|
50
33
|
* the first text chunk; `ANSWER_LANE_NEVER_OPENS` suppresses it. */
|
|
51
34
|
minInitialChars: number
|
|
52
|
-
/** Whether the lane streams to the invisible compose-box draft transport. */
|
|
53
|
-
usesDraftTransport: boolean
|
|
54
35
|
/** Whether a USER-VISIBLE chat-timeline preview opens — i.e. the surface that
|
|
55
36
|
* flashed (raw preview → formatted reply → preview deleted). This is THE
|
|
56
|
-
* regression invariant: it must equal `visibleEnabled
|
|
57
|
-
* draft flag. */
|
|
37
|
+
* regression invariant: it must equal `visibleEnabled`. */
|
|
58
38
|
opensVisiblePreview: boolean
|
|
59
39
|
/** Label for the boot log. */
|
|
60
40
|
state: AnswerLaneState
|
|
61
41
|
}
|
|
62
42
|
|
|
63
43
|
/**
|
|
64
|
-
* Resolve the answer-lane config from the
|
|
44
|
+
* Resolve the answer-lane config from the single input.
|
|
65
45
|
*
|
|
66
|
-
* The
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* re-opening the flash v0.14.52 had removed. The load-bearing invariant —
|
|
71
|
-
* `opensVisiblePreview === visibleEnabled` for EVERY `draftFnAvailable` — is
|
|
72
|
-
* what this function exists to make total-enumerable (the gateway IIFE is not).
|
|
46
|
+
* The draft transport (`sendMessageDraft`) is permanently retired — the lane
|
|
47
|
+
* is either VISIBLE (opt-in via SWITCHROOM_VISIBLE_ANSWER_STREAM=1) or
|
|
48
|
+
* DORMANT (the unconditional default). In dormant mode no preview opens and
|
|
49
|
+
* the reply tool is the single canonical formatted message.
|
|
73
50
|
*
|
|
74
|
-
* visibleEnabled
|
|
75
|
-
* !
|
|
76
|
-
* !visible, no draft transport → 'dormant' (no preview, no draft:
|
|
77
|
-
* the reply tool is the
|
|
78
|
-
* only message — the default)
|
|
51
|
+
* visibleEnabled → 'visible' (preview opens on first chunk, minChars 1)
|
|
52
|
+
* !visibleEnabled → 'dormant' (no preview, no draft — reply tool only)
|
|
79
53
|
*/
|
|
80
54
|
export function resolveAnswerLaneConfig(input: {
|
|
81
55
|
visibleEnabled: boolean
|
|
82
|
-
draftFnAvailable: boolean
|
|
83
56
|
}): AnswerLaneConfig {
|
|
84
57
|
if (input.visibleEnabled) {
|
|
85
58
|
return {
|
|
86
59
|
minInitialChars: 1,
|
|
87
|
-
usesDraftTransport: false,
|
|
88
60
|
opensVisiblePreview: true,
|
|
89
61
|
state: 'visible',
|
|
90
62
|
}
|
|
91
63
|
}
|
|
92
|
-
if (input.draftFnAvailable) {
|
|
93
|
-
return {
|
|
94
|
-
minInitialChars: ANSWER_LANE_NEVER_OPENS,
|
|
95
|
-
usesDraftTransport: true,
|
|
96
|
-
opensVisiblePreview: false,
|
|
97
|
-
state: 'draft',
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
64
|
return {
|
|
101
65
|
minInitialChars: ANSWER_LANE_NEVER_OPENS,
|
|
102
|
-
usesDraftTransport: false,
|
|
103
66
|
opensVisiblePreview: false,
|
|
104
67
|
state: 'dormant',
|
|
105
68
|
}
|
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import { isSilentFlushMarker } from './turn-flush-safety.js'
|
|
2
|
-
import {
|
|
3
|
-
DRAFT_METHOD_UNAVAILABLE_RE as _DRAFT_METHOD_UNAVAILABLE_RE,
|
|
4
|
-
DRAFT_CHAT_UNSUPPORTED_RE as _DRAFT_CHAT_UNSUPPORTED_RE,
|
|
5
|
-
shouldFallbackFromDraftTransport as _shouldFallbackFromDraftTransport,
|
|
6
|
-
allocateDraftId as _allocateDraftId,
|
|
7
|
-
__resetDraftIdForTests as _resetDraftIdForTests,
|
|
8
|
-
} from './draft-transport.js'
|
|
9
2
|
|
|
10
3
|
/**
|
|
11
4
|
* Answer-lane incremental streaming for long Telegram replies.
|
|
@@ -19,9 +12,8 @@ import {
|
|
|
19
12
|
*
|
|
20
13
|
* Design constraints honored:
|
|
21
14
|
* 1. Separate message ID from the progress card — never overwritten.
|
|
22
|
-
* 2.
|
|
23
|
-
*
|
|
24
|
-
* when draft API rejects (DRAFT_METHOD_UNAVAILABLE_RE / DRAFT_CHAT_UNSUPPORTED_RE).
|
|
15
|
+
* 2. sendMessage+editMessageText for all chats (in-place edit-in-place engine).
|
|
16
|
+
* The draft transport (sendMessageDraft) has been permanently retired.
|
|
25
17
|
* 3. minInitialChars (~50) — don't open the answer lane until enough text
|
|
26
18
|
* has arrived. Lowered 400 → 50 in #553 PR 3 so short replies
|
|
27
19
|
* ("yes", "done", "the answer is 42") become visible on the first
|
|
@@ -38,21 +30,12 @@ import {
|
|
|
38
30
|
* - No finalizable-draft-lifecycle SDK — we implement the loop directly.
|
|
39
31
|
* - materialize() always sends a fresh message regardless of transport,
|
|
40
32
|
* to guarantee a push notification on turn completion.
|
|
41
|
-
*
|
|
42
|
-
* Draft-transport helpers (regex constants, shouldFallbackFromDraftTransport,
|
|
43
|
-
* allocateDraftId) live in draft-transport.ts and are re-exported here so
|
|
44
|
-
* existing callers that import from this module continue to work.
|
|
45
33
|
*/
|
|
46
34
|
|
|
47
35
|
export const MIN_INITIAL_CHARS = 50
|
|
48
36
|
export const DEFAULT_THROTTLE_MS = 1000
|
|
49
37
|
const TELEGRAM_MAX_CHARS = 4096
|
|
50
38
|
|
|
51
|
-
// Re-export shared constants so existing callers / tests keep working.
|
|
52
|
-
export const DRAFT_METHOD_UNAVAILABLE_RE = _DRAFT_METHOD_UNAVAILABLE_RE
|
|
53
|
-
export const DRAFT_CHAT_UNSUPPORTED_RE = _DRAFT_CHAT_UNSUPPORTED_RE
|
|
54
|
-
export { _shouldFallbackFromDraftTransport as shouldFallbackFromDraftTransport }
|
|
55
|
-
|
|
56
39
|
/** Called when a late sendMessage/edit resolves after a new turn has started. */
|
|
57
40
|
export type OnSupersededCallback = (params: {
|
|
58
41
|
messageId: number
|
|
@@ -62,8 +45,6 @@ export type OnSupersededCallback = (params: {
|
|
|
62
45
|
export interface AnswerStreamConfig {
|
|
63
46
|
/** chatId for all API calls */
|
|
64
47
|
chatId: string
|
|
65
|
-
/** True if this is a DM — tries sendMessageDraft first */
|
|
66
|
-
isPrivateChat: boolean
|
|
67
48
|
/** Optional forum thread */
|
|
68
49
|
threadId?: number
|
|
69
50
|
/** Minimum chars before opening the answer lane. Default: MIN_INITIAL_CHARS */
|
|
@@ -74,16 +55,6 @@ export interface AnswerStreamConfig {
|
|
|
74
55
|
replyToMessageId?: number
|
|
75
56
|
|
|
76
57
|
// ── Transport callbacks ────────────────────────────────────────────────
|
|
77
|
-
/**
|
|
78
|
-
* sendMessageDraft(chatId, draftId, text, params?). Optional — when absent,
|
|
79
|
-
* the answer stream falls back immediately to sendMessage+editMessageText.
|
|
80
|
-
*/
|
|
81
|
-
sendMessageDraft?: (
|
|
82
|
-
chatId: string,
|
|
83
|
-
draftId: number,
|
|
84
|
-
text: string,
|
|
85
|
-
params?: { message_thread_id?: number; parse_mode?: 'HTML' },
|
|
86
|
-
) => Promise<unknown>
|
|
87
58
|
sendMessage: (
|
|
88
59
|
chatId: string,
|
|
89
60
|
text: string,
|
|
@@ -132,7 +103,7 @@ export interface AnswerStreamConfig {
|
|
|
132
103
|
* Acceptance #203: answer_lane_update / answer_lane_materialized events.
|
|
133
104
|
*/
|
|
134
105
|
onMetric?: (ev:
|
|
135
|
-
| { kind: 'answer_lane_update'; chatId: string; messageId: number | undefined; charCount: number; transport: '
|
|
106
|
+
| { kind: 'answer_lane_update'; chatId: string; messageId: number | undefined; charCount: number; transport: 'message' | 'edit' }
|
|
136
107
|
| { kind: 'answer_lane_materialized'; chatId: string; messageId: number | undefined; suppressed?: boolean }
|
|
137
108
|
) => void
|
|
138
109
|
|
|
@@ -199,19 +170,13 @@ export interface AnswerStreamHandle {
|
|
|
199
170
|
retract(): Promise<void>
|
|
200
171
|
}
|
|
201
172
|
|
|
202
|
-
// Draft-id allocation now lives in draft-transport.ts (shared with
|
|
203
|
-
// draft-stream.ts). Re-alias locally so the rest of this file is unchanged.
|
|
204
|
-
const allocateDraftId = _allocateDraftId
|
|
205
|
-
|
|
206
173
|
export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHandle {
|
|
207
174
|
const {
|
|
208
175
|
chatId,
|
|
209
|
-
isPrivateChat,
|
|
210
176
|
threadId,
|
|
211
177
|
minInitialChars = MIN_INITIAL_CHARS,
|
|
212
178
|
throttleMs = DEFAULT_THROTTLE_MS,
|
|
213
179
|
replyToMessageId,
|
|
214
|
-
sendMessageDraft: draftApi,
|
|
215
180
|
sendMessage,
|
|
216
181
|
editMessageText,
|
|
217
182
|
onSuperseded,
|
|
@@ -225,11 +190,6 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
225
190
|
|
|
226
191
|
const effectiveThrottle = Math.max(250, throttleMs)
|
|
227
192
|
|
|
228
|
-
// Draft transport is only used in DMs and only when the API method is available.
|
|
229
|
-
const preferDraft = isPrivateChat && draftApi != null
|
|
230
|
-
let usesDraftTransport = preferDraft
|
|
231
|
-
let draftId = preferDraft ? allocateDraftId() : undefined
|
|
232
|
-
|
|
233
193
|
// Stream state
|
|
234
194
|
let streamMsgId: number | undefined
|
|
235
195
|
let pendingText: string | null = null
|
|
@@ -249,62 +209,6 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
249
209
|
}
|
|
250
210
|
}
|
|
251
211
|
|
|
252
|
-
/**
|
|
253
|
-
* Clear the in-progress sendMessageDraft (#1704). When the answer
|
|
254
|
-
* stream was using draft transport (DMs), Telegram leaves the draft
|
|
255
|
-
* sitting in the user's compose area until something replaces or
|
|
256
|
-
* clears it. On Telegram Desktop this blocks the user from typing —
|
|
257
|
-
* the compose field is occupied by the bot's draft preview.
|
|
258
|
-
*
|
|
259
|
-
* Every terminal path on the stream (materialize / stop / retract)
|
|
260
|
-
* must clear the draft. Best-effort: a failed clear is logged but
|
|
261
|
-
* not re-thrown — the worst case is a transient stale draft that
|
|
262
|
-
* Telegram's own 30 s draft expiry eventually mops up.
|
|
263
|
-
*
|
|
264
|
-
* #1792 — accepts an explicit `targetDraftId` so `forceNewMessage`
|
|
265
|
-
* can clear the OLD id before bumping the closure's `draftId`. The
|
|
266
|
-
* default reads the live closure, which is what stop() / retract()
|
|
267
|
-
* want — clear whatever's current at the time the call lands.
|
|
268
|
-
*/
|
|
269
|
-
async function clearDraftBestEffort(
|
|
270
|
-
targetDraftId: number | undefined = draftId,
|
|
271
|
-
): Promise<void> {
|
|
272
|
-
if (!usesDraftTransport || draftApi == null || targetDraftId == null) return
|
|
273
|
-
try {
|
|
274
|
-
const params: { message_thread_id?: number } = {}
|
|
275
|
-
if (threadId != null) params.message_thread_id = threadId
|
|
276
|
-
await draftApi(
|
|
277
|
-
chatId,
|
|
278
|
-
targetDraftId,
|
|
279
|
-
'',
|
|
280
|
-
Object.keys(params).length > 0 ? params : undefined,
|
|
281
|
-
)
|
|
282
|
-
} catch {
|
|
283
|
-
// Best-effort cleanup
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function sendDraft(text: string): Promise<boolean> {
|
|
288
|
-
if (!draftApi || draftId == null) return false
|
|
289
|
-
try {
|
|
290
|
-
const params: { message_thread_id?: number } = {}
|
|
291
|
-
if (threadId != null) params.message_thread_id = threadId
|
|
292
|
-
await draftApi(chatId, draftId, text, Object.keys(params).length > 0 ? params : undefined)
|
|
293
|
-
onMetric?.({ kind: 'answer_lane_update', chatId, messageId: streamMsgId, charCount: text.length, transport: 'draft' })
|
|
294
|
-
return true
|
|
295
|
-
} catch (err) {
|
|
296
|
-
if (_shouldFallbackFromDraftTransport(err)) {
|
|
297
|
-
warn?.(
|
|
298
|
-
`answer-stream: sendMessageDraft rejected — falling back to sendMessage/editMessageText (${err instanceof Error ? err.message : String(err)})`,
|
|
299
|
-
)
|
|
300
|
-
usesDraftTransport = false
|
|
301
|
-
draftId = undefined
|
|
302
|
-
return false
|
|
303
|
-
}
|
|
304
|
-
throw err
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
212
|
async function sendOrEdit(text: string, gen: number): Promise<void> {
|
|
309
213
|
if (stopped) return
|
|
310
214
|
const trimmed = text.trimEnd()
|
|
@@ -315,15 +219,6 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
315
219
|
lastSentText = trimmed
|
|
316
220
|
|
|
317
221
|
try {
|
|
318
|
-
if (usesDraftTransport) {
|
|
319
|
-
const ok = await sendDraft(trimmed)
|
|
320
|
-
if (!ok) {
|
|
321
|
-
// Draft failed with a permanent error → fell back to message transport
|
|
322
|
-
// Retry the same text via message transport
|
|
323
|
-
await sendOrEditViaMessage(trimmed, gen, prevText)
|
|
324
|
-
}
|
|
325
|
-
return
|
|
326
|
-
}
|
|
327
222
|
await sendOrEditViaMessage(trimmed, gen, prevText)
|
|
328
223
|
} catch (err) {
|
|
329
224
|
// Log but don't crash — transient errors are common
|
|
@@ -419,7 +314,7 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
419
314
|
if (!trimmed) return
|
|
420
315
|
|
|
421
316
|
// minInitialChars gate: don't open the lane yet
|
|
422
|
-
if (streamMsgId == null &&
|
|
317
|
+
if (streamMsgId == null && trimmed.length < minInitialChars) return
|
|
423
318
|
|
|
424
319
|
pendingText = trimmed
|
|
425
320
|
|
|
@@ -460,9 +355,6 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
460
355
|
try { await inFlight } catch { /* ignore */ }
|
|
461
356
|
}
|
|
462
357
|
|
|
463
|
-
// Clear draft so Telegram input area doesn't show stale text
|
|
464
|
-
await clearDraftBestEffort()
|
|
465
|
-
|
|
466
358
|
// The text we want to materialize. Prefer pendingText (most recent
|
|
467
359
|
// snapshot from the model) over lastSentText (what last reached the
|
|
468
360
|
// wire). They usually match, but if a buffered update was scheduled
|
|
@@ -527,7 +419,7 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
527
419
|
purpose: 'materialize',
|
|
528
420
|
}
|
|
529
421
|
if (threadId != null) sendParams.message_thread_id = threadId
|
|
530
|
-
// Don't quote-reply on materialize — the
|
|
422
|
+
// Don't quote-reply on materialize — the stream already established
|
|
531
423
|
// the reply context visually. A second reply_parameters would create a
|
|
532
424
|
// nested quote that looks wrong.
|
|
533
425
|
|
|
@@ -563,21 +455,6 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
563
455
|
pendingText = null
|
|
564
456
|
stopped = false
|
|
565
457
|
materialized = false
|
|
566
|
-
if (usesDraftTransport) {
|
|
567
|
-
// #1792: clear the OLD draftId BEFORE rotating. Otherwise the
|
|
568
|
-
// stale content stays in the user's compose box until the 30 s
|
|
569
|
-
// Telegram draft expiry — the typical caller (gateway.ts mid-
|
|
570
|
-
// turn rapid-steer path: `forceNewMessage(); stop();`) cleans
|
|
571
|
-
// up the prior turn's stream, so the prior draft's content is
|
|
572
|
-
// semantically retracted. Fire-and-forget — forceNewMessage is
|
|
573
|
-
// sync; the worst-case failure mode is the same 30 s expiry
|
|
574
|
-
// we'd have had without the call.
|
|
575
|
-
const staleDraftId = draftId
|
|
576
|
-
if (staleDraftId != null) {
|
|
577
|
-
void clearDraftBestEffort(staleDraftId)
|
|
578
|
-
}
|
|
579
|
-
draftId = allocateDraftId()
|
|
580
|
-
}
|
|
581
458
|
log?.(`answer-stream: forceNewMessage (gen=${generation})`)
|
|
582
459
|
},
|
|
583
460
|
|
|
@@ -588,14 +465,6 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
588
465
|
stop(): void {
|
|
589
466
|
stopped = true
|
|
590
467
|
cancelScheduled()
|
|
591
|
-
// #1704: clear the compose-box draft. stop() is sync — fire and
|
|
592
|
-
// forget. A dropped clear falls back on Telegram's own 30 s
|
|
593
|
-
// draft expiry; the worst case is a transient stale preview.
|
|
594
|
-
// (#1792: the stale-id-after-rotation hazard is owned by
|
|
595
|
-
// forceNewMessage itself now — it clears its own draftId before
|
|
596
|
-
// rotating. stop() just clears whatever's current; clearing an
|
|
597
|
-
// already-cleared or never-used id is a harmless no-op.)
|
|
598
|
-
void clearDraftBestEffort()
|
|
599
468
|
},
|
|
600
469
|
|
|
601
470
|
async retract(): Promise<void> {
|
|
@@ -607,14 +476,6 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
607
476
|
if (inFlight) {
|
|
608
477
|
try { await inFlight } catch { /* ignore */ }
|
|
609
478
|
}
|
|
610
|
-
// #1704: clear the compose-box draft when this stream was using
|
|
611
|
-
// draft transport. Without this, Telegram Desktop leaves the
|
|
612
|
-
// draft sitting in the user's input area and blocks them from
|
|
613
|
-
// typing until the 30 s draft expiry. Awaited so a follow-up
|
|
614
|
-
// sendMessage on the same chat doesn't race a stale draft edit.
|
|
615
|
-
// (See #1792 note in stop() — forceNewMessage owns its own stale
|
|
616
|
-
// id cleanup; retract just clears whatever's current.)
|
|
617
|
-
await clearDraftBestEffort()
|
|
618
479
|
// Delete the preliminary message if one was sent and deleteMessage
|
|
619
480
|
// is wired. Best-effort: failures are logged but not re-thrown.
|
|
620
481
|
const msgId = streamMsgId
|
|
@@ -633,9 +494,3 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
633
494
|
},
|
|
634
495
|
}
|
|
635
496
|
}
|
|
636
|
-
|
|
637
|
-
/** Reset the draft-id counter for tests. */
|
|
638
|
-
/** Reset the shared draft-id counter for tests. Delegates to draft-transport.ts. */
|
|
639
|
-
export function __resetDraftIdForTests(): void {
|
|
640
|
-
_resetDraftIdForTests()
|
|
641
|
-
}
|