switchroom 0.14.18 → 0.14.20
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 +6 -1
- package/dist/auth-broker/index.js +6 -1
- package/dist/cli/notion-write-pretool.mjs +6 -1
- package/dist/cli/switchroom.js +48 -3
- package/dist/host-control/main.js +6 -1
- package/dist/vault/approvals/kernel-server.js +6 -1
- package/dist/vault/broker/server.js +6 -1
- package/package.json +1 -1
- package/telegram-plugin/README.md +7 -3
- package/telegram-plugin/bridge/bridge.ts +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +381 -153
- package/telegram-plugin/dist/server.js +1 -1
- package/telegram-plugin/gateway/coalesce-attachments.ts +70 -0
- package/telegram-plugin/gateway/gateway.ts +296 -37
- package/telegram-plugin/gateway/interrupt-defer.ts +100 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +21 -4
- package/telegram-plugin/status-reactions.ts +18 -0
- package/telegram-plugin/tests/coalesce-attachments.test.ts +152 -0
- package/telegram-plugin/tests/interrupt-defer.test.ts +147 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +36 -0
- package/telegram-plugin/tests/status-reactions.test.ts +69 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +113 -0
- package/telegram-plugin/uat/scenarios/jtbd-forwarded-burst-dm.test.ts +158 -0
- package/telegram-plugin/worker-activity-feed.ts +54 -4
|
@@ -23738,8 +23738,13 @@ var init_schema = __esm(() => {
|
|
|
23738
23738
|
author_name: exports_external.string().optional().describe("Telegraph article byline. Defaults to soul.name when set.")
|
|
23739
23739
|
}).optional().describe("Long-reply publishing via Telegraph (#579). When enabled, replies " + "above the threshold publish as a Telegraph article rendered in " + "Telegram via native Instant View. Off by default \u2014 content " + "residency is real for some personas (lawyer, health-coach with PHI). " + "Cascades from defaults.channels.telegram.telegraph. " + "(Migrated from per-agent root in #596.)"),
|
|
23740
23740
|
coalesce: exports_external.object({
|
|
23741
|
-
window_ms: exports_external.number().int().nonnegative().optional().describe("Sliding-window (ms) for merging consecutive inbound messages from " + "the same sender+topic into ONE Claude turn. Each new message resets " + "the timer; the turn starts once the sender pauses for this long. " + "Catches forwarded bursts, pasted text the Telegram client split " + "into several messages, and mixed text+media forwards. Default 500. " + "Set 0 to disable (every message becomes its own turn). Raise for " + "users who think in multiple short messages; the trade-off is the " + "single-message turn start is delayed by this much (the \uD83D\uDC40 ack still " + "fires immediately, so perceived latency is unchanged).")
|
|
23741
|
+
window_ms: exports_external.number().int().nonnegative().optional().describe("Sliding-window (ms) for merging consecutive inbound messages from " + "the same sender+topic into ONE Claude turn. Each new message resets " + "the timer; the turn starts once the sender pauses for this long. " + "Catches forwarded bursts, pasted text the Telegram client split " + "into several messages, and mixed text+media forwards. Default 500. " + "Set 0 to disable (every message becomes its own turn). Raise for " + "users who think in multiple short messages; the trade-off is the " + "single-message turn start is delayed by this much (the \uD83D\uDC40 ack still " + "fires immediately, so perceived latency is unchanged)."),
|
|
23742
|
+
max_attachments: exports_external.number().int().positive().optional().describe("Maximum number of media attachments carried into ONE coalesced " + "Claude turn. Default 1 \u2014 a second photo/document/voice within the " + "coalesce window (or an album / media_group_id) starts its own turn, " + "preserving the historical single-attachment behaviour. Raise to let " + "a forwarded album or a text+multi-image burst arrive as one turn; " + "the agent then sees numbered attachment fields (image_path, " + "image_path_2, \u2026). Excess attachments beyond the cap spill into the " + "next turn. Each attachment is downloaded, so a high cap on a slow " + "link delays turn start.")
|
|
23742
23743
|
}).optional().describe("Inbound coalescing \u2014 how the gateway groups rapid consecutive messages " + "into a single turn so a forwarded album or split paste doesn't fan out " + "into N separate turns. Cascades from defaults.channels.telegram.coalesce."),
|
|
23744
|
+
interrupt: exports_external.object({
|
|
23745
|
+
safe_boundary: exports_external.boolean().optional().describe("When true, a `!`-prefix interrupt that arrives while the agent is " + "mid-tool-call is DEFERRED: the SIGINT and the replacement turn wait " + "until the in-flight tool call finishes (a clean boundary) instead of " + "C-c'ing the agent mid-write/mid-bash. If no tool is in flight the " + "interrupt still fires immediately. Bounded by max_wait_ms so a long " + "tool never strands the user. Default false \u2014 the interrupt fires " + "synchronously the moment `!` is received (historical behaviour). " + "Rapid repeated `!` while one is pending coalesce into a single " + "deferred interrupt carrying the latest body."),
|
|
23746
|
+
max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short \u2014 the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
|
|
23747
|
+
}).optional().describe("Interrupt timing \u2014 how a `!`-prefix interrupt behaves when it lands " + "mid-tool-call. Off by default (fire immediately). Cascades from " + "defaults.channels.telegram.interrupt."),
|
|
23743
23748
|
webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default \u2014 webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 \u2014 see #577.)"),
|
|
23744
23749
|
webhook_dispatch: exports_external.object({
|
|
23745
23750
|
github: exports_external.array(exports_external.object({
|
|
@@ -30154,6 +30159,136 @@ var init_materialize_bot_token = __esm(() => {
|
|
|
30154
30159
|
};
|
|
30155
30160
|
});
|
|
30156
30161
|
|
|
30162
|
+
// ../src/agents/tmux.ts
|
|
30163
|
+
var exports_tmux = {};
|
|
30164
|
+
__export(exports_tmux, {
|
|
30165
|
+
sendAgentInterrupt: () => sendAgentInterrupt,
|
|
30166
|
+
captureAgentPane: () => captureAgentPane
|
|
30167
|
+
});
|
|
30168
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
30169
|
+
import { mkdirSync as mkdirSync25, readdirSync as readdirSync6, statSync as statSync12, unlinkSync as unlinkSync14, writeFileSync as writeFileSync24 } from "node:fs";
|
|
30170
|
+
import { resolve as resolve7 } from "node:path";
|
|
30171
|
+
function captureAgentPane(opts) {
|
|
30172
|
+
const { agentName: agentName3, agentDir, reason } = opts;
|
|
30173
|
+
const scrollback = opts.scrollback !== false;
|
|
30174
|
+
const retain = typeof opts.retain === "number" && opts.retain > 0 ? opts.retain : 20;
|
|
30175
|
+
const socket = `switchroom-${agentName3}`;
|
|
30176
|
+
const outDir = resolve7(agentDir, "crash-reports");
|
|
30177
|
+
const ts = isoStamp(new Date);
|
|
30178
|
+
const reasonSlug = sanitizeReason(reason);
|
|
30179
|
+
const outPath = resolve7(outDir, `${ts}-${reasonSlug}.txt`);
|
|
30180
|
+
try {
|
|
30181
|
+
mkdirSync25(outDir, { recursive: true, mode: 493 });
|
|
30182
|
+
} catch (err) {
|
|
30183
|
+
const msg = `mkdir crash-reports failed: ${err.message}`;
|
|
30184
|
+
console.error(`[tmux-capture] ${agentName3}: ${msg}`);
|
|
30185
|
+
return { error: msg };
|
|
30186
|
+
}
|
|
30187
|
+
const args = ["-L", socket, "capture-pane", "-p"];
|
|
30188
|
+
if (scrollback) {
|
|
30189
|
+
args.push("-S", "-");
|
|
30190
|
+
}
|
|
30191
|
+
args.push("-t", agentName3);
|
|
30192
|
+
let pane;
|
|
30193
|
+
try {
|
|
30194
|
+
pane = execFileSync4("tmux", args, {
|
|
30195
|
+
timeout: 5000,
|
|
30196
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
30197
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
30198
|
+
});
|
|
30199
|
+
} catch (err) {
|
|
30200
|
+
const msg = `tmux capture-pane failed: ${err.message}`;
|
|
30201
|
+
console.error(`[tmux-capture] ${agentName3}: ${msg}`);
|
|
30202
|
+
return { error: msg };
|
|
30203
|
+
}
|
|
30204
|
+
let body = pane;
|
|
30205
|
+
if (body.byteLength > MAX_BYTES) {
|
|
30206
|
+
body = body.subarray(body.byteLength - MAX_BYTES);
|
|
30207
|
+
}
|
|
30208
|
+
const header = `# agent: ${agentName3}
|
|
30209
|
+
` + `# reason: ${reason}
|
|
30210
|
+
` + `# captured-at: ${ts}
|
|
30211
|
+
` + `# tmux-socket: ${socket}
|
|
30212
|
+
` + `
|
|
30213
|
+
`;
|
|
30214
|
+
try {
|
|
30215
|
+
writeFileSync24(outPath, Buffer.concat([Buffer.from(header, "utf8"), body]), {
|
|
30216
|
+
mode: 420
|
|
30217
|
+
});
|
|
30218
|
+
} catch (err) {
|
|
30219
|
+
const msg = `write crash-report failed: ${err.message}`;
|
|
30220
|
+
console.error(`[tmux-capture] ${agentName3}: ${msg}`);
|
|
30221
|
+
return { error: msg };
|
|
30222
|
+
}
|
|
30223
|
+
try {
|
|
30224
|
+
pruneOldReports(outDir, retain);
|
|
30225
|
+
} catch (err) {
|
|
30226
|
+
console.error(`[tmux-capture] ${agentName3}: retention prune failed: ${err.message}`);
|
|
30227
|
+
}
|
|
30228
|
+
return { path: outPath };
|
|
30229
|
+
}
|
|
30230
|
+
function isoStamp(d) {
|
|
30231
|
+
return d.toISOString().replace(/\.\d+Z$/, "Z").replace(/:/g, "-");
|
|
30232
|
+
}
|
|
30233
|
+
function sanitizeReason(reason) {
|
|
30234
|
+
const slug = reason.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
30235
|
+
return slug || "unknown";
|
|
30236
|
+
}
|
|
30237
|
+
function sendAgentInterrupt(opts) {
|
|
30238
|
+
const { agentName: agentName3 } = opts;
|
|
30239
|
+
const attempts = typeof opts.attempts === "number" && opts.attempts > 0 ? opts.attempts : 1;
|
|
30240
|
+
const retryDelayMs = typeof opts.retryDelayMs === "number" && opts.retryDelayMs >= 0 ? opts.retryDelayMs : 100;
|
|
30241
|
+
const socket = `switchroom-${agentName3}`;
|
|
30242
|
+
const args = ["-L", socket, "send-keys", "-t", agentName3, "C-c"];
|
|
30243
|
+
let lastError = null;
|
|
30244
|
+
for (let i = 0;i < attempts; i++) {
|
|
30245
|
+
try {
|
|
30246
|
+
execFileSync4("tmux", args, {
|
|
30247
|
+
timeout: 3000,
|
|
30248
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
30249
|
+
});
|
|
30250
|
+
return { ok: true };
|
|
30251
|
+
} catch (err) {
|
|
30252
|
+
lastError = `tmux send-keys C-c failed: ${err.message}`;
|
|
30253
|
+
console.error(`[tmux-interrupt] ${agentName3}: ${lastError}`);
|
|
30254
|
+
}
|
|
30255
|
+
if (i < attempts - 1 && retryDelayMs > 0) {
|
|
30256
|
+
sleepSync2(retryDelayMs);
|
|
30257
|
+
}
|
|
30258
|
+
}
|
|
30259
|
+
return { error: lastError ?? "tmux send-keys C-c failed" };
|
|
30260
|
+
}
|
|
30261
|
+
function sleepSync2(ms) {
|
|
30262
|
+
const sab = new SharedArrayBuffer(4);
|
|
30263
|
+
const view = new Int32Array(sab);
|
|
30264
|
+
Atomics.wait(view, 0, 0, ms);
|
|
30265
|
+
}
|
|
30266
|
+
function pruneOldReports(dir, retain) {
|
|
30267
|
+
let entries;
|
|
30268
|
+
try {
|
|
30269
|
+
entries = readdirSync6(dir);
|
|
30270
|
+
} catch {
|
|
30271
|
+
return;
|
|
30272
|
+
}
|
|
30273
|
+
const files = entries.filter((n) => n.endsWith(".txt")).map((n) => {
|
|
30274
|
+
const full = resolve7(dir, n);
|
|
30275
|
+
let mtimeMs = 0;
|
|
30276
|
+
try {
|
|
30277
|
+
mtimeMs = statSync12(full).mtimeMs;
|
|
30278
|
+
} catch {}
|
|
30279
|
+
return { full, mtimeMs };
|
|
30280
|
+
}).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
30281
|
+
for (const stale of files.slice(retain)) {
|
|
30282
|
+
try {
|
|
30283
|
+
unlinkSync14(stale.full);
|
|
30284
|
+
} catch {}
|
|
30285
|
+
}
|
|
30286
|
+
}
|
|
30287
|
+
var MAX_BYTES;
|
|
30288
|
+
var init_tmux = __esm(() => {
|
|
30289
|
+
MAX_BYTES = 10 * 1024 * 1024;
|
|
30290
|
+
});
|
|
30291
|
+
|
|
30157
30292
|
// gateway/config-approval-handler.ts
|
|
30158
30293
|
var exports_config_approval_handler = {};
|
|
30159
30294
|
__export(exports_config_approval_handler, {
|
|
@@ -30372,136 +30507,6 @@ var init_config_approval_handler = __esm(() => {
|
|
|
30372
30507
|
pending = new Map;
|
|
30373
30508
|
});
|
|
30374
30509
|
|
|
30375
|
-
// ../src/agents/tmux.ts
|
|
30376
|
-
var exports_tmux = {};
|
|
30377
|
-
__export(exports_tmux, {
|
|
30378
|
-
sendAgentInterrupt: () => sendAgentInterrupt,
|
|
30379
|
-
captureAgentPane: () => captureAgentPane
|
|
30380
|
-
});
|
|
30381
|
-
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
30382
|
-
import { mkdirSync as mkdirSync25, readdirSync as readdirSync6, statSync as statSync12, unlinkSync as unlinkSync14, writeFileSync as writeFileSync24 } from "node:fs";
|
|
30383
|
-
import { resolve as resolve7 } from "node:path";
|
|
30384
|
-
function captureAgentPane(opts) {
|
|
30385
|
-
const { agentName: agentName3, agentDir, reason } = opts;
|
|
30386
|
-
const scrollback = opts.scrollback !== false;
|
|
30387
|
-
const retain = typeof opts.retain === "number" && opts.retain > 0 ? opts.retain : 20;
|
|
30388
|
-
const socket = `switchroom-${agentName3}`;
|
|
30389
|
-
const outDir = resolve7(agentDir, "crash-reports");
|
|
30390
|
-
const ts = isoStamp(new Date);
|
|
30391
|
-
const reasonSlug = sanitizeReason(reason);
|
|
30392
|
-
const outPath = resolve7(outDir, `${ts}-${reasonSlug}.txt`);
|
|
30393
|
-
try {
|
|
30394
|
-
mkdirSync25(outDir, { recursive: true, mode: 493 });
|
|
30395
|
-
} catch (err) {
|
|
30396
|
-
const msg = `mkdir crash-reports failed: ${err.message}`;
|
|
30397
|
-
console.error(`[tmux-capture] ${agentName3}: ${msg}`);
|
|
30398
|
-
return { error: msg };
|
|
30399
|
-
}
|
|
30400
|
-
const args = ["-L", socket, "capture-pane", "-p"];
|
|
30401
|
-
if (scrollback) {
|
|
30402
|
-
args.push("-S", "-");
|
|
30403
|
-
}
|
|
30404
|
-
args.push("-t", agentName3);
|
|
30405
|
-
let pane;
|
|
30406
|
-
try {
|
|
30407
|
-
pane = execFileSync4("tmux", args, {
|
|
30408
|
-
timeout: 5000,
|
|
30409
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
30410
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
30411
|
-
});
|
|
30412
|
-
} catch (err) {
|
|
30413
|
-
const msg = `tmux capture-pane failed: ${err.message}`;
|
|
30414
|
-
console.error(`[tmux-capture] ${agentName3}: ${msg}`);
|
|
30415
|
-
return { error: msg };
|
|
30416
|
-
}
|
|
30417
|
-
let body = pane;
|
|
30418
|
-
if (body.byteLength > MAX_BYTES) {
|
|
30419
|
-
body = body.subarray(body.byteLength - MAX_BYTES);
|
|
30420
|
-
}
|
|
30421
|
-
const header = `# agent: ${agentName3}
|
|
30422
|
-
` + `# reason: ${reason}
|
|
30423
|
-
` + `# captured-at: ${ts}
|
|
30424
|
-
` + `# tmux-socket: ${socket}
|
|
30425
|
-
` + `
|
|
30426
|
-
`;
|
|
30427
|
-
try {
|
|
30428
|
-
writeFileSync24(outPath, Buffer.concat([Buffer.from(header, "utf8"), body]), {
|
|
30429
|
-
mode: 420
|
|
30430
|
-
});
|
|
30431
|
-
} catch (err) {
|
|
30432
|
-
const msg = `write crash-report failed: ${err.message}`;
|
|
30433
|
-
console.error(`[tmux-capture] ${agentName3}: ${msg}`);
|
|
30434
|
-
return { error: msg };
|
|
30435
|
-
}
|
|
30436
|
-
try {
|
|
30437
|
-
pruneOldReports(outDir, retain);
|
|
30438
|
-
} catch (err) {
|
|
30439
|
-
console.error(`[tmux-capture] ${agentName3}: retention prune failed: ${err.message}`);
|
|
30440
|
-
}
|
|
30441
|
-
return { path: outPath };
|
|
30442
|
-
}
|
|
30443
|
-
function isoStamp(d) {
|
|
30444
|
-
return d.toISOString().replace(/\.\d+Z$/, "Z").replace(/:/g, "-");
|
|
30445
|
-
}
|
|
30446
|
-
function sanitizeReason(reason) {
|
|
30447
|
-
const slug = reason.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
30448
|
-
return slug || "unknown";
|
|
30449
|
-
}
|
|
30450
|
-
function sendAgentInterrupt(opts) {
|
|
30451
|
-
const { agentName: agentName3 } = opts;
|
|
30452
|
-
const attempts = typeof opts.attempts === "number" && opts.attempts > 0 ? opts.attempts : 1;
|
|
30453
|
-
const retryDelayMs = typeof opts.retryDelayMs === "number" && opts.retryDelayMs >= 0 ? opts.retryDelayMs : 100;
|
|
30454
|
-
const socket = `switchroom-${agentName3}`;
|
|
30455
|
-
const args = ["-L", socket, "send-keys", "-t", agentName3, "C-c"];
|
|
30456
|
-
let lastError = null;
|
|
30457
|
-
for (let i = 0;i < attempts; i++) {
|
|
30458
|
-
try {
|
|
30459
|
-
execFileSync4("tmux", args, {
|
|
30460
|
-
timeout: 3000,
|
|
30461
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
30462
|
-
});
|
|
30463
|
-
return { ok: true };
|
|
30464
|
-
} catch (err) {
|
|
30465
|
-
lastError = `tmux send-keys C-c failed: ${err.message}`;
|
|
30466
|
-
console.error(`[tmux-interrupt] ${agentName3}: ${lastError}`);
|
|
30467
|
-
}
|
|
30468
|
-
if (i < attempts - 1 && retryDelayMs > 0) {
|
|
30469
|
-
sleepSync2(retryDelayMs);
|
|
30470
|
-
}
|
|
30471
|
-
}
|
|
30472
|
-
return { error: lastError ?? "tmux send-keys C-c failed" };
|
|
30473
|
-
}
|
|
30474
|
-
function sleepSync2(ms) {
|
|
30475
|
-
const sab = new SharedArrayBuffer(4);
|
|
30476
|
-
const view = new Int32Array(sab);
|
|
30477
|
-
Atomics.wait(view, 0, 0, ms);
|
|
30478
|
-
}
|
|
30479
|
-
function pruneOldReports(dir, retain) {
|
|
30480
|
-
let entries;
|
|
30481
|
-
try {
|
|
30482
|
-
entries = readdirSync6(dir);
|
|
30483
|
-
} catch {
|
|
30484
|
-
return;
|
|
30485
|
-
}
|
|
30486
|
-
const files = entries.filter((n) => n.endsWith(".txt")).map((n) => {
|
|
30487
|
-
const full = resolve7(dir, n);
|
|
30488
|
-
let mtimeMs = 0;
|
|
30489
|
-
try {
|
|
30490
|
-
mtimeMs = statSync12(full).mtimeMs;
|
|
30491
|
-
} catch {}
|
|
30492
|
-
return { full, mtimeMs };
|
|
30493
|
-
}).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
30494
|
-
for (const stale of files.slice(retain)) {
|
|
30495
|
-
try {
|
|
30496
|
-
unlinkSync14(stale.full);
|
|
30497
|
-
} catch {}
|
|
30498
|
-
}
|
|
30499
|
-
}
|
|
30500
|
-
var MAX_BYTES;
|
|
30501
|
-
var init_tmux = __esm(() => {
|
|
30502
|
-
MAX_BYTES = 10 * 1024 * 1024;
|
|
30503
|
-
});
|
|
30504
|
-
|
|
30505
30510
|
// ../src/vault/approvals/client.ts
|
|
30506
30511
|
function resolveKernelSocketPath2(opts) {
|
|
30507
30512
|
if (opts?.socket)
|
|
@@ -31025,6 +31030,51 @@ function parseInterruptMarker(text) {
|
|
|
31025
31030
|
};
|
|
31026
31031
|
}
|
|
31027
31032
|
|
|
31033
|
+
// gateway/interrupt-defer.ts
|
|
31034
|
+
class ToolFlightTracker {
|
|
31035
|
+
inFlight = new Set;
|
|
31036
|
+
onEvent(ev) {
|
|
31037
|
+
switch (ev.kind) {
|
|
31038
|
+
case "tool_use":
|
|
31039
|
+
if (typeof ev.toolUseId === "string" && ev.toolUseId.length > 0) {
|
|
31040
|
+
this.inFlight.add(ev.toolUseId);
|
|
31041
|
+
}
|
|
31042
|
+
break;
|
|
31043
|
+
case "tool_result":
|
|
31044
|
+
if (typeof ev.toolUseId === "string" && ev.toolUseId.length > 0) {
|
|
31045
|
+
this.inFlight.delete(ev.toolUseId);
|
|
31046
|
+
}
|
|
31047
|
+
break;
|
|
31048
|
+
case "turn_end":
|
|
31049
|
+
case "enqueue":
|
|
31050
|
+
this.inFlight.clear();
|
|
31051
|
+
break;
|
|
31052
|
+
default:
|
|
31053
|
+
break;
|
|
31054
|
+
}
|
|
31055
|
+
}
|
|
31056
|
+
isMidToolCall() {
|
|
31057
|
+
return this.inFlight.size > 0;
|
|
31058
|
+
}
|
|
31059
|
+
inFlightCount() {
|
|
31060
|
+
return this.inFlight.size;
|
|
31061
|
+
}
|
|
31062
|
+
clear() {
|
|
31063
|
+
this.inFlight.clear();
|
|
31064
|
+
}
|
|
31065
|
+
}
|
|
31066
|
+
function decideInterruptTiming(opts) {
|
|
31067
|
+
if (!opts.safeBoundaryEnabled)
|
|
31068
|
+
return "fire-now";
|
|
31069
|
+
return opts.midToolCall ? "defer" : "fire-now";
|
|
31070
|
+
}
|
|
31071
|
+
var DEFAULT_INTERRUPT_MAX_WAIT_MS = 8000;
|
|
31072
|
+
function resolveInterruptMaxWaitMs(configured) {
|
|
31073
|
+
if (typeof configured === "number" && configured > 0)
|
|
31074
|
+
return configured;
|
|
31075
|
+
return DEFAULT_INTERRUPT_MAX_WAIT_MS;
|
|
31076
|
+
}
|
|
31077
|
+
|
|
31028
31078
|
// sticker-aliases.ts
|
|
31029
31079
|
function looksLikeFileId(s) {
|
|
31030
31080
|
return /^[A-Za-z0-9_-]{10,200}$/.test(s);
|
|
@@ -31562,6 +31612,33 @@ function inboundCoalesceKey(chatId, threadId, userId) {
|
|
|
31562
31612
|
return `${chatId}:${t}:${userId}`;
|
|
31563
31613
|
}
|
|
31564
31614
|
|
|
31615
|
+
// gateway/coalesce-attachments.ts
|
|
31616
|
+
function splitCoalescedAttachments(entries, hasAttachment, maxAttachments) {
|
|
31617
|
+
const withAttachment = entries.filter(hasAttachment);
|
|
31618
|
+
const capped = withAttachment.slice(0, Math.max(1, maxAttachments));
|
|
31619
|
+
const [primary, ...extras] = capped;
|
|
31620
|
+
return { primary, extras };
|
|
31621
|
+
}
|
|
31622
|
+
function buildExtraAttachmentMeta(resolved) {
|
|
31623
|
+
const out = {};
|
|
31624
|
+
resolved.forEach((ex, i) => {
|
|
31625
|
+
const n = i + 2;
|
|
31626
|
+
if (ex.imagePath)
|
|
31627
|
+
out[`image_path_${n}`] = ex.imagePath;
|
|
31628
|
+
if (ex.attachment) {
|
|
31629
|
+
out[`attachment_kind_${n}`] = ex.attachment.kind;
|
|
31630
|
+
out[`attachment_file_id_${n}`] = ex.attachment.file_id;
|
|
31631
|
+
if (ex.attachment.size != null)
|
|
31632
|
+
out[`attachment_size_${n}`] = String(ex.attachment.size);
|
|
31633
|
+
if (ex.attachment.mime)
|
|
31634
|
+
out[`attachment_mime_${n}`] = ex.attachment.mime;
|
|
31635
|
+
if (ex.attachment.name)
|
|
31636
|
+
out[`attachment_name_${n}`] = ex.attachment.name;
|
|
31637
|
+
}
|
|
31638
|
+
});
|
|
31639
|
+
return out;
|
|
31640
|
+
}
|
|
31641
|
+
|
|
31565
31642
|
// status-reactions.ts
|
|
31566
31643
|
var TELEGRAM_REACTION_WHITELIST = new Set([
|
|
31567
31644
|
"\uD83D\uDC4D",
|
|
@@ -31645,6 +31722,7 @@ var REACTION_VARIANTS = {
|
|
|
31645
31722
|
coding: ["\uD83D\uDC68\u200d\uD83D\uDCBB", "\u270d", "\u26a1"],
|
|
31646
31723
|
web: ["\u26a1", "\uD83E\uDD14", "\uD83D\uDC4C"],
|
|
31647
31724
|
compacting: ["\u270d", "\uD83E\uDD14", "\uD83D\uDC40"],
|
|
31725
|
+
awaiting: ["\uD83D\uDE4F", "\uD83E\uDD14", "\uD83D\uDC40"],
|
|
31648
31726
|
done: ["\uD83D\uDC4D", "\uD83D\uDCAF", "\uD83C\uDF89"],
|
|
31649
31727
|
error: ["\uD83D\uDE31", "\uD83D\uDE28", "\uD83E\uDD2F"],
|
|
31650
31728
|
stallSoft: ["\uD83E\uDD71", "\uD83D\uDE34", "\uD83E\uDD14"],
|
|
@@ -31697,6 +31775,12 @@ class StatusReactionController {
|
|
|
31697
31775
|
setCompacting() {
|
|
31698
31776
|
this.scheduleState("compacting");
|
|
31699
31777
|
}
|
|
31778
|
+
setAwaiting() {
|
|
31779
|
+
if (this.finished)
|
|
31780
|
+
return;
|
|
31781
|
+
this.scheduleState("awaiting", { immediate: true, skipStallReset: true });
|
|
31782
|
+
this.clearStallTimers();
|
|
31783
|
+
}
|
|
31700
31784
|
setError() {
|
|
31701
31785
|
this.scheduleState("error");
|
|
31702
31786
|
}
|
|
@@ -31875,6 +31959,7 @@ class DeferredDoneReactions {
|
|
|
31875
31959
|
var DESC_MAX = 80;
|
|
31876
31960
|
var TOOL_ARG_MAX = 64;
|
|
31877
31961
|
var SUMMARY_MAX = 100;
|
|
31962
|
+
var NARRATIVE_MAX_LINES = 6;
|
|
31878
31963
|
function renderWorkerActivity(v) {
|
|
31879
31964
|
const desc = truncate(v.description.trim() || "background task", DESC_MAX);
|
|
31880
31965
|
const elapsed = formatDuration(v.elapsedMs);
|
|
@@ -31893,10 +31978,17 @@ function renderWorkerActivity(v) {
|
|
|
31893
31978
|
} else {
|
|
31894
31979
|
activity = `<i>starting\u2026 (${elapsed})</i>`;
|
|
31895
31980
|
}
|
|
31896
|
-
const summary = v.latestSummary.trim();
|
|
31897
31981
|
const lines = [header, activity];
|
|
31898
|
-
|
|
31899
|
-
|
|
31982
|
+
const narrative = (v.narrativeLines ?? []).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
31983
|
+
if (narrative.length > 0) {
|
|
31984
|
+
for (const line of narrative) {
|
|
31985
|
+
lines.push(` \u21b3 <i>${escapeHtml(truncate(line, SUMMARY_MAX))}</i>`);
|
|
31986
|
+
}
|
|
31987
|
+
} else {
|
|
31988
|
+
const summary = v.latestSummary.trim();
|
|
31989
|
+
if (summary.length > 0) {
|
|
31990
|
+
lines.push(` \u21b3 <i>${escapeHtml(truncate(summary, SUMMARY_MAX))}</i>`);
|
|
31991
|
+
}
|
|
31900
31992
|
}
|
|
31901
31993
|
return lines.join(`
|
|
31902
31994
|
`);
|
|
@@ -31933,10 +32025,22 @@ function createWorkerActivityFeed(opts) {
|
|
|
31933
32025
|
h.cooldownUntil = nowFn() + retryAfter * 1000 + COOLDOWN_JITTER_MS;
|
|
31934
32026
|
log(`worker-feed: ${label} 429 \u2014 backing off ${retryAfter}s`);
|
|
31935
32027
|
}
|
|
32028
|
+
function accumulateNarrative(h, view) {
|
|
32029
|
+
const line = view.latestSummary.trim();
|
|
32030
|
+
if (line.length === 0)
|
|
32031
|
+
return;
|
|
32032
|
+
if (h.narrative[h.narrative.length - 1] === line)
|
|
32033
|
+
return;
|
|
32034
|
+
h.narrative.push(line);
|
|
32035
|
+
if (h.narrative.length > NARRATIVE_MAX_LINES) {
|
|
32036
|
+
h.narrative.splice(0, h.narrative.length - NARRATIVE_MAX_LINES);
|
|
32037
|
+
}
|
|
32038
|
+
}
|
|
31936
32039
|
async function doUpdate(h, view) {
|
|
32040
|
+
accumulateNarrative(h, view);
|
|
31937
32041
|
if (nowFn() < h.cooldownUntil)
|
|
31938
32042
|
return;
|
|
31939
|
-
const body = renderWorkerActivity(view);
|
|
32043
|
+
const body = renderWorkerActivity({ ...view, narrativeLines: h.narrative });
|
|
31940
32044
|
if (h.messageId == null) {
|
|
31941
32045
|
if (view.elapsedMs < firstPaintMin)
|
|
31942
32046
|
return;
|
|
@@ -32003,6 +32107,7 @@ function createWorkerActivityFeed(opts) {
|
|
|
32003
32107
|
lastBody: null,
|
|
32004
32108
|
lastEditAt: 0,
|
|
32005
32109
|
cooldownUntil: 0,
|
|
32110
|
+
narrative: [],
|
|
32006
32111
|
chain: Promise.resolve()
|
|
32007
32112
|
};
|
|
32008
32113
|
handles.set(agentId, h);
|
|
@@ -46392,6 +46497,7 @@ function planBufferedRedelivery(pending) {
|
|
|
46392
46497
|
flush();
|
|
46393
46498
|
return out;
|
|
46394
46499
|
}
|
|
46500
|
+
var ATTACHMENT_META_RE = /^(image_path|attachment_)/;
|
|
46395
46501
|
function mergeRun(run2) {
|
|
46396
46502
|
const last = run2[run2.length - 1];
|
|
46397
46503
|
const mediaEntry = run2.find(inboundHasMedia);
|
|
@@ -46402,6 +46508,14 @@ function mergeRun(run2) {
|
|
|
46402
46508
|
};
|
|
46403
46509
|
delete merged.imagePath;
|
|
46404
46510
|
delete merged.attachment;
|
|
46511
|
+
if (mediaEntry != null && mediaEntry !== last) {
|
|
46512
|
+
const splicedMeta = { ...merged.meta };
|
|
46513
|
+
for (const [k, v] of Object.entries(mediaEntry.meta)) {
|
|
46514
|
+
if (ATTACHMENT_META_RE.test(k))
|
|
46515
|
+
splicedMeta[k] = v;
|
|
46516
|
+
}
|
|
46517
|
+
merged.meta = splicedMeta;
|
|
46518
|
+
}
|
|
46405
46519
|
if (mediaEntry?.imagePath != null)
|
|
46406
46520
|
merged.imagePath = mediaEntry.imagePath;
|
|
46407
46521
|
if (mediaEntry?.attachment != null)
|
|
@@ -47030,6 +47144,7 @@ function planBufferedRedelivery2(pending) {
|
|
|
47030
47144
|
flush();
|
|
47031
47145
|
return out;
|
|
47032
47146
|
}
|
|
47147
|
+
var ATTACHMENT_META_RE2 = /^(image_path|attachment_)/;
|
|
47033
47148
|
function mergeRun2(run2) {
|
|
47034
47149
|
const last = run2[run2.length - 1];
|
|
47035
47150
|
const mediaEntry = run2.find(inboundHasMedia2);
|
|
@@ -47040,6 +47155,14 @@ function mergeRun2(run2) {
|
|
|
47040
47155
|
};
|
|
47041
47156
|
delete merged.imagePath;
|
|
47042
47157
|
delete merged.attachment;
|
|
47158
|
+
if (mediaEntry != null && mediaEntry !== last) {
|
|
47159
|
+
const splicedMeta = { ...merged.meta };
|
|
47160
|
+
for (const [k, v] of Object.entries(mediaEntry.meta)) {
|
|
47161
|
+
if (ATTACHMENT_META_RE2.test(k))
|
|
47162
|
+
splicedMeta[k] = v;
|
|
47163
|
+
}
|
|
47164
|
+
merged.meta = splicedMeta;
|
|
47165
|
+
}
|
|
47043
47166
|
if (mediaEntry?.imagePath != null)
|
|
47044
47167
|
merged.imagePath = mediaEntry.imagePath;
|
|
47045
47168
|
if (mediaEntry?.attachment != null)
|
|
@@ -51237,10 +51360,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
51237
51360
|
}
|
|
51238
51361
|
|
|
51239
51362
|
// ../src/build-info.ts
|
|
51240
|
-
var VERSION = "0.14.
|
|
51241
|
-
var COMMIT_SHA = "
|
|
51242
|
-
var COMMIT_DATE = "2026-05-
|
|
51243
|
-
var LATEST_PR =
|
|
51363
|
+
var VERSION = "0.14.20";
|
|
51364
|
+
var COMMIT_SHA = "c8b965b2";
|
|
51365
|
+
var COMMIT_DATE = "2026-05-31T01:51:10Z";
|
|
51366
|
+
var LATEST_PR = 2018;
|
|
51244
51367
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
51245
51368
|
|
|
51246
51369
|
// gateway/boot-version.ts
|
|
@@ -52008,6 +52131,9 @@ function readAccessFile() {
|
|
|
52008
52131
|
parseMode: parsed.parseMode,
|
|
52009
52132
|
disableLinkPreview: parsed.disableLinkPreview,
|
|
52010
52133
|
coalescingGapMs: parsed.coalescingGapMs,
|
|
52134
|
+
coalesceMaxAttachments: parsed.coalesceMaxAttachments,
|
|
52135
|
+
interruptSafeBoundary: parsed.interruptSafeBoundary,
|
|
52136
|
+
interruptMaxWaitMs: parsed.interruptMaxWaitMs,
|
|
52011
52137
|
statusReactions: parsed.statusReactions,
|
|
52012
52138
|
historyEnabled: parsed.historyEnabled,
|
|
52013
52139
|
historyRetentionDays: parsed.historyRetentionDays,
|
|
@@ -52215,6 +52341,40 @@ var lastPtyPreviewByChat = new Map;
|
|
|
52215
52341
|
var progressUpdateLastSent = new Map;
|
|
52216
52342
|
var progressUpdateTurnCount = new Map;
|
|
52217
52343
|
var currentTurn = null;
|
|
52344
|
+
var toolFlightTracker = new ToolFlightTracker;
|
|
52345
|
+
var pendingDeferredInterrupt = null;
|
|
52346
|
+
async function fireDeferredInterrupt(reason) {
|
|
52347
|
+
const pending2 = pendingDeferredInterrupt;
|
|
52348
|
+
if (pending2 == null)
|
|
52349
|
+
return;
|
|
52350
|
+
pendingDeferredInterrupt = null;
|
|
52351
|
+
clearTimeout(pending2.deadlineTimer);
|
|
52352
|
+
const waitedMs = Date.now() - pending2.registeredAt;
|
|
52353
|
+
process.stderr.write(`telegram gateway: deferred-interrupt firing reason=${reason} agent=${pending2.agentName} chat=${pending2.chatId} waited_ms=${waitedMs} in_flight=${toolFlightTracker.inFlightCount()}
|
|
52354
|
+
`);
|
|
52355
|
+
try {
|
|
52356
|
+
const { sendAgentInterrupt: sendAgentInterrupt2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
|
|
52357
|
+
const r = sendAgentInterrupt2({ agentName: pending2.agentName });
|
|
52358
|
+
if ("ok" in r) {
|
|
52359
|
+
process.stderr.write(`telegram gateway: deferred-interrupt SIGINT delivered via tmux send-keys agent=${pending2.agentName}
|
|
52360
|
+
`);
|
|
52361
|
+
} else {
|
|
52362
|
+
process.stderr.write(`telegram gateway: deferred-interrupt SIGINT via tmux failed agent=${pending2.agentName}: ${r.error}
|
|
52363
|
+
`);
|
|
52364
|
+
}
|
|
52365
|
+
} catch (err) {
|
|
52366
|
+
process.stderr.write(`telegram gateway: deferred-interrupt SIGINT failed: ${err.message}
|
|
52367
|
+
`);
|
|
52368
|
+
}
|
|
52369
|
+
const delivered = ipcServer.sendToAgent(pending2.agentName, pending2.inboundMsg);
|
|
52370
|
+
if (delivered) {
|
|
52371
|
+
markClaudeBusyForInbound(pending2.inboundMsg);
|
|
52372
|
+
} else {
|
|
52373
|
+
pendingInboundBuffer.push(pending2.agentName, pending2.inboundMsg);
|
|
52374
|
+
process.stderr.write(`telegram gateway: deferred-interrupt body buffered (bridge miss) agent=${pending2.agentName} chat=${pending2.chatId}
|
|
52375
|
+
`);
|
|
52376
|
+
}
|
|
52377
|
+
}
|
|
52218
52378
|
var preambleSuppressor = new PreambleSuppressor({
|
|
52219
52379
|
emitAnswer: (cumulative) => {
|
|
52220
52380
|
const stream = currentTurn?.answerStream ?? null;
|
|
@@ -52458,6 +52618,12 @@ function countRunningWorkers() {
|
|
|
52458
52618
|
}
|
|
52459
52619
|
return n;
|
|
52460
52620
|
}
|
|
52621
|
+
function resumeReactionAfterVerdict() {
|
|
52622
|
+
const turn = currentTurn;
|
|
52623
|
+
if (turn == null)
|
|
52624
|
+
return;
|
|
52625
|
+
activeStatusReactions.get(statusKey(turn.sessionChatId, turn.sessionThreadId))?.setThinking();
|
|
52626
|
+
}
|
|
52461
52627
|
function resolveThreadId(chat_id, explicit) {
|
|
52462
52628
|
if (explicit != null)
|
|
52463
52629
|
return Number(explicit);
|
|
@@ -52891,6 +53057,7 @@ var pendingStateReaper = setInterval(() => {
|
|
|
52891
53057
|
for (const [k, v] of pendingPermissions) {
|
|
52892
53058
|
if (now - v.startedAt > PERMISSION_TTL_MS) {
|
|
52893
53059
|
dispatchPermissionVerdict({ type: "permission", requestId: k, behavior: "deny" });
|
|
53060
|
+
resumeReactionAfterVerdict();
|
|
52894
53061
|
process.stderr.write(`telegram gateway: permission TTL expired \u2014 auto-deny request=${k} tool=${v.tool_name} (no operator response in ${Math.round(PERMISSION_TTL_MS / 60000)}m)
|
|
52895
53062
|
`);
|
|
52896
53063
|
pendingPermissions.delete(k);
|
|
@@ -52949,23 +53116,27 @@ function looksLikeAuthCode(text) {
|
|
|
52949
53116
|
return true;
|
|
52950
53117
|
return false;
|
|
52951
53118
|
}
|
|
52952
|
-
var bufferedAttachmentKeys = new
|
|
53119
|
+
var bufferedAttachmentKeys = new Map;
|
|
53120
|
+
function coalesceMaxAttachments() {
|
|
53121
|
+
return Math.max(1, loadAccess().coalesceMaxAttachments ?? 1);
|
|
53122
|
+
}
|
|
52953
53123
|
var inboundCoalescer = createInboundCoalescer({
|
|
52954
53124
|
gapMs: () => loadAccess().coalescingGapMs ?? 500,
|
|
52955
53125
|
merge: (entries) => {
|
|
52956
53126
|
const last = entries[entries.length - 1];
|
|
52957
|
-
const
|
|
53127
|
+
const { primary, extras } = splitCoalescedAttachments(entries, (e) => e.downloadImage != null || e.attachment != null, coalesceMaxAttachments());
|
|
52958
53128
|
return {
|
|
52959
|
-
text: entries.map((e) => e.text).join(`
|
|
53129
|
+
text: entries.map((e) => e.text).filter((t) => t.length > 0).join(`
|
|
52960
53130
|
`),
|
|
52961
53131
|
ctx: last.ctx,
|
|
52962
|
-
downloadImage:
|
|
52963
|
-
attachment:
|
|
53132
|
+
downloadImage: primary?.downloadImage,
|
|
53133
|
+
attachment: primary?.attachment,
|
|
53134
|
+
extraAttachments: extras.length > 0 ? extras.map((e) => ({ downloadImage: e.downloadImage, attachment: e.attachment })) : undefined
|
|
52964
53135
|
};
|
|
52965
53136
|
},
|
|
52966
53137
|
onFlush: (key, merged) => {
|
|
52967
53138
|
bufferedAttachmentKeys.delete(key);
|
|
52968
|
-
handleInbound(merged.ctx, merged.text, merged.downloadImage, merged.attachment);
|
|
53139
|
+
handleInbound(merged.ctx, merged.text, merged.downloadImage, merged.attachment, merged.extraAttachments);
|
|
52969
53140
|
}
|
|
52970
53141
|
});
|
|
52971
53142
|
function emitGatewayOperatorEvent(event) {
|
|
@@ -53484,6 +53655,10 @@ var ipcServer = createIpcServer({
|
|
|
53484
53655
|
const threadHint = msg.threadId != null ? String(msg.threadId) : undefined;
|
|
53485
53656
|
progressDriver?.ingest(ev, chatHint, threadHint);
|
|
53486
53657
|
handleSessionEvent(ev);
|
|
53658
|
+
toolFlightTracker.onEvent(ev);
|
|
53659
|
+
if (pendingDeferredInterrupt != null && !toolFlightTracker.isMidToolCall()) {
|
|
53660
|
+
fireDeferredInterrupt("boundary");
|
|
53661
|
+
}
|
|
53487
53662
|
if (currentTurn != null) {
|
|
53488
53663
|
const key = statusKey(currentTurn.sessionChatId, currentTurn.sessionThreadId);
|
|
53489
53664
|
if (ev.kind === "thinking") {
|
|
@@ -53532,6 +53707,9 @@ var ipcServer = createIpcServer({
|
|
|
53532
53707
|
`);
|
|
53533
53708
|
});
|
|
53534
53709
|
}
|
|
53710
|
+
if (activeTurn != null) {
|
|
53711
|
+
activeStatusReactions.get(statusKey(activeTurn.sessionChatId, activeTurn.sessionThreadId))?.setAwaiting();
|
|
53712
|
+
}
|
|
53535
53713
|
},
|
|
53536
53714
|
onHeartbeat(_client, _msg) {},
|
|
53537
53715
|
onScheduleRestart(client3, msg) {
|
|
@@ -56124,7 +56302,8 @@ async function handleInboundCoalesced(ctx, text, downloadImage, attachment) {
|
|
|
56124
56302
|
return handleInbound(ctx, text, downloadImage, attachment);
|
|
56125
56303
|
}
|
|
56126
56304
|
const hasAttachment = downloadImage != null || attachment != null;
|
|
56127
|
-
|
|
56305
|
+
const maxAttachments = coalesceMaxAttachments();
|
|
56306
|
+
if (hasAttachment && ctx.message?.media_group_id != null && maxAttachments <= 1) {
|
|
56128
56307
|
return handleInbound(ctx, text, downloadImage, attachment);
|
|
56129
56308
|
}
|
|
56130
56309
|
const from = ctx.from;
|
|
@@ -56132,7 +56311,7 @@ async function handleInboundCoalesced(ctx, text, downloadImage, attachment) {
|
|
|
56132
56311
|
return;
|
|
56133
56312
|
if (hasAttachment) {
|
|
56134
56313
|
const probeKey = inboundCoalesceKey(String(ctx.chat.id), ctx.message?.message_thread_id, String(from.id));
|
|
56135
|
-
if (bufferedAttachmentKeys.
|
|
56314
|
+
if ((bufferedAttachmentKeys.get(probeKey) ?? 0) >= maxAttachments) {
|
|
56136
56315
|
return handleInbound(ctx, text, downloadImage, attachment);
|
|
56137
56316
|
}
|
|
56138
56317
|
}
|
|
@@ -56142,7 +56321,7 @@ async function handleInboundCoalesced(ctx, text, downloadImage, attachment) {
|
|
|
56142
56321
|
if (result.bypass)
|
|
56143
56322
|
return handleInbound(ctx, text, downloadImage, attachment);
|
|
56144
56323
|
if (hasAttachment)
|
|
56145
|
-
bufferedAttachmentKeys.
|
|
56324
|
+
bufferedAttachmentKeys.set(key, (bufferedAttachmentKeys.get(key) ?? 0) + 1);
|
|
56146
56325
|
}
|
|
56147
56326
|
function maybeEarlyAckReaction(ctx, from) {
|
|
56148
56327
|
const msgId = ctx.message?.message_id;
|
|
@@ -56163,7 +56342,7 @@ function maybeEarlyAckReaction(ctx, from) {
|
|
|
56163
56342
|
]).catch(() => {});
|
|
56164
56343
|
bot.api.sendChatAction(chatId, "typing").catch(() => {});
|
|
56165
56344
|
}
|
|
56166
|
-
async function handleInbound(ctx, text, downloadImage, attachment) {
|
|
56345
|
+
async function handleInbound(ctx, text, downloadImage, attachment, extraAttachments) {
|
|
56167
56346
|
const isTopicMessage = ctx.message?.is_topic_message ?? false;
|
|
56168
56347
|
const messageThreadId = ctx.message?.message_thread_id;
|
|
56169
56348
|
if (TOPIC_ID != null) {
|
|
@@ -56223,16 +56402,22 @@ async function handleInbound(ctx, text, downloadImage, attachment) {
|
|
|
56223
56402
|
`);
|
|
56224
56403
|
}
|
|
56225
56404
|
const interrupt = parseInterruptMarker(text);
|
|
56405
|
+
let deferInterrupt = false;
|
|
56226
56406
|
if (interrupt.isInterrupt) {
|
|
56227
56407
|
const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
|
|
56228
|
-
|
|
56408
|
+
const access2 = loadAccess();
|
|
56409
|
+
deferInterrupt = !interrupt.emptyBody && decideInterruptTiming({
|
|
56410
|
+
safeBoundaryEnabled: access2.interruptSafeBoundary === true,
|
|
56411
|
+
midToolCall: toolFlightTracker.isMidToolCall()
|
|
56412
|
+
}) === "defer";
|
|
56413
|
+
process.stderr.write(`telegram gateway: interrupt-marker received chat_id=${chat_id} agent=${agentName3 ?? "-"} body_len=${interrupt.body.length} empty=${interrupt.emptyBody} defer=${deferInterrupt} in_flight=${toolFlightTracker.inFlightCount()}
|
|
56229
56414
|
`);
|
|
56230
56415
|
if (msgId != null) {
|
|
56231
56416
|
bot.api.setMessageReaction(chat_id, msgId, [
|
|
56232
56417
|
{ type: "emoji", emoji: "\u26A1" }
|
|
56233
56418
|
]).catch(() => {});
|
|
56234
56419
|
}
|
|
56235
|
-
if (agentName3) {
|
|
56420
|
+
if (agentName3 && !deferInterrupt) {
|
|
56236
56421
|
try {
|
|
56237
56422
|
const { sendAgentInterrupt: sendAgentInterrupt2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
|
|
56238
56423
|
const r = sendAgentInterrupt2({ agentName: agentName3 });
|
|
@@ -56283,6 +56468,7 @@ async function handleInbound(ctx, text, downloadImage, attachment) {
|
|
|
56283
56468
|
requestId: request_id,
|
|
56284
56469
|
behavior
|
|
56285
56470
|
});
|
|
56471
|
+
resumeReactionAfterVerdict();
|
|
56286
56472
|
if (msgId != null) {
|
|
56287
56473
|
const emoji = behavior === "allow" ? "\u2705" : "\u274C";
|
|
56288
56474
|
bot.api.setMessageReaction(chat_id, msgId, [
|
|
@@ -56670,6 +56856,16 @@ ${preBlock(write.output)}`;
|
|
|
56670
56856
|
}
|
|
56671
56857
|
}
|
|
56672
56858
|
const imagePath = downloadImage ? await downloadImage() : undefined;
|
|
56859
|
+
const extraResolved = [];
|
|
56860
|
+
if (extraAttachments && extraAttachments.length > 0) {
|
|
56861
|
+
for (const ex of extraAttachments) {
|
|
56862
|
+
const exImagePath = ex.downloadImage ? await ex.downloadImage() : undefined;
|
|
56863
|
+
extraResolved.push({ imagePath: exImagePath, attachment: ex.attachment });
|
|
56864
|
+
}
|
|
56865
|
+
}
|
|
56866
|
+
const extraMeta = buildExtraAttachmentMeta(extraResolved);
|
|
56867
|
+
const primaryHasAttachment = imagePath != null || attachment != null;
|
|
56868
|
+
const attachmentCount = (primaryHasAttachment ? 1 : 0) + extraResolved.length;
|
|
56673
56869
|
const replyToMsg = ctx.message?.reply_to_message;
|
|
56674
56870
|
const replyToMessageId = replyToMsg?.message_id;
|
|
56675
56871
|
const replyToTextRaw = replyToMsg ? replyToMsg.text ?? replyToMsg.caption ?? undefined : undefined;
|
|
@@ -56753,10 +56949,37 @@ ${preBlock(write.output)}`;
|
|
|
56753
56949
|
...attachment.size != null ? { attachment_size: String(attachment.size) } : {},
|
|
56754
56950
|
...attachment.mime ? { attachment_mime: attachment.mime } : {},
|
|
56755
56951
|
...attachment.name ? { attachment_name: attachment.name } : {}
|
|
56756
|
-
} : {}
|
|
56952
|
+
} : {},
|
|
56953
|
+
...attachmentCount > 1 ? { attachment_count: String(attachmentCount) } : {},
|
|
56954
|
+
...extraMeta
|
|
56757
56955
|
}
|
|
56758
56956
|
};
|
|
56759
56957
|
const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
56958
|
+
if (deferInterrupt) {
|
|
56959
|
+
const selfAgentDefer = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
56960
|
+
if (pendingDeferredInterrupt != null) {
|
|
56961
|
+
pendingDeferredInterrupt.inboundMsg = inboundMsg;
|
|
56962
|
+
pendingDeferredInterrupt.msgId = msgId ?? null;
|
|
56963
|
+
process.stderr.write(`telegram gateway: deferred-interrupt coalesced (replacing pending body) agent=${selfAgentDefer} chat=${chat_id} msg=${msgId ?? "-"}
|
|
56964
|
+
`);
|
|
56965
|
+
} else {
|
|
56966
|
+
const maxWaitMs = resolveInterruptMaxWaitMs(loadAccess().interruptMaxWaitMs);
|
|
56967
|
+
pendingDeferredInterrupt = {
|
|
56968
|
+
agentName: selfAgentDefer,
|
|
56969
|
+
inboundMsg,
|
|
56970
|
+
chatId: chat_id,
|
|
56971
|
+
msgId: msgId ?? null,
|
|
56972
|
+
threadId: messageThreadId ?? undefined,
|
|
56973
|
+
registeredAt: Date.now(),
|
|
56974
|
+
deadlineTimer: setTimeout(() => {
|
|
56975
|
+
fireDeferredInterrupt("timeout");
|
|
56976
|
+
}, maxWaitMs)
|
|
56977
|
+
};
|
|
56978
|
+
process.stderr.write(`telegram gateway: deferred-interrupt parked agent=${selfAgentDefer} chat=${chat_id} msg=${msgId ?? "-"} max_wait_ms=${maxWaitMs} in_flight=${toolFlightTracker.inFlightCount()}
|
|
56979
|
+
`);
|
|
56980
|
+
}
|
|
56981
|
+
return;
|
|
56982
|
+
}
|
|
56760
56983
|
if (decideInboundDelivery({
|
|
56761
56984
|
turnInFlight: turnInFlightAtReceipt,
|
|
56762
56985
|
isSteering,
|
|
@@ -57988,6 +58211,7 @@ async function handlePermissionSlash(ctx, behavior) {
|
|
|
57988
58211
|
return;
|
|
57989
58212
|
}
|
|
57990
58213
|
dispatchPermissionVerdict({ type: "permission", requestId: request_id, behavior });
|
|
58214
|
+
resumeReactionAfterVerdict();
|
|
57991
58215
|
pendingPermissions.delete(request_id);
|
|
57992
58216
|
process.stderr.write(`[telegram gateway] slash-${behavior} request_id=${request_id} tool=${details.tool_name} by=${senderId}
|
|
57993
58217
|
`);
|
|
@@ -60284,6 +60508,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
60284
60508
|
behavior: "allow",
|
|
60285
60509
|
rule: chosen.rule
|
|
60286
60510
|
});
|
|
60511
|
+
resumeReactionAfterVerdict();
|
|
60287
60512
|
let durable = false;
|
|
60288
60513
|
let legacy = false;
|
|
60289
60514
|
let failReason = "";
|
|
@@ -60385,7 +60610,9 @@ ${editLabel}` : editLabel,
|
|
|
60385
60610
|
return;
|
|
60386
60611
|
}
|
|
60387
60612
|
pendingPermissions.delete(request_id);
|
|
60388
|
-
const
|
|
60613
|
+
const resumeAgent = process.env.SWITCHROOM_AGENT_NAME;
|
|
60614
|
+
const resumeBeat = resumeAgent ? `\u25B6\uFE0F ${escapeHtmlForTg(resumeAgent)} resuming\u2026` : "\u25B6\uFE0F resuming\u2026";
|
|
60615
|
+
const label = `${behavior === "allow" ? "\u2705 Allowed" : "\u274C Denied"} \xB7 ${resumeBeat}`;
|
|
60389
60616
|
const msg = ctx.callbackQuery?.message;
|
|
60390
60617
|
const baseText = msg && "text" in msg && msg.text ? escapeHtmlForTg(msg.text) : "";
|
|
60391
60618
|
await finalizeCallback(ctx, {
|
|
@@ -60400,6 +60627,7 @@ ${label}` : label,
|
|
|
60400
60627
|
requestId: request_id,
|
|
60401
60628
|
behavior
|
|
60402
60629
|
});
|
|
60630
|
+
resumeReactionAfterVerdict();
|
|
60403
60631
|
}
|
|
60404
60632
|
});
|
|
60405
60633
|
});
|