switchroom 0.18.3 → 0.18.7
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 +3 -1
- package/dist/auth-broker/index.js +3 -1
- package/dist/cli/autoaccept-poll.js +140 -33
- package/dist/cli/notion-write-pretool.mjs +3 -1
- package/dist/cli/switchroom.js +386 -128
- package/dist/host-control/main.js +4 -2
- package/dist/vault/approvals/kernel-server.js +3 -1
- package/dist/vault/broker/server.js +38 -8
- package/package.json +3 -3
- package/profiles/_base/cron-session.sh.hbs +55 -16
- package/profiles/_base/start.sh.hbs +35 -16
- package/profiles/default/CLAUDE.md.hbs +1 -1
- package/skills/switchroom-release/SKILL.md +78 -0
- package/telegram-plugin/auth-snapshot-format.ts +15 -1
- package/telegram-plugin/dist/bridge/bridge.js +22 -0
- package/telegram-plugin/dist/gateway/gateway.js +2852 -1032
- package/telegram-plugin/dist/server.js +24 -0
- package/telegram-plugin/gateway/always-allow-persist-queue.ts +438 -0
- package/telegram-plugin/gateway/approval-timeout-inbound-builders.ts +150 -0
- package/telegram-plugin/gateway/clean-shutdown-marker.ts +68 -20
- package/telegram-plugin/gateway/gateway.ts +1331 -151
- package/telegram-plugin/gateway/inbound-spool.ts +2 -1
- package/telegram-plugin/gateway/inject-handler.test.ts +19 -0
- package/telegram-plugin/gateway/inject-handler.ts +17 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +44 -2
- package/telegram-plugin/gateway/ipc-server.ts +40 -0
- package/telegram-plugin/gateway/model-command.ts +212 -51
- package/telegram-plugin/gateway/pending-card-expiry.ts +98 -0
- package/telegram-plugin/gateway/pending-card-store.ts +173 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +12 -2
- package/telegram-plugin/gateway/resolve-person.ts +304 -0
- package/telegram-plugin/gateway/resume-inbound-builder.ts +240 -2
- package/telegram-plugin/gateway/session-model-source.ts +73 -0
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +21 -1
- package/telegram-plugin/gateway/worker-feed-dispatch.ts +24 -1
- package/telegram-plugin/hooks/silent-end-scan.mjs +164 -40
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +30 -7
- package/telegram-plugin/model-label.ts +69 -0
- package/telegram-plugin/operator-events.ts +45 -0
- package/telegram-plugin/pending-work-progress.ts +42 -7
- package/telegram-plugin/permission-diff.ts +128 -0
- package/telegram-plugin/quota-bar-format.ts +360 -0
- package/telegram-plugin/registry/subagents-schema.ts +80 -1
- package/telegram-plugin/registry/subagents.test.ts +90 -0
- package/telegram-plugin/session-tail.ts +28 -0
- package/telegram-plugin/silent-end.ts +49 -4
- package/telegram-plugin/subagent-watcher.ts +249 -46
- package/telegram-plugin/tests/always-allow-persist-queue.test.ts +529 -0
- package/telegram-plugin/tests/approval-timeout-inbound-builders.test.ts +94 -0
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +21 -0
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +263 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +85 -27
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +4 -2
- package/telegram-plugin/tests/ipc-server-query-pending-permission.test.ts +157 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -5
- package/telegram-plugin/tests/model-command.test.ts +202 -42
- package/telegram-plugin/tests/model-label.test.ts +64 -0
- package/telegram-plugin/tests/operator-events.test.ts +17 -0
- package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +202 -0
- package/telegram-plugin/tests/pending-card-expiry.test.ts +190 -0
- package/telegram-plugin/tests/pending-card-store.test.ts +173 -0
- package/telegram-plugin/tests/pending-work-progress.test.ts +116 -3
- package/telegram-plugin/tests/permission-diff.test.ts +111 -0
- package/telegram-plugin/tests/quota-bar-format.test.ts +444 -0
- package/telegram-plugin/tests/resolve-person.test.ts +290 -0
- package/telegram-plugin/tests/resume-inbound-builder.test.ts +286 -0
- package/telegram-plugin/tests/session-model-source.test.ts +67 -0
- package/telegram-plugin/tests/session-tail.test.ts +64 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-integration.test.ts +53 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-scan.test.ts +138 -0
- package/telegram-plugin/tests/silent-end.test.ts +46 -1
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +39 -0
- package/telegram-plugin/tests/subagent-watcher-boot-promotion-replay.test.ts +107 -4
- package/telegram-plugin/tests/subagent-watcher-handback-gaps.test.ts +42 -4
- package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +47 -0
- package/telegram-plugin/tests/subagent-watcher-terminated-ids-cap.test.ts +150 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +115 -0
- package/telegram-plugin/tests/tool-activity-summary.test.ts +37 -0
- package/telegram-plugin/tests/typing-wrap.test.ts +23 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +19 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +108 -0
- package/telegram-plugin/tests/worker-feed-dispatch.test.ts +126 -0
- package/telegram-plugin/tool-activity-summary.ts +22 -2
- package/telegram-plugin/typing-wrap.ts +72 -25
- package/telegram-plugin/worker-activity-feed.ts +229 -15
- package/profiles/default/CLAUDE.md +0 -116
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
|
@@ -21495,6 +21495,7 @@ var SessionContinuitySchema = exports_external.object({
|
|
|
21495
21495
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
21496
21496
|
resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
|
|
21497
21497
|
resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale."),
|
|
21498
|
+
boot_resume: exports_external.enum(["always", "in-flight", "never"]).optional().describe("How the gateway auto-resumes a turn that was IN FLIGHT when the " + "agent restarted. 'in-flight' (default) resumes genuinely " + "interrupted work even after a deliberate/operator restart — a " + "sanctioned restart landing mid-turn no longer silently drops the " + "work. 'always' forces resume unconditionally (same as the " + "SWITCHROOM_BOOT_RESUME_ALWAYS=1 escape hatch). 'never' is the " + "quota-saving posture: don't auto-replay work across a clean " + "restart — but the user is STILL sent a passive notice of what was " + "in flight (silence is never used). Independent of the at-most-once " + "resume ledger and the bounded resume-chain loop-guard, which always " + "apply. Threaded to the gateway as SWITCHROOM_BOOT_RESUME."),
|
|
21498
21499
|
session_retention_max_count: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): keep at most this many " + "newest session transcripts under .claude/projects; older ones " + "past both this count and the age bound are pruned by the Stop " + "hook. The newest sessions (and the handoff source) are always " + "kept. Default 20; set 0 to disable the count bound."),
|
|
21499
21500
|
session_retention_max_age_days: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): prune session transcripts " + "older than this many days (a file is deleted only when it is BOTH " + "over the count bound and older than this). Default 30; set 0 to " + "disable the age bound.")
|
|
21500
21501
|
}).optional();
|
|
@@ -21959,7 +21960,8 @@ var CronConfigSchema = exports_external.object({
|
|
|
21959
21960
|
var UserSchema = exports_external.object({
|
|
21960
21961
|
name: exports_external.string().optional().describe("Display name for the user."),
|
|
21961
21962
|
telegram_ids: exports_external.array(exports_external.string()).min(1).describe("Telegram username(s) and/or numeric user id(s) identifying this user " + "(a leading @ is optional). Matched against the message sender for " + "per-speaker memory routing."),
|
|
21962
|
-
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`).")
|
|
21963
|
+
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
|
|
21964
|
+
person_id: exports_external.string().optional().describe("Free-text display name projected into the inbound `<channel>` " + 'tag\'s `user` attribute (e.g. "Lisa") so an agent can greet by ' + "name instead of only seeing the raw Telegram id/username. NOT a " + "stable identity system — just a label. Resolution is boot-time-only " + "(no hot-reload; a config change needs an agent restart) and " + "chat-scoped (a resolved name is only shown in a chat/group the " + "person is actually a member of, per that chat's access.json " + "membership — never broadcast into every chat the agent operates " + "in). Keep this broadly safe to show, the same discipline as " + "picking a `profile_bank` name: there is no automated enforcement " + "that a `person_id` stays safe if a group's membership changes " + "later — see docs/configuration.md.")
|
|
21963
21965
|
});
|
|
21964
21966
|
var SwitchroomConfigSchema = exports_external.object({
|
|
21965
21967
|
switchroom: exports_external.object({
|
|
@@ -28736,7 +28738,7 @@ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:f
|
|
|
28736
28738
|
import { dirname as dirname4, join as join6 } from "node:path";
|
|
28737
28739
|
|
|
28738
28740
|
// src/build-info.ts
|
|
28739
|
-
var VERSION = "0.18.
|
|
28741
|
+
var VERSION = "0.18.7";
|
|
28740
28742
|
|
|
28741
28743
|
// src/cli/resolve-version.ts
|
|
28742
28744
|
function readPackageVersion() {
|
|
@@ -4212,6 +4212,7 @@ var init_schema = __esm(() => {
|
|
|
4212
4212
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
4213
4213
|
resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
|
|
4214
4214
|
resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale."),
|
|
4215
|
+
boot_resume: exports_external.enum(["always", "in-flight", "never"]).optional().describe("How the gateway auto-resumes a turn that was IN FLIGHT when the " + "agent restarted. 'in-flight' (default) resumes genuinely " + "interrupted work even after a deliberate/operator restart — a " + "sanctioned restart landing mid-turn no longer silently drops the " + "work. 'always' forces resume unconditionally (same as the " + "SWITCHROOM_BOOT_RESUME_ALWAYS=1 escape hatch). 'never' is the " + "quota-saving posture: don't auto-replay work across a clean " + "restart — but the user is STILL sent a passive notice of what was " + "in flight (silence is never used). Independent of the at-most-once " + "resume ledger and the bounded resume-chain loop-guard, which always " + "apply. Threaded to the gateway as SWITCHROOM_BOOT_RESUME."),
|
|
4215
4216
|
session_retention_max_count: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): keep at most this many " + "newest session transcripts under .claude/projects; older ones " + "past both this count and the age bound are pruned by the Stop " + "hook. The newest sessions (and the handoff source) are always " + "kept. Default 20; set 0 to disable the count bound."),
|
|
4216
4217
|
session_retention_max_age_days: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): prune session transcripts " + "older than this many days (a file is deleted only when it is BOTH " + "over the count bound and older than this). Default 30; set 0 to " + "disable the age bound.")
|
|
4217
4218
|
}).optional();
|
|
@@ -4676,7 +4677,8 @@ var init_schema = __esm(() => {
|
|
|
4676
4677
|
UserSchema = exports_external.object({
|
|
4677
4678
|
name: exports_external.string().optional().describe("Display name for the user."),
|
|
4678
4679
|
telegram_ids: exports_external.array(exports_external.string()).min(1).describe("Telegram username(s) and/or numeric user id(s) identifying this user " + "(a leading @ is optional). Matched against the message sender for " + "per-speaker memory routing."),
|
|
4679
|
-
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`).")
|
|
4680
|
+
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
|
|
4681
|
+
person_id: exports_external.string().optional().describe("Free-text display name projected into the inbound `<channel>` " + 'tag\'s `user` attribute (e.g. "Lisa") so an agent can greet by ' + "name instead of only seeing the raw Telegram id/username. NOT a " + "stable identity system — just a label. Resolution is boot-time-only " + "(no hot-reload; a config change needs an agent restart) and " + "chat-scoped (a resolved name is only shown in a chat/group the " + "person is actually a member of, per that chat's access.json " + "membership — never broadcast into every chat the agent operates " + "in). Keep this broadly safe to show, the same discipline as " + "picking a `profile_bank` name: there is no automated enforcement " + "that a `person_id` stays safe if a group's membership changes " + "later — see docs/configuration.md.")
|
|
4680
4682
|
});
|
|
4681
4683
|
SwitchroomConfigSchema = exports_external.object({
|
|
4682
4684
|
switchroom: exports_external.object({
|
|
@@ -4212,6 +4212,7 @@ var init_schema = __esm(() => {
|
|
|
4212
4212
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
4213
4213
|
resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
|
|
4214
4214
|
resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale."),
|
|
4215
|
+
boot_resume: exports_external.enum(["always", "in-flight", "never"]).optional().describe("How the gateway auto-resumes a turn that was IN FLIGHT when the " + "agent restarted. 'in-flight' (default) resumes genuinely " + "interrupted work even after a deliberate/operator restart — a " + "sanctioned restart landing mid-turn no longer silently drops the " + "work. 'always' forces resume unconditionally (same as the " + "SWITCHROOM_BOOT_RESUME_ALWAYS=1 escape hatch). 'never' is the " + "quota-saving posture: don't auto-replay work across a clean " + "restart — but the user is STILL sent a passive notice of what was " + "in flight (silence is never used). Independent of the at-most-once " + "resume ledger and the bounded resume-chain loop-guard, which always " + "apply. Threaded to the gateway as SWITCHROOM_BOOT_RESUME."),
|
|
4215
4216
|
session_retention_max_count: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): keep at most this many " + "newest session transcripts under .claude/projects; older ones " + "past both this count and the age bound are pruned by the Stop " + "hook. The newest sessions (and the handoff source) are always " + "kept. Default 20; set 0 to disable the count bound."),
|
|
4216
4217
|
session_retention_max_age_days: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): prune session transcripts " + "older than this many days (a file is deleted only when it is BOTH " + "over the count bound and older than this). Default 30; set 0 to " + "disable the age bound.")
|
|
4217
4218
|
}).optional();
|
|
@@ -4676,7 +4677,8 @@ var init_schema = __esm(() => {
|
|
|
4676
4677
|
UserSchema = exports_external.object({
|
|
4677
4678
|
name: exports_external.string().optional().describe("Display name for the user."),
|
|
4678
4679
|
telegram_ids: exports_external.array(exports_external.string()).min(1).describe("Telegram username(s) and/or numeric user id(s) identifying this user " + "(a leading @ is optional). Matched against the message sender for " + "per-speaker memory routing."),
|
|
4679
|
-
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`).")
|
|
4680
|
+
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
|
|
4681
|
+
person_id: exports_external.string().optional().describe("Free-text display name projected into the inbound `<channel>` " + 'tag\'s `user` attribute (e.g. "Lisa") so an agent can greet by ' + "name instead of only seeing the raw Telegram id/username. NOT a " + "stable identity system — just a label. Resolution is boot-time-only " + "(no hot-reload; a config change needs an agent restart) and " + "chat-scoped (a resolved name is only shown in a chat/group the " + "person is actually a member of, per that chat's access.json " + "membership — never broadcast into every chat the agent operates " + "in). Keep this broadly safe to show, the same discipline as " + "picking a `profile_bank` name: there is no automated enforcement " + "that a `person_id` stays safe if a group's membership changes " + "later — see docs/configuration.md.")
|
|
4680
4682
|
});
|
|
4681
4683
|
SwitchroomConfigSchema = exports_external.object({
|
|
4682
4684
|
switchroom: exports_external.object({
|
|
@@ -19569,7 +19571,7 @@ function allocateAgentUid(name) {
|
|
|
19569
19571
|
var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
|
|
19570
19572
|
|
|
19571
19573
|
// src/vault/broker/server.ts
|
|
19572
|
-
import { dirname as
|
|
19574
|
+
import { dirname as dirname5, resolve as resolve8, basename as basename4 } from "node:path";
|
|
19573
19575
|
import * as os3 from "node:os";
|
|
19574
19576
|
import * as path3 from "node:path";
|
|
19575
19577
|
|
|
@@ -19957,10 +19959,38 @@ function rotateAuditLog(logPath, maxFiles) {
|
|
|
19957
19959
|
return;
|
|
19958
19960
|
}
|
|
19959
19961
|
}
|
|
19962
|
+
const snapshotPath = `${logPath}.1`;
|
|
19960
19963
|
try {
|
|
19961
|
-
fs.
|
|
19964
|
+
fs.copyFileSync(logPath, snapshotPath);
|
|
19962
19965
|
} catch (err) {
|
|
19963
|
-
process.stderr.write(`[vault-audit] ERROR: could not
|
|
19966
|
+
process.stderr.write(`[vault-audit] ERROR: could not snapshot active audit log ${logPath} → ${snapshotPath}: ${err.message}
|
|
19967
|
+
`);
|
|
19968
|
+
return;
|
|
19969
|
+
}
|
|
19970
|
+
try {
|
|
19971
|
+
const fd = fs.openSync(snapshotPath, "r");
|
|
19972
|
+
try {
|
|
19973
|
+
fs.fsyncSync(fd);
|
|
19974
|
+
} finally {
|
|
19975
|
+
fs.closeSync(fd);
|
|
19976
|
+
}
|
|
19977
|
+
} catch (err) {
|
|
19978
|
+
process.stderr.write(`[vault-audit] ERROR: could not fsync audit snapshot ${snapshotPath}; leaving active log intact to avoid data loss: ${err.message}
|
|
19979
|
+
`);
|
|
19980
|
+
return;
|
|
19981
|
+
}
|
|
19982
|
+
try {
|
|
19983
|
+
const dirFd = fs.openSync(path.dirname(snapshotPath), "r");
|
|
19984
|
+
try {
|
|
19985
|
+
fs.fsyncSync(dirFd);
|
|
19986
|
+
} finally {
|
|
19987
|
+
fs.closeSync(dirFd);
|
|
19988
|
+
}
|
|
19989
|
+
} catch {}
|
|
19990
|
+
try {
|
|
19991
|
+
fs.truncateSync(logPath, 0);
|
|
19992
|
+
} catch (err) {
|
|
19993
|
+
process.stderr.write(`[vault-audit] ERROR: could not truncate active audit log ${logPath}: ${err.message}
|
|
19964
19994
|
`);
|
|
19965
19995
|
}
|
|
19966
19996
|
}
|
|
@@ -22369,7 +22399,7 @@ class VaultBroker {
|
|
|
22369
22399
|
this.passphrase = this.testOpts._testPassphrase;
|
|
22370
22400
|
}
|
|
22371
22401
|
process.umask(63);
|
|
22372
|
-
const parentDir =
|
|
22402
|
+
const parentDir = dirname5(this.socketPath);
|
|
22373
22403
|
mkdirSync6(parentDir, { recursive: true, mode: 448 });
|
|
22374
22404
|
try {
|
|
22375
22405
|
chmodSync4(parentDir, 448);
|
|
@@ -23850,15 +23880,15 @@ class VaultBroker {
|
|
|
23850
23880
|
}
|
|
23851
23881
|
}
|
|
23852
23882
|
function detectVaultLayoutDrift(vaultPath) {
|
|
23853
|
-
const dir =
|
|
23883
|
+
const dir = dirname5(vaultPath);
|
|
23854
23884
|
if (basename4(dir) !== "vault")
|
|
23855
23885
|
return;
|
|
23856
23886
|
if (basename4(vaultPath) !== "vault.enc")
|
|
23857
23887
|
return;
|
|
23858
|
-
const switchroomDir =
|
|
23888
|
+
const switchroomDir = dirname5(dir);
|
|
23859
23889
|
if (basename4(switchroomDir) !== ".switchroom")
|
|
23860
23890
|
return;
|
|
23861
|
-
const home2 =
|
|
23891
|
+
const home2 = dirname5(switchroomDir);
|
|
23862
23892
|
const result = inspectVaultLayout(home2);
|
|
23863
23893
|
if (result.kind === "divergent") {
|
|
23864
23894
|
throw new VaultError(`Vault layout divergence detected at boot: ${result.details.oldPath} and ${result.details.newPath} are both regular files with different content. An older switchroom CLI may have written to the legacy path after migration ran. Run \`switchroom apply\` from the host to surface the recovery recipe (state E refusal with literal \`mv\` commands). See docs/operators/state-e-recovery.md.`);
|
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.7",
|
|
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/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/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/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/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",
|
|
@@ -87,36 +87,75 @@ CRON_APPEND_PROMPT="You are the cheap background cron worker for {{name}}. You r
|
|
|
87
87
|
# claude's project key matches the pre-seeded trust state (.claude-cron).
|
|
88
88
|
cd "{{agentDir}}" || exit 1
|
|
89
89
|
|
|
90
|
-
# LiteLLM routing for the cron session —
|
|
90
|
+
# LiteLLM routing for the cron session — ports start.sh's boot contract.
|
|
91
91
|
# IMPORTANT: the vault key is provisioned under the BASE agent name ({{name}}),
|
|
92
92
|
# NOT the cron identity ({{name}}-cron). Use the Handlebars template var so the
|
|
93
|
-
# lookup is a compile-time literal — no fragile runtime suffix-stripping.
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
93
|
+
# lookup is a compile-time literal — no fragile runtime suffix-stripping. The
|
|
94
|
+
# attribution headers use the SAME compile-time-literal form ({{name}}-cron,
|
|
95
|
+
# identical to $SWITCHROOM_AGENT_NAME here — line 69 exports it) so this block
|
|
96
|
+
# refers to the agent one consistent way instead of mixing a template literal
|
|
97
|
+
# for the key with a runtime env var for the tags. Cron spend is attributed to
|
|
98
|
+
# {{name}}-cron so LiteLLM distinguishes it from main-session spend per agent.
|
|
99
|
+
#
|
|
100
|
+
# Two boot failure modes, handled DIFFERENTLY (ported 2026-07 from start.sh so
|
|
101
|
+
# cron no longer fails OPEN on a transient blip and silently runs UNTRACKED
|
|
102
|
+
# direct OAuth for the fire — the product invariant is all model traffic stays
|
|
103
|
+
# proxy-routed for cost/token tracking):
|
|
104
|
+
# - MISSING KEY → fail-open: strip routing, fall back to direct OAuth, LOUD
|
|
105
|
+
# log. Without a key there is no metered route to reach — correct here.
|
|
106
|
+
# - PROXY UNREACHABLE with key in hand → do NOT strip. Keep routing pointed at
|
|
107
|
+
# the proxy AND export the auth header so the session authenticates the
|
|
108
|
+
# moment the proxy recovers (the socat forwarder reconnects per-connection),
|
|
109
|
+
# LOUD log. Never a silent untracked direct-OAuth fallback.
|
|
110
|
+
# Retry budget is DELIBERATELY short — 3 attempts, worst-case ~25s (3×5s curl
|
|
111
|
+
# timeouts + 2×5s sleeps) — vs start.sh's 120s: a cron fire is
|
|
112
|
+
# latency-sensitive and short-lived, and unreachable now KEEPS routing anyway,
|
|
113
|
+
# so a long boot-probe would only add dead latency for no gain.
|
|
98
114
|
if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; then
|
|
99
115
|
sr_ll_key="$(switchroom vault get "litellm/{{name}}/api-key" 2>/dev/null || true)"
|
|
100
116
|
sr_ll_ok=""
|
|
117
|
+
sr_ll_unreachable=""
|
|
101
118
|
if [ -z "$sr_ll_key" ]; then
|
|
102
|
-
echo "litellm: no virtual key for
|
|
103
|
-
elif command -v curl >/dev/null 2>&1 && [ -n "$ANTHROPIC_BASE_URL" ]
|
|
104
|
-
|
|
105
|
-
|
|
119
|
+
echo "litellm(cron): no virtual key for '{{name}}' — falling back to direct OAuth (untracked, unguarded)" >&2
|
|
120
|
+
elif command -v curl >/dev/null 2>&1 && [ -n "$ANTHROPIC_BASE_URL" ]; then
|
|
121
|
+
# Bounded retry probe — short budget for cron latency (3 attempts,
|
|
122
|
+
# worst-case ~25s: 3×5s curl timeouts + 2×5s sleeps).
|
|
123
|
+
sr_ll_url="${SWITCHROOM_LITELLM_BASE:-${ANTHROPIC_BASE_URL%/anthropic}}/health/liveliness"
|
|
124
|
+
sr_ll_try=0
|
|
125
|
+
sr_ll_up=""
|
|
126
|
+
while :; do
|
|
127
|
+
if curl -fsS -m 5 -o /dev/null "$sr_ll_url" 2>/dev/null; then sr_ll_up="1"; break; fi
|
|
128
|
+
sr_ll_try=$(( sr_ll_try + 1 ))
|
|
129
|
+
[ "$sr_ll_try" -ge 3 ] && break
|
|
130
|
+
sleep 5
|
|
131
|
+
done
|
|
132
|
+
if [ -z "$sr_ll_up" ]; then
|
|
133
|
+
# Proxy unreachable but the key is present. Do NOT strip: keep routing so
|
|
134
|
+
# the cron session self-heals once the proxy is back.
|
|
135
|
+
sr_ll_unreachable="1"
|
|
136
|
+
echo "litellm(cron): proxy unreachable at ${SWITCHROOM_LITELLM_BASE:-$ANTHROPIC_BASE_URL} after 3 retries — routing LEFT IN PLACE so it self-heals; NOT falling back to untracked direct Anthropic OAuth." >&2
|
|
137
|
+
else
|
|
138
|
+
sr_ll_ok="1"
|
|
139
|
+
fi
|
|
140
|
+
unset sr_ll_url sr_ll_try sr_ll_up
|
|
106
141
|
else
|
|
107
142
|
sr_ll_ok="1"
|
|
108
143
|
fi
|
|
109
|
-
if [ -n "$sr_ll_ok" ]; then
|
|
144
|
+
if [ -n "$sr_ll_ok" ] || [ -n "$sr_ll_unreachable" ]; then
|
|
145
|
+
# Key fetched successfully — export the auth header REGARDLESS of the probe
|
|
146
|
+
# outcome so the virtual key reaches the process env. On unreachable this is
|
|
147
|
+
# what lets the session authenticate the moment the proxy recovers instead
|
|
148
|
+
# of 401-ing (the same self-heal fix as start.sh's inner/outer blocks).
|
|
110
149
|
export ANTHROPIC_CUSTOM_HEADERS="x-litellm-api-key: Bearer $sr_ll_key
|
|
111
|
-
x-litellm-customer-id:
|
|
112
|
-
x-litellm-tags: agent
|
|
150
|
+
x-litellm-customer-id: {{name}}-cron
|
|
151
|
+
x-litellm-tags: agent:{{name}}-cron,profile:${SWITCHROOM_AGENT_PROFILE:-default}"
|
|
113
152
|
export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
|
|
114
153
|
else
|
|
115
|
-
#
|
|
116
|
-
# directly on its OAuth credential (subscription path), unproxied.
|
|
154
|
+
# Missing-key fail-open: drop every routing var so the claude CLI talks to
|
|
155
|
+
# Anthropic directly on its OAuth credential (subscription path), unproxied.
|
|
117
156
|
unset ANTHROPIC_BASE_URL ANTHROPIC_SMALL_FAST_MODEL SWITCHROOM_LITELLM SWITCHROOM_LITELLM_BASE
|
|
118
157
|
fi
|
|
119
|
-
unset sr_ll_key sr_ll_ok
|
|
158
|
+
unset sr_ll_key sr_ll_ok sr_ll_unreachable
|
|
120
159
|
fi
|
|
121
160
|
|
|
122
161
|
# Create the cron tmux session DETACHED (-d), not in attach mode. This is a
|
|
@@ -46,6 +46,17 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
|
|
|
46
46
|
# compose env and the inner-pass export. Pinned by the scaffold-order test.
|
|
47
47
|
export SWITCHROOM_AGENT_NAME="{{name}}"
|
|
48
48
|
|
|
49
|
+
# Boot-resume policy (session_continuity.boot_resume; default 'in-flight').
|
|
50
|
+
# The gateway's boot-resume block reads SWITCHROOM_BOOT_RESUME to decide
|
|
51
|
+
# whether a genuinely in-flight turn is auto-resumed across a deliberate
|
|
52
|
+
# restart ('in-flight' — default), always ('always'), or downgraded to a
|
|
53
|
+
# passive notice ('never'). MUST be exported here, before the gateway fork
|
|
54
|
+
# below, or the daemon never sees it and every agent defaults silently — same
|
|
55
|
+
# start.sh env-fork landmine as the channels.telegram knobs. The later
|
|
56
|
+
# SWITCHROOM_RESUME_MODE block is set AFTER the fork (it drives the inner
|
|
57
|
+
# `claude` --continue path, not the gateway), so this needs its own hoist.
|
|
58
|
+
export SWITCHROOM_BOOT_RESUME="{{#if bootResumeMode}}{{{bootResumeMode}}}{{else}}in-flight{{/if}}"
|
|
59
|
+
|
|
49
60
|
# Gateway-consumed env MUST be exported HERE, before the gateway fork
|
|
50
61
|
# below. The gateway daemon reads channels.telegram.* knobs (and any
|
|
51
62
|
# agent env) from process.env at startup — e.g. SWITCHROOM_TG_STREAM_
|
|
@@ -120,17 +131,19 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
|
|
|
120
131
|
else
|
|
121
132
|
sr_ll_ok="1"
|
|
122
133
|
fi
|
|
123
|
-
if [ -n "$sr_ll_ok" ]; then
|
|
134
|
+
if [ -n "$sr_ll_ok" ] || [ -n "$sr_ll_unreachable" ]; then
|
|
135
|
+
# Key fetched successfully — export the proxy auth header REGARDLESS of the
|
|
136
|
+
# reachability probe outcome. The header is what carries the virtual key
|
|
137
|
+
# into the process env; without it the gateway's sr-* discovery
|
|
138
|
+
# (gateway.ts discoverSrModels) is dead and, once the proxy recovers,
|
|
139
|
+
# every request 401s until a manual restart. Since routing is LEFT IN
|
|
140
|
+
# PLACE on unreachable (below/above), the header must be too so the
|
|
141
|
+
# session actually self-heals when litellm returns. The probe result gates
|
|
142
|
+
# only log wording + inner-pass _LITELLM_OK, never the header.
|
|
124
143
|
export ANTHROPIC_CUSTOM_HEADERS="x-litellm-api-key: Bearer $sr_ll_key
|
|
125
144
|
x-litellm-customer-id: $SWITCHROOM_AGENT_NAME
|
|
126
145
|
x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:-default}"
|
|
127
146
|
export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
|
|
128
|
-
elif [ -n "$sr_ll_unreachable" ]; then
|
|
129
|
-
# Proxy unreachable at boot: routing intentionally LEFT IN PLACE (loud
|
|
130
|
-
# warning already emitted) so it survives into the inner pass and
|
|
131
|
-
# self-heals once litellm returns. NO unset — the permanent strip here
|
|
132
|
-
# was what made the inner self-heal fix inert on docker.
|
|
133
|
-
:
|
|
134
147
|
else
|
|
135
148
|
# Missing-key fail-open: gateway talks direct OAuth, unproxied.
|
|
136
149
|
unset ANTHROPIC_BASE_URL ANTHROPIC_SMALL_FAST_MODEL SWITCHROOM_LITELLM SWITCHROOM_LITELLM_BASE
|
|
@@ -1016,8 +1029,15 @@ if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; th
|
|
|
1016
1029
|
else
|
|
1017
1030
|
sr_ll_ok="1"
|
|
1018
1031
|
fi
|
|
1019
|
-
if [ -n "$sr_ll_ok" ]; then
|
|
1020
|
-
|
|
1032
|
+
if [ -n "$sr_ll_ok" ] || [ -n "$sr_ll_unreachable" ]; then
|
|
1033
|
+
# Key fetched successfully — export the proxy auth header REGARDLESS of the
|
|
1034
|
+
# reachability probe outcome. Routing is LEFT IN PLACE on unreachable so the
|
|
1035
|
+
# session self-heals when litellm returns; the header MUST survive with it,
|
|
1036
|
+
# otherwise every request 401s once the proxy recovers (the "self-heals"
|
|
1037
|
+
# promise below was inert without this) and gateway sr-* discovery stays
|
|
1038
|
+
# dead. The probe outcome gates ONLY the _LITELLM_OK flag (sr-* override
|
|
1039
|
+
# drop) and the log wording above — never the header.
|
|
1040
|
+
#
|
|
1021
1041
|
# Newline-separated Name: Value pairs (claude CLI ANTHROPIC_CUSTOM_HEADERS format).
|
|
1022
1042
|
# Tags: agent:<name> for per-agent spend tracking; profile:<profile> for
|
|
1023
1043
|
# fleet-level cost breakdown by role. Per-turn tags (cron vs telegram) are
|
|
@@ -1031,13 +1051,12 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
|
|
|
1031
1051
|
# models configured in the proxy appear in the /model picker and can be
|
|
1032
1052
|
# selected via --model. Without this, the CLI only knows its bundled list.
|
|
1033
1053
|
export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
:
|
|
1054
|
+
if [ -n "$sr_ll_ok" ]; then
|
|
1055
|
+
# Probe succeeded: proxy is live, so honor any sr-* session model override
|
|
1056
|
+
# below. On unreachable, _LITELLM_OK stays empty so the override is dropped
|
|
1057
|
+
# (claude would 4xx an unknown model against a down endpoint).
|
|
1058
|
+
_LITELLM_OK="1"
|
|
1059
|
+
fi
|
|
1041
1060
|
else
|
|
1042
1061
|
# Missing key fail-open: drop every routing var so the claude CLI talks to
|
|
1043
1062
|
# Anthropic directly on its OAuth credential (subscription path), unproxied.
|
|
@@ -105,7 +105,7 @@ By default, every restart starts a **fresh `claude` session** — the in-flight
|
|
|
105
105
|
- **`resume_interrupted`** (operator restart / SIGTERM / crash): pick the work back up and carry it to completion. Briefly tell the user you're resuming and roughly how long ago it was interrupted — then just do it. Do NOT ask whether to resume.
|
|
106
106
|
- **`resume_watchdog_timeout`** (hang-watchdog killed it after no progress): do NOT silently resume — it may hang the same way. Tell the user plainly that your last turn was killed after N minutes of no progress, roughly what it was doing, and ask whether to retry or take a different angle. Report only the honest cause; don't invent a deeper root cause.
|
|
107
107
|
The one-shot `SWITCHROOM_PENDING_*` env vars are passive forensic context for the wake-audit / "why did you restart" protocols — not the resume trigger.
|
|
108
|
-
- **`.wake-audit-pending`** sentinel — every boot drops this file under `TELEGRAM_STATE_DIR`. On your first turn, run the three-signal check (owed reply / orphan sub-agents / open todos) per the wake-audit protocol in
|
|
108
|
+
- **`.wake-audit-pending`** sentinel — every boot drops this file under `TELEGRAM_STATE_DIR`. On your first turn, run the three-signal check (owed reply / orphan sub-agents / open todos) per the wake-audit protocol in the `switchroom-runtime` skill (`skills/switchroom-runtime/SKILL.md`), then `rm -f` the sentinel.
|
|
109
109
|
|
|
110
110
|
A config-summary greeting card is sent automatically by the SessionStart hook — you don't need to announce yourself. If your context feels thin (after compaction or any fresh session), proactively recall from Hindsight before proceeding.
|
|
111
111
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: switchroom-release
|
|
3
|
+
description: "Cut and ship a switchroom release end-to-end: CHANGELOG consolidation, tag, npm publish, image build, and the fleet rollout gate. Use when the user says 'cut a release', 'ship a release', 'release vX', 'publish a new version', 'roll out the latest', or otherwise wants the merged work on main to go live for the fleet. This is the ONLY skill that authorizes a fleet rollout, and it enforces the npm-publish + image-build gates that were historically skipped (v0.18.4/v0.18.5 shipped to the fleet but never hit npm). Do NOT use for adding agents (switchroom-manage), diagnostics (switchroom-health), or a plain `switchroom update` on one agent (switchroom-cli)."
|
|
4
|
+
allowed-tools: Bash(git *) Bash(gh *) Bash(npm view *) Bash(npm pack *) Bash(docker manifest inspect *) Bash(docker buildx imagetools inspect *)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Switchroom release
|
|
8
|
+
|
|
9
|
+
Cut a release of `switchroom/switchroom` and get it live on the fleet. This is a **gated, ordered checklist** — do not skip steps, do not reorder. The whole point of this skill is that two steps that used to be skipped silently (npm publish, image-build verification) are now **hard gates before rollout**.
|
|
10
|
+
|
|
11
|
+
## What a release actually is
|
|
12
|
+
|
|
13
|
+
- A release = a `vX.Y.Z` **git tag** on `main` (the merge commit of the CHANGELOG PR).
|
|
14
|
+
- The tag is the version source of truth (`scripts/build.mjs:resolveVersion()`). `package.json` `version` is a **stale placeholder by design** — never bump it in a commit (the `//version` comment + #2733 discipline). The uncommitted pack-time bump happens in CI now, not by hand.
|
|
15
|
+
- Cutting the tag fires TWO workflows in parallel on tag push: `docker-images` (builds + pushes the 6 ghcr images) and `npm-publish` (builds, packs, verifies, publishes to npm). **Rollout is gated on BOTH going green.**
|
|
16
|
+
|
|
17
|
+
## Before you start — pre-flight (verify, don't assume)
|
|
18
|
+
|
|
19
|
+
1. `git fetch origin`, confirm `main` is at the commit you want released.
|
|
20
|
+
2. `gh pr list --state open` — confirm no PR meant for this release is still open. Ask the operator if unsure.
|
|
21
|
+
3. Confirm CI on `main` is green (`gh run list --branch main --limit 3`).
|
|
22
|
+
4. Read `CHANGELOG.md` — the `## Unreleased` section is the staging area for this release's notes. If it's empty, there's nothing to release.
|
|
23
|
+
5. Pick the next version: read the latest tag (`git tag --list 'v*' --sort=-v:refname | head -1`) and bump the patch (or minor if the operator asks). Confirm with the operator which.
|
|
24
|
+
|
|
25
|
+
## Step 1 — Consolidate the changelog (the release commit is CHANGELOG-only)
|
|
26
|
+
|
|
27
|
+
- Move the `## Unreleased` entries under a new `## vX.Y.Z — <one-line summary>` heading.
|
|
28
|
+
- The release commit touches **CHANGELOG.md only**. Do NOT bump `package.json` (placeholder discipline).
|
|
29
|
+
- Branch protection blocks direct push to `main`, so: create a `release/vX.Y.Z` branch, push it, open a `chore: release vX.Y.Z` PR (base `main`), arm auto-merge (squash, delete-branch) on green CI.
|
|
30
|
+
|
|
31
|
+
## Step 2 — Cut the tag (on the merge commit, not the PR branch)
|
|
32
|
+
|
|
33
|
+
Once the changelog PR is merged:
|
|
34
|
+
- `git fetch origin && git checkout main && git pull --ff-only`
|
|
35
|
+
- Tag the merge commit and create the GitHub Release:
|
|
36
|
+
- `gh release create vX.Y.Z -R switchroom/switchroom --target main --title 'vX.Y.Z — <summary>' --notes-file <notes-file>`
|
|
37
|
+
- **Notes extraction gotcha (historical):** the naive `awk '/^## vX/,/^## v/' CHANGELOG` range collapses to a single line. Use a start-flag awk: `awk 'f{print} /^## vX\.Y\.Z/{print; f=1} f && /^## v/ && !/^## vX\.Y\.Z/{exit}'` — or extract the section to a temp file by line range.
|
|
38
|
+
- **`gh release create` has been silently dropped in past runs.** After running it, verify: `gh release view vX.Y.Z` must return the release. If it didn't create, re-run.
|
|
39
|
+
|
|
40
|
+
## Step 3 — Wait for BOTH tag-push workflows (hard gates)
|
|
41
|
+
|
|
42
|
+
The tag push triggers `docker-images` AND `npm-publish` in parallel. **Do not proceed to rollout until both are green AND verified.**
|
|
43
|
+
|
|
44
|
+
### Gate A — npm publish (`npm-publish.yml`)
|
|
45
|
+
- `gh run list --workflow=npm-publish.yml --limit 1` — wait for it to reach `completed` / `success`.
|
|
46
|
+
- Verify the publish is live: `npm view switchroom version` must return `X.Y.Z` (not the old version). Retry a few times — npm registry propagation can lag a few seconds.
|
|
47
|
+
- If this workflow fails: **the release is not published.** Do NOT roll. Diagnose (NPM_TOKEN unset? npm 5xx? empty dist?). Re-run via `gh workflow run npm-publish.yml --ref vX.Y.Z` after fixing.
|
|
48
|
+
|
|
49
|
+
### Gate B — docker images (`docker-images.yml`)
|
|
50
|
+
- `gh run list --workflow=docker-images.yml --limit 1` — wait for `completed` / `success`.
|
|
51
|
+
- Verify all 6 images are published: `docker manifest inspect ghcr.io/switchroom/<image>:vX.Y.Z` for `agent`, `auth-broker`, `kernel`, `broker`, `web`, `hostd`. Each must resolve.
|
|
52
|
+
- If any image is missing: do NOT roll — the rollout canary version-assert fails on an unpublished tag. Wait + re-check.
|
|
53
|
+
|
|
54
|
+
**Only when Gate A AND Gate B are green + verified** do you proceed.
|
|
55
|
+
|
|
56
|
+
## Step 4 — Fleet rollout (operator-gated, canary-first)
|
|
57
|
+
|
|
58
|
+
- Fire the rollout via the hostd rollout path (`mcp__hostd__rollout`), which pops an **approval card**. Do NOT roll without the operator tapping approve.
|
|
59
|
+
- Canary discipline: roll the release-critical canary agent first (the test-harness agent, per CLAUDE.md > Release canary discipline), monitor its logs + a smoke check, then stagger the rest per-agent with a `--version` assertion each (guards the `:latest` pull-race).
|
|
60
|
+
- For a release that changes agent runtime behavior (a CLI pin bump, a behavioral template change), offer the operator a canary-first path (one agent → monitor → sweep) vs all-at-once; let them choose.
|
|
61
|
+
|
|
62
|
+
## What you must NOT do
|
|
63
|
+
|
|
64
|
+
- **Never bump `package.json` `version` in a commit.** It's a stale placeholder; the tag is the source of truth and `npm-publish.yml` does the uncommitted pack-time bump.
|
|
65
|
+
- **Never run `npm publish` by hand from the agent container.** You can't reach the operator's npm auth, and the workflow is the reliable path. If the workflow is broken, fix the workflow — don't side-step it.
|
|
66
|
+
- **Never roll the fleet before Gate A (npm) AND Gate B (images) are both green + verified.** A release that's on the fleet but not on npm is the exact regression this skill exists to prevent.
|
|
67
|
+
- **Never push directly to `main`.** The CHANGELOG PR goes through auto-merge on green.
|
|
68
|
+
- **Never force-push `main` or bypass hooks (`--no-verify`).**
|
|
69
|
+
|
|
70
|
+
## If something goes wrong
|
|
71
|
+
|
|
72
|
+
- **npm-publish failed but the tag is already pushed:** fix + `gh workflow run npm-publish.yml --ref vX.Y.Z`. Do NOT roll until it's green + `npm view` confirms.
|
|
73
|
+
- **Images failed but npm succeeded:** the npm package is live but the fleet can't roll yet. Fix the image workflow / re-run. (npm being ahead of images is fine — the CLI is published for npm consumers; the fleet waits on images.)
|
|
74
|
+
- **Rollout started before publish verified (the old bug):** abort the rollout, publish, then re-roll. Do not let a half-published release sit on the fleet.
|
|
75
|
+
|
|
76
|
+
## Operator one-time setup (tell them once, not every release)
|
|
77
|
+
|
|
78
|
+
The `npm-publish.yml` workflow needs an `NPM_TOKEN` repo secret (automation-scoped npm access token with publish rights on `switchroom`). Set it once in repo settings → Secrets and variables → Actions → `NPM_TOKEN`. Without it, Gate A fails loudly on the first release — that's the design (loud > silent).
|
|
@@ -273,6 +273,16 @@ export interface SnapshotRenderOpts {
|
|
|
273
273
|
* Takes precedence over `liveProbedAtMs`.
|
|
274
274
|
*/
|
|
275
275
|
staleCachedAtMs?: number;
|
|
276
|
+
/**
|
|
277
|
+
* True when the live probe returned no usable data for ANY account (probe
|
|
278
|
+
* threw / timed out / returned zero rows) AND nothing was served from
|
|
279
|
+
* cache either. The footer renders `⚠ probe failed — no live data`
|
|
280
|
+
* instead of a false bare `_Live_`. Subscription-honesty: a "Live" footer
|
|
281
|
+
* next to "no data" rows is the exact lie this closes. `staleCachedAtMs`
|
|
282
|
+
* (cache-served data) takes precedence over this; this takes precedence
|
|
283
|
+
* over the bare-`Live` fallback.
|
|
284
|
+
*/
|
|
285
|
+
probeFailed?: boolean;
|
|
276
286
|
/**
|
|
277
287
|
* Demo mode (the `/usage demo` / `/auth demo` suffix). When true, every
|
|
278
288
|
* account label is run through `maskEmail` before rendering so a screen
|
|
@@ -521,11 +531,15 @@ export function renderAuthSnapshotFormat2(
|
|
|
521
531
|
lines.push(`_${recommendation(snapshots, now, opts.demo ?? false)}_`);
|
|
522
532
|
// #2495 Change 2 — a failed probe-on-open renders an explicit "cached Nm
|
|
523
533
|
// ago" warning, never a false live stamp. The degraded variant takes
|
|
524
|
-
// precedence over the live stamp.
|
|
534
|
+
// precedence over the live stamp. A TOTAL probe failure (no rows, no
|
|
535
|
+
// cache) renders an explicit "probe failed" marker — without it the
|
|
536
|
+
// bare-else rendered `_Live_` next to no-data rows (the honesty gap).
|
|
525
537
|
if (opts.staleCachedAtMs != null) {
|
|
526
538
|
lines.push(`_⚠ cached ${formatAgeStamp(opts.staleCachedAtMs, now)}_`);
|
|
527
539
|
} else if (opts.liveProbedAtMs != null) {
|
|
528
540
|
lines.push(`_Live · refreshed ${formatAgeStamp(opts.liveProbedAtMs, now)}_`);
|
|
541
|
+
} else if (opts.probeFailed) {
|
|
542
|
+
lines.push('_⚠ probe failed — no live data_');
|
|
529
543
|
} else {
|
|
530
544
|
lines.push('_Live_');
|
|
531
545
|
}
|
|
@@ -23132,6 +23132,20 @@ function createToolLabelSidecar(opts) {
|
|
|
23132
23132
|
};
|
|
23133
23133
|
}
|
|
23134
23134
|
|
|
23135
|
+
// model-label.ts
|
|
23136
|
+
function isModelSentinel(model) {
|
|
23137
|
+
if (typeof model !== "string")
|
|
23138
|
+
return true;
|
|
23139
|
+
const m = model.trim();
|
|
23140
|
+
if (m.length === 0)
|
|
23141
|
+
return true;
|
|
23142
|
+
if (m.startsWith("<"))
|
|
23143
|
+
return true;
|
|
23144
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(m))
|
|
23145
|
+
return true;
|
|
23146
|
+
return false;
|
|
23147
|
+
}
|
|
23148
|
+
|
|
23135
23149
|
// session-tail.ts
|
|
23136
23150
|
function isMultiAgentEnabled(env = process.env) {
|
|
23137
23151
|
return env.PROGRESS_CARD_MULTI_AGENT !== "0";
|
|
@@ -23263,6 +23277,10 @@ function projectTranscriptLine(line) {
|
|
|
23263
23277
|
if (!Array.isArray(content))
|
|
23264
23278
|
return [];
|
|
23265
23279
|
const events = [];
|
|
23280
|
+
const mainModel = message?.model;
|
|
23281
|
+
if (typeof mainModel === "string" && !isModelSentinel(mainModel)) {
|
|
23282
|
+
events.push({ kind: "model", model: mainModel });
|
|
23283
|
+
}
|
|
23266
23284
|
const textEvents = projectAssistantTextBlocks(content, (text, blockIndex, lastInMessage) => ({ kind: "text", text, blockIndex, lastInMessage }));
|
|
23267
23285
|
content.forEach((c, i) => {
|
|
23268
23286
|
const ct = c.type;
|
|
@@ -23365,6 +23383,10 @@ function projectSubagentLine(line, agentId, state) {
|
|
|
23365
23383
|
if (!Array.isArray(content))
|
|
23366
23384
|
return [];
|
|
23367
23385
|
const events = [];
|
|
23386
|
+
const subModel = message?.model;
|
|
23387
|
+
if (typeof subModel === "string" && !isModelSentinel(subModel)) {
|
|
23388
|
+
events.push({ kind: "sub_agent_model", agentId, model: subModel });
|
|
23389
|
+
}
|
|
23368
23390
|
const textEvents = projectAssistantTextBlocks(content, (text, blockIndex, lastInMessage) => ({
|
|
23369
23391
|
kind: "sub_agent_text",
|
|
23370
23392
|
agentId,
|