switchroom 0.18.12 → 0.18.14
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 +57 -9
- package/dist/auth-broker/index.js +174 -72
- package/dist/cli/autoaccept-poll.js +23 -0
- package/dist/cli/drive-write-pretool.mjs +24 -1
- package/dist/cli/foreground-hog-pretool.mjs +264 -0
- package/dist/cli/ms-365-write-pretool.mjs +31 -8
- package/dist/cli/notion-write-pretool.mjs +9 -2
- package/dist/cli/skill-validate-pretool.mjs +144 -2847
- package/dist/cli/switchroom.js +986 -3131
- package/dist/host-control/main.js +216 -2863
- package/dist/vault/approvals/kernel-server.js +67 -1
- package/dist/vault/broker/server.js +98 -45
- package/package.json +1 -1
- package/profiles/coding/CLAUDE.md.hbs +2 -0
- package/profiles/default/CLAUDE.md.hbs +2 -0
- package/skills/switchroom-architecture/telegram.md +0 -1
- package/telegram-plugin/auth-snapshot-format.ts +37 -5
- package/telegram-plugin/auto-fallback-fleet.ts +29 -1
- package/telegram-plugin/bridge/bridge.ts +2 -0
- package/telegram-plugin/dist/bridge/bridge.js +51 -3
- package/telegram-plugin/dist/gateway/gateway.js +1251 -2368
- package/telegram-plugin/dist/server.js +67 -3
- package/telegram-plugin/format.ts +19 -0
- package/telegram-plugin/gateway/approval-hold.ts +21 -2
- package/telegram-plugin/gateway/auth-broker-client.ts +1 -0
- package/telegram-plugin/gateway/auth-command.ts +14 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +12 -0
- package/telegram-plugin/gateway/forward-origin.ts +235 -0
- package/telegram-plugin/gateway/gateway.ts +445 -83
- package/telegram-plugin/gateway/throttle-tier-wiring.ts +268 -0
- package/telegram-plugin/history.ts +106 -6
- package/telegram-plugin/inline-keyboard-callbacks.ts +94 -0
- package/telegram-plugin/model-unavailable.ts +61 -13
- package/telegram-plugin/outbound-field-redact.ts +69 -0
- package/telegram-plugin/render/render.ts +32 -14
- package/telegram-plugin/render/rich-render.ts +40 -32
- package/telegram-plugin/scoped-approval.ts +11 -2
- package/telegram-plugin/secret-detect/chunker.ts +18 -4
- package/telegram-plugin/secret-detect/index.ts +12 -56
- package/telegram-plugin/send-gate-degraded.test.ts +131 -0
- package/telegram-plugin/send-gate.test.ts +25 -6
- package/telegram-plugin/send-gate.ts +82 -8
- package/telegram-plugin/session-tail.ts +82 -7
- package/telegram-plugin/stream-controller.ts +3 -2
- package/telegram-plugin/subagent-watcher.ts +71 -16
- package/telegram-plugin/tests/approval-hold-outcome.test.ts +36 -5
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +72 -0
- package/telegram-plugin/tests/callback-query-handlers.test.ts +65 -0
- package/telegram-plugin/tests/forward-origin.test.ts +309 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +57 -0
- package/telegram-plugin/tests/history.test.ts +272 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +5 -1
- package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +164 -0
- package/telegram-plugin/tests/operator-events-session-tail.test.ts +74 -0
- package/telegram-plugin/tests/outbound-field-redact.test.ts +107 -0
- package/telegram-plugin/tests/reaction-gate-routing.test.ts +173 -0
- package/telegram-plugin/tests/render/render-outbound-chunks.test.ts +6 -4
- package/telegram-plugin/tests/render/render.test.ts +88 -0
- package/telegram-plugin/tests/render/rich-render.test.ts +41 -22
- package/telegram-plugin/tests/scoped-approval.test.ts +27 -0
- package/telegram-plugin/tests/secret-detect-chunk-overlap.test.ts +65 -0
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +5 -4
- package/telegram-plugin/tests/session-tail-sidecar-reap.test.ts +268 -0
- package/telegram-plugin/tests/single-mode-stream-reply.test.ts +5 -3
- package/telegram-plugin/tests/status-accent.test.ts +5 -3
- package/telegram-plugin/tests/stream-controller-chunk-cap.test.ts +20 -20
- package/telegram-plugin/tests/stream-reply-handler.test.ts +5 -2
- package/telegram-plugin/tests/subagent-watcher-fd-leak.test.ts +275 -0
- package/telegram-plugin/tests/throttle-tier-wiring.test.ts +290 -0
- package/telegram-plugin/tests/throttle-tier.test.ts +278 -0
- package/telegram-plugin/tests/worktree-watch-cwds.test.ts +215 -1
- package/telegram-plugin/throttle-tier.ts +226 -0
- package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +8 -7
- package/telegram-plugin/worktree-watch-cwds.ts +194 -5
- package/telegram-plugin/secret-detect/secretlint-source.ts +0 -95
- package/telegram-plugin/tests/secret-detect-secretlint.test.ts +0 -105
|
@@ -4244,7 +4244,6 @@ var init_schema = __esm(() => {
|
|
|
4244
4244
|
enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
|
|
4245
4245
|
plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' — the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
|
|
4246
4246
|
format: exports_external.enum(["html", "markdownv2", "text"]).optional().describe("Default reply format passed to the plugin"),
|
|
4247
|
-
rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
|
|
4248
4247
|
stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI — " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events — stable " + "order, per-tool status emojis, fires only on semantic transitions."),
|
|
4249
4248
|
stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive in-place stream edits " + "during a turn. Lower = more responsive stream, higher = fewer API " + "calls. Floored at 250 by draft-stream itself. Default 400 ms for DMs " + "and 1000 ms for groups/forums (respects Telegram's ~1 edit/sec/message " + "practical ceiling). Override per-agent if a particular agent needs " + "snappier or quieter streaming."),
|
|
4250
4249
|
clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message — Reading X, Searching the web for Y, …) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) — no post-then-delete. Per-agent " + "override; cascades defaults → profile → agent (per-key)."),
|
|
@@ -4778,6 +4777,13 @@ var init_schema = __esm(() => {
|
|
|
4778
4777
|
});
|
|
4779
4778
|
});
|
|
4780
4779
|
|
|
4780
|
+
// src/util/atomic.ts
|
|
4781
|
+
import { closeSync, constants, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
|
|
4782
|
+
var TMP_OPEN_FLAGS;
|
|
4783
|
+
var init_atomic = __esm(() => {
|
|
4784
|
+
TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
|
|
4785
|
+
});
|
|
4786
|
+
|
|
4781
4787
|
// src/config/merge.ts
|
|
4782
4788
|
function dedupe(items) {
|
|
4783
4789
|
const seen = new Set;
|
|
@@ -18249,6 +18255,13 @@ var ApprovalLookupRequestSchema = exports_external.object({
|
|
|
18249
18255
|
action: exports_external.string().min(1),
|
|
18250
18256
|
current_approver_set: exports_external.array(exports_external.string())
|
|
18251
18257
|
});
|
|
18258
|
+
var ApprovalLookupByRequestRequestSchema = exports_external.object({
|
|
18259
|
+
v: exports_external.literal(1),
|
|
18260
|
+
op: exports_external.literal("approval_lookup_by_request"),
|
|
18261
|
+
agent_unit: exports_external.string().min(1),
|
|
18262
|
+
request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
|
|
18263
|
+
current_approver_set: exports_external.array(exports_external.string())
|
|
18264
|
+
});
|
|
18252
18265
|
var ApprovalConsumeRequestSchema = exports_external.object({
|
|
18253
18266
|
v: exports_external.literal(1),
|
|
18254
18267
|
op: exports_external.literal("approval_consume"),
|
|
@@ -18303,6 +18316,7 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
|
|
|
18303
18316
|
RevokeGrantRequestSchema,
|
|
18304
18317
|
ApprovalRequestRequestSchema,
|
|
18305
18318
|
ApprovalLookupRequestSchema,
|
|
18319
|
+
ApprovalLookupByRequestRequestSchema,
|
|
18306
18320
|
ApprovalConsumeRequestSchema,
|
|
18307
18321
|
ApprovalRevokeRequestSchema,
|
|
18308
18322
|
ApprovalListRequestSchema,
|
|
@@ -18664,6 +18678,30 @@ function lookupDecision(db, query, opts = {}, now = Date.now()) {
|
|
|
18664
18678
|
ORDER BY granted_at DESC LIMIT 1`).get(query.agent_unit, query.scope, query.action);
|
|
18665
18679
|
if (!row)
|
|
18666
18680
|
return { state: "no_decision" };
|
|
18681
|
+
return evaluateDecisionRow(db, row, currentCanonical, opts, now);
|
|
18682
|
+
}
|
|
18683
|
+
function lookupDecisionByRequestId(db, query, opts = {}, now = Date.now()) {
|
|
18684
|
+
const currentCanonical = canonicalizeApproverSet(query.current_approver_set);
|
|
18685
|
+
const nonceRow = db.query(`SELECT * FROM approval_nonces WHERE request_id = ?`).get(query.request_id);
|
|
18686
|
+
if (!nonceRow)
|
|
18687
|
+
return { state: "no_decision" };
|
|
18688
|
+
const nonce = nonceFromRow(nonceRow);
|
|
18689
|
+
if (nonce.agent_unit !== query.agent_unit)
|
|
18690
|
+
return { state: "no_decision" };
|
|
18691
|
+
if (nonce.decision_id === null) {
|
|
18692
|
+
if (nonce.expires_at < now)
|
|
18693
|
+
return { state: "expired" };
|
|
18694
|
+
return { state: "pending", request_id: nonce.request_id };
|
|
18695
|
+
}
|
|
18696
|
+
const decRow = db.query(`SELECT * FROM approval_decisions WHERE id = ?`).get(nonce.decision_id);
|
|
18697
|
+
if (!decRow)
|
|
18698
|
+
return { state: "expired" };
|
|
18699
|
+
if (decRow.revoked_at != null) {
|
|
18700
|
+
return { state: "denied", decision: rowToDecision(decRow) };
|
|
18701
|
+
}
|
|
18702
|
+
return evaluateDecisionRow(db, decRow, currentCanonical, opts, now);
|
|
18703
|
+
}
|
|
18704
|
+
function evaluateDecisionRow(db, row, currentCanonical, opts, now) {
|
|
18667
18705
|
const decision = rowToDecision(row);
|
|
18668
18706
|
if (decision.decision === "deny_perm") {
|
|
18669
18707
|
return { state: "denied", decision };
|
|
@@ -18860,6 +18898,7 @@ init_schema();
|
|
|
18860
18898
|
|
|
18861
18899
|
// src/agents/scaffold.ts
|
|
18862
18900
|
import { join as join4, resolve as resolve5 } from "node:path";
|
|
18901
|
+
init_atomic();
|
|
18863
18902
|
init_schema();
|
|
18864
18903
|
|
|
18865
18904
|
// src/config/users.ts
|
|
@@ -19301,6 +19340,33 @@ function handleRequest(socket, req, agent, db, peerUid, isOperator = false) {
|
|
|
19301
19340
|
socket.write(encodeResponse({ ok: true, state: r.state, decision }));
|
|
19302
19341
|
return;
|
|
19303
19342
|
}
|
|
19343
|
+
if (req.op === "approval_lookup_by_request") {
|
|
19344
|
+
const acl = checkApprovalAclByAgent(agent, req.agent_unit);
|
|
19345
|
+
if (!acl.allow) {
|
|
19346
|
+
socket.write(encodeResponse(errorResponse("DENIED", acl.reason)));
|
|
19347
|
+
return;
|
|
19348
|
+
}
|
|
19349
|
+
const r = lookupDecisionByRequestId(db, {
|
|
19350
|
+
agent_unit: req.agent_unit,
|
|
19351
|
+
request_id: req.request_id,
|
|
19352
|
+
current_approver_set: req.current_approver_set
|
|
19353
|
+
});
|
|
19354
|
+
const decision = r.state === "granted" || r.state === "denied" ? {
|
|
19355
|
+
id: r.decision.id,
|
|
19356
|
+
agent_unit: r.decision.agent_unit,
|
|
19357
|
+
scope: r.decision.scope,
|
|
19358
|
+
action: r.decision.action,
|
|
19359
|
+
decision: r.decision.decision,
|
|
19360
|
+
granted_at: r.decision.granted_at,
|
|
19361
|
+
granted_by_user_id: r.decision.granted_by_user_id,
|
|
19362
|
+
ttl_expires_at: r.decision.ttl_expires_at,
|
|
19363
|
+
last_used_at: r.decision.last_used_at,
|
|
19364
|
+
revoked_at: r.decision.revoked_at,
|
|
19365
|
+
revoke_reason: r.decision.revoke_reason
|
|
19366
|
+
} : null;
|
|
19367
|
+
socket.write(encodeResponse({ ok: true, state: r.state, decision }));
|
|
19368
|
+
return;
|
|
19369
|
+
}
|
|
19304
19370
|
if (req.op === "approval_consume") {
|
|
19305
19371
|
const peek = getNonce(db, req.request_id);
|
|
19306
19372
|
if (peek !== null) {
|
|
@@ -4244,7 +4244,6 @@ var init_schema = __esm(() => {
|
|
|
4244
4244
|
enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
|
|
4245
4245
|
plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' — the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
|
|
4246
4246
|
format: exports_external.enum(["html", "markdownv2", "text"]).optional().describe("Default reply format passed to the plugin"),
|
|
4247
|
-
rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
|
|
4248
4247
|
stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI — " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events — stable " + "order, per-tool status emojis, fires only on semantic transitions."),
|
|
4249
4248
|
stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive in-place stream edits " + "during a turn. Lower = more responsive stream, higher = fewer API " + "calls. Floored at 250 by draft-stream itself. Default 400 ms for DMs " + "and 1000 ms for groups/forums (respects Telegram's ~1 edit/sec/message " + "practical ceiling). Override per-agent if a particular agent needs " + "snappier or quieter streaming."),
|
|
4250
4249
|
clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message — Reading X, Searching the web for Y, …) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) — no post-then-delete. Per-agent " + "override; cascades defaults → profile → agent (per-key)."),
|
|
@@ -4778,6 +4777,13 @@ var init_schema = __esm(() => {
|
|
|
4778
4777
|
});
|
|
4779
4778
|
});
|
|
4780
4779
|
|
|
4780
|
+
// src/util/atomic.ts
|
|
4781
|
+
import { closeSync, constants, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
|
|
4782
|
+
var TMP_OPEN_FLAGS;
|
|
4783
|
+
var init_atomic = __esm(() => {
|
|
4784
|
+
TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
|
|
4785
|
+
});
|
|
4786
|
+
|
|
4781
4787
|
// src/config/merge.ts
|
|
4782
4788
|
function dedupe(items) {
|
|
4783
4789
|
const seen = new Set;
|
|
@@ -18087,7 +18093,7 @@ var require_lib = __commonJS((exports, module) => {
|
|
|
18087
18093
|
|
|
18088
18094
|
// src/vault/broker/server.ts
|
|
18089
18095
|
import * as net from "node:net";
|
|
18090
|
-
import { mkdirSync as mkdirSync6, chmodSync as chmodSync4, chownSync, existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync4, statSync as statSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync3, renameSync as
|
|
18096
|
+
import { mkdirSync as mkdirSync6, chmodSync as chmodSync4, chownSync, existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync4, statSync as statSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync3, renameSync as renameSync7 } from "node:fs";
|
|
18091
18097
|
|
|
18092
18098
|
// src/agents/compose.ts
|
|
18093
18099
|
import { createHash } from "node:crypto";
|
|
@@ -18095,6 +18101,7 @@ init_schema();
|
|
|
18095
18101
|
|
|
18096
18102
|
// src/agents/scaffold.ts
|
|
18097
18103
|
import { join as join4, resolve as resolve6 } from "node:path";
|
|
18104
|
+
init_atomic();
|
|
18098
18105
|
init_schema();
|
|
18099
18106
|
|
|
18100
18107
|
// src/config/users.ts
|
|
@@ -18424,14 +18431,14 @@ init_loader();
|
|
|
18424
18431
|
import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
|
|
18425
18432
|
import {
|
|
18426
18433
|
readFileSync as readFileSync5,
|
|
18427
|
-
writeSync as
|
|
18434
|
+
writeSync as writeSync3,
|
|
18428
18435
|
existsSync as existsSync6,
|
|
18429
|
-
renameSync,
|
|
18436
|
+
renameSync as renameSync2,
|
|
18430
18437
|
mkdirSync as mkdirSync2,
|
|
18431
18438
|
unlinkSync as unlinkSync2,
|
|
18432
|
-
fsyncSync as
|
|
18433
|
-
openSync as
|
|
18434
|
-
closeSync as
|
|
18439
|
+
fsyncSync as fsyncSync3,
|
|
18440
|
+
openSync as openSync3,
|
|
18441
|
+
closeSync as closeSync3,
|
|
18435
18442
|
lstatSync,
|
|
18436
18443
|
realpathSync as realpathSync2
|
|
18437
18444
|
} from "node:fs";
|
|
@@ -18440,10 +18447,10 @@ import { dirname, basename as basename2, resolve as resolve5 } from "node:path";
|
|
|
18440
18447
|
// src/vault/flock.ts
|
|
18441
18448
|
import {
|
|
18442
18449
|
existsSync as existsSync5,
|
|
18443
|
-
openSync,
|
|
18444
|
-
closeSync,
|
|
18445
|
-
writeSync,
|
|
18446
|
-
fsyncSync,
|
|
18450
|
+
openSync as openSync2,
|
|
18451
|
+
closeSync as closeSync2,
|
|
18452
|
+
writeSync as writeSync2,
|
|
18453
|
+
fsyncSync as fsyncSync2,
|
|
18447
18454
|
unlinkSync,
|
|
18448
18455
|
readFileSync as readFileSync4,
|
|
18449
18456
|
readdirSync as readdirSync3,
|
|
@@ -18568,7 +18575,7 @@ function acquireLock(vaultPath, options = {}) {
|
|
|
18568
18575
|
while (true) {
|
|
18569
18576
|
let fd = null;
|
|
18570
18577
|
try {
|
|
18571
|
-
fd =
|
|
18578
|
+
fd = openSync2(lockPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 384);
|
|
18572
18579
|
} catch (err) {
|
|
18573
18580
|
const code = err?.code ?? "";
|
|
18574
18581
|
if (code === "EEXIST") {
|
|
@@ -18604,11 +18611,11 @@ function acquireLock(vaultPath, options = {}) {
|
|
|
18604
18611
|
${Date.now()}
|
|
18605
18612
|
${process.argv[1] ?? ""}
|
|
18606
18613
|
`;
|
|
18607
|
-
|
|
18608
|
-
|
|
18614
|
+
writeSync2(fd, meta);
|
|
18615
|
+
fsyncSync2(fd);
|
|
18609
18616
|
} catch {
|
|
18610
18617
|
try {
|
|
18611
|
-
|
|
18618
|
+
closeSync2(fd);
|
|
18612
18619
|
} catch {}
|
|
18613
18620
|
try {
|
|
18614
18621
|
unlinkSync(lockPath);
|
|
@@ -18619,7 +18626,7 @@ ${process.argv[1] ?? ""}
|
|
|
18619
18626
|
return {
|
|
18620
18627
|
release: () => {
|
|
18621
18628
|
try {
|
|
18622
|
-
|
|
18629
|
+
closeSync2(ownedFd);
|
|
18623
18630
|
} catch {}
|
|
18624
18631
|
try {
|
|
18625
18632
|
unlinkSync(lockPath);
|
|
@@ -18675,20 +18682,20 @@ function atomicWriteFileSync(path, data, mode) {
|
|
|
18675
18682
|
const dir = dirname(resolve5(effectivePath));
|
|
18676
18683
|
const tmp = resolve5(dir, `.${basename2(effectivePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
18677
18684
|
try {
|
|
18678
|
-
const fd =
|
|
18685
|
+
const fd = openSync3(tmp, "wx", mode);
|
|
18679
18686
|
try {
|
|
18680
|
-
|
|
18681
|
-
|
|
18687
|
+
writeSync3(fd, data);
|
|
18688
|
+
fsyncSync3(fd);
|
|
18682
18689
|
} finally {
|
|
18683
|
-
|
|
18690
|
+
closeSync3(fd);
|
|
18684
18691
|
}
|
|
18685
|
-
|
|
18692
|
+
renameSync2(tmp, effectivePath);
|
|
18686
18693
|
try {
|
|
18687
|
-
const dirFd =
|
|
18694
|
+
const dirFd = openSync3(dir, "r");
|
|
18688
18695
|
try {
|
|
18689
|
-
|
|
18696
|
+
fsyncSync3(dirFd);
|
|
18690
18697
|
} finally {
|
|
18691
|
-
|
|
18698
|
+
closeSync3(dirFd);
|
|
18692
18699
|
}
|
|
18693
18700
|
} catch {}
|
|
18694
18701
|
} catch (err) {
|
|
@@ -19264,6 +19271,13 @@ var ApprovalLookupRequestSchema = exports_external.object({
|
|
|
19264
19271
|
action: exports_external.string().min(1),
|
|
19265
19272
|
current_approver_set: exports_external.array(exports_external.string())
|
|
19266
19273
|
});
|
|
19274
|
+
var ApprovalLookupByRequestRequestSchema = exports_external.object({
|
|
19275
|
+
v: exports_external.literal(1),
|
|
19276
|
+
op: exports_external.literal("approval_lookup_by_request"),
|
|
19277
|
+
agent_unit: exports_external.string().min(1),
|
|
19278
|
+
request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
|
|
19279
|
+
current_approver_set: exports_external.array(exports_external.string())
|
|
19280
|
+
});
|
|
19267
19281
|
var ApprovalConsumeRequestSchema = exports_external.object({
|
|
19268
19282
|
v: exports_external.literal(1),
|
|
19269
19283
|
op: exports_external.literal("approval_consume"),
|
|
@@ -19318,6 +19332,7 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
|
|
|
19318
19332
|
RevokeGrantRequestSchema,
|
|
19319
19333
|
ApprovalRequestRequestSchema,
|
|
19320
19334
|
ApprovalLookupRequestSchema,
|
|
19335
|
+
ApprovalLookupByRequestRequestSchema,
|
|
19321
19336
|
ApprovalConsumeRequestSchema,
|
|
19322
19337
|
ApprovalRevokeRequestSchema,
|
|
19323
19338
|
ApprovalListRequestSchema,
|
|
@@ -19570,13 +19585,13 @@ import {
|
|
|
19570
19585
|
copyFileSync as copyFileSync2,
|
|
19571
19586
|
chmodSync,
|
|
19572
19587
|
existsSync as existsSync7,
|
|
19573
|
-
fsyncSync as
|
|
19588
|
+
fsyncSync as fsyncSync4,
|
|
19574
19589
|
lstatSync as lstatSync2,
|
|
19575
19590
|
mkdirSync as mkdirSync3,
|
|
19576
|
-
openSync as
|
|
19577
|
-
closeSync as
|
|
19591
|
+
openSync as openSync4,
|
|
19592
|
+
closeSync as closeSync4,
|
|
19578
19593
|
readFileSync as readFileSync7,
|
|
19579
|
-
renameSync as
|
|
19594
|
+
renameSync as renameSync3,
|
|
19580
19595
|
statSync as statSync4,
|
|
19581
19596
|
symlinkSync,
|
|
19582
19597
|
unlinkSync as unlinkSync3
|
|
@@ -19642,7 +19657,7 @@ function runMigration(home2, opts) {
|
|
|
19642
19657
|
copyFileSync2(oldPath, tempNew);
|
|
19643
19658
|
chmodSync(tempNew, 384);
|
|
19644
19659
|
fsyncFile(tempNew);
|
|
19645
|
-
|
|
19660
|
+
renameSync3(tempNew, newPath);
|
|
19646
19661
|
fsyncDir(parent);
|
|
19647
19662
|
atomicReplaceWithSymlink(oldPath, "vault/vault.enc");
|
|
19648
19663
|
fsyncDir(switchroomRoot);
|
|
@@ -19676,22 +19691,22 @@ function atomicReplaceWithSymlink(target, linkTarget) {
|
|
|
19676
19691
|
} catch {}
|
|
19677
19692
|
}
|
|
19678
19693
|
symlinkSync(linkTarget, tmp);
|
|
19679
|
-
|
|
19694
|
+
renameSync3(tmp, target);
|
|
19680
19695
|
}
|
|
19681
19696
|
function fsyncFile(path) {
|
|
19682
|
-
const fd =
|
|
19697
|
+
const fd = openSync4(path, "r+");
|
|
19683
19698
|
try {
|
|
19684
|
-
|
|
19699
|
+
fsyncSync4(fd);
|
|
19685
19700
|
} finally {
|
|
19686
|
-
|
|
19701
|
+
closeSync4(fd);
|
|
19687
19702
|
}
|
|
19688
19703
|
}
|
|
19689
19704
|
function fsyncDir(path) {
|
|
19690
|
-
const fd =
|
|
19705
|
+
const fd = openSync4(path, "r");
|
|
19691
19706
|
try {
|
|
19692
|
-
|
|
19707
|
+
fsyncSync4(fd);
|
|
19693
19708
|
} finally {
|
|
19694
|
-
|
|
19709
|
+
closeSync4(fd);
|
|
19695
19710
|
}
|
|
19696
19711
|
}
|
|
19697
19712
|
|
|
@@ -19702,15 +19717,15 @@ init_loader();
|
|
|
19702
19717
|
import { createHmac, randomBytes as randomBytes2, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2 } from "node:crypto";
|
|
19703
19718
|
import {
|
|
19704
19719
|
chmodSync as chmodSync2,
|
|
19705
|
-
closeSync as
|
|
19720
|
+
closeSync as closeSync5,
|
|
19706
19721
|
existsSync as existsSync8,
|
|
19707
|
-
fsyncSync as
|
|
19722
|
+
fsyncSync as fsyncSync5,
|
|
19708
19723
|
mkdirSync as mkdirSync4,
|
|
19709
|
-
openSync as
|
|
19724
|
+
openSync as openSync5,
|
|
19710
19725
|
readFileSync as readFileSync8,
|
|
19711
|
-
renameSync as
|
|
19726
|
+
renameSync as renameSync4,
|
|
19712
19727
|
unlinkSync as unlinkSync4,
|
|
19713
|
-
writeSync as
|
|
19728
|
+
writeSync as writeSync4
|
|
19714
19729
|
} from "node:fs";
|
|
19715
19730
|
var FORMAT_VERSION = 1;
|
|
19716
19731
|
var SALT_LEN = 16;
|
|
@@ -19803,7 +19818,7 @@ import * as path from "node:path";
|
|
|
19803
19818
|
|
|
19804
19819
|
// src/util/audit-hashchain.ts
|
|
19805
19820
|
import { createHash as createHash4 } from "node:crypto";
|
|
19806
|
-
import { openSync as
|
|
19821
|
+
import { openSync as openSync6, readSync, fstatSync as fstatSync2, closeSync as closeSync6 } from "node:fs";
|
|
19807
19822
|
var CHAIN_GENESIS = "GENESIS";
|
|
19808
19823
|
var SEP = "\x00";
|
|
19809
19824
|
function rowHash(prev, seq, rowJsonNoChain) {
|
|
@@ -19824,7 +19839,7 @@ function chainRow(state, entry) {
|
|
|
19824
19839
|
function readTail(path, tailBytes = 64 * 1024) {
|
|
19825
19840
|
let fd;
|
|
19826
19841
|
try {
|
|
19827
|
-
fd =
|
|
19842
|
+
fd = openSync6(path, "r");
|
|
19828
19843
|
} catch {
|
|
19829
19844
|
return null;
|
|
19830
19845
|
}
|
|
@@ -19840,7 +19855,7 @@ function readTail(path, tailBytes = 64 * 1024) {
|
|
|
19840
19855
|
return null;
|
|
19841
19856
|
} finally {
|
|
19842
19857
|
try {
|
|
19843
|
-
|
|
19858
|
+
closeSync6(fd);
|
|
19844
19859
|
} catch {}
|
|
19845
19860
|
}
|
|
19846
19861
|
}
|
|
@@ -22196,6 +22211,9 @@ function lookupDecision(db, query, opts = {}, now = Date.now()) {
|
|
|
22196
22211
|
ORDER BY granted_at DESC LIMIT 1`).get(query.agent_unit, query.scope, query.action);
|
|
22197
22212
|
if (!row)
|
|
22198
22213
|
return { state: "no_decision" };
|
|
22214
|
+
return evaluateDecisionRow(db, row, currentCanonical, opts, now);
|
|
22215
|
+
}
|
|
22216
|
+
function evaluateDecisionRow(db, row, currentCanonical, opts, now) {
|
|
22199
22217
|
const decision = rowToDecision(row);
|
|
22200
22218
|
if (decision.decision === "deny_perm") {
|
|
22201
22219
|
return { state: "denied", decision };
|
|
@@ -23203,6 +23221,41 @@ class VaultBroker {
|
|
|
23203
23221
|
if (req.token !== undefined && req.token !== "") {
|
|
23204
23222
|
const v = await validateGrantForWrite(this.grantsDb, req.token, req.key);
|
|
23205
23223
|
if (v.ok) {
|
|
23224
|
+
const writeGrantAgentSlug = v.grant.agent_slug;
|
|
23225
|
+
if (agentName !== null && writeGrantAgentSlug !== agentName) {
|
|
23226
|
+
this.auditLogger.write({
|
|
23227
|
+
ts: new Date().toISOString(),
|
|
23228
|
+
op: "put",
|
|
23229
|
+
key: req.key,
|
|
23230
|
+
caller: auditCaller,
|
|
23231
|
+
pid: auditPid,
|
|
23232
|
+
cgroup: auditCgroup,
|
|
23233
|
+
result: "denied:grant-identity-mismatch",
|
|
23234
|
+
method: "grant",
|
|
23235
|
+
grant_id: v.grant.id
|
|
23236
|
+
});
|
|
23237
|
+
socket.write(encodeResponse(errorResponse("DENIED", "grant-identity-mismatch")));
|
|
23238
|
+
return;
|
|
23239
|
+
}
|
|
23240
|
+
const existingForScope = this.secrets[req.key];
|
|
23241
|
+
if (existingForScope !== undefined) {
|
|
23242
|
+
const writeScope = checkEntryScope(existingForScope.scope, writeGrantAgentSlug);
|
|
23243
|
+
if (!writeScope.allow) {
|
|
23244
|
+
this.auditLogger.write({
|
|
23245
|
+
ts: new Date().toISOString(),
|
|
23246
|
+
op: "put",
|
|
23247
|
+
key: req.key,
|
|
23248
|
+
caller: auditCaller,
|
|
23249
|
+
pid: auditPid,
|
|
23250
|
+
cgroup: auditCgroup,
|
|
23251
|
+
result: `denied:${writeScope.reason}`,
|
|
23252
|
+
method: "grant",
|
|
23253
|
+
grant_id: v.grant.id
|
|
23254
|
+
});
|
|
23255
|
+
socket.write(encodeResponse(errorResponse("DENIED", writeScope.reason)));
|
|
23256
|
+
return;
|
|
23257
|
+
}
|
|
23258
|
+
}
|
|
23206
23259
|
writeGrantId = v.grant.id;
|
|
23207
23260
|
} else if (v.reason === "grant-expired" || v.reason === "grant-revoked") {
|
|
23208
23261
|
this.auditLogger.write({
|
|
@@ -23550,7 +23603,7 @@ class VaultBroker {
|
|
|
23550
23603
|
const tokenPath = path3.join(tokenDir, ".vault-token");
|
|
23551
23604
|
const tmpPath = `${tokenPath}.tmp.${process.pid}`;
|
|
23552
23605
|
writeFileSync3(tmpPath, mintResult.token, { mode: 384 });
|
|
23553
|
-
|
|
23606
|
+
renameSync7(tmpPath, tokenPath);
|
|
23554
23607
|
try {
|
|
23555
23608
|
const uid = allocateAgentUid(agent);
|
|
23556
23609
|
chownSync(tokenPath, uid, uid);
|
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.14",
|
|
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": {
|
|
@@ -51,6 +51,8 @@ Save proactively: architecture decisions, codebase patterns, conventions, known
|
|
|
51
51
|
|
|
52
52
|
If sub-agents are configured, delegate implementation to `@worker` (background, own worktree) and research to `@researcher` (background). Keep your turns short — dispatch and acknowledge quickly so you stay available for the user.
|
|
53
53
|
|
|
54
|
+
If the user amends in-flight delegated work mid-turn, steer the running worker now (`SendMessage` to the worker by name, or by the agent id from its spawn result) instead of holding the update for handback — and say in your reply whether you folded the update into the running worker or queued it as a separate task; never classify silently. If unsure whether a message amends in-flight work, queue it and say so — queue is the default. If the steer lands too late (worker effectively done), say so and apply the update yourself.
|
|
55
|
+
|
|
54
56
|
{{#if schedule}}
|
|
55
57
|
## Schedule
|
|
56
58
|
You are running on a schedule: `{{schedule}}`. Use scheduled runs to check CI status, review open PRs, or surface stale issues.
|
|
@@ -92,6 +92,8 @@ The main session is for conversation. Execution belongs in sub-agents. Before ma
|
|
|
92
92
|
|
|
93
93
|
**Anti-patterns:** starting a task inline then realizing it's complex mid-way; doing 5+ tool calls "because it's almost done"; polling sub-agent status in a loop.
|
|
94
94
|
|
|
95
|
+
**Steer running workers — don't sit on amendments.** When a mid-turn user message amends work you've already delegated to a running sub-agent, forward the amendment to that worker now — `SendMessage` to the worker by name (or by the agent id from the Agent tool's spawn result, if it has no name) — rather than holding it for handback. If the message is a new independent task, queue it instead; if you're unsure whether it amends the in-flight work, queue it and say so — queue is the default. Either way, state in your reply which you did ("folded your update into the running worker" / "queued as a separate task after the current one") — the steer-or-queue rule is that the classification is always visible in chat, never inferred. If the steer arrives too late (the worker is effectively done), say that and apply the update yourself in the parent.
|
|
96
|
+
|
|
95
97
|
If no sub-agents are configured, do the work yourself.
|
|
96
98
|
|
|
97
99
|
## Session Continuity
|
|
@@ -81,7 +81,6 @@ agents:
|
|
|
81
81
|
| Var | Source | Purpose |
|
|
82
82
|
|-----|--------|---------|
|
|
83
83
|
| `SWITCHROOM_TG_FORMAT` | `channels.telegram.format` | Default reply format |
|
|
84
|
-
| `SWITCHROOM_TG_RATE_LIMIT_MS` | `channels.telegram.rate_limit_ms` | Min delay between outgoing messages |
|
|
85
84
|
| `TELEGRAM_STATE_DIR` | Auto-set by scaffold | Path to `telegram/` dir |
|
|
86
85
|
| `SWITCHROOM_AGENT_NAME` | Auto-set by scaffold | Agent name (used for self-restart detection) |
|
|
87
86
|
| `SWITCHROOM_CONFIG` | Auto-set by scaffold | Path to switchroom.yaml |
|
|
@@ -704,6 +704,22 @@ export interface FallbackAnnouncementInput {
|
|
|
704
704
|
* shows the target's headroom and is unchanged.
|
|
705
705
|
*/
|
|
706
706
|
fleetSnapshots?: AccountSnapshot[];
|
|
707
|
+
/**
|
|
708
|
+
* 429 throttle tier enrichment — the reset time PARSED from the error
|
|
709
|
+
* prose ("resets 8:50am (TZ)" / "retry after 60s") that triggered the
|
|
710
|
+
* fallback. Fallback for the recovery line when the old account's probe
|
|
711
|
+
* carried no reset (probe failed / thin headers), so the announcement
|
|
712
|
+
* still names when the account frees. A probe-derived reset wins when
|
|
713
|
+
* present — it is the fresher, server-authoritative signal.
|
|
714
|
+
*/
|
|
715
|
+
parsedResetAt?: Date | null;
|
|
716
|
+
/**
|
|
717
|
+
* 429 throttle tier escalation — the trigger was a long-reset transient
|
|
718
|
+
* RATE LIMIT, not a quota wall. Names the headline honestly ("rate limit
|
|
719
|
+
* on X" instead of a utilization-derived "5-hour limit on X", which would
|
|
720
|
+
* be wrong: a rate-limited account's utilization is typically LOW).
|
|
721
|
+
*/
|
|
722
|
+
cause?: 'rate-limit';
|
|
707
723
|
tz?: string;
|
|
708
724
|
now?: Date;
|
|
709
725
|
}
|
|
@@ -728,7 +744,12 @@ export function renderFallbackAnnouncement(input: FallbackAnnouncementInput): st
|
|
|
728
744
|
const lines: string[] = [];
|
|
729
745
|
|
|
730
746
|
const limitWord = input.oldQuota ? limitWordFor(input.oldQuota) : 'quota';
|
|
731
|
-
const headerLimit =
|
|
747
|
+
const headerLimit =
|
|
748
|
+
input.cause === 'rate-limit'
|
|
749
|
+
? 'rate limit'
|
|
750
|
+
: limitWord === 'quota'
|
|
751
|
+
? 'quota cap'
|
|
752
|
+
: `${limitWord} limit`;
|
|
732
753
|
|
|
733
754
|
if (!input.newLabel) {
|
|
734
755
|
// All-blocked path — no swap occurred. Tell user what's broken and, so they
|
|
@@ -765,9 +786,14 @@ export function renderFallbackAnnouncement(input: FallbackAnnouncementInput): st
|
|
|
765
786
|
`${formatAbsolute(earliest.at, tz)} (in ${formatRelative(earliest.at, now)})`,
|
|
766
787
|
);
|
|
767
788
|
}
|
|
768
|
-
} else
|
|
789
|
+
} else {
|
|
769
790
|
// Back-compat: no fleet snapshot supplied → old single-account shape.
|
|
770
|
-
|
|
791
|
+
// Probe-derived reset first; the prose-parsed reset (429 throttle tier
|
|
792
|
+
// enrichment) covers the probe-failed case.
|
|
793
|
+
const recovery =
|
|
794
|
+
(input.oldQuota ? recoveryAtFor(input.oldQuota) : null) ??
|
|
795
|
+
input.parsedResetAt ??
|
|
796
|
+
null;
|
|
771
797
|
if (recovery) {
|
|
772
798
|
lines.push(
|
|
773
799
|
`${escapeMarkdown(input.oldLabel)} recovers ${formatAbsolute(recovery, tz)} ` +
|
|
@@ -794,8 +820,14 @@ export function renderFallbackAnnouncement(input: FallbackAnnouncementInput): st
|
|
|
794
820
|
lines.push(`Triggered by: agent **${escapeMarkdown(input.triggerAgent)}**`);
|
|
795
821
|
lines.push('');
|
|
796
822
|
|
|
797
|
-
|
|
798
|
-
|
|
823
|
+
{
|
|
824
|
+
// Probe-derived reset first; the prose-parsed reset (429 throttle tier
|
|
825
|
+
// enrichment) keeps the recovery line honest when the old account's
|
|
826
|
+
// probe failed at the moment of the wall.
|
|
827
|
+
const recovery =
|
|
828
|
+
(input.oldQuota ? recoveryAtFor(input.oldQuota) : null) ??
|
|
829
|
+
input.parsedResetAt ??
|
|
830
|
+
null;
|
|
799
831
|
if (recovery) {
|
|
800
832
|
lines.push(
|
|
801
833
|
`\`${codeSpanSafe(input.oldLabel)}\` recovers ` +
|
|
@@ -183,6 +183,26 @@ export interface FleetFallbackDeps {
|
|
|
183
183
|
/** Operator timezone for absolute reset times in the announcement. */
|
|
184
184
|
tz?: string;
|
|
185
185
|
now?: Date;
|
|
186
|
+
/**
|
|
187
|
+
* The reset time PARSED from the triggering error prose (429 throttle tier
|
|
188
|
+
* enrichment) — threaded into the announcement as the recovery-line
|
|
189
|
+
* fallback when the old account's live probe carried no reset.
|
|
190
|
+
*/
|
|
191
|
+
parsedResetAt?: Date;
|
|
192
|
+
/**
|
|
193
|
+
* 429 throttle tier escalation: the trigger is a TERMINAL transient 429
|
|
194
|
+
* whose parsed reset lies beyond the retry-in-place threshold. Its wording
|
|
195
|
+
* explicitly NEGATES the usage-limit reading, so the old account's
|
|
196
|
+
* UTILIZATION probe typically classifies healthy — the healthy-idempotency
|
|
197
|
+
* guard would self-cancel the swap ("probed healthy / Stale event?"),
|
|
198
|
+
* silently dropping the spec's ">threshold → mark + fail over" leg. When
|
|
199
|
+
* set, the terminal parsed-reset signal is trusted over the utilization
|
|
200
|
+
* probe: the guard is bypassed and the swap proceeds (the broker mark
|
|
201
|
+
* honors the caller-passed `until` — the parsed reset). Staleness safety
|
|
202
|
+
* holds upstream: the flag is only set for a terminal error line the
|
|
203
|
+
* session-tail just read, never for replayed/late events.
|
|
204
|
+
*/
|
|
205
|
+
rateLimitTrigger?: boolean;
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
/**
|
|
@@ -217,8 +237,12 @@ export async function runFleetAutoFallback(
|
|
|
217
237
|
// normalization uses the same clock as the rest of the decision (a default
|
|
218
238
|
// `new Date()` would diverge from `deps.now` and could mis-zero a window
|
|
219
239
|
// whose reset is still future relative to the event's clock).
|
|
240
|
+
// 429 throttle tier: a rate-limit trigger's wording NEGATES the usage-limit
|
|
241
|
+
// reading, so healthy utilization is the EXPECTED state, not evidence of a
|
|
242
|
+
// stale event — the guard must not self-cancel that swap (see
|
|
243
|
+
// FleetFallbackDeps.rateLimitTrigger).
|
|
220
244
|
const oldHealth = classifyHealth(oldSnap, now);
|
|
221
|
-
if (oldHealth === 'healthy') {
|
|
245
|
+
if (oldHealth === 'healthy' && !deps.rateLimitTrigger) {
|
|
222
246
|
return {
|
|
223
247
|
kind: 'no-eligible-target',
|
|
224
248
|
oldLabel: oldSnap.label,
|
|
@@ -255,6 +279,8 @@ export async function runFleetAutoFallback(
|
|
|
255
279
|
// card enumerates EVERY account (5h%/7d% + recovery ETA), letting the
|
|
256
280
|
// user verify the fleet is truly exhausted, not just the trigger account.
|
|
257
281
|
fleetSnapshots: snapshots,
|
|
282
|
+
parsedResetAt: deps.parsedResetAt ?? null,
|
|
283
|
+
cause: deps.rateLimitTrigger ? 'rate-limit' : undefined,
|
|
258
284
|
tz,
|
|
259
285
|
now,
|
|
260
286
|
}),
|
|
@@ -278,6 +304,8 @@ export async function runFleetAutoFallback(
|
|
|
278
304
|
newLabel: rolledTo,
|
|
279
305
|
newQuota,
|
|
280
306
|
triggerAgent: deps.triggerAgent,
|
|
307
|
+
parsedResetAt: deps.parsedResetAt ?? null,
|
|
308
|
+
cause: deps.rateLimitTrigger ? 'rate-limit' : undefined,
|
|
281
309
|
tz,
|
|
282
310
|
now,
|
|
283
311
|
}),
|
|
@@ -80,6 +80,8 @@ const mcp = new Server(
|
|
|
80
80
|
'',
|
|
81
81
|
'Messages from Telegram arrive as <channel source="telegram" chat_id="..." message_id="..." user="..." ts="...">. If the tag has an image_path attribute, Read that file — it is a photo the sender attached. If the tag has attachment_file_id, call download_attachment with that file_id to fetch the file, then Read the returned path. A single message may carry SEVERAL attachments (a forwarded album or a text+multi-image burst): when attachment_count is set (>1), also handle the numbered siblings — image_path_2, image_path_3, … (Read each) and attachment_file_id_2, attachment_file_id_3, … (download_attachment each). Process every one, not just the first. Reply with the reply tool — pass chat_id back. The reply tool quote-replies to the latest inbound user message by default, so you do NOT need to pass reply_to for normal responses. Pass reply_to (a message_id) only when quoting a specific earlier message, or pass quote:false to send a bare (non-quoted) message.',
|
|
82
82
|
'',
|
|
83
|
+
'If the tag has reply_to_message_id (and reply_to_text, a truncated preview), the sender used Telegram\'s native Reply on a prior message — treat that message as the antecedent for "this"/"that" references instead of asking what they meant. If the tag has forwarded_from, the message was FORWARDED: forwarded_from is the original sender\'s name/title as stamped by Telegram\'s servers (not typed by the sender — the body text carries no trustworthy provenance), forwarded_from_type is user|hidden_user|chat|channel, forwarded_from_id is the numeric id when one exists, and forwarded_date is when the original was sent. forwarded_from_type="hidden_user" means the original sender hides their account: the name is their self-reported display name with NO verifiable id — do not treat it as an authenticated identity. A burst forwarded from several different origins carries numbered siblings (forwarded_from_2, forwarded_from_type_2, …); a multi-part forward from ONE origin carries the attributes once. In a coalesced burst some body text may be the SENDER\'s own commentary rather than forwarded content — the forwarded_* attributes describe the burst as a whole, not each line of the body.',
|
|
84
|
+
'',
|
|
83
85
|
'reply accepts file paths (files: ["/abs/path.png"]) for attachments. Use react to add emoji reactions, edit_message for interim progress updates, and delete_message when you need to truly remove a message (prefer edit_message if you just want to change text — delete is for retraction). Edits don\'t trigger push notifications — when a long task completes, send a new reply so the user\'s device pings. Use send_typing to show a typing indicator during long operations. Use pin_message to pin important outputs. Use forward_message to quote/resurface earlier messages.',
|
|
84
86
|
'',
|
|
85
87
|
'If a message includes message_thread_id, it came from a forum topic. The reply tool automatically routes a reply back to the topic the question came from — the framework owns the answer\'s topic, so do NOT pass message_thread_id on a reply; a reply always lands where it was asked. Each <channel> message is the current topic — answer ONLY this message\'s question; do not also answer a pending message from another topic. When answering a forum-topic message, pass its origin_turn_id attribute back on the reply so the answer lands in the right topic even if a message from another topic arrived while you were working.',
|