switchroom 0.13.24 → 0.13.26

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.
@@ -13769,7 +13769,6 @@ var SessionSchema = exports_external.object({
13769
13769
  var SessionContinuitySchema = exports_external.object({
13770
13770
  enabled: exports_external.boolean().optional().describe("Master switch for the session-handoff briefing (default true)."),
13771
13771
  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)."),
13772
- summarizer_model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional().describe("Anthropic model used to produce the handoff briefing."),
13773
13772
  max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
13774
13773
  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."),
13775
13774
  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.")
@@ -13816,10 +13815,9 @@ var TelegramChannelSchema = exports_external.object({
13816
13815
  start: exports_external.number().int().min(0).max(23),
13817
13816
  end: exports_external.number().int().min(0).max(23),
13818
13817
  tz: exports_external.string().optional()
13819
- }).optional(),
13820
- model: exports_external.string().optional()
13818
+ }).optional()
13821
13819
  })).optional()
13822
- }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "spawn a one-shot `claude -p` turn for the agent with the rendered " + "prompt. Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
13820
+ }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
13823
13821
  webhook_rate_limit: exports_external.object({
13824
13822
  rpm: exports_external.number().int().positive()
13825
13823
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit.")
@@ -11026,7 +11026,6 @@ var init_schema = __esm(() => {
11026
11026
  SessionContinuitySchema = exports_external.object({
11027
11027
  enabled: exports_external.boolean().optional().describe("Master switch for the session-handoff briefing (default true)."),
11028
11028
  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)."),
11029
- summarizer_model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional().describe("Anthropic model used to produce the handoff briefing."),
11030
11029
  max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
11031
11030
  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."),
11032
11031
  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.")
@@ -11073,10 +11072,9 @@ var init_schema = __esm(() => {
11073
11072
  start: exports_external.number().int().min(0).max(23),
11074
11073
  end: exports_external.number().int().min(0).max(23),
11075
11074
  tz: exports_external.string().optional()
11076
- }).optional(),
11077
- model: exports_external.string().optional()
11075
+ }).optional()
11078
11076
  })).optional()
11079
- }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "spawn a one-shot `claude -p` turn for the agent with the rendered " + "prompt. Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
11077
+ }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
11080
11078
  webhook_rate_limit: exports_external.object({
11081
11079
  rpm: exports_external.number().int().positive()
11082
11080
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit.")
@@ -11674,7 +11672,70 @@ import { Database } from "bun:sqlite";
11674
11672
 
11675
11673
  // src/vault/broker/protocol.ts
11676
11674
  init_zod();
11675
+
11676
+ // src/vault/broker/peercred-ffi.ts
11677
+ function getPeerCred(fd) {
11678
+ if (process.platform !== "linux")
11679
+ return null;
11680
+ try {
11681
+ const ffi = __require("bun:ffi");
11682
+ const { dlopen, FFIType, ptr } = ffi;
11683
+ const SOL_SOCKET = 1;
11684
+ const SO_PEERCRED = 17;
11685
+ const UCRED_SIZE = 12;
11686
+ const cache = getPeerCred;
11687
+ const lib = cache._lib ?? (() => {
11688
+ const candidates = ["libc.so.6", "libc.so"];
11689
+ const symbolSpec = {
11690
+ getsockopt: {
11691
+ args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.ptr],
11692
+ returns: FFIType.i32
11693
+ }
11694
+ };
11695
+ const errors2 = [];
11696
+ for (const name of candidates) {
11697
+ try {
11698
+ const opened = dlopen(name, symbolSpec);
11699
+ cache._lib = opened;
11700
+ return opened;
11701
+ } catch (e) {
11702
+ errors2.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
11703
+ }
11704
+ }
11705
+ process.stderr.write(`[vault-broker] peercred-ffi: dlopen failed for all libc candidates ` + `(${errors2.join("; ")}); falling back to ss-parsing.
11706
+ `);
11707
+ throw new Error("no libc candidate could be opened");
11708
+ })();
11709
+ const credBuf = new ArrayBuffer(UCRED_SIZE);
11710
+ const lenBuf = new Uint32Array(1);
11711
+ lenBuf[0] = UCRED_SIZE;
11712
+ const rc = lib.symbols.getsockopt(fd, SOL_SOCKET, SO_PEERCRED, ptr(credBuf), ptr(lenBuf.buffer));
11713
+ if (rc !== 0)
11714
+ return null;
11715
+ if (lenBuf[0] !== UCRED_SIZE)
11716
+ return null;
11717
+ const view = new DataView(credBuf);
11718
+ return {
11719
+ pid: view.getInt32(0, true),
11720
+ uid: view.getInt32(4, true),
11721
+ gid: view.getInt32(8, true)
11722
+ };
11723
+ } catch {
11724
+ return null;
11725
+ }
11726
+ }
11727
+
11728
+ // src/vault/broker/peercred.ts
11729
+ var RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
11730
+ function isReservedAgentName(name) {
11731
+ return RESERVED_AGENT_NAMES.has(name);
11732
+ }
11733
+
11734
+ // src/vault/broker/protocol.ts
11677
11735
  var MAX_FRAME_BYTES = 64 * 1024;
11736
+ var AgentNameSchema = exports_external.string().min(1).max(64, "agent name max 64 chars").regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, "agent name must be kebab-case ASCII (alnum + _- only, first char alnum)").refine((s) => !isReservedAgentName(s), {
11737
+ message: "agent name is reserved"
11738
+ });
11678
11739
  var GetRequestSchema = exports_external.object({
11679
11740
  v: exports_external.literal(1),
11680
11741
  op: exports_external.literal("get"),
@@ -11702,7 +11763,7 @@ var ListRequestSchema = exports_external.object({
11702
11763
  var MintGrantRequestSchema = exports_external.object({
11703
11764
  v: exports_external.literal(1),
11704
11765
  op: exports_external.literal("mint_grant"),
11705
- agent: exports_external.string().min(1),
11766
+ agent: AgentNameSchema,
11706
11767
  keys: exports_external.array(exports_external.string().min(1)),
11707
11768
  ttl_seconds: exports_external.number().int().positive().nullable(),
11708
11769
  description: exports_external.string().optional(),
@@ -11713,7 +11774,7 @@ var MintGrantRequestSchema = exports_external.object({
11713
11774
  var ListGrantsRequestSchema = exports_external.object({
11714
11775
  v: exports_external.literal(1),
11715
11776
  op: exports_external.literal("list_grants"),
11716
- agent: exports_external.string().optional(),
11777
+ agent: AgentNameSchema.optional(),
11717
11778
  passphrase: exports_external.string().optional(),
11718
11779
  attest_via_posture: exports_external.boolean().optional()
11719
11780
  });
@@ -11733,7 +11794,7 @@ var LockRequestSchema = exports_external.object({
11733
11794
  var PreflightAccessRequestSchema = exports_external.object({
11734
11795
  v: exports_external.literal(1),
11735
11796
  op: exports_external.literal("preflight_access"),
11736
- agent: exports_external.string().min(1),
11797
+ agent: AgentNameSchema,
11737
11798
  keys: exports_external.array(exports_external.string().min(1)).min(1).max(128)
11738
11799
  });
11739
11800
  var OkPreflightAccessResponseSchema = exports_external.object({
@@ -12654,64 +12715,6 @@ function mergeAgentConfig(defaultsIn, agentIn) {
12654
12715
  mergeAgentConfig.notifiedWorkerIsolationMove = false;
12655
12716
  })(mergeAgentConfig ||= {});
12656
12717
 
12657
- // src/vault/broker/peercred-ffi.ts
12658
- function getPeerCred(fd) {
12659
- if (process.platform !== "linux")
12660
- return null;
12661
- try {
12662
- const ffi = __require("bun:ffi");
12663
- const { dlopen, FFIType, ptr } = ffi;
12664
- const SOL_SOCKET = 1;
12665
- const SO_PEERCRED = 17;
12666
- const UCRED_SIZE = 12;
12667
- const cache = getPeerCred;
12668
- const lib = cache._lib ?? (() => {
12669
- const candidates = ["libc.so.6", "libc.so"];
12670
- const symbolSpec = {
12671
- getsockopt: {
12672
- args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.ptr],
12673
- returns: FFIType.i32
12674
- }
12675
- };
12676
- const errors2 = [];
12677
- for (const name of candidates) {
12678
- try {
12679
- const opened = dlopen(name, symbolSpec);
12680
- cache._lib = opened;
12681
- return opened;
12682
- } catch (e) {
12683
- errors2.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
12684
- }
12685
- }
12686
- process.stderr.write(`[vault-broker] peercred-ffi: dlopen failed for all libc candidates ` + `(${errors2.join("; ")}); falling back to ss-parsing.
12687
- `);
12688
- throw new Error("no libc candidate could be opened");
12689
- })();
12690
- const credBuf = new ArrayBuffer(UCRED_SIZE);
12691
- const lenBuf = new Uint32Array(1);
12692
- lenBuf[0] = UCRED_SIZE;
12693
- const rc = lib.symbols.getsockopt(fd, SOL_SOCKET, SO_PEERCRED, ptr(credBuf), ptr(lenBuf.buffer));
12694
- if (rc !== 0)
12695
- return null;
12696
- if (lenBuf[0] !== UCRED_SIZE)
12697
- return null;
12698
- const view = new DataView(credBuf);
12699
- return {
12700
- pid: view.getInt32(0, true),
12701
- uid: view.getInt32(4, true),
12702
- gid: view.getInt32(8, true)
12703
- };
12704
- } catch {
12705
- return null;
12706
- }
12707
- }
12708
-
12709
- // src/vault/broker/peercred.ts
12710
- var RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
12711
- function isReservedAgentName(name) {
12712
- return RESERVED_AGENT_NAMES.has(name);
12713
- }
12714
-
12715
12718
  // src/memory/hindsight.ts
12716
12719
  var DEFAULT_RETAIN_MISSION = "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise.";
12717
12720
 
@@ -11026,7 +11026,6 @@ var init_schema = __esm(() => {
11026
11026
  SessionContinuitySchema = exports_external.object({
11027
11027
  enabled: exports_external.boolean().optional().describe("Master switch for the session-handoff briefing (default true)."),
11028
11028
  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)."),
11029
- summarizer_model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional().describe("Anthropic model used to produce the handoff briefing."),
11030
11029
  max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
11031
11030
  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."),
11032
11031
  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.")
@@ -11073,10 +11072,9 @@ var init_schema = __esm(() => {
11073
11072
  start: exports_external.number().int().min(0).max(23),
11074
11073
  end: exports_external.number().int().min(0).max(23),
11075
11074
  tz: exports_external.string().optional()
11076
- }).optional(),
11077
- model: exports_external.string().optional()
11075
+ }).optional()
11078
11076
  })).optional()
11079
- }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "spawn a one-shot `claude -p` turn for the agent with the rendered " + "prompt. Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
11077
+ }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
11080
11078
  webhook_rate_limit: exports_external.object({
11081
11079
  rpm: exports_external.number().int().positive()
11082
11080
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit.")
@@ -13103,6 +13101,9 @@ function checkGoogleAccountAcl(config, agentName, account, key) {
13103
13101
  // src/vault/broker/protocol.ts
13104
13102
  init_zod();
13105
13103
  var MAX_FRAME_BYTES = 64 * 1024;
13104
+ var AgentNameSchema = exports_external.string().min(1).max(64, "agent name max 64 chars").regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, "agent name must be kebab-case ASCII (alnum + _- only, first char alnum)").refine((s) => !isReservedAgentName(s), {
13105
+ message: "agent name is reserved"
13106
+ });
13106
13107
  var GetRequestSchema = exports_external.object({
13107
13108
  v: exports_external.literal(1),
13108
13109
  op: exports_external.literal("get"),
@@ -13130,7 +13131,7 @@ var ListRequestSchema = exports_external.object({
13130
13131
  var MintGrantRequestSchema = exports_external.object({
13131
13132
  v: exports_external.literal(1),
13132
13133
  op: exports_external.literal("mint_grant"),
13133
- agent: exports_external.string().min(1),
13134
+ agent: AgentNameSchema,
13134
13135
  keys: exports_external.array(exports_external.string().min(1)),
13135
13136
  ttl_seconds: exports_external.number().int().positive().nullable(),
13136
13137
  description: exports_external.string().optional(),
@@ -13141,7 +13142,7 @@ var MintGrantRequestSchema = exports_external.object({
13141
13142
  var ListGrantsRequestSchema = exports_external.object({
13142
13143
  v: exports_external.literal(1),
13143
13144
  op: exports_external.literal("list_grants"),
13144
- agent: exports_external.string().optional(),
13145
+ agent: AgentNameSchema.optional(),
13145
13146
  passphrase: exports_external.string().optional(),
13146
13147
  attest_via_posture: exports_external.boolean().optional()
13147
13148
  });
@@ -13161,7 +13162,7 @@ var LockRequestSchema = exports_external.object({
13161
13162
  var PreflightAccessRequestSchema = exports_external.object({
13162
13163
  v: exports_external.literal(1),
13163
13164
  op: exports_external.literal("preflight_access"),
13164
- agent: exports_external.string().min(1),
13165
+ agent: AgentNameSchema,
13165
13166
  keys: exports_external.array(exports_external.string().min(1)).min(1).max(128)
13166
13167
  });
13167
13168
  var OkPreflightAccessResponseSchema = exports_external.object({
@@ -16008,20 +16009,48 @@ class VaultBroker {
16008
16009
  try {
16009
16010
  chmodSync4(abs, 432);
16010
16011
  } catch {}
16012
+ let agentUid = null;
16011
16013
  try {
16012
- const uid = allocateAgentUid(agentName);
16014
+ agentUid = allocateAgentUid(agentName);
16013
16015
  try {
16014
- chownSync(abs, uid, uid);
16016
+ chownSync(abs, agentUid, agentUid);
16015
16017
  } catch {}
16016
16018
  if (abs.endsWith("/sock")) {
16017
16019
  const dir = abs.slice(0, -"/sock".length);
16018
16020
  try {
16019
- chownSync(dir, uid, uid);
16021
+ chownSync(dir, agentUid, agentUid);
16020
16022
  } catch {}
16021
16023
  }
16022
16024
  } catch {}
16023
16025
  this.agentServers.set(abs, { server, agentName });
16024
- resolveP(agentName);
16026
+ const unlockAbs = unlockSocketFor(abs);
16027
+ if (existsSync8(unlockAbs)) {
16028
+ try {
16029
+ unlinkSync4(unlockAbs);
16030
+ } catch {}
16031
+ }
16032
+ const unlockServer = net.createServer((sock) => {
16033
+ this._handleUnlockConnection(sock, false);
16034
+ });
16035
+ unlockServer.on("error", (err) => {
16036
+ try {
16037
+ server.close();
16038
+ } catch {}
16039
+ this.agentServers.delete(abs);
16040
+ rejectP(err);
16041
+ });
16042
+ unlockServer.listen(unlockAbs, () => {
16043
+ try {
16044
+ chmodSync4(unlockAbs, 432);
16045
+ } catch {}
16046
+ if (agentUid !== null) {
16047
+ try {
16048
+ chownSync(unlockAbs, agentUid, agentUid);
16049
+ } catch {}
16050
+ }
16051
+ this.agentServers.set(unlockAbs, { server: unlockServer, agentName });
16052
+ resolveP(agentName);
16053
+ });
16025
16054
  });
16026
16055
  });
16027
16056
  }
@@ -16948,7 +16977,7 @@ class VaultBroker {
16948
16977
  const revoked = revokeGrant(this.grantsDb, id);
16949
16978
  try {
16950
16979
  const row = this.grantsDb.query("SELECT agent_slug FROM vault_grants WHERE id = ?").get(id);
16951
- if (row) {
16980
+ if (row && AgentNameSchema.safeParse(row.agent_slug).success) {
16952
16981
  const tokenPath = path3.join(os3.homedir(), ".switchroom", "agents", row.agent_slug, ".vault-token");
16953
16982
  if (existsSync8(tokenPath)) {
16954
16983
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.13.24",
3
+ "version": "0.13.26",
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": {
@@ -5,10 +5,10 @@ Telegram is a chat — replies should feel like one, not a terminal dump or a tr
5
5
  **Every turn that responds to a user message MUST end with a `reply` (or `stream_reply` with `done=true`).** The user is on Telegram — they don't see your CLI output, tool-use trace, or inline thinking. The ONLY path for words to reach them is an MCP tool call. If you have a final answer, send it via `reply`. The text in your terminal is not the conversation.
6
6
 
7
7
  **Conversational pacing — a human is on the other side.** Match the rhythm of a capable colleague messaging you back. Five beats:
8
- - **1 · Acknowledge first.** Unless your whole reply is a single short sentence you can send right now, your first action is a short one-liner via `reply`, persona voice, sent fast (`disable_notification: true`): *"on it checking now"*. This holds whether the work ahead is a tool call or a paragraph of pure reasoning — if the answer will run long, ack *before* you compose it. Skip the ack only for an immediate one-sentence answer (*"what's 2+2"*). This is a beat, not filler — it's the line between a colleague and a black box.
8
+ - **1 · Acknowledge first.** Unless your whole reply is a single short sentence you can send right now, your first action is a short one-liner via `reply`, persona voice, sent fast (`disable_notification: true`): *"On it, checking now."*. This holds whether the work ahead is a tool call or a paragraph of pure reasoning — if the answer will run long, ack *before* you compose it. Skip the ack only for an immediate one-sentence answer (*"What's 2+2?"*). This is a beat, not filler — it's the line between a colleague and a black box.
9
9
  - **2 · Then go quiet and work.** Heads-down is right — do **not** narrate every tool call. A typing indicator runs for you automatically; you don't keep it alive.
10
10
  - **3 · Surface meaningful progress** at genuine inflection points — a hard step finished, a blocker, a pivot, dispatching a sub-agent, a notably slow wait, a finding worth knowing now. One short `reply`, `disable_notification: true` (no mid-turn ping).
11
- - **4 · Hand back delegations with synthesis.** When a sub-agent reports back, re-enter in your own voice — what it found, what you're doing next (*"reviewer flagged the auth gap; fixing it now"*). Never let its raw report stand as your reply. A *background* worker finishes after your turn has ended — its result is delivered to you as a fresh `<channel source="subagent_handback">` turn. That turn IS your cue: synthesise it for the user right then; don't treat it as noise and don't stay silent.
11
+ - **4 · Hand back delegations with synthesis.** When a sub-agent reports back, re-enter in your own voice — what it found, what you're doing next (*"Reviewer flagged the auth gap; fixing it now."*). Never let its raw report stand as your reply. A *background* worker finishes after your turn has ended — its result is delivered to you as a fresh `<channel source="subagent_handback">` turn. That turn IS your cue: synthesise it for the user right then; don't treat it as noise and don't stay silent.
12
12
  - **5 · Deliver the answer** as a fresh `reply` (omit `disable_notification` — pings once).
13
13
 
14
14
  The one thing to avoid is **spam**: a reply on every tool call, on a cadence, or repeating yourself. Responsive and human, never a flood. A `<system-reminder>` containing `[silence-poke]` means you've gone quiet too long — send one short `reply` and carry on; skip it only if you're within ~5s of finishing.
@@ -29,7 +29,7 @@ The 👀→🤔→🔥→👍 status reaction and the typing indicator are *ambi
29
29
 
30
30
  If both `queued` and `steering` are somehow present, `steering` wins (explicit opt-in beats default). If `prior_turn_in_progress="true"` is set without either flag (shouldn't happen but defensive), treat the message as a follow-up related to your last reply.
31
31
 
32
- **Self-narrate the classification.** At the top of your reply for any `steering` or `queued` message, include a brief italic one-liner so the user can correct you — e.g. `_↪️ treating as steer on the prior task_` or `_📥 queued as a new task_`.
32
+ **Self-narrate the classification.** At the top of your reply for any `steering` or `queued` message, include a brief italic one-liner so the user can correct you — e.g. `_↪️ Treating as steer on the prior task_` or `_📥 Queued as a new task_`.
33
33
 
34
34
  **Formatting** (Telegram HTML — `reply` and `stream_reply` default to `format: "html"` and convert markdown for you):
35
35
  - Use **bold** sparingly for emphasis on key facts only
@@ -62,7 +62,7 @@ Don't use `accent` on routine conversational replies — it's for status communi
62
62
 
63
63
  **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.
64
64
 
65
- **Interrupt marker.** If a user asks how to stop you mid-turn, tell them: *"Start your message with `!` it interrupts whatever I'm doing and treats 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 wakes a fresh `SWITCHROOM_PENDING_TURN` cycle, so the resume protocol fires on the next turn.
65
+ **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 wakes a fresh `SWITCHROOM_PENDING_TURN` cycle, so the resume protocol fires on the next turn.
66
66
 
67
67
  **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.
68
68
 
@@ -23670,7 +23670,6 @@ var init_schema = __esm(() => {
23670
23670
  SessionContinuitySchema = exports_external.object({
23671
23671
  enabled: exports_external.boolean().optional().describe("Master switch for the session-handoff briefing (default true)."),
23672
23672
  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)."),
23673
- summarizer_model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional().describe("Anthropic model used to produce the handoff briefing."),
23674
23673
  max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
23675
23674
  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."),
23676
23675
  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.")
@@ -23717,10 +23716,9 @@ var init_schema = __esm(() => {
23717
23716
  start: exports_external.number().int().min(0).max(23),
23718
23717
  end: exports_external.number().int().min(0).max(23),
23719
23718
  tz: exports_external.string().optional()
23720
- }).optional(),
23721
- model: exports_external.string().optional()
23719
+ }).optional()
23722
23720
  })).optional()
23723
- }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "spawn a one-shot `claude -p` turn for the agent with the rendered " + "prompt. Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default \u2014 opt in per agent. See src/web/webhook-dispatch.ts."),
23721
+ }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default \u2014 opt in per agent. See src/web/webhook-dispatch.ts."),
23724
23722
  webhook_rate_limit: exports_external.object({
23725
23723
  rpm: exports_external.number().int().positive()
23726
23724
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default \u2014 when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit.")
@@ -24658,6 +24656,20 @@ function formatResetRelative(target, now = new Date) {
24658
24656
  }
24659
24657
  var OAUTH_BETA = "oauth-2025-04-20", DEFAULT_USER_AGENT = "claude-cli/1.0.0 (external, cli)", DEFAULT_PROBE_MODEL = "claude-haiku-4-5-20251001";
24660
24658
  var init_quota_check = () => {};
24659
+ // ../src/vault/broker/peercred.ts
24660
+ function isReservedAgentName(name) {
24661
+ return RESERVED_AGENT_NAMES.has(name);
24662
+ }
24663
+ function unlockSocketFor(dataSocketPath) {
24664
+ if (dataSocketPath.endsWith("/sock")) {
24665
+ return dataSocketPath.slice(0, -"/sock".length) + "/unlock";
24666
+ }
24667
+ return dataSocketPath.replace(/\.sock$/, ".unlock.sock");
24668
+ }
24669
+ var RESERVED_AGENT_NAMES;
24670
+ var init_peercred = __esm(() => {
24671
+ RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
24672
+ });
24661
24673
 
24662
24674
  // ../src/vault/broker/protocol.ts
24663
24675
  function encodeRequest2(req) {
@@ -24675,10 +24687,14 @@ function decodeResponse2(line) {
24675
24687
  const obj = JSON.parse(line);
24676
24688
  return ResponseSchema2.parse(obj);
24677
24689
  }
24678
- var MAX_FRAME_BYTES2, GetRequestSchema, PutRequestSchema, ListRequestSchema, MintGrantRequestSchema, ListGrantsRequestSchema, RevokeGrantRequestSchema, StatusRequestSchema, LockRequestSchema, PreflightAccessRequestSchema, OkPreflightAccessResponseSchema, ApprovalRequestRequestSchema, ApprovalLookupRequestSchema, ApprovalConsumeRequestSchema, ApprovalRevokeRequestSchema, ApprovalListRequestSchema, ApprovalDecisionModeSchema, ApprovalRecordRequestSchema, ApprovalConsumeRecordRequestSchema, RequestSchema2, VaultEntrySchema, ErrorCode, OkEntryResponseSchema, OkKeysResponseSchema, BrokerStatus, OkStatusResponseSchema, OkLockResponseSchema, OkPutResponseSchema, OkMintGrantResponseSchema, GrantMetaSchema, OkListGrantsResponseSchema, OkRevokeGrantResponseSchema, OkApprovalRequestResponseSchema, ApprovalDecisionMetaSchema, OkApprovalLookupResponseSchema, OkApprovalConsumeResponseSchema, OkApprovalRevokeResponseSchema, OkApprovalListResponseSchema, OkApprovalRecordResponseSchema, OkApprovalConsumeRecordResponseSchema, ErrorResponseSchema2, ResponseSchema2;
24690
+ var MAX_FRAME_BYTES2, AgentNameSchema, GetRequestSchema, PutRequestSchema, ListRequestSchema, MintGrantRequestSchema, ListGrantsRequestSchema, RevokeGrantRequestSchema, StatusRequestSchema, LockRequestSchema, PreflightAccessRequestSchema, OkPreflightAccessResponseSchema, ApprovalRequestRequestSchema, ApprovalLookupRequestSchema, ApprovalConsumeRequestSchema, ApprovalRevokeRequestSchema, ApprovalListRequestSchema, ApprovalDecisionModeSchema, ApprovalRecordRequestSchema, ApprovalConsumeRecordRequestSchema, RequestSchema2, VaultEntrySchema, ErrorCode, OkEntryResponseSchema, OkKeysResponseSchema, BrokerStatus, OkStatusResponseSchema, OkLockResponseSchema, OkPutResponseSchema, OkMintGrantResponseSchema, GrantMetaSchema, OkListGrantsResponseSchema, OkRevokeGrantResponseSchema, OkApprovalRequestResponseSchema, ApprovalDecisionMetaSchema, OkApprovalLookupResponseSchema, OkApprovalConsumeResponseSchema, OkApprovalRevokeResponseSchema, OkApprovalListResponseSchema, OkApprovalRecordResponseSchema, OkApprovalConsumeRecordResponseSchema, ErrorResponseSchema2, ResponseSchema2;
24679
24691
  var init_protocol2 = __esm(() => {
24680
24692
  init_zod();
24693
+ init_peercred();
24681
24694
  MAX_FRAME_BYTES2 = 64 * 1024;
24695
+ AgentNameSchema = exports_external.string().min(1).max(64, "agent name max 64 chars").regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, "agent name must be kebab-case ASCII (alnum + _- only, first char alnum)").refine((s) => !isReservedAgentName(s), {
24696
+ message: "agent name is reserved"
24697
+ });
24682
24698
  GetRequestSchema = exports_external.object({
24683
24699
  v: exports_external.literal(1),
24684
24700
  op: exports_external.literal("get"),
@@ -24706,7 +24722,7 @@ var init_protocol2 = __esm(() => {
24706
24722
  MintGrantRequestSchema = exports_external.object({
24707
24723
  v: exports_external.literal(1),
24708
24724
  op: exports_external.literal("mint_grant"),
24709
- agent: exports_external.string().min(1),
24725
+ agent: AgentNameSchema,
24710
24726
  keys: exports_external.array(exports_external.string().min(1)),
24711
24727
  ttl_seconds: exports_external.number().int().positive().nullable(),
24712
24728
  description: exports_external.string().optional(),
@@ -24717,7 +24733,7 @@ var init_protocol2 = __esm(() => {
24717
24733
  ListGrantsRequestSchema = exports_external.object({
24718
24734
  v: exports_external.literal(1),
24719
24735
  op: exports_external.literal("list_grants"),
24720
- agent: exports_external.string().optional(),
24736
+ agent: AgentNameSchema.optional(),
24721
24737
  passphrase: exports_external.string().optional(),
24722
24738
  attest_via_posture: exports_external.boolean().optional()
24723
24739
  });
@@ -24737,7 +24753,7 @@ var init_protocol2 = __esm(() => {
24737
24753
  PreflightAccessRequestSchema = exports_external.object({
24738
24754
  v: exports_external.literal(1),
24739
24755
  op: exports_external.literal("preflight_access"),
24740
- agent: exports_external.string().min(1),
24756
+ agent: AgentNameSchema,
24741
24757
  keys: exports_external.array(exports_external.string().min(1)).min(1).max(128)
24742
24758
  });
24743
24759
  OkPreflightAccessResponseSchema = exports_external.object({
@@ -24984,17 +25000,6 @@ var init_protocol2 = __esm(() => {
24984
25000
  ErrorResponseSchema2
24985
25001
  ]);
24986
25002
  });
24987
- // ../src/vault/broker/peercred.ts
24988
- function unlockSocketFor(dataSocketPath) {
24989
- if (dataSocketPath.endsWith("/sock")) {
24990
- return dataSocketPath.slice(0, -"/sock".length) + "/unlock";
24991
- }
24992
- return dataSocketPath.replace(/\.sock$/, ".unlock.sock");
24993
- }
24994
- var RESERVED_AGENT_NAMES;
24995
- var init_peercred = __esm(() => {
24996
- RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
24997
- });
24998
25003
 
24999
25004
  // ../src/runtime-mode.ts
25000
25005
  function isDockerRuntime() {
@@ -43194,7 +43199,7 @@ var GetStatusRequestSchema = exports_external.object({
43194
43199
  target_request_id: exports_external.string().min(1).max(128)
43195
43200
  })
43196
43201
  });
43197
- var AgentNameSchema = exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, "agent name must be kebab-case ASCII");
43202
+ var AgentNameSchema2 = exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, "agent name must be kebab-case ASCII");
43198
43203
  var UpdateCheckRequestSchema = exports_external.object({
43199
43204
  ...RequestEnvelope,
43200
43205
  op: exports_external.literal("update_check"),
@@ -43219,21 +43224,21 @@ var AgentStartRequestSchema = exports_external.object({
43219
43224
  ...RequestEnvelope,
43220
43225
  op: exports_external.literal("agent_start"),
43221
43226
  args: exports_external.object({
43222
- name: AgentNameSchema
43227
+ name: AgentNameSchema2
43223
43228
  })
43224
43229
  });
43225
43230
  var AgentStopRequestSchema = exports_external.object({
43226
43231
  ...RequestEnvelope,
43227
43232
  op: exports_external.literal("agent_stop"),
43228
43233
  args: exports_external.object({
43229
- name: AgentNameSchema
43234
+ name: AgentNameSchema2
43230
43235
  })
43231
43236
  });
43232
43237
  var AgentLogsRequestSchema = exports_external.object({
43233
43238
  ...RequestEnvelope,
43234
43239
  op: exports_external.literal("agent_logs"),
43235
43240
  args: exports_external.object({
43236
- name: AgentNameSchema,
43241
+ name: AgentNameSchema2,
43237
43242
  tail: exports_external.number().int().positive().max(2000).optional()
43238
43243
  })
43239
43244
  });
@@ -43241,7 +43246,7 @@ var AgentExecRequestSchema = exports_external.object({
43241
43246
  ...RequestEnvelope,
43242
43247
  op: exports_external.literal("agent_exec"),
43243
43248
  args: exports_external.object({
43244
- name: AgentNameSchema,
43249
+ name: AgentNameSchema2,
43245
43250
  argv: exports_external.array(exports_external.string().min(1)).min(1).max(32)
43246
43251
  })
43247
43252
  });
@@ -43254,7 +43259,7 @@ var AgentSmokeRequestSchema = exports_external.object({
43254
43259
  ...RequestEnvelope,
43255
43260
  op: exports_external.literal("agent_smoke"),
43256
43261
  args: exports_external.object({
43257
- name: AgentNameSchema,
43262
+ name: AgentNameSchema2,
43258
43263
  deep: exports_external.boolean().optional()
43259
43264
  })
43260
43265
  });
@@ -48323,10 +48328,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
48323
48328
  }
48324
48329
 
48325
48330
  // ../src/build-info.ts
48326
- var VERSION = "0.13.24";
48327
- var COMMIT_SHA = "9bfca233";
48328
- var COMMIT_DATE = "2026-05-24T03:50:31Z";
48329
- var LATEST_PR = 1708;
48331
+ var VERSION = "0.13.26";
48332
+ var COMMIT_SHA = "b2767bb7";
48333
+ var COMMIT_DATE = "2026-05-24T07:35:41Z";
48334
+ var LATEST_PR = 1723;
48330
48335
  var COMMITS_AHEAD_OF_TAG = 0;
48331
48336
 
48332
48337
  // gateway/boot-version.ts
@@ -32,8 +32,8 @@ describe('markdownToHtml', () => {
32
32
  expect(markdownToHtml('Hello _world_')).toContain('<i>world</i>')
33
33
  })
34
34
 
35
- test('converts emoji-leading _📥 queued as a new task_', () => {
36
- expect(markdownToHtml('_📥 queued as a new task_')).toContain('<i>📥 queued as a new task</i>')
35
+ test('converts emoji-leading _📥 Queued as a new task_', () => {
36
+ expect(markdownToHtml('_📥 Queued as a new task_')).toContain('<i>📥 Queued as a new task</i>')
37
37
  })
38
38
 
39
39
  test('converts emoji-trailing _steer on the prior task 🔁_', () => {
@@ -45,7 +45,7 @@ describe("uat: rapid follow-ups — steering vs queued classification", () => {
45
45
 
46
46
  // The agent should reply mentioning md5 AND surface the italic
47
47
  // classification line per the prompt
48
- // ("_↪️ treating as steer on the prior task_" or similar).
48
+ // ("_↪️ Treating as steer on the prior task_" or similar).
49
49
  // We match either explicit-steer narration OR the steer emoji
50
50
  // (`↪️`) to allow for natural-language variation while still
51
51
  // failing if no narration appears (the previous version of