switchroom 0.12.20 → 0.12.21
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/cli/switchroom.js +2 -2
- package/package.json +3 -2
- package/telegram-plugin/dist/gateway/gateway.js +25 -13
- package/telegram-plugin/gateway/chat-key.ts +67 -0
- package/telegram-plugin/gateway/gateway.ts +29 -9
- package/telegram-plugin/pty-partial-handler.ts +4 -1
- package/telegram-plugin/stream-reply-handler.ts +6 -1
- package/telegram-plugin/tests/e2e.test.ts +5 -1
- package/telegram-plugin/tests/races.test.ts +5 -1
package/dist/cli/switchroom.js
CHANGED
|
@@ -47247,8 +47247,8 @@ var {
|
|
|
47247
47247
|
} = import__.default;
|
|
47248
47248
|
|
|
47249
47249
|
// src/build-info.ts
|
|
47250
|
-
var VERSION = "0.12.
|
|
47251
|
-
var COMMIT_SHA = "
|
|
47250
|
+
var VERSION = "0.12.21";
|
|
47251
|
+
var COMMIT_SHA = "e32c064";
|
|
47252
47252
|
|
|
47253
47253
|
// src/cli/agent.ts
|
|
47254
47254
|
init_source();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.21",
|
|
4
4
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,12 +26,13 @@
|
|
|
26
26
|
"test:vitest": "vitest run",
|
|
27
27
|
"test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/uat/load-env.test.ts",
|
|
28
28
|
"test:watch": "vitest",
|
|
29
|
-
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs",
|
|
29
|
+
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs",
|
|
30
30
|
"lint:tsc": "tsc --noEmit",
|
|
31
31
|
"lint:plugin-references": "node scripts/check-plugin-references.mjs",
|
|
32
32
|
"lint:bot-api-wrapping": "bash scripts/check-bot-api-wrapping.sh",
|
|
33
33
|
"lint:bun-test-imports": "node scripts/check-bun-test-imports.mjs",
|
|
34
34
|
"lint:no-pii": "node scripts/check-no-pii-secrets.mjs",
|
|
35
|
+
"lint:no-broadcast-delivery": "node scripts/check-no-broadcast-delivery.mjs",
|
|
35
36
|
"prepublishOnly": "npm run build && npm run lint && npm test"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
@@ -31461,7 +31461,8 @@ function createStreamController(cfg) {
|
|
|
31461
31461
|
|
|
31462
31462
|
// pty-partial-handler.ts
|
|
31463
31463
|
function streamKey(chatId, threadId) {
|
|
31464
|
-
|
|
31464
|
+
const t = threadId == null || threadId === 0 ? "_" : String(threadId);
|
|
31465
|
+
return `${chatId}:${t}`;
|
|
31465
31466
|
}
|
|
31466
31467
|
function handlePtyPartialPure(text, state, deps) {
|
|
31467
31468
|
if (state.currentSessionChatId == null) {
|
|
@@ -31547,7 +31548,8 @@ function buildAccentHeader(accent) {
|
|
|
31547
31548
|
}
|
|
31548
31549
|
}
|
|
31549
31550
|
function streamKey2(chatId, threadId, lane, turnKey) {
|
|
31550
|
-
const
|
|
31551
|
+
const t = threadId == null || threadId === 0 ? "_" : String(threadId);
|
|
31552
|
+
const base = `${chatId}:${t}`;
|
|
31551
31553
|
const withLane = lane != null && lane.length > 0 ? `${base}:${lane}` : base;
|
|
31552
31554
|
return turnKey != null && turnKey.length > 0 ? `${withLane}:${turnKey}` : withLane;
|
|
31553
31555
|
}
|
|
@@ -43803,6 +43805,15 @@ function createPendingPermissionBuffer(opts = {}) {
|
|
|
43803
43805
|
};
|
|
43804
43806
|
}
|
|
43805
43807
|
|
|
43808
|
+
// gateway/chat-key.ts
|
|
43809
|
+
function chatKey(chatId, threadId) {
|
|
43810
|
+
const t = threadId == null || threadId === 0 ? "_" : String(threadId);
|
|
43811
|
+
return `${chatId}:${t}`;
|
|
43812
|
+
}
|
|
43813
|
+
function chatKeyWithSuffix(chatId, threadId, suffix) {
|
|
43814
|
+
return `${chatKey(chatId, threadId)}:${suffix}`;
|
|
43815
|
+
}
|
|
43816
|
+
|
|
43806
43817
|
// gateway/vault-grant-inbound-builders.ts
|
|
43807
43818
|
function buildVaultGrantApprovedInbound(opts) {
|
|
43808
43819
|
const ts = opts.nowMs ?? Date.now();
|
|
@@ -47115,11 +47126,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
47115
47126
|
}
|
|
47116
47127
|
|
|
47117
47128
|
// ../src/build-info.ts
|
|
47118
|
-
var VERSION = "0.12.
|
|
47119
|
-
var COMMIT_SHA = "
|
|
47120
|
-
var COMMIT_DATE = "2026-05-
|
|
47121
|
-
var LATEST_PR =
|
|
47122
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
47129
|
+
var VERSION = "0.12.21";
|
|
47130
|
+
var COMMIT_SHA = "e32c064";
|
|
47131
|
+
var COMMIT_DATE = "2026-05-20T02:05:49Z";
|
|
47132
|
+
var LATEST_PR = 1572;
|
|
47133
|
+
var COMMITS_AHEAD_OF_TAG = null;
|
|
47123
47134
|
|
|
47124
47135
|
// gateway/boot-version.ts
|
|
47125
47136
|
function formatRelativeAgo(iso) {
|
|
@@ -48062,10 +48073,10 @@ var CONTEXT_EXHAUSTION_COOLDOWN_MS = 600000;
|
|
|
48062
48073
|
var lastContextExhaustionWarningAt = 0;
|
|
48063
48074
|
var pendingPtyPartial = null;
|
|
48064
48075
|
function statusKey(chatId, threadId) {
|
|
48065
|
-
return
|
|
48076
|
+
return chatKey(chatId, threadId);
|
|
48066
48077
|
}
|
|
48067
48078
|
function streamKey3(chatId, threadId) {
|
|
48068
|
-
return
|
|
48079
|
+
return chatKey(chatId, threadId);
|
|
48069
48080
|
}
|
|
48070
48081
|
function purgeReactionTracking(key) {
|
|
48071
48082
|
const msgInfo = activeReactionMsgIds.get(key);
|
|
@@ -50526,7 +50537,7 @@ function resetOrphanedReplyTimeout() {
|
|
|
50526
50537
|
}
|
|
50527
50538
|
}
|
|
50528
50539
|
function closeActivityLane(chatId, threadId) {
|
|
50529
|
-
const key =
|
|
50540
|
+
const key = chatKeyWithSuffix(chatId, threadId, "activity");
|
|
50530
50541
|
const stream = activeDraftStreams.get(key);
|
|
50531
50542
|
if (stream == null)
|
|
50532
50543
|
return;
|
|
@@ -50535,7 +50546,7 @@ function closeActivityLane(chatId, threadId) {
|
|
|
50535
50546
|
stream.finalize().catch(() => {});
|
|
50536
50547
|
}
|
|
50537
50548
|
function closeProgressLane(chatId, threadId) {
|
|
50538
|
-
const prefix =
|
|
50549
|
+
const prefix = chatKeyWithSuffix(chatId, threadId, "progress");
|
|
50539
50550
|
for (const [key, stream] of activeDraftStreams) {
|
|
50540
50551
|
if (key.startsWith(prefix)) {
|
|
50541
50552
|
activeDraftStreams.delete(key);
|
|
@@ -50575,7 +50586,8 @@ function handleSessionEvent(ev) {
|
|
|
50575
50586
|
currentTurn = next;
|
|
50576
50587
|
preambleSuppressor.reset();
|
|
50577
50588
|
if (turnsDb != null) {
|
|
50578
|
-
const
|
|
50589
|
+
const evThreadIdNum = ev.threadId != null ? Number(ev.threadId) : null;
|
|
50590
|
+
const turnKey = chatKeyWithSuffix(ev.chatId, evThreadIdNum, String(startedAt));
|
|
50579
50591
|
next.registryKey = turnKey;
|
|
50580
50592
|
const userPromptPreview = extractUserPromptPreview(ev.rawContent);
|
|
50581
50593
|
try {
|
|
@@ -50808,7 +50820,7 @@ function handleSessionEvent(ev) {
|
|
|
50808
50820
|
if (flushDecision.kind === "skip" && flushDecision.reason === "silent-marker") {
|
|
50809
50821
|
process.stderr.write(`telegram gateway: silent-turn-suppression: chat=${chatId} turnKey=${turn.startedAt} reason=silent-marker
|
|
50810
50822
|
`);
|
|
50811
|
-
const suppressPrefix =
|
|
50823
|
+
const suppressPrefix = chatKeyWithSuffix(chatId, threadId, "progress");
|
|
50812
50824
|
for (const [key] of activeDraftStreams) {
|
|
50813
50825
|
if (key.startsWith(suppressPrefix)) {
|
|
50814
50826
|
activeDraftStreams.delete(key);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical chat-key construction with branded type.
|
|
3
|
+
*
|
|
4
|
+
* The gateway tracks per-chat state (active turn, draft stream, status
|
|
5
|
+
* reactions, progress throttle, etc.) in `Map<string, ...>` instances
|
|
6
|
+
* keyed by a string derived from `(chatId, threadId | null)`. Multiple
|
|
7
|
+
* callsites historically constructed those keys inline as
|
|
8
|
+
* `` `${chatId}:${threadId ?? '_'}` ``. That worked until PR #1564 hit
|
|
9
|
+
* the silence-poke wedge: an `activeTurnStartedAt` entry survived a
|
|
10
|
+
* turn-end because the entry was set under one rendering of the key
|
|
11
|
+
* (`null` thread → `_`) and the cleanup ran against another
|
|
12
|
+
* (`undefined` thread → `_`, but also `0` thread → `0`, which doesn't
|
|
13
|
+
* coalesce). The fallback's `purgeReactionTracking` cleared one
|
|
14
|
+
* statusKey at a time; sibling keys for the same chat persisted, so
|
|
15
|
+
* `activeTurnStartedAt.size > 0` stayed true forever and every
|
|
16
|
+
* subsequent inbound was held mid-turn.
|
|
17
|
+
*
|
|
18
|
+
* This module makes the bug class hard to reintroduce:
|
|
19
|
+
*
|
|
20
|
+
* 1. `chatKey()` is the canonical constructor for "this chat+thread"
|
|
21
|
+
* keys. It collapses `null`, `undefined`, and `0` thread IDs
|
|
22
|
+
* into the same token (`_`). All sites in `gateway.ts`,
|
|
23
|
+
* `stream-reply-handler.ts`, and `pty-partial-handler.ts` route
|
|
24
|
+
* through it (or mirror its expression inline).
|
|
25
|
+
* 2. The returned `ChatKey` is a branded string — a documentation
|
|
26
|
+
* marker for human readers. `telegram-plugin/` is bun-bundled
|
|
27
|
+
* and NOT in `tsconfig.json`'s `include`; `lint:plugin-references`
|
|
28
|
+
* filters tsc output to reference-class errors only (TS2304/2552/
|
|
29
|
+
* 2722/2561). A future opt-in `Map<ChatKey, T>` migration could
|
|
30
|
+
* provide enforcement teeth, but that requires either bringing
|
|
31
|
+
* `telegram-plugin/` into the strict tsc check or rewriting the
|
|
32
|
+
* lint to fail on TS2345 type-mismatch. Today the brand is a
|
|
33
|
+
* nudge, not a fence.
|
|
34
|
+
*
|
|
35
|
+
* `chatKeyWithSuffix()` extends a ChatKey with a lane suffix
|
|
36
|
+
* (`activity`, `progress`, etc.) — same documentation brand.
|
|
37
|
+
*
|
|
38
|
+
* Non-ChatKey keyspaces (chat:message, chat:user, chat:sender:reason)
|
|
39
|
+
* are deliberately NOT branded — they have different shape and
|
|
40
|
+
* different invariants, see `deferredKey` / `inboundCoalesceKey` /
|
|
41
|
+
* the gate-dedup helper in `gateway.ts`.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
export type ChatKey = string & { readonly __brand: 'ChatKey' }
|
|
45
|
+
|
|
46
|
+
export function chatKey(chatId: string, threadId?: number | null): ChatKey {
|
|
47
|
+
const t = threadId == null || threadId === 0 ? '_' : String(threadId)
|
|
48
|
+
return `${chatId}:${t}` as ChatKey
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function chatKeyWithSuffix(
|
|
52
|
+
chatId: string,
|
|
53
|
+
threadId: number | null | undefined,
|
|
54
|
+
suffix: string,
|
|
55
|
+
): ChatKey {
|
|
56
|
+
return `${chatKey(chatId, threadId)}:${suffix}` as ChatKey
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract the chatId portion of a ChatKey. Used by sibling-key sweeps
|
|
61
|
+
* (PR #1564's `purgeStaleTurnsForChat`) that need to enumerate every
|
|
62
|
+
* thread under a single chat.
|
|
63
|
+
*/
|
|
64
|
+
export function chatIdOfChatKey(key: ChatKey): string {
|
|
65
|
+
const idx = key.indexOf(':')
|
|
66
|
+
return idx === -1 ? key : key.slice(0, idx)
|
|
67
|
+
}
|
|
@@ -253,6 +253,7 @@ import { createInboundSpool } from './inbound-spool.js'
|
|
|
253
253
|
import { purgeStaleTurnsForChat } from './turn-state-purge.js'
|
|
254
254
|
import { decideInboundDelivery } from './inbound-delivery-gate.js'
|
|
255
255
|
import { createPendingPermissionBuffer } from './pending-permission-decisions.js'
|
|
256
|
+
import { chatKey, chatKeyWithSuffix } from './chat-key.js'
|
|
256
257
|
import {
|
|
257
258
|
buildVaultGrantApprovedInbound,
|
|
258
259
|
buildVaultGrantDeniedInbound,
|
|
@@ -1026,6 +1027,7 @@ function checkApprovals(): void {
|
|
|
1026
1027
|
try { files = readdirSync(APPROVED_DIR) } catch { return }
|
|
1027
1028
|
for (const senderId of files) {
|
|
1028
1029
|
const file = join(APPROVED_DIR, senderId)
|
|
1030
|
+
// allow-raw-bot-api: Paired! sendMessage to DM senderId; no thread_id, cannot trigger THREAD_NOT_FOUND
|
|
1029
1031
|
void bot.api.sendMessage(senderId, "Paired! Say hi to Claude.").then(
|
|
1030
1032
|
() => rmSync(file, { force: true }),
|
|
1031
1033
|
err => {
|
|
@@ -1248,12 +1250,18 @@ let lastContextExhaustionWarningAt = 0
|
|
|
1248
1250
|
|
|
1249
1251
|
let pendingPtyPartial: string | null = null
|
|
1250
1252
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
+
// statusKey + streamKey are thin re-exports of the canonical chatKey()
|
|
1254
|
+
// constructor (see telegram-plugin/gateway/chat-key.ts). chatKey()
|
|
1255
|
+
// collapses 0/null/undefined thread IDs to the same token (`_`),
|
|
1256
|
+
// closing the sibling-key class behind #1564 (where one site set the
|
|
1257
|
+
// entry under `null → _` and another cleared under `0 → 0`, leaving
|
|
1258
|
+
// activeTurnStartedAt non-empty forever).
|
|
1259
|
+
function statusKey(chatId: string, threadId?: number | null): string {
|
|
1260
|
+
return chatKey(chatId, threadId)
|
|
1253
1261
|
}
|
|
1254
1262
|
|
|
1255
|
-
function streamKey(chatId: string, threadId?: number): string {
|
|
1256
|
-
return
|
|
1263
|
+
function streamKey(chatId: string, threadId?: number | null): string {
|
|
1264
|
+
return chatKey(chatId, threadId)
|
|
1257
1265
|
}
|
|
1258
1266
|
|
|
1259
1267
|
function purgeReactionTracking(key: string): void {
|
|
@@ -2675,6 +2683,7 @@ function emitGatewayOperatorEvent(event: OperatorEvent): void {
|
|
|
2675
2683
|
parse_mode: 'HTML' as const,
|
|
2676
2684
|
...(renderedKeyboard ? { reply_markup: renderedKeyboard } : {}),
|
|
2677
2685
|
}
|
|
2686
|
+
// allow-raw-bot-api: operator-event broadcast loop; opts has no message_thread_id
|
|
2678
2687
|
void bot.api.sendMessage(chat_id, renderedText, opts as never).catch(e => {
|
|
2679
2688
|
process.stderr.write(
|
|
2680
2689
|
`telegram gateway: operator-event send to ${chat_id} failed agent=${agent} kind=${kind}: ${e}\n`,
|
|
@@ -3453,6 +3462,7 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
3453
3462
|
.text(`🔁 Always allow ${alwaysRule.label}`, `perm:always:${requestId}`)
|
|
3454
3463
|
}
|
|
3455
3464
|
for (const chat_id of access.allowFrom) {
|
|
3465
|
+
// allow-raw-bot-api: permission-request keyboard fan-out; reply_markup-only opts, no thread_id
|
|
3456
3466
|
void bot.api.sendMessage(chat_id, text, { reply_markup: keyboard }).catch(e => {
|
|
3457
3467
|
process.stderr.write(`telegram gateway: permission_request send to ${chat_id} failed: ${e}\n`)
|
|
3458
3468
|
})
|
|
@@ -4069,6 +4079,7 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
4069
4079
|
stripped.length > 0
|
|
4070
4080
|
? stripped
|
|
4071
4081
|
: '⚠️ (a formatted fragment could not be rendered for Telegram)'
|
|
4082
|
+
// allow-raw-bot-api: plaintext last-resort fallback; wrapping would re-enter the parse-mode policy that just rejected the payload
|
|
4072
4083
|
const sent = await lockedBot.api.sendMessage(chat_id, plain, plainOpts as never)
|
|
4073
4084
|
sentIds.push(sent.message_id)
|
|
4074
4085
|
logOutbound('reply', chat_id, sent.message_id, plain.length, `chunk=${i + 1}/${chunks.length} plaintext-fallback`)
|
|
@@ -4090,6 +4101,7 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
4090
4101
|
const retryOpts = { ...sendOpts }
|
|
4091
4102
|
delete (retryOpts as any).message_thread_id
|
|
4092
4103
|
try {
|
|
4104
|
+
// allow-raw-bot-api: chunk-loop THREAD_NOT_FOUND fallback; thread already dropped, wrapping would re-enter the throw
|
|
4093
4105
|
const sent = await lockedBot.api.sendMessage(chat_id, chunks[i], retryOpts as never)
|
|
4094
4106
|
sentIds.push(sent.message_id)
|
|
4095
4107
|
} catch (retryErr) {
|
|
@@ -5392,7 +5404,7 @@ function resetOrphanedReplyTimeout(): void {
|
|
|
5392
5404
|
}
|
|
5393
5405
|
|
|
5394
5406
|
function closeActivityLane(chatId: string, threadId: number | undefined): void {
|
|
5395
|
-
const key =
|
|
5407
|
+
const key = chatKeyWithSuffix(chatId, threadId, 'activity')
|
|
5396
5408
|
const stream = activeDraftStreams.get(key)
|
|
5397
5409
|
if (stream == null) return
|
|
5398
5410
|
activeDraftStreams.delete(key)
|
|
@@ -5404,7 +5416,7 @@ function closeProgressLane(chatId: string, threadId: number | undefined): void {
|
|
|
5404
5416
|
// Progress-card streams include a turnKey suffix in their key
|
|
5405
5417
|
// (e.g. "chatId:_:progress:chatId:1"). Iterate and match by prefix
|
|
5406
5418
|
// so the backstop actually finds the stream.
|
|
5407
|
-
const prefix =
|
|
5419
|
+
const prefix = chatKeyWithSuffix(chatId, threadId, 'progress')
|
|
5408
5420
|
for (const [key, stream] of activeDraftStreams) {
|
|
5409
5421
|
if (key.startsWith(prefix)) {
|
|
5410
5422
|
activeDraftStreams.delete(key)
|
|
@@ -5460,7 +5472,11 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
5460
5472
|
// progress-card-driver's per-chat sequence number (these are two
|
|
5461
5473
|
// independent identifier schemes and don't need to align).
|
|
5462
5474
|
if (turnsDb != null) {
|
|
5463
|
-
|
|
5475
|
+
// ev.threadId is `string | null` (Telegram emits as string); convert
|
|
5476
|
+
// to number for chatKeyWithSuffix. Number(null) = 0 which canonicalizes
|
|
5477
|
+
// to '_' — same as the explicit `null` branch below.
|
|
5478
|
+
const evThreadIdNum = ev.threadId != null ? Number(ev.threadId) : null
|
|
5479
|
+
const turnKey = chatKeyWithSuffix(ev.chatId, evThreadIdNum, String(startedAt))
|
|
5464
5480
|
next.registryKey = turnKey
|
|
5465
5481
|
// Phase 1 of #332: capture first ~200 chars of the user's message.
|
|
5466
5482
|
const userPromptPreview = extractUserPromptPreview(ev.rawContent)
|
|
@@ -5857,7 +5873,7 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
5857
5873
|
// Drop progress-card streams without finalising — the normal
|
|
5858
5874
|
// closeProgressLane call below would call stream.finalize() which
|
|
5859
5875
|
// sends a final "Done" edit to Telegram. Skip that for silent turns.
|
|
5860
|
-
const suppressPrefix =
|
|
5876
|
+
const suppressPrefix = chatKeyWithSuffix(chatId, threadId, 'progress')
|
|
5861
5877
|
for (const [key] of activeDraftStreams) {
|
|
5862
5878
|
if (key.startsWith(suppressPrefix)) {
|
|
5863
5879
|
activeDraftStreams.delete(key)
|
|
@@ -10978,6 +10994,7 @@ async function grantWizardStep2(ctx: Context, chatId: string, agent: string, wiz
|
|
|
10978
10994
|
const kb = buildGrantKeysKeyboard(keys, selected)
|
|
10979
10995
|
const text = `<b>Grant capability token — Step 2/3</b>\n\nWhich keys for <code>${escapeHtmlForTg(agent)}</code>?\n<i>Tap to toggle; tap Continue when done.</i>`
|
|
10980
10996
|
if (wizardMsgId != null) {
|
|
10997
|
+
// allow-raw-bot-api: vault grant wizard step 2/3; already .catch-swallows, tap-driven UI re-renders on retry
|
|
10981
10998
|
await ctx.api.editMessageText(chatId, wizardMsgId, text, { parse_mode: 'HTML', reply_markup: kb }).catch(() => {})
|
|
10982
10999
|
} else {
|
|
10983
11000
|
const sent = await switchroomReply(ctx, text, { html: true, reply_markup: kb })
|
|
@@ -11001,6 +11018,7 @@ async function grantWizardStep3(ctx: Context, chatId: string, state: Extract<Pen
|
|
|
11001
11018
|
const text = `<b>Grant capability token — Step 3/3</b>\n\nKeys for <code>${escapeHtmlForTg(state.agent!)}</code>:\n${keyList}\n\nHow long should this grant be valid?`
|
|
11002
11019
|
const msgId = state.wizardMsgId
|
|
11003
11020
|
if (msgId != null) {
|
|
11021
|
+
// allow-raw-bot-api: vault grant wizard step 3/3 (TTL select); already .catch-swallows, tap-driven UI re-renders on retry
|
|
11004
11022
|
await ctx.api.editMessageText(chatId, msgId, text, { parse_mode: 'HTML', reply_markup: kb }).catch(() => {})
|
|
11005
11023
|
} else {
|
|
11006
11024
|
const sent = await switchroomReply(ctx, text, { html: true, reply_markup: kb })
|
|
@@ -11025,6 +11043,7 @@ async function grantWizardConfirm(ctx: Context, chatId: string, state: Extract<P
|
|
|
11025
11043
|
].join('\n')
|
|
11026
11044
|
const msgId = state.wizardMsgId
|
|
11027
11045
|
if (msgId != null) {
|
|
11046
|
+
// allow-raw-bot-api: vault grant wizard confirm step; already .catch-swallows, tap-driven UI re-renders on retry
|
|
11028
11047
|
await ctx.api.editMessageText(chatId, msgId, text, { parse_mode: 'HTML', reply_markup: kb }).catch(() => {})
|
|
11029
11048
|
} else {
|
|
11030
11049
|
const sent = await switchroomReply(ctx, text, { html: true, reply_markup: kb })
|
|
@@ -13462,6 +13481,7 @@ function handleChecklistUpdate(
|
|
|
13462
13481
|
ts: new Date(ts * 1000).toISOString(),
|
|
13463
13482
|
},
|
|
13464
13483
|
}
|
|
13484
|
+
// allow-broadcast: informational checklist task notification, not turn-driving
|
|
13465
13485
|
ipcServer.broadcast(inboundMsg)
|
|
13466
13486
|
process.stderr.write(
|
|
13467
13487
|
`telegram gateway: checklist ${kind}: chat_id=${chat_id} message_id=${message_id} task_id=${taskId} state=${state} user=${userName}\n`,
|
|
@@ -13913,7 +13933,7 @@ async function shutdown(signal: string): Promise<void> {
|
|
|
13913
13933
|
// gateway no longer pre-allocates drafts on inbound, so there is
|
|
13914
13934
|
// nothing to clear at SIGTERM time.
|
|
13915
13935
|
|
|
13916
|
-
//
|
|
13936
|
+
// allow-broadcast: informational shutdown notify — bridges mark themselves disconnected
|
|
13917
13937
|
ipcServer.broadcast({ type: 'status', status: 'gateway_shutting_down' })
|
|
13918
13938
|
|
|
13919
13939
|
// Hard force-exit safety net at budget + 5s. systemd's TimeoutStopSec
|
|
@@ -94,7 +94,10 @@ export interface PtyHandlerDeps {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
function streamKey(chatId: string, threadId?: number): string {
|
|
97
|
-
|
|
97
|
+
// Canonical chat-key derivation lives in gateway/chat-key.ts — keep this
|
|
98
|
+
// expression in lockstep (treats 0/null/undefined the same). See #1564.
|
|
99
|
+
const t = threadId == null || threadId === 0 ? '_' : String(threadId)
|
|
100
|
+
return `${chatId}:${t}`
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
/**
|
|
@@ -313,7 +313,12 @@ function streamKey(
|
|
|
313
313
|
lane?: string,
|
|
314
314
|
turnKey?: string,
|
|
315
315
|
): string {
|
|
316
|
-
|
|
316
|
+
// Canonical chat-key derivation lives in gateway/chat-key.ts — keep this
|
|
317
|
+
// expression in lockstep with that helper (treats 0/null/undefined the
|
|
318
|
+
// same), but inline here so this file doesn't introduce a cross-package
|
|
319
|
+
// import for one expression. See #1564 for the sibling-key bug class.
|
|
320
|
+
const t = threadId == null || threadId === 0 ? '_' : String(threadId)
|
|
321
|
+
const base = `${chatId}:${t}`
|
|
317
322
|
const withLane = lane != null && lane.length > 0 ? `${base}:${lane}` : base
|
|
318
323
|
return turnKey != null && turnKey.length > 0 ? `${withLane}:${turnKey}` : withLane
|
|
319
324
|
}
|
|
@@ -50,7 +50,11 @@ function statusKey(chatId: string, threadId?: number): string {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function streamKey(chatId: string, threadId?: number): string {
|
|
53
|
-
|
|
53
|
+
// Mirrors production's canonical chat-key derivation (gateway/chat-key.ts):
|
|
54
|
+
// 0/null/undefined thread IDs all collapse to '_'. Diverging sentinels
|
|
55
|
+
// here would let the harness pass with a key shape production rejects.
|
|
56
|
+
const t = threadId == null || threadId === 0 ? '_' : String(threadId)
|
|
57
|
+
return `${chatId}:${t}`
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
interface PluginState {
|
|
@@ -29,7 +29,11 @@ function statusKey(chatId: string, threadId?: number): string {
|
|
|
29
29
|
return `${chatId}:${threadId ?? '_'}`
|
|
30
30
|
}
|
|
31
31
|
function streamKey(chatId: string, threadId?: number): string {
|
|
32
|
-
|
|
32
|
+
// Mirrors production's canonical chat-key derivation (gateway/chat-key.ts):
|
|
33
|
+
// 0/null/undefined thread IDs all collapse to '_'. Diverging sentinels
|
|
34
|
+
// here would let the harness pass with a key shape production rejects.
|
|
35
|
+
const t = threadId == null || threadId === 0 ? '_' : String(threadId)
|
|
36
|
+
return `${chatId}:${t}`
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
interface PluginState {
|