switchroom 0.14.21 → 0.14.23
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 +0 -1
- package/dist/auth-broker/index.js +0 -1
- package/dist/cli/notion-write-pretool.mjs +0 -1
- package/dist/cli/switchroom.js +14 -6
- package/dist/host-control/main.js +0 -1
- package/dist/vault/approvals/kernel-server.js +0 -1
- package/dist/vault/broker/server.js +0 -1
- package/package.json +3 -3
- package/profiles/_base/start.sh.hbs +11 -24
- package/profiles/_shared/telegram-style.md.hbs +2 -2
- package/profiles/default/CLAUDE.md.hbs +4 -1
- package/skills/switchroom-runtime/SKILL.md +6 -16
- package/telegram-plugin/agent-dir.ts +15 -0
- package/telegram-plugin/dist/gateway/gateway.js +788 -513
- package/telegram-plugin/gateway/gateway.ts +216 -61
- package/telegram-plugin/gateway/inbound-spool.ts +15 -0
- package/telegram-plugin/gateway/resume-inbound-builder.ts +180 -0
- package/telegram-plugin/registry/turns-schema.ts +138 -33
- package/telegram-plugin/stream-reply-handler.ts +1 -11
- package/telegram-plugin/subagent-watcher.ts +79 -5
- package/telegram-plugin/tests/agent-dir.test.ts +25 -0
- package/telegram-plugin/tests/e2e.test.ts +2 -77
- package/telegram-plugin/tests/inbound-spool.test.ts +45 -0
- package/telegram-plugin/tests/multi-turn-continuity.test.ts +0 -1
- package/telegram-plugin/tests/outbound-ordering.test.ts +0 -1
- package/telegram-plugin/tests/parse-mode-rotation.test.ts +0 -1
- package/telegram-plugin/tests/races.test.ts +0 -26
- package/telegram-plugin/tests/registry-turns.test.ts +106 -29
- package/telegram-plugin/tests/resume-inbound-builder.test.ts +182 -0
- package/telegram-plugin/tests/status-accent.test.ts +0 -1
- package/telegram-plugin/tests/stream-reply-error-paths.test.ts +0 -1
- package/telegram-plugin/tests/stream-reply-handler.test.ts +0 -24
- package/telegram-plugin/tests/streaming-e2e.test.ts +0 -1
- package/telegram-plugin/tests/streaming-orchestration.test.ts +0 -1
- package/telegram-plugin/tests/subagent-registry-bugs.test.ts +7 -3
- package/telegram-plugin/tests/subagent-watcher-handback-gaps.test.ts +293 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +23 -15
- package/telegram-plugin/tests/tool-activity-summary.test.ts +44 -0
- package/telegram-plugin/tests/turns-writer.test.ts +16 -6
- package/telegram-plugin/tool-activity-summary.ts +55 -0
- package/telegram-plugin/uat/driver.ts +3 -1
- package/telegram-plugin/handoff-continuity.ts +0 -206
- package/telegram-plugin/tests/handoff-continuity.test.ts +0 -262
|
@@ -11038,7 +11038,6 @@ var SessionSchema = exports_external.object({
|
|
|
11038
11038
|
}).optional();
|
|
11039
11039
|
var SessionContinuitySchema = exports_external.object({
|
|
11040
11040
|
enabled: exports_external.boolean().optional().describe("Master switch for the session-handoff briefing (default true)."),
|
|
11041
|
-
show_handoff_line: exports_external.boolean().optional().describe("Whether the telegram plugin prepends a visible '↩️ Picked up…' " + "line to the first assistant reply after a restart (default true)."),
|
|
11042
11041
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
11043
11042
|
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."),
|
|
11044
11043
|
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.")
|
|
@@ -11038,7 +11038,6 @@ var SessionSchema = exports_external.object({
|
|
|
11038
11038
|
}).optional();
|
|
11039
11039
|
var SessionContinuitySchema = exports_external.object({
|
|
11040
11040
|
enabled: exports_external.boolean().optional().describe("Master switch for the session-handoff briefing (default true)."),
|
|
11041
|
-
show_handoff_line: exports_external.boolean().optional().describe("Whether the telegram plugin prepends a visible '↩️ Picked up…' " + "line to the first assistant reply after a restart (default true)."),
|
|
11042
11041
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
11043
11042
|
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."),
|
|
11044
11043
|
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.")
|
|
@@ -11785,7 +11785,6 @@ var SessionSchema = exports_external.object({
|
|
|
11785
11785
|
}).optional();
|
|
11786
11786
|
var SessionContinuitySchema = exports_external.object({
|
|
11787
11787
|
enabled: exports_external.boolean().optional().describe("Master switch for the session-handoff briefing (default true)."),
|
|
11788
|
-
show_handoff_line: exports_external.boolean().optional().describe("Whether the telegram plugin prepends a visible '\u21a9\ufe0f Picked up\u2026' " + "line to the first assistant reply after a restart (default true)."),
|
|
11789
11788
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
11790
11789
|
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."),
|
|
11791
11790
|
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.")
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -13602,7 +13602,6 @@ var init_schema = __esm(() => {
|
|
|
13602
13602
|
}).optional();
|
|
13603
13603
|
SessionContinuitySchema = exports_external.object({
|
|
13604
13604
|
enabled: exports_external.boolean().optional().describe("Master switch for the session-handoff briefing (default true)."),
|
|
13605
|
-
show_handoff_line: exports_external.boolean().optional().describe("Whether the telegram plugin prepends a visible '\u21a9\ufe0f Picked up\u2026' " + "line to the first assistant reply after a restart (default true)."),
|
|
13606
13605
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
13607
13606
|
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."),
|
|
13608
13607
|
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.")
|
|
@@ -49421,8 +49420,8 @@ var {
|
|
|
49421
49420
|
} = import__.default;
|
|
49422
49421
|
|
|
49423
49422
|
// src/build-info.ts
|
|
49424
|
-
var VERSION = "0.14.
|
|
49425
|
-
var COMMIT_SHA = "
|
|
49423
|
+
var VERSION = "0.14.23";
|
|
49424
|
+
var COMMIT_SHA = "8ac2987a";
|
|
49426
49425
|
|
|
49427
49426
|
// src/cli/agent.ts
|
|
49428
49427
|
init_source();
|
|
@@ -51162,7 +51161,6 @@ function buildWorkspaceContext(args) {
|
|
|
51162
51161
|
sessionMaxIdleSecs: parseDurationToSeconds(agentConfig.session?.max_idle),
|
|
51163
51162
|
sessionMaxTurns: agentConfig.session?.max_turns,
|
|
51164
51163
|
handoffEnabled: agentConfig.session_continuity?.enabled !== false,
|
|
51165
|
-
handoffShowLine: agentConfig.session_continuity?.show_handoff_line !== false,
|
|
51166
51164
|
resumeMode: agentConfig.session_continuity?.resume_mode ?? "handoff",
|
|
51167
51165
|
resumeMaxBytes: agentConfig.session_continuity?.resume_max_bytes ?? 2000000,
|
|
51168
51166
|
resumeModeHasContinuePath: (() => {
|
|
@@ -52202,7 +52200,6 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
52202
52200
|
sessionMaxIdleSecs: parseDurationToSeconds(agentConfig.session?.max_idle),
|
|
52203
52201
|
sessionMaxTurns: agentConfig.session?.max_turns,
|
|
52204
52202
|
handoffEnabled: agentConfig.session_continuity?.enabled !== false,
|
|
52205
|
-
handoffShowLine: agentConfig.session_continuity?.show_handoff_line !== false,
|
|
52206
52203
|
resumeMode: agentConfig.session_continuity?.resume_mode ?? "handoff",
|
|
52207
52204
|
resumeMaxBytes: agentConfig.session_continuity?.resume_max_bytes ?? 2000000
|
|
52208
52205
|
};
|
|
@@ -70835,6 +70832,7 @@ var SCHEMA_SQL = `
|
|
|
70835
70832
|
user_prompt_preview TEXT,
|
|
70836
70833
|
assistant_reply_preview TEXT,
|
|
70837
70834
|
tool_call_count INTEGER,
|
|
70835
|
+
interrupt_reason TEXT,
|
|
70838
70836
|
created_at INTEGER NOT NULL,
|
|
70839
70837
|
updated_at INTEGER NOT NULL
|
|
70840
70838
|
);
|
|
@@ -70845,11 +70843,14 @@ var PHASE1_MIGRATIONS = [
|
|
|
70845
70843
|
`ALTER TABLE turns ADD COLUMN assistant_reply_preview TEXT`,
|
|
70846
70844
|
`ALTER TABLE turns ADD COLUMN tool_call_count INTEGER`
|
|
70847
70845
|
];
|
|
70846
|
+
var PHASE2_MIGRATIONS = [
|
|
70847
|
+
`ALTER TABLE turns ADD COLUMN interrupt_reason TEXT`
|
|
70848
|
+
];
|
|
70848
70849
|
function applySchema(db) {
|
|
70849
70850
|
db.exec("PRAGMA journal_mode = WAL");
|
|
70850
70851
|
db.exec("PRAGMA synchronous = NORMAL");
|
|
70851
70852
|
db.exec(SCHEMA_SQL);
|
|
70852
|
-
for (const sql of PHASE1_MIGRATIONS) {
|
|
70853
|
+
for (const sql of [...PHASE1_MIGRATIONS, ...PHASE2_MIGRATIONS]) {
|
|
70853
70854
|
try {
|
|
70854
70855
|
db.exec(sql);
|
|
70855
70856
|
} catch (err) {
|
|
@@ -70885,6 +70886,7 @@ function mapRow(row) {
|
|
|
70885
70886
|
user_prompt_preview: row.user_prompt_preview,
|
|
70886
70887
|
assistant_reply_preview: row.assistant_reply_preview,
|
|
70887
70888
|
tool_call_count: row.tool_call_count,
|
|
70889
|
+
interrupt_reason: row.interrupt_reason,
|
|
70888
70890
|
created_at: row.created_at,
|
|
70889
70891
|
updated_at: row.updated_at
|
|
70890
70892
|
};
|
|
@@ -70898,6 +70900,12 @@ function listTurnsForAgent(db, opts = {}) {
|
|
|
70898
70900
|
`).all(limit);
|
|
70899
70901
|
return rows.map(mapRow);
|
|
70900
70902
|
}
|
|
70903
|
+
var INTERRUPTED_VIA = new Set([
|
|
70904
|
+
"restart",
|
|
70905
|
+
"sigterm",
|
|
70906
|
+
"timeout",
|
|
70907
|
+
"unknown"
|
|
70908
|
+
]);
|
|
70901
70909
|
|
|
70902
70910
|
// telegram-plugin/registry/subagents-schema.ts
|
|
70903
70911
|
var SUBAGENTS_SCHEMA_SQL = `
|
|
@@ -13773,7 +13773,6 @@ var SessionSchema = exports_external.object({
|
|
|
13773
13773
|
}).optional();
|
|
13774
13774
|
var SessionContinuitySchema = exports_external.object({
|
|
13775
13775
|
enabled: exports_external.boolean().optional().describe("Master switch for the session-handoff briefing (default true)."),
|
|
13776
|
-
show_handoff_line: exports_external.boolean().optional().describe("Whether the telegram plugin prepends a visible '↩️ Picked up…' " + "line to the first assistant reply after a restart (default true)."),
|
|
13777
13776
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
13778
13777
|
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."),
|
|
13779
13778
|
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.")
|
|
@@ -11353,7 +11353,6 @@ var init_schema = __esm(() => {
|
|
|
11353
11353
|
}).optional();
|
|
11354
11354
|
SessionContinuitySchema = exports_external.object({
|
|
11355
11355
|
enabled: exports_external.boolean().optional().describe("Master switch for the session-handoff briefing (default true)."),
|
|
11356
|
-
show_handoff_line: exports_external.boolean().optional().describe("Whether the telegram plugin prepends a visible '↩️ Picked up…' " + "line to the first assistant reply after a restart (default true)."),
|
|
11357
11356
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
11358
11357
|
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."),
|
|
11359
11358
|
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.")
|
|
@@ -11353,7 +11353,6 @@ var init_schema = __esm(() => {
|
|
|
11353
11353
|
}).optional();
|
|
11354
11354
|
SessionContinuitySchema = exports_external.object({
|
|
11355
11355
|
enabled: exports_external.boolean().optional().describe("Master switch for the session-handoff briefing (default true)."),
|
|
11356
|
-
show_handoff_line: exports_external.boolean().optional().describe("Whether the telegram plugin prepends a visible '↩️ Picked up…' " + "line to the first assistant reply after a restart (default true)."),
|
|
11357
11356
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
11358
11357
|
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."),
|
|
11359
11358
|
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.")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.23",
|
|
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/tests/turns-writer.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/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/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/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/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/tests/turns-writer.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/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/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/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.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",
|
|
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",
|
|
31
31
|
"lint:tsc": "tsc --noEmit",
|
|
@@ -27,20 +27,6 @@
|
|
|
27
27
|
# same one v0.6 systemd has always honored, just enforced inside the
|
|
28
28
|
# container instead of by the host's user systemd manager.
|
|
29
29
|
|
|
30
|
-
{{#if handoffEnabled}}
|
|
31
|
-
# The telegram-plugin gateway reads SWITCHROOM_HANDOFF_SHOW_LINE to
|
|
32
|
-
# decide whether to prepend the visible "↩️ Picked up where we left
|
|
33
|
-
# off …" line on the first reply after a restart. It MUST be exported
|
|
34
|
-
# *before* the gateway is forked in the docker preamble below (and
|
|
35
|
-
# before the tmux re-exec), otherwise the gateway — the sole consumer —
|
|
36
|
-
# never inherits it and session_continuity.show_handoff_line:false
|
|
37
|
-
# silently no-ops on every docker agent. Living here, ahead of the
|
|
38
|
-
# runtime branch, covers docker (both the outer fork pass and the inner
|
|
39
|
-
# tmux pass) and the v0.6 non-docker path in one place. Gated on
|
|
40
|
-
# handoffEnabled so a handoff-disabled agent emits no handoff env at all.
|
|
41
|
-
export SWITCHROOM_HANDOFF_SHOW_LINE={{#if handoffShowLine}}true{{else}}false{{/if}}
|
|
42
|
-
{{/if}}
|
|
43
|
-
|
|
44
30
|
if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER" ]; then
|
|
45
31
|
# Hoist TELEGRAM_STATE_DIR up here so the gateway daemon (forked
|
|
46
32
|
# below) finds gateway.sock / gateway.pid.json / history.db at the
|
|
@@ -441,14 +427,16 @@ export SWITCHROOM_PRIOR_SESSION_EPOCH
|
|
|
441
427
|
#
|
|
442
428
|
# The gateway writes <agentDir>/.pending-turn.env on boot if the previous
|
|
443
429
|
# shutdown left an interrupted turn (ended_via='restart' / 'sigterm' /
|
|
444
|
-
# 'timeout' or never closed at all).
|
|
445
|
-
#
|
|
446
|
-
#
|
|
447
|
-
#
|
|
448
|
-
#
|
|
430
|
+
# 'timeout' or never closed at all). These vars are now PASSIVE forensic
|
|
431
|
+
# context only — the actual wake-and-resume is driven by a synthesized
|
|
432
|
+
# inbound the gateway injects (<channel source="resume_interrupted"> or
|
|
433
|
+
# "resume_watchdog_timeout">), not by the agent polling these vars. They
|
|
434
|
+
# stay exported so the wake-audit / "why did you restart" protocols
|
|
435
|
+
# (skills/switchroom-runtime) can read what was in flight without
|
|
436
|
+
# re-deriving it. SWITCHROOM_PENDING_INTERRUPT_REASON carries the boot
|
|
437
|
+
# classifier's idle snapshot for a 'timeout'-killed turn.
|
|
449
438
|
#
|
|
450
|
-
# Consume the file (rm) so it only applies to ONE boot
|
|
451
|
-
# scenarios shouldn't re-fire the resume prompt repeatedly.
|
|
439
|
+
# Consume the file (rm) so it only applies to ONE boot.
|
|
452
440
|
SWITCHROOM_PENDING_TURN_ENV="{{agentDir}}/.pending-turn.env"
|
|
453
441
|
if [ -f "$SWITCHROOM_PENDING_TURN_ENV" ]; then
|
|
454
442
|
# shellcheck disable=SC1090
|
|
@@ -460,7 +448,8 @@ if [ -f "$SWITCHROOM_PENDING_TURN_ENV" ]; then
|
|
|
460
448
|
SWITCHROOM_PENDING_THREAD_ID \
|
|
461
449
|
SWITCHROOM_PENDING_USER_MSG_ID \
|
|
462
450
|
SWITCHROOM_PENDING_ENDED_VIA \
|
|
463
|
-
SWITCHROOM_PENDING_STARTED_AT
|
|
451
|
+
SWITCHROOM_PENDING_STARTED_AT \
|
|
452
|
+
SWITCHROOM_PENDING_INTERRUPT_REASON
|
|
464
453
|
fi
|
|
465
454
|
|
|
466
455
|
# --- Wake audit sentinel ---
|
|
@@ -525,8 +514,6 @@ if [ ! -s "$HANDOFF_FILE" ]; then
|
|
|
525
514
|
timeout 5 handoff-briefing.sh 2>/dev/null || true
|
|
526
515
|
fi
|
|
527
516
|
fi
|
|
528
|
-
# SWITCHROOM_HANDOFF_SHOW_LINE is exported near the top of this script
|
|
529
|
-
# (ahead of the docker preamble) so the gateway sidecar inherits it.
|
|
530
517
|
APPEND_PROMPT={{#if systemPromptAppendShellQuoted}}{{{systemPromptAppendShellQuoted}}}{{else}}""{{/if}}
|
|
531
518
|
# Inject .handoff-briefing.md first (assembled from live sources), then
|
|
532
519
|
# .handoff.md (raw transcript tail from the Stop hook). If both
|
|
@@ -53,7 +53,7 @@ If both `queued` and `steering` are somehow present, `steering` wins (explicit o
|
|
|
53
53
|
|
|
54
54
|
Don't use `accent` on routine conversational replies — it's for status communication, not decoration. Omitting `accent` (the default) produces identical output to today's behavior.
|
|
55
55
|
|
|
56
|
-
**Resume protocol — interrupted turns.** If
|
|
56
|
+
**Resume protocol — interrupted turns.** If your previous turn was interrupted, the gateway wakes you on its own with a synthesized inbound (`<channel source="resume_interrupted">` or `<channel source="resume_watchdog_timeout">`) — you don't poll for it. A clean interrupt (operator restart / SIGTERM / crash) means **resume the work and tell the user you're picking it back up; don't ask.** A hang-watchdog kill means **don't silently resume — report what happened (killed after N min of no progress) and ask whether to retry.** The inbound text spells out which case applies; `/switchroom-runtime` has the full protocol.
|
|
57
57
|
|
|
58
58
|
**Long replies → Telegraph Instant View.** When the operator has telegraph enabled (per-agent flag `telegraph.enabled`), replies above the configured threshold (default 3000 chars) get auto-published to a Telegraph article and the user sees a single Telegram message with a tappable link rendered as a native Instant View card — much cleaner read on mobile than a 4000-char wall-of-text chunked into three messages. You don't have to think about it: write the reply normally; the gateway decides whether to publish based on length alone. Two practical implications: (a) if the user asks "what was in that link?" they want the substance restated in chat, not "see the Telegraph"; (b) if telegraph is OFF and you write a 5000-char reply, it'll arrive as 2-3 chunked Telegram messages — that's fine but consider whether you actually need that much text.
|
|
59
59
|
|
|
@@ -65,7 +65,7 @@ Don't use `accent` on routine conversational replies — it's for status communi
|
|
|
65
65
|
|
|
66
66
|
**When stickers / GIFs land badly**: in lieu of an actual answer, decorating routine acknowledgements ("got it 👍 [+sticker]"), peppering a long thread, or any time the user is task-focused. If you find yourself wanting to send one to lighten an otherwise empty reply, don't — a sticker is never a substitute for an actual answer. Two stickers in a row is always wrong.
|
|
67
67
|
|
|
68
|
-
**Interrupt marker.** If a user asks how to stop you mid-turn, tell them: *"Start your message with `!` to interrupt whatever I'm doing and treat the rest as a fresh request."* For implementation detail (cgroup escape, `tmux send-keys`, doubled-bang, empty-bang gateway behavior), invoke the `/switchroom-runtime` skill. The `!` interrupt
|
|
68
|
+
**Interrupt marker.** If a user asks how to stop you mid-turn, tell them: *"Start your message with `!` to interrupt whatever I'm doing and treat the rest as a fresh request."* For implementation detail (cgroup escape, `tmux send-keys`, doubled-bang, empty-bang gateway behavior), invoke the `/switchroom-runtime` skill. The `!` interrupt is in-process (no restart), so it does not trigger the boot-resume path — the remainder is delivered as a fresh turn immediately.
|
|
69
69
|
|
|
70
70
|
**Wake audit on fresh boot.** If `$TELEGRAM_STATE_DIR/.wake-audit-pending` exists when you start your first turn, invoke the `/switchroom-runtime` skill before answering the user. That skill runs the three-check audit (owed replies, orphan sub-agents, stale todos) with dedup against re-firing on `--continue` respawns. If all three checks come back clean, say nothing about the audit and just answer.
|
|
71
71
|
|
|
@@ -118,7 +118,10 @@ By default, every restart starts a **fresh `claude` session** — the in-flight
|
|
|
118
118
|
- **Handoff briefing** — on a clean shutdown, the Stop hook writes a bounded raw transcript tail of the prior session to `.handoff.md`. On boot, start.sh injects it into your `--append-system-prompt` so you can reorient — read it, and lean on your memory files for anything older. If `.handoff.md` is missing or stale (fresh agent, or pre-Stop-hook crash), `start.sh` runs `handoff-briefing.sh` to assemble `.handoff-briefing.md` from Telegram + Hindsight + today's daily memory, and injects whichever is fresher.
|
|
119
119
|
- **Hindsight memory** — auto-recall fires on every inbound user message and surfaces relevant memories from past sessions. Long-term facts, decisions, and mental models live here, not in the transcript.
|
|
120
120
|
- **Telegram history** — the gateway's SQLite buffer remembers every inbound/outbound message. Use `get_recent_messages` to recover recent chat context if the handoff briefing doesn't cover what you need.
|
|
121
|
-
-
|
|
121
|
+
- **Boot-resume inbound** — if your previous session was killed mid-turn, the gateway wakes you on its own with a synthesized inbound (you'll see `<channel source="resume_interrupted">` or `<channel source="resume_watchdog_timeout">`). You don't poll for this — it arrives as your first turn. Two cases, and the inbound text spells out which:
|
|
122
|
+
- **`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.
|
|
123
|
+
- **`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.
|
|
124
|
+
The one-shot `SWITCHROOM_PENDING_*` env vars are passive forensic context for the wake-audit / "why did you restart" protocols — not the resume trigger.
|
|
122
125
|
- **`.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 your CLAUDE.md, then `rm -f` the sentinel.
|
|
123
126
|
|
|
124
127
|
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.
|
|
@@ -56,29 +56,19 @@ This skill holds the runtime protocols that fire on specific boot signals or use
|
|
|
56
56
|
|
|
57
57
|
## Resume protocol — interrupted turns
|
|
58
58
|
|
|
59
|
-
**
|
|
59
|
+
**You do not poll for this.** When your previous turn was interrupted, the gateway wakes you on its own at boot by injecting a synthesized inbound — it arrives as your first turn, tagged `<channel source="resume_interrupted">` or `<channel source="resume_watchdog_timeout">`. The inbound text carries the specifics (elapsed time, the original request, tool-call count); this section is the *why* behind the two shapes so you handle each correctly. The policy is decided by how the prior turn ended, not by you.
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
- `SWITCHROOM_PENDING_THREAD_ID` — the forum topic id (empty if not a forum)
|
|
63
|
-
- `SWITCHROOM_PENDING_USER_MSG_ID` — the inbound message_id that started the turn (you can quote-reply to it for context)
|
|
64
|
-
- `SWITCHROOM_PENDING_ENDED_VIA` — `restart` (user ran `switchroom agent restart`), `sigterm` (systemd/manual kill), `timeout` (watchdog), or `unknown` (crash before stamp)
|
|
65
|
-
- `SWITCHROOM_PENDING_STARTED_AT` — unix-ms when the turn started
|
|
61
|
+
**Branch 1 — `resume_interrupted` (clean mid-flight interrupt).** The turn was cut off by an operator `switchroom agent restart`, a SIGTERM, or a crash before the turn could finish — it was *making progress*, just stopped short. **Resume it. Do not ask whether to.** In your first message, briefly tell the user you're picking the work back up and roughly how long ago it was interrupted (the inbound gives you the elapsed framing, e.g. "~3h ago"), then carry the actual task through to completion. The user has no way to know you remember — the one-line "resuming the X you asked ~3h ago" is what closes that gap. Only if you genuinely can't tell what the work was (no Hindsight, no handoff briefing, empty original prompt) do you say so and ask.
|
|
66
62
|
|
|
67
|
-
**
|
|
63
|
+
**Branch 2 — `resume_watchdog_timeout` (hang-watchdog kill).** The turn made *no observable progress* for the full hang window (default 5 min) and was killed as a wedge. **Do NOT silently resume it — it may hang the same way.** Instead, tell the user plainly what happened: that your last turn was killed after ~N minutes of no progress, and roughly what it was doing (the inbound carries the idle duration and tool-call count). Then ask whether they want you to retry it or take a different angle. Report **only the honest cause** — no observable progress for that long — don't speculate about a deeper root cause you can't actually see from a boot record. Use `reply` with `accent: 'issue'` so the report is visually distinct.
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
>
|
|
71
|
-
> I was killed mid-turn. Looks like my previous shutdown was via `<endedVia>`. Don't have full context on what I'd already done. Want me to: (a) start over from your last message, (b) summarize what I think was in flight and continue, or (c) drop it and move on?
|
|
72
|
-
|
|
73
|
-
The env vars are one-shot (start.sh deletes the file after sourcing), so this prompt only fires on the immediately-following session, not every restart afterward. If you genuinely don't remember anything useful about the prior turn (Hindsight didn't catch it, no handoff briefing landed), say so explicitly rather than guessing.
|
|
74
|
-
|
|
75
|
-
If `SWITCHROOM_PENDING_TURN` is unset or empty, do nothing special: the previous turn ended cleanly.
|
|
65
|
+
The `SWITCHROOM_PENDING_*` env vars (`_CHAT_ID`, `_THREAD_ID`, `_USER_MSG_ID`, `_ENDED_VIA`, `_STARTED_AT`, `_INTERRUPT_REASON`) are one-shot passive context for the wake-audit and "why did you restart" protocols below — they are NOT the resume trigger and you don't need to act on them directly.
|
|
76
66
|
|
|
77
67
|
---
|
|
78
68
|
|
|
79
69
|
## Wake audit — every fresh boot
|
|
80
70
|
|
|
81
|
-
**Trigger:** the sentinel file `$TELEGRAM_STATE_DIR/.wake-audit-pending` exists. `start.sh` drops it on every process boot. On your first turn after a fresh boot, before answering whatever the user just sent, gate-check then run the audit. This complements the resume protocol above: `
|
|
71
|
+
**Trigger:** the sentinel file `$TELEGRAM_STATE_DIR/.wake-audit-pending` exists. `start.sh` drops it on every process boot. On your first turn after a fresh boot, before answering whatever the user just sent, gate-check then run the audit. This complements the resume protocol above: the injected `resume_interrupted` / `resume_watchdog_timeout` inbound covers "killed mid-turn"; the wake audit covers "anything else owed since last seen."
|
|
82
72
|
|
|
83
73
|
**Conversation-aware dedup.** start.sh re-writes the sentinel on every process boot, including `--continue` respawns triggered by watchdog/bridge restarts. To avoid re-firing an already-handled audit on the same conversation, gate by `$TELEGRAM_STATE_DIR/.wake-audit-last-completed`:
|
|
84
74
|
|
|
@@ -149,7 +139,7 @@ The gateway treats a Telegram message starting with `!` (single bang, not `!!` o
|
|
|
149
139
|
|
|
150
140
|
If the user sends `! actually never mind, do X instead`, you'll boot up and see `actually never mind, do X instead` with no record of what you were doing before. That's intentional.
|
|
151
141
|
|
|
152
|
-
Doubled `!!` (typo / emphasis) reaches you verbatim. Empty `!` gets a "Send your replacement instruction now" reply from the gateway and never reaches you. The interrupt
|
|
142
|
+
Doubled `!!` (typo / emphasis) reaches you verbatim. Empty `!` gets a "Send your replacement instruction now" reply from the gateway and never reaches you. The interrupt is in-process — the gateway keeps running and delivers the remainder as a fresh turn immediately — so it does NOT trigger the boot-resume path (that fires only on a real restart). The turn you were running is simply abandoned in favour of the new instruction.
|
|
153
143
|
|
|
154
144
|
---
|
|
155
145
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the agent's state directory from the environment.
|
|
3
|
+
*
|
|
4
|
+
* `TELEGRAM_STATE_DIR` points at `<agentDir>/telegram`; the agent dir is
|
|
5
|
+
* its parent. Returns null when the env var is unset or blank so callers
|
|
6
|
+
* can degrade gracefully (the gateway runs in non-agent contexts too —
|
|
7
|
+
* tests, local dev).
|
|
8
|
+
*/
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
|
|
11
|
+
export function resolveAgentDirFromEnv(): string | null {
|
|
12
|
+
const state = process.env.TELEGRAM_STATE_DIR;
|
|
13
|
+
if (!state || state.trim().length === 0) return null;
|
|
14
|
+
return dirname(state);
|
|
15
|
+
}
|