switchroom 0.14.19 → 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 +17 -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 +357 -152
- package/telegram-plugin/dist/server.js +1 -1
- package/telegram-plugin/gateway/coalesce-attachments.ts +70 -0
- package/telegram-plugin/gateway/gateway.ts +246 -36
- package/telegram-plugin/gateway/interrupt-defer.ts +100 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +21 -4
- 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/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",
|
|
@@ -31882,6 +31959,7 @@ class DeferredDoneReactions {
|
|
|
31882
31959
|
var DESC_MAX = 80;
|
|
31883
31960
|
var TOOL_ARG_MAX = 64;
|
|
31884
31961
|
var SUMMARY_MAX = 100;
|
|
31962
|
+
var NARRATIVE_MAX_LINES = 6;
|
|
31885
31963
|
function renderWorkerActivity(v) {
|
|
31886
31964
|
const desc = truncate(v.description.trim() || "background task", DESC_MAX);
|
|
31887
31965
|
const elapsed = formatDuration(v.elapsedMs);
|
|
@@ -31900,10 +31978,17 @@ function renderWorkerActivity(v) {
|
|
|
31900
31978
|
} else {
|
|
31901
31979
|
activity = `<i>starting\u2026 (${elapsed})</i>`;
|
|
31902
31980
|
}
|
|
31903
|
-
const summary = v.latestSummary.trim();
|
|
31904
31981
|
const lines = [header, activity];
|
|
31905
|
-
|
|
31906
|
-
|
|
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
|
+
}
|
|
31907
31992
|
}
|
|
31908
31993
|
return lines.join(`
|
|
31909
31994
|
`);
|
|
@@ -31940,10 +32025,22 @@ function createWorkerActivityFeed(opts) {
|
|
|
31940
32025
|
h.cooldownUntil = nowFn() + retryAfter * 1000 + COOLDOWN_JITTER_MS;
|
|
31941
32026
|
log(`worker-feed: ${label} 429 \u2014 backing off ${retryAfter}s`);
|
|
31942
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
|
+
}
|
|
31943
32039
|
async function doUpdate(h, view) {
|
|
32040
|
+
accumulateNarrative(h, view);
|
|
31944
32041
|
if (nowFn() < h.cooldownUntil)
|
|
31945
32042
|
return;
|
|
31946
|
-
const body = renderWorkerActivity(view);
|
|
32043
|
+
const body = renderWorkerActivity({ ...view, narrativeLines: h.narrative });
|
|
31947
32044
|
if (h.messageId == null) {
|
|
31948
32045
|
if (view.elapsedMs < firstPaintMin)
|
|
31949
32046
|
return;
|
|
@@ -32010,6 +32107,7 @@ function createWorkerActivityFeed(opts) {
|
|
|
32010
32107
|
lastBody: null,
|
|
32011
32108
|
lastEditAt: 0,
|
|
32012
32109
|
cooldownUntil: 0,
|
|
32110
|
+
narrative: [],
|
|
32013
32111
|
chain: Promise.resolve()
|
|
32014
32112
|
};
|
|
32015
32113
|
handles.set(agentId, h);
|
|
@@ -46399,6 +46497,7 @@ function planBufferedRedelivery(pending) {
|
|
|
46399
46497
|
flush();
|
|
46400
46498
|
return out;
|
|
46401
46499
|
}
|
|
46500
|
+
var ATTACHMENT_META_RE = /^(image_path|attachment_)/;
|
|
46402
46501
|
function mergeRun(run2) {
|
|
46403
46502
|
const last = run2[run2.length - 1];
|
|
46404
46503
|
const mediaEntry = run2.find(inboundHasMedia);
|
|
@@ -46409,6 +46508,14 @@ function mergeRun(run2) {
|
|
|
46409
46508
|
};
|
|
46410
46509
|
delete merged.imagePath;
|
|
46411
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
|
+
}
|
|
46412
46519
|
if (mediaEntry?.imagePath != null)
|
|
46413
46520
|
merged.imagePath = mediaEntry.imagePath;
|
|
46414
46521
|
if (mediaEntry?.attachment != null)
|
|
@@ -47037,6 +47144,7 @@ function planBufferedRedelivery2(pending) {
|
|
|
47037
47144
|
flush();
|
|
47038
47145
|
return out;
|
|
47039
47146
|
}
|
|
47147
|
+
var ATTACHMENT_META_RE2 = /^(image_path|attachment_)/;
|
|
47040
47148
|
function mergeRun2(run2) {
|
|
47041
47149
|
const last = run2[run2.length - 1];
|
|
47042
47150
|
const mediaEntry = run2.find(inboundHasMedia2);
|
|
@@ -47047,6 +47155,14 @@ function mergeRun2(run2) {
|
|
|
47047
47155
|
};
|
|
47048
47156
|
delete merged.imagePath;
|
|
47049
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
|
+
}
|
|
47050
47166
|
if (mediaEntry?.imagePath != null)
|
|
47051
47167
|
merged.imagePath = mediaEntry.imagePath;
|
|
47052
47168
|
if (mediaEntry?.attachment != null)
|
|
@@ -51244,10 +51360,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
51244
51360
|
}
|
|
51245
51361
|
|
|
51246
51362
|
// ../src/build-info.ts
|
|
51247
|
-
var VERSION = "0.14.
|
|
51248
|
-
var COMMIT_SHA = "
|
|
51249
|
-
var COMMIT_DATE = "2026-05-
|
|
51250
|
-
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;
|
|
51251
51367
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
51252
51368
|
|
|
51253
51369
|
// gateway/boot-version.ts
|
|
@@ -52015,6 +52131,9 @@ function readAccessFile() {
|
|
|
52015
52131
|
parseMode: parsed.parseMode,
|
|
52016
52132
|
disableLinkPreview: parsed.disableLinkPreview,
|
|
52017
52133
|
coalescingGapMs: parsed.coalescingGapMs,
|
|
52134
|
+
coalesceMaxAttachments: parsed.coalesceMaxAttachments,
|
|
52135
|
+
interruptSafeBoundary: parsed.interruptSafeBoundary,
|
|
52136
|
+
interruptMaxWaitMs: parsed.interruptMaxWaitMs,
|
|
52018
52137
|
statusReactions: parsed.statusReactions,
|
|
52019
52138
|
historyEnabled: parsed.historyEnabled,
|
|
52020
52139
|
historyRetentionDays: parsed.historyRetentionDays,
|
|
@@ -52222,6 +52341,40 @@ var lastPtyPreviewByChat = new Map;
|
|
|
52222
52341
|
var progressUpdateLastSent = new Map;
|
|
52223
52342
|
var progressUpdateTurnCount = new Map;
|
|
52224
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
|
+
}
|
|
52225
52378
|
var preambleSuppressor = new PreambleSuppressor({
|
|
52226
52379
|
emitAnswer: (cumulative) => {
|
|
52227
52380
|
const stream = currentTurn?.answerStream ?? null;
|
|
@@ -52963,23 +53116,27 @@ function looksLikeAuthCode(text) {
|
|
|
52963
53116
|
return true;
|
|
52964
53117
|
return false;
|
|
52965
53118
|
}
|
|
52966
|
-
var bufferedAttachmentKeys = new
|
|
53119
|
+
var bufferedAttachmentKeys = new Map;
|
|
53120
|
+
function coalesceMaxAttachments() {
|
|
53121
|
+
return Math.max(1, loadAccess().coalesceMaxAttachments ?? 1);
|
|
53122
|
+
}
|
|
52967
53123
|
var inboundCoalescer = createInboundCoalescer({
|
|
52968
53124
|
gapMs: () => loadAccess().coalescingGapMs ?? 500,
|
|
52969
53125
|
merge: (entries) => {
|
|
52970
53126
|
const last = entries[entries.length - 1];
|
|
52971
|
-
const
|
|
53127
|
+
const { primary, extras } = splitCoalescedAttachments(entries, (e) => e.downloadImage != null || e.attachment != null, coalesceMaxAttachments());
|
|
52972
53128
|
return {
|
|
52973
|
-
text: entries.map((e) => e.text).join(`
|
|
53129
|
+
text: entries.map((e) => e.text).filter((t) => t.length > 0).join(`
|
|
52974
53130
|
`),
|
|
52975
53131
|
ctx: last.ctx,
|
|
52976
|
-
downloadImage:
|
|
52977
|
-
attachment:
|
|
53132
|
+
downloadImage: primary?.downloadImage,
|
|
53133
|
+
attachment: primary?.attachment,
|
|
53134
|
+
extraAttachments: extras.length > 0 ? extras.map((e) => ({ downloadImage: e.downloadImage, attachment: e.attachment })) : undefined
|
|
52978
53135
|
};
|
|
52979
53136
|
},
|
|
52980
53137
|
onFlush: (key, merged) => {
|
|
52981
53138
|
bufferedAttachmentKeys.delete(key);
|
|
52982
|
-
handleInbound(merged.ctx, merged.text, merged.downloadImage, merged.attachment);
|
|
53139
|
+
handleInbound(merged.ctx, merged.text, merged.downloadImage, merged.attachment, merged.extraAttachments);
|
|
52983
53140
|
}
|
|
52984
53141
|
});
|
|
52985
53142
|
function emitGatewayOperatorEvent(event) {
|
|
@@ -53498,6 +53655,10 @@ var ipcServer = createIpcServer({
|
|
|
53498
53655
|
const threadHint = msg.threadId != null ? String(msg.threadId) : undefined;
|
|
53499
53656
|
progressDriver?.ingest(ev, chatHint, threadHint);
|
|
53500
53657
|
handleSessionEvent(ev);
|
|
53658
|
+
toolFlightTracker.onEvent(ev);
|
|
53659
|
+
if (pendingDeferredInterrupt != null && !toolFlightTracker.isMidToolCall()) {
|
|
53660
|
+
fireDeferredInterrupt("boundary");
|
|
53661
|
+
}
|
|
53501
53662
|
if (currentTurn != null) {
|
|
53502
53663
|
const key = statusKey(currentTurn.sessionChatId, currentTurn.sessionThreadId);
|
|
53503
53664
|
if (ev.kind === "thinking") {
|
|
@@ -56141,7 +56302,8 @@ async function handleInboundCoalesced(ctx, text, downloadImage, attachment) {
|
|
|
56141
56302
|
return handleInbound(ctx, text, downloadImage, attachment);
|
|
56142
56303
|
}
|
|
56143
56304
|
const hasAttachment = downloadImage != null || attachment != null;
|
|
56144
|
-
|
|
56305
|
+
const maxAttachments = coalesceMaxAttachments();
|
|
56306
|
+
if (hasAttachment && ctx.message?.media_group_id != null && maxAttachments <= 1) {
|
|
56145
56307
|
return handleInbound(ctx, text, downloadImage, attachment);
|
|
56146
56308
|
}
|
|
56147
56309
|
const from = ctx.from;
|
|
@@ -56149,7 +56311,7 @@ async function handleInboundCoalesced(ctx, text, downloadImage, attachment) {
|
|
|
56149
56311
|
return;
|
|
56150
56312
|
if (hasAttachment) {
|
|
56151
56313
|
const probeKey = inboundCoalesceKey(String(ctx.chat.id), ctx.message?.message_thread_id, String(from.id));
|
|
56152
|
-
if (bufferedAttachmentKeys.
|
|
56314
|
+
if ((bufferedAttachmentKeys.get(probeKey) ?? 0) >= maxAttachments) {
|
|
56153
56315
|
return handleInbound(ctx, text, downloadImage, attachment);
|
|
56154
56316
|
}
|
|
56155
56317
|
}
|
|
@@ -56159,7 +56321,7 @@ async function handleInboundCoalesced(ctx, text, downloadImage, attachment) {
|
|
|
56159
56321
|
if (result.bypass)
|
|
56160
56322
|
return handleInbound(ctx, text, downloadImage, attachment);
|
|
56161
56323
|
if (hasAttachment)
|
|
56162
|
-
bufferedAttachmentKeys.
|
|
56324
|
+
bufferedAttachmentKeys.set(key, (bufferedAttachmentKeys.get(key) ?? 0) + 1);
|
|
56163
56325
|
}
|
|
56164
56326
|
function maybeEarlyAckReaction(ctx, from) {
|
|
56165
56327
|
const msgId = ctx.message?.message_id;
|
|
@@ -56180,7 +56342,7 @@ function maybeEarlyAckReaction(ctx, from) {
|
|
|
56180
56342
|
]).catch(() => {});
|
|
56181
56343
|
bot.api.sendChatAction(chatId, "typing").catch(() => {});
|
|
56182
56344
|
}
|
|
56183
|
-
async function handleInbound(ctx, text, downloadImage, attachment) {
|
|
56345
|
+
async function handleInbound(ctx, text, downloadImage, attachment, extraAttachments) {
|
|
56184
56346
|
const isTopicMessage = ctx.message?.is_topic_message ?? false;
|
|
56185
56347
|
const messageThreadId = ctx.message?.message_thread_id;
|
|
56186
56348
|
if (TOPIC_ID != null) {
|
|
@@ -56240,16 +56402,22 @@ async function handleInbound(ctx, text, downloadImage, attachment) {
|
|
|
56240
56402
|
`);
|
|
56241
56403
|
}
|
|
56242
56404
|
const interrupt = parseInterruptMarker(text);
|
|
56405
|
+
let deferInterrupt = false;
|
|
56243
56406
|
if (interrupt.isInterrupt) {
|
|
56244
56407
|
const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
|
|
56245
|
-
|
|
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()}
|
|
56246
56414
|
`);
|
|
56247
56415
|
if (msgId != null) {
|
|
56248
56416
|
bot.api.setMessageReaction(chat_id, msgId, [
|
|
56249
56417
|
{ type: "emoji", emoji: "\u26A1" }
|
|
56250
56418
|
]).catch(() => {});
|
|
56251
56419
|
}
|
|
56252
|
-
if (agentName3) {
|
|
56420
|
+
if (agentName3 && !deferInterrupt) {
|
|
56253
56421
|
try {
|
|
56254
56422
|
const { sendAgentInterrupt: sendAgentInterrupt2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
|
|
56255
56423
|
const r = sendAgentInterrupt2({ agentName: agentName3 });
|
|
@@ -56688,6 +56856,16 @@ ${preBlock(write.output)}`;
|
|
|
56688
56856
|
}
|
|
56689
56857
|
}
|
|
56690
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;
|
|
56691
56869
|
const replyToMsg = ctx.message?.reply_to_message;
|
|
56692
56870
|
const replyToMessageId = replyToMsg?.message_id;
|
|
56693
56871
|
const replyToTextRaw = replyToMsg ? replyToMsg.text ?? replyToMsg.caption ?? undefined : undefined;
|
|
@@ -56771,10 +56949,37 @@ ${preBlock(write.output)}`;
|
|
|
56771
56949
|
...attachment.size != null ? { attachment_size: String(attachment.size) } : {},
|
|
56772
56950
|
...attachment.mime ? { attachment_mime: attachment.mime } : {},
|
|
56773
56951
|
...attachment.name ? { attachment_name: attachment.name } : {}
|
|
56774
|
-
} : {}
|
|
56952
|
+
} : {},
|
|
56953
|
+
...attachmentCount > 1 ? { attachment_count: String(attachmentCount) } : {},
|
|
56954
|
+
...extraMeta
|
|
56775
56955
|
}
|
|
56776
56956
|
};
|
|
56777
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
|
+
}
|
|
56778
56983
|
if (decideInboundDelivery({
|
|
56779
56984
|
turnInFlight: turnInFlightAtReceipt,
|
|
56780
56985
|
isSteering,
|