@llblab/pi-telegram 0.24.0 → 0.24.2
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/AGENTS.md +3 -2
- package/BACKLOG.md +5 -19
- package/CHANGELOG.md +128 -274
- package/docs/architecture.md +13 -1
- package/docs/multi-instance-bus.md +3 -3
- package/docs/public-api.md +2 -0
- package/lib/bus-transport.ts +7 -14
- package/lib/bus.ts +48 -19
- package/lib/command-templates.ts +36 -20
- package/lib/config.ts +6 -34
- package/lib/inbound.ts +22 -26
- package/lib/lifecycle.ts +0 -13
- package/lib/locks.ts +46 -26
- package/lib/logs.ts +76 -18
- package/lib/media.ts +0 -9
- package/lib/menu-model.ts +5 -12
- package/lib/menu.ts +0 -19
- package/lib/outbound.ts +2 -12
- package/lib/pi.ts +0 -4
- package/lib/queue.ts +0 -5
- package/lib/replies.ts +0 -23
- package/lib/runtime.ts +1 -2
- package/lib/sections.ts +1 -48
- package/lib/status.ts +14 -8
- package/lib/telegram-api.ts +0 -4
- package/lib/threads.ts +41 -8
- package/package.json +1 -1
package/docs/architecture.md
CHANGED
|
@@ -105,6 +105,8 @@ Mirrored domain regressions live in `/tests/*.test.ts`. Shared test fixtures sho
|
|
|
105
105
|
|
|
106
106
|
Telegram configuration lives in `~/.pi/agent/telegram.json`. Bot/session identity (`botToken`, `botUsername`, `botId`, `allowedUserId`, `lastUpdateId`) persists only under `profiles.default` or `profiles.<name>`; shared handlers and assistant/voice/time settings stay top-level. Authoritative transport ownership lives separately in the pi-telegram-private `~/.pi/agent/tmp/telegram/owners.json` store. Its top-level slots are `default` and validated named profile names; unrelated extensions never read or write this file.
|
|
107
107
|
|
|
108
|
+
`telegram.json` is one global cross-instance configuration document. Ordinary reads rely on atomic publication and do not take the mutation guard. Every cooperating Pi instance persists only its recursive delta from the snapshot it loaded, merges that delta into the latest disk document inside `telegram.json.transaction`, and publishes atomically only when the semantic result differs; a no-op merge adopts the newer disk snapshot in memory without replacing the file. Unrelated global and profile changes therefore survive stale writers, while `lastUpdateId` additionally merges monotonically. Two serialized writers changing the same leaf use commit order, so the later local delta wins. A non-transactional external editor cannot participate in that conflict protocol: it should write through same-directory atomic replacement while Pi is idle, then let instances reload; an editor racing the transaction may lose its same-leaf change and must retry from the resulting file.
|
|
109
|
+
|
|
108
110
|
### Setup Flow
|
|
109
111
|
|
|
110
112
|
`/telegram-setup` progressively resolves the bot token:
|
|
@@ -123,9 +125,19 @@ Telegram configuration lives in `~/.pi/agent/telegram.json`. Bot/session identit
|
|
|
123
125
|
- Session replacement suspends polling/watchers without releasing ownership so the next session in the same process can resume. A registered follower snapshots its assigned target into a short-lived same-process handoff, stops the old receiver/heartbeat, and re-registers through the live leader without marking or replacing its Telegram thread.
|
|
124
126
|
- Live external owners require explicit takeover confirmation. Long-lived timers compare against snapshotted owner identity and stop local transport work when the slot no longer matches.
|
|
125
127
|
- `owners.json` owns only Telegram transport control. Local extension and accepted queue state remain per Pi instance when ownership moves, but previews, final delivery, dispatch transport mutations, and delayed Bot API work fail closed until exact direct or follower authority becomes valid again.
|
|
126
|
-
- Every acquisition, refresh, release, takeover, and stale recovery serializes through the sibling `owners.json.transaction` guard. The guard publishes one private generation-named owner record atomically, validates filename/payload generation agreement, fences stale recovery and delayed release against replacement-owner ABA, and fails closed on malformed state, unverifiable ownership, contention timeout, or unsupported filesystem behavior. The JSON store publishes through a private same-directory temporary file and atomic rename; atomic payload replacement does not replace transaction serialization.
|
|
128
|
+
- Exact ownership remains checked every second, while the durable owner heartbeat refresh runs every two seconds and becomes stale after eight seconds. This keeps replacement detection responsive while halving steady-state atomic `owners.json` rewrites without changing the cross-platform file-transaction authority. Every acquisition, refresh, release, takeover, and stale recovery serializes through the sibling `owners.json.transaction` guard. The guard publishes one private generation-named owner record atomically, validates filename/payload generation agreement, fences stale recovery and delayed release against replacement-owner ABA, and fails closed on malformed state, unverifiable ownership, contention timeout, or unsupported filesystem behavior. The JSON store publishes through a private same-directory temporary file and atomic rename; atomic payload replacement does not replace transaction serialization.
|
|
127
129
|
- `owners.json` is authoritative and private. `state.json` remains an observable snapshot, `logs.jsonl` remains diagnostics, and followers remain authenticated bus registrations rather than ownership-file writers.
|
|
128
130
|
|
|
131
|
+
### Persistence I/O Baseline
|
|
132
|
+
|
|
133
|
+
The three runtime files have different authority and write pressure. Preserve that distinction when optimizing them:
|
|
134
|
+
|
|
135
|
+
- `owners.json` is safety-critical transport authority. Acquire, release, takeover, stale recovery, and two-second leader lease refresh mutate it. The steady-state baseline is one cached atomic rewrite every two seconds per active profile, or 43,200 refreshes/day; one-second ownership checks are read-only. Every mutation serializes the full cross-process read/check/write through `owners.json.transaction`.
|
|
136
|
+
- `state.json` combines recovery-critical thread/capability state with observational runtime projections. Every explicit thread-store `persist()` builds a semantic snapshot, but an unchanged payload skips temporary-file creation and rename after ignoring `writtenAtMs`; changed snapshots retain the full atomic replacement path. Diagnostics scheduling coalesces requests across a bounded 100 ms window. Only the exact transport owner commits; non-owners reload current disk state instead of publishing.
|
|
137
|
+
- `logs.jsonl` is fail-soft observational evidence, never routing authority. Runtime events admitted in one JavaScript turn batch by captured profile path into one size check, one profile-wide file transaction, and one append while preserving event order. Batching adds no timer or shutdown-loss window; separate profiles remain isolated, and one failed group does not drop another. Scope reset and rotation retain their serialized copy/replace path. The 5 MiB value is a rotation threshold: an authorized writer rotates between batched records before the next record crosses it, so overshoot is bounded to one admitted record plus reset metadata; a writer without reset authority defers rotation to the owner.
|
|
138
|
+
|
|
139
|
+
This baseline counts write-producing code paths rather than filesystem implementation details that vary between ext4, APFS, NTFS, and network-backed home directories. Optimization evidence should compare these deterministic triggers first, then use platform smoke evidence for rename, named-pipe, crash, and cleanup behavior. Recovery-critical `state.json` fields are `bot`, `identities`, `reservations`, `pendingProvisions`, `syncObservations`, and `threads`; `runtime`, `liveRoster`, `diagnostics`, and `writtenAtMs` are observational and may use bounded coalescing when authority checks remain unchanged.
|
|
140
|
+
|
|
129
141
|
Version `0.24.0` intentionally does not read or migrate the former agent-level `locks.json`; upgrading resets Telegram ownership. Run `/telegram-connect` when a fresh owner is not elected automatically. Delete `~/.pi/agent/tmp/telegram/owners.json` and its stale transaction guard, when no owner process is live, to reset only pi-telegram transport ownership without deleting configuration, diagnostics, or other extensions' state.
|
|
130
142
|
|
|
131
143
|
### Threaded Mode Multi-Instance Bus
|
|
@@ -147,7 +147,7 @@ A registered instance exposes:
|
|
|
147
147
|
|
|
148
148
|
## Leader Election
|
|
149
149
|
|
|
150
|
-
Leader election is heartbeat-gated and lock-backed
|
|
150
|
+
Leader election is heartbeat-gated and lock-backed. The polling owner checks exact lock ownership every second, refreshes its durable lease every two seconds, and becomes stale after eight seconds; the serialized expected-owner transaction remains the final cross-platform election authority.
|
|
151
151
|
|
|
152
152
|
1. On startup, read the Telegram lock.
|
|
153
153
|
2. If no leader exists, acquire leadership and start polling.
|
|
@@ -161,9 +161,9 @@ Followers first try to re-register after leader reload or unknown-heartbeat resp
|
|
|
161
161
|
|
|
162
162
|
Implemented transport:
|
|
163
163
|
|
|
164
|
-
### Local IPC endpoint
|
|
164
|
+
### Local IPC endpoint with bounded native paths
|
|
165
165
|
|
|
166
|
-
Leader opens a local Node `net` endpoint: a Unix-domain socket under the agent temp directory on Unix-like platforms, or a deterministic Windows named pipe (`\\.\pipe\pi-telegram-...`) on native Windows. On Unix, each server listens on a private generation socket and atomically publishes the stable profile path as a relative symlink; delayed shutdown closes only its private path and cannot remove a replacement generation's link. Followers register, heartbeat, and exchange routed events. The transport boundary owns endpoint derivation, socket-vs-pipe detection, bounded operation-aware retry policy, timeout/transient IPC error classification, endpoint reachability probes, and request-scoped transport events. Follower registration uses a longer registration-specific response timeout than ordinary heartbeat/forwarding calls because the leader may need to provision a Telegram thread before it can return the assigned target; timing out that handshake leaves a visible tab with no follower heartbeat. Keep this handshake to the true critical path: create/reuse the target, persist the live binding, and return it. Connected notices and replaced-thread reconciliation cleanup are non-critical and should run after registration so a follower becomes routable before Telegram client/server UI convergence work finishes.
|
|
166
|
+
Leader opens a local Node `net` endpoint: a Unix-domain socket under the agent temp directory on Unix-like platforms, or a deterministic Windows named pipe (`\\.\pipe\pi-telegram-...`) on native Windows. A filesystem-style endpoint supplied by legacy state or a transport harness normalizes deterministically to the same Windows pipe boundary before listen/connect. On Unix, an endpoint that would exceed conservative domain-socket pathname limits maps to a private user-scoped, hash-derived path under the OS temp directory; ordinary agent paths remain under `tmp/telegram`. On Unix, each server listens on a private generation socket and atomically publishes the stable profile path as a relative symlink; delayed shutdown closes only its private path and cannot remove a replacement generation's link. Followers register, heartbeat, and exchange routed events. The transport boundary owns endpoint derivation, socket-vs-pipe detection, bounded operation-aware retry policy, timeout/transient IPC error classification, endpoint reachability probes, and request-scoped transport events. Follower registration uses a longer registration-specific response timeout than ordinary heartbeat/forwarding calls because the leader may need to provision a Telegram thread before it can return the assigned target; timing out that handshake leaves a visible tab with no follower heartbeat. Keep this handshake to the true critical path: create/reuse the target, persist the live binding, and return it. Connected notices and replaced-thread reconciliation cleanup are non-critical and should run after registration so a follower becomes routable before Telegram client/server UI convergence work finishes.
|
|
167
167
|
|
|
168
168
|
Pros:
|
|
169
169
|
|
package/docs/public-api.md
CHANGED
|
@@ -109,6 +109,8 @@ interface TelegramConfig {
|
|
|
109
109
|
|
|
110
110
|
Bot/session identity always persists under `profiles.<name>`. The ordinary setup path uses `profiles.default`; `/telegram-setup default` and `/telegram-connect default` are exact aliases for the bare commands. Named profiles use the same shape. Shared handlers plus `assistant`, `voice`, and `time` remain top-level. On the first `0.24.0` load, unambiguous legacy root identity moves atomically into `profiles.default`; identical duplicates collapse, complementary fields merge, and conflicting values fail closed without modifying the file.
|
|
111
111
|
|
|
112
|
+
The file is global across Pi instances. Cooperating instances serialize recursive delta merges through `telegram.json.transaction`, preserve unrelated global/profile changes from newer disk snapshots, and merge `lastUpdateId` monotonically. A semantically unchanged merge adopts the latest disk state in memory without replacing the file; later commits win when two deltas intentionally change the same leaf. For manual edits, stop or idle the connected instances, publish a complete valid file atomically, and let them reload. A non-transactional editor racing Pi persistence has no same-leaf conflict guarantee.
|
|
113
|
+
|
|
112
114
|
Hidden/default semantics are represented by absence:
|
|
113
115
|
|
|
114
116
|
- `assistant.proactivePush` defaults to `true`; omit it to keep projection enabled, or set it explicitly to `false` to disable it. When enabled, each completed public assistant text block from local or autonomous work is projected to the authorized Telegram target once and in source order. This includes visible intermediate commentary/checkpoints and the final block. It excludes token deltas, hidden reasoning, tool calls/arguments/results, Telegram-owned turns, empty blocks, and stale authority. Projection uses the configured Rich or HTML assistant renderer and binds admitted work to the exact target, profile/token transport generation, direct leader epoch or follower registration generation, and session generation. The old top-level `proactivePush` key is ignored; move the setting manually under `assistant`.
|
package/lib/bus-transport.ts
CHANGED
|
@@ -35,21 +35,17 @@ export interface TelegramBusTransportRetryPolicyOverrides {
|
|
|
35
35
|
|
|
36
36
|
export type TelegramBusTransportOperation = "registration" | "operation";
|
|
37
37
|
|
|
38
|
-
export const TELEGRAM_BUS_REGISTRATION_RETRY: TelegramBusTransportRetryPolicy =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
export const TELEGRAM_BUS_REGISTRATION_RETRY: TelegramBusTransportRetryPolicy =
|
|
39
|
+
{
|
|
40
|
+
attempts: 10,
|
|
41
|
+
delayMs: 150,
|
|
42
|
+
};
|
|
42
43
|
|
|
43
44
|
export const TELEGRAM_BUS_OPERATION_RETRY: TelegramBusTransportRetryPolicy = {
|
|
44
45
|
attempts: 3,
|
|
45
46
|
delayMs: 100,
|
|
46
47
|
};
|
|
47
48
|
|
|
48
|
-
export const TELEGRAM_BUS_PIPE_REGISTRATION_RETRY =
|
|
49
|
-
TELEGRAM_BUS_REGISTRATION_RETRY;
|
|
50
|
-
|
|
51
|
-
export const TELEGRAM_BUS_PIPE_OPERATION_RETRY = TELEGRAM_BUS_OPERATION_RETRY;
|
|
52
|
-
|
|
53
49
|
export function getTelegramBusPipePath(input: {
|
|
54
50
|
agentDir: string;
|
|
55
51
|
scope: string;
|
|
@@ -200,13 +196,11 @@ export function createTelegramBusTransportTimeoutError(
|
|
|
200
196
|
|
|
201
197
|
export function delayTelegramBusTransportRetry(ms: number): Promise<void> {
|
|
202
198
|
return new Promise((resolve) => {
|
|
203
|
-
|
|
204
|
-
timer.unref?.();
|
|
199
|
+
setTimeout(resolve, ms);
|
|
205
200
|
});
|
|
206
201
|
}
|
|
207
202
|
|
|
208
|
-
export interface TelegramBusTransportProbeResult
|
|
209
|
-
extends TelegramBusTransportEndpointDiagnostics {
|
|
203
|
+
export interface TelegramBusTransportProbeResult extends TelegramBusTransportEndpointDiagnostics {
|
|
210
204
|
reachable: boolean;
|
|
211
205
|
error?: TelegramBusTransportErrorInfo;
|
|
212
206
|
}
|
|
@@ -238,7 +232,6 @@ export function probeTelegramBusEndpoint(input: {
|
|
|
238
232
|
),
|
|
239
233
|
});
|
|
240
234
|
}, timeoutMs);
|
|
241
|
-
timeout.unref?.();
|
|
242
235
|
socket.once("connect", () => settle({ ...diagnostics, reachable: true }));
|
|
243
236
|
socket.once("error", (error) =>
|
|
244
237
|
settle({
|
package/lib/bus.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* cross-instance forwarding helpers, and the live follower registry model.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { randomBytes } from "node:crypto";
|
|
8
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
9
9
|
import {
|
|
10
10
|
chmodSync,
|
|
11
11
|
existsSync,
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
type Server,
|
|
24
24
|
type Socket,
|
|
25
25
|
} from "node:net";
|
|
26
|
-
import { platform as getPlatform } from "node:os";
|
|
26
|
+
import { platform as getPlatform, tmpdir } from "node:os";
|
|
27
27
|
import { basename, dirname, join } from "node:path";
|
|
28
28
|
|
|
29
29
|
import {
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
getTelegramBusEndpointDiagnostics,
|
|
34
34
|
getTelegramBusFollowerEndpoint,
|
|
35
35
|
getTelegramBusLeaderEndpoint,
|
|
36
|
+
getTelegramBusPipePath,
|
|
36
37
|
getTelegramBusTransportRetryPolicy,
|
|
37
38
|
isTelegramBusPipePath,
|
|
38
39
|
isRetryableTelegramBusTransportError,
|
|
@@ -43,8 +44,6 @@ import {
|
|
|
43
44
|
import type { TelegramTarget } from "./target.ts";
|
|
44
45
|
import { resolveAgentDir } from "./paths.ts";
|
|
45
46
|
|
|
46
|
-
export type TelegramBusRole = "leader" | "follower";
|
|
47
|
-
|
|
48
47
|
export interface TelegramBusProcessRuntime {
|
|
49
48
|
instanceId: string;
|
|
50
49
|
manualFollowerOwnerId: string;
|
|
@@ -524,10 +523,38 @@ export interface TelegramBusLocalServer {
|
|
|
524
523
|
|
|
525
524
|
export type TelegramBusSocketPathSource = string | (() => string);
|
|
526
525
|
|
|
526
|
+
const TELEGRAM_BUS_MAX_DIRECT_UNIX_ENDPOINT_BYTES = 80;
|
|
527
|
+
|
|
527
528
|
export function resolveTelegramBusSocketPath(
|
|
528
529
|
source: TelegramBusSocketPathSource,
|
|
530
|
+
platform: NodeJS.Platform | string = getPlatform(),
|
|
529
531
|
): string {
|
|
530
|
-
|
|
532
|
+
const endpoint = typeof source === "function" ? source() : source;
|
|
533
|
+
if (platform === "win32") {
|
|
534
|
+
if (isTelegramBusPipePath(endpoint)) return endpoint;
|
|
535
|
+
return getTelegramBusPipePath({
|
|
536
|
+
agentDir: dirname(endpoint),
|
|
537
|
+
scope: basename(endpoint),
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
const ownerScope = process.getuid?.() ?? "user";
|
|
541
|
+
const fallbackDir = join(tmpdir(), `pi-telegram-${ownerScope}`);
|
|
542
|
+
if (
|
|
543
|
+
dirname(endpoint) === fallbackDir &&
|
|
544
|
+
/^[0-9a-f]{16}\.sock$/u.test(basename(endpoint))
|
|
545
|
+
) {
|
|
546
|
+
return endpoint;
|
|
547
|
+
}
|
|
548
|
+
if (
|
|
549
|
+
Buffer.byteLength(endpoint) <= TELEGRAM_BUS_MAX_DIRECT_UNIX_ENDPOINT_BYTES
|
|
550
|
+
) {
|
|
551
|
+
return endpoint;
|
|
552
|
+
}
|
|
553
|
+
const digest = createHash("sha256")
|
|
554
|
+
.update(endpoint)
|
|
555
|
+
.digest("hex")
|
|
556
|
+
.slice(0, 16);
|
|
557
|
+
return join(fallbackDir, `${digest}.sock`);
|
|
531
558
|
}
|
|
532
559
|
|
|
533
560
|
export interface TelegramBusLocalServerDeps {
|
|
@@ -617,7 +644,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
|
|
|
617
644
|
deps.recordRuntimeEvent?.(
|
|
618
645
|
"bus",
|
|
619
646
|
response?.kind === "bus.ack"
|
|
620
|
-
? response.message ?? "Follower rejected forwarded Telegram update."
|
|
647
|
+
? (response.message ?? "Follower rejected forwarded Telegram update.")
|
|
621
648
|
: "Follower returned no forwarding acknowledgement.",
|
|
622
649
|
{
|
|
623
650
|
phase: "foreign-update-forward-rejected",
|
|
@@ -885,7 +912,7 @@ export function createTelegramBusLocalServer(
|
|
|
885
912
|
const endpointGeneration = randomBytes(8).toString("hex");
|
|
886
913
|
const listenPath = usesWindowsPipe
|
|
887
914
|
? socketPath
|
|
888
|
-
: join(dirname(socketPath), `.
|
|
915
|
+
: join(dirname(socketPath), `.pt-${endpointGeneration}.sock`);
|
|
889
916
|
activeSocketPath = socketPath;
|
|
890
917
|
activeListenPath = listenPath;
|
|
891
918
|
deps.recordTransportEvent?.(
|
|
@@ -1110,7 +1137,6 @@ function sendTelegramBusLocalEnvelopeOnce(
|
|
|
1110
1137
|
),
|
|
1111
1138
|
);
|
|
1112
1139
|
}, timeoutMs);
|
|
1113
|
-
timeout.unref?.();
|
|
1114
1140
|
socket.setEncoding("utf8");
|
|
1115
1141
|
socket.once("connect", () => {
|
|
1116
1142
|
socket.write(encodeTelegramBusEnvelope(options.envelope));
|
|
@@ -1130,16 +1156,20 @@ function sendTelegramBusLocalEnvelopeOnce(
|
|
|
1130
1156
|
export async function sendTelegramBusLocalEnvelope(
|
|
1131
1157
|
options: TelegramBusLocalClientOptions,
|
|
1132
1158
|
): Promise<TelegramBusEnvelope | undefined> {
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1159
|
+
const resolvedOptions = {
|
|
1160
|
+
...options,
|
|
1161
|
+
socketPath: resolveTelegramBusSocketPath(options.socketPath),
|
|
1162
|
+
};
|
|
1163
|
+
const attempts = Math.max(1, resolvedOptions.retry?.attempts ?? 1);
|
|
1164
|
+
const delayMs = Math.max(0, resolvedOptions.retry?.delayMs ?? 0);
|
|
1135
1165
|
for (let attempt = 1; ; attempt += 1) {
|
|
1136
1166
|
try {
|
|
1137
|
-
return await sendTelegramBusLocalEnvelopeOnce(
|
|
1167
|
+
return await sendTelegramBusLocalEnvelopeOnce(resolvedOptions);
|
|
1138
1168
|
} catch (error) {
|
|
1139
1169
|
const info = classifyTelegramBusTransportError(error);
|
|
1140
|
-
|
|
1141
|
-
...getTelegramBusEndpointDiagnostics(
|
|
1142
|
-
...getTelegramBusEnvelopeDiagnostics(
|
|
1170
|
+
resolvedOptions.recordTransportEvent?.("client-failed", {
|
|
1171
|
+
...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
|
|
1172
|
+
...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
|
|
1143
1173
|
attempt,
|
|
1144
1174
|
attempts,
|
|
1145
1175
|
...info,
|
|
@@ -1147,9 +1177,9 @@ export async function sendTelegramBusLocalEnvelope(
|
|
|
1147
1177
|
if (attempt >= attempts || !isRetryableTelegramBusTransportError(error)) {
|
|
1148
1178
|
throw error;
|
|
1149
1179
|
}
|
|
1150
|
-
|
|
1151
|
-
...getTelegramBusEndpointDiagnostics(
|
|
1152
|
-
...getTelegramBusEnvelopeDiagnostics(
|
|
1180
|
+
resolvedOptions.recordTransportEvent?.("client-retry", {
|
|
1181
|
+
...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
|
|
1182
|
+
...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
|
|
1153
1183
|
attempt,
|
|
1154
1184
|
attempts,
|
|
1155
1185
|
delayMs,
|
|
@@ -1408,8 +1438,7 @@ function parseForwardMessageEnvelope(
|
|
|
1408
1438
|
(value.forwardCommentBatchPosition === "comment" ||
|
|
1409
1439
|
value.forwardCommentBatchPosition === "forward")
|
|
1410
1440
|
? {
|
|
1411
|
-
forwardCommentBatchPosition:
|
|
1412
|
-
value.forwardCommentBatchPosition,
|
|
1441
|
+
forwardCommentBatchPosition: value.forwardCommentBatchPosition,
|
|
1413
1442
|
}
|
|
1414
1443
|
: {}),
|
|
1415
1444
|
sentAtMs: value.sentAtMs,
|
package/lib/command-templates.ts
CHANGED
|
@@ -36,9 +36,7 @@ export interface CommandTemplateObjectConfig {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export type CommandTemplateValue =
|
|
39
|
-
|
|
|
40
|
-
| CommandTemplateConfig[]
|
|
41
|
-
| CommandTemplateObjectConfig;
|
|
39
|
+
string | CommandTemplateConfig[] | CommandTemplateObjectConfig;
|
|
42
40
|
|
|
43
41
|
export type CommandTemplateConfig = string | CommandTemplateObjectConfig;
|
|
44
42
|
|
|
@@ -90,12 +88,6 @@ const COMMAND_TEMPLATE_RISK_LABEL_ORDER: CommandTemplateRiskLabel[] = [
|
|
|
90
88
|
"risk.platform_specific",
|
|
91
89
|
];
|
|
92
90
|
|
|
93
|
-
export type CommandTemplateExecCommand = (
|
|
94
|
-
command: string,
|
|
95
|
-
args: string[],
|
|
96
|
-
options?: CommandTemplateExecOptions,
|
|
97
|
-
) => Promise<CommandTemplateExecResult>;
|
|
98
|
-
|
|
99
91
|
function normalizeCommandTemplateArgs(value: string[] | undefined): string[] {
|
|
100
92
|
if (!Array.isArray(value)) return [];
|
|
101
93
|
return value.map(String).map((item) => item.trim());
|
|
@@ -257,9 +249,18 @@ function getLeafCommandTemplateRiskLabels(
|
|
|
257
249
|
labels.add("risk.broad_fs_write");
|
|
258
250
|
}
|
|
259
251
|
if (
|
|
260
|
-
[
|
|
261
|
-
|
|
262
|
-
|
|
252
|
+
[
|
|
253
|
+
"curl",
|
|
254
|
+
"wget",
|
|
255
|
+
"ssh",
|
|
256
|
+
"scp",
|
|
257
|
+
"sftp",
|
|
258
|
+
"rsync",
|
|
259
|
+
"nc",
|
|
260
|
+
"ncat",
|
|
261
|
+
"telnet",
|
|
262
|
+
"ftp",
|
|
263
|
+
].includes(command) ||
|
|
263
264
|
(command === "git" &&
|
|
264
265
|
hasAnyArg(args, ["clone", "fetch", "pull", "push", "ls-remote"])) ||
|
|
265
266
|
["npm", "pnpm", "yarn", "pip", "cargo"].includes(command)
|
|
@@ -283,13 +284,32 @@ function getLeafCommandTemplateRiskLabels(
|
|
|
283
284
|
labels.add("risk.long_running");
|
|
284
285
|
}
|
|
285
286
|
if (
|
|
286
|
-
[
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
[
|
|
288
|
+
"systemctl",
|
|
289
|
+
"launchctl",
|
|
290
|
+
"osascript",
|
|
291
|
+
"open",
|
|
292
|
+
"xdg-open",
|
|
293
|
+
"powershell",
|
|
294
|
+
"pwsh",
|
|
295
|
+
"cmd.exe",
|
|
296
|
+
"apt",
|
|
297
|
+
"apt-get",
|
|
298
|
+
"dnf",
|
|
299
|
+
"yum",
|
|
300
|
+
"brew",
|
|
301
|
+
"pacman",
|
|
302
|
+
"apk",
|
|
303
|
+
"xclip",
|
|
304
|
+
"wl-copy",
|
|
305
|
+
].includes(command)
|
|
289
306
|
) {
|
|
290
307
|
labels.add("risk.platform_specific");
|
|
291
308
|
}
|
|
292
|
-
if (
|
|
309
|
+
if (
|
|
310
|
+
["pass", "gpg", "ssh-add"].includes(command) ||
|
|
311
|
+
hasSecretTouchingText(parts)
|
|
312
|
+
) {
|
|
293
313
|
labels.add("risk.secret_touching");
|
|
294
314
|
}
|
|
295
315
|
return sortRiskLabels(labels);
|
|
@@ -347,10 +367,6 @@ function pad(value: number, width: number): string {
|
|
|
347
367
|
return String(value).padStart(width, "0");
|
|
348
368
|
}
|
|
349
369
|
|
|
350
|
-
export function isCommandTemplateRepeatPlaceholder(name: string): boolean {
|
|
351
|
-
return /^_{0,6}(?:index|prev|next|repeat)$/.test(name);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
370
|
export function getCommandTemplateRepeatDefaults(
|
|
355
371
|
index: number,
|
|
356
372
|
repeat: number,
|
package/lib/config.ts
CHANGED
|
@@ -119,32 +119,6 @@ export function isValidTelegramProfileName(name: string): boolean {
|
|
|
119
119
|
);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/**
|
|
123
|
-
* Resolve bot/session identity from the canonical default or named profile.
|
|
124
|
-
* Shared bridge settings always remain top-level.
|
|
125
|
-
*/
|
|
126
|
-
export function resolveTelegramActiveProfile(
|
|
127
|
-
config: TelegramConfig,
|
|
128
|
-
profileName?: string,
|
|
129
|
-
): {
|
|
130
|
-
botToken?: string;
|
|
131
|
-
botUsername?: string;
|
|
132
|
-
botId?: number;
|
|
133
|
-
allowedUserId?: number;
|
|
134
|
-
lastUpdateId?: number;
|
|
135
|
-
} {
|
|
136
|
-
const effectiveProfileName = profileName ?? TELEGRAM_DEFAULT_PROFILE_NAME;
|
|
137
|
-
const profile = config.profiles?.[effectiveProfileName];
|
|
138
|
-
if (!profile) return {};
|
|
139
|
-
return {
|
|
140
|
-
botToken: profile.botToken,
|
|
141
|
-
botUsername: profile.botUsername,
|
|
142
|
-
botId: profile.botId,
|
|
143
|
-
allowedUserId: profile.allowedUserId,
|
|
144
|
-
lastUpdateId: profile.lastUpdateId,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
122
|
/** List defined profile names. */
|
|
149
123
|
export function getTelegramProfileNames(config: TelegramConfig): string[] {
|
|
150
124
|
return Object.keys(config.profiles ?? {}).sort();
|
|
@@ -560,10 +534,7 @@ export function createTelegramConfigStore(
|
|
|
560
534
|
!profileName || profileName === TELEGRAM_DEFAULT_PROFILE_NAME
|
|
561
535
|
? undefined
|
|
562
536
|
: profileName;
|
|
563
|
-
if (
|
|
564
|
-
normalizedProfileName &&
|
|
565
|
-
!config.profiles?.[normalizedProfileName]
|
|
566
|
-
) {
|
|
537
|
+
if (normalizedProfileName && !config.profiles?.[normalizedProfileName]) {
|
|
567
538
|
return false;
|
|
568
539
|
}
|
|
569
540
|
activeProfileName = normalizedProfileName;
|
|
@@ -607,9 +578,8 @@ export function createTelegramConfigStore(
|
|
|
607
578
|
config = normalized.changed
|
|
608
579
|
? withTelegramFileTransaction(`${configPath}.transaction`, () => {
|
|
609
580
|
const latestConfig = readTelegramConfigForTransaction(configPath);
|
|
610
|
-
const latestNormalized =
|
|
611
|
-
latestConfig
|
|
612
|
-
);
|
|
581
|
+
const latestNormalized =
|
|
582
|
+
normalizeTelegramDefaultProfileConfig(latestConfig);
|
|
613
583
|
if (latestNormalized.changed) {
|
|
614
584
|
writeTelegramConfigInTransaction(
|
|
615
585
|
agentDir,
|
|
@@ -642,7 +612,9 @@ export function createTelegramConfigStore(
|
|
|
642
612
|
desiredConfig as Record<string, unknown>,
|
|
643
613
|
latestConfig as Record<string, unknown>,
|
|
644
614
|
) as TelegramConfig;
|
|
645
|
-
|
|
615
|
+
if (!configValuesEqual(latestConfig, merged)) {
|
|
616
|
+
writeTelegramConfigInTransaction(agentDir, configPath, merged);
|
|
617
|
+
}
|
|
646
618
|
return merged;
|
|
647
619
|
},
|
|
648
620
|
);
|
package/lib/inbound.ts
CHANGED
|
@@ -23,8 +23,7 @@ const MAX_INBOUND_HANDLER_OUTPUT_LENGTH = 12_000;
|
|
|
23
23
|
const MAX_INBOUND_HANDLER_FAILURE_STREAM_LENGTH = 4_000;
|
|
24
24
|
|
|
25
25
|
type TelegramInboundCommandTemplateConfig =
|
|
26
|
-
|
|
|
27
|
-
| CommandTemplateObjectConfig;
|
|
26
|
+
string | CommandTemplateObjectConfig;
|
|
28
27
|
|
|
29
28
|
export interface TelegramInboundHandlerConfig {
|
|
30
29
|
match?: string | string[];
|
|
@@ -74,10 +73,6 @@ export interface TelegramInboundHandlerExecResult {
|
|
|
74
73
|
killed: boolean;
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
export interface TelegramInboundHandlerRuntimeContext {
|
|
78
|
-
cwd: string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
76
|
export interface TelegramInboundHandlerRuntimeDeps<TContext> {
|
|
82
77
|
getHandlers: () => TelegramInboundHandlerConfig[] | undefined;
|
|
83
78
|
execCommand: (
|
|
@@ -102,9 +97,7 @@ export interface TelegramInboundHandlerRuntime<TContext> {
|
|
|
102
97
|
}
|
|
103
98
|
|
|
104
99
|
export type TelegramInboundProgrammaticHandlerResult =
|
|
105
|
-
| string
|
|
106
|
-
| { text: string }
|
|
107
|
-
| undefined;
|
|
100
|
+
string | { text: string } | undefined;
|
|
108
101
|
|
|
109
102
|
export interface TelegramInboundProgrammaticHandlerInput {
|
|
110
103
|
kind: string;
|
|
@@ -179,12 +172,6 @@ export function getTelegramInboundProgrammaticHandlers(
|
|
|
179
172
|
];
|
|
180
173
|
}
|
|
181
174
|
|
|
182
|
-
export function hasTelegramInboundHandler(kind?: string): boolean {
|
|
183
|
-
const registry = getOrCreateInboundHandlerRegistry();
|
|
184
|
-
if (kind !== undefined) return (registry.handlers.get(kind)?.length ?? 0) > 0;
|
|
185
|
-
return Array.from(registry.handlers.values()).some((list) => list.length > 0);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
175
|
export function clearTelegramInboundHandlers(): void {
|
|
189
176
|
getOrCreateInboundHandlerRegistry().handlers.clear();
|
|
190
177
|
}
|
|
@@ -281,13 +268,6 @@ export function findTelegramInboundHandlers(
|
|
|
281
268
|
);
|
|
282
269
|
}
|
|
283
270
|
|
|
284
|
-
export function findTelegramInboundHandler(
|
|
285
|
-
handlers: TelegramInboundHandlerConfig[] | undefined,
|
|
286
|
-
file: TelegramInboundHandlerFile,
|
|
287
|
-
): TelegramInboundHandlerConfig | undefined {
|
|
288
|
-
return findTelegramInboundHandlers(handlers, file)[0];
|
|
289
|
-
}
|
|
290
|
-
|
|
291
271
|
function hasInboundFilePlaceholder(value: string): boolean {
|
|
292
272
|
return /\{file\}/.test(value);
|
|
293
273
|
}
|
|
@@ -420,9 +400,13 @@ function formatTelegramInboundHandlerFailure(
|
|
|
420
400
|
`Inbound handler exited with code ${result.code}${result.killed ? " (killed)" : ""}`,
|
|
421
401
|
];
|
|
422
402
|
if (result.stderr.trim())
|
|
423
|
-
parts.push(
|
|
403
|
+
parts.push(
|
|
404
|
+
`stderr:\n${truncateTelegramInboundFailureStream(result.stderr)}`,
|
|
405
|
+
);
|
|
424
406
|
if (result.stdout.trim())
|
|
425
|
-
parts.push(
|
|
407
|
+
parts.push(
|
|
408
|
+
`stdout:\n${truncateTelegramInboundFailureStream(result.stdout)}`,
|
|
409
|
+
);
|
|
426
410
|
return parts.join("\n\n");
|
|
427
411
|
}
|
|
428
412
|
|
|
@@ -445,7 +429,13 @@ async function executeTelegramInboundHandlerInvocation(
|
|
|
445
429
|
cwd,
|
|
446
430
|
timeout,
|
|
447
431
|
...(typeof handler === "object" && handler.retry !== undefined
|
|
448
|
-
? {
|
|
432
|
+
? {
|
|
433
|
+
retry: resolveTelegramInboundNumericControlField(
|
|
434
|
+
handler.retry,
|
|
435
|
+
{},
|
|
436
|
+
"retry",
|
|
437
|
+
),
|
|
438
|
+
}
|
|
449
439
|
: {}),
|
|
450
440
|
...(stdin !== undefined ? { stdin } : {}),
|
|
451
441
|
});
|
|
@@ -519,7 +509,13 @@ async function executeTelegramTextHandlerInvocation(
|
|
|
519
509
|
timeout,
|
|
520
510
|
stdin: text,
|
|
521
511
|
...(typeof handler === "object" && handler.retry !== undefined
|
|
522
|
-
? {
|
|
512
|
+
? {
|
|
513
|
+
retry: resolveTelegramInboundNumericControlField(
|
|
514
|
+
handler.retry,
|
|
515
|
+
{},
|
|
516
|
+
"retry",
|
|
517
|
+
),
|
|
518
|
+
}
|
|
523
519
|
: {}),
|
|
524
520
|
});
|
|
525
521
|
if (result.code !== 0)
|
package/lib/lifecycle.ts
CHANGED
|
@@ -196,19 +196,6 @@ export function createTelegramSessionContextStore<TContext>(
|
|
|
196
196
|
};
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
export function createTelegramSessionContextTracker(
|
|
200
|
-
store: Pick<TelegramSessionContextStore<ExtensionContext>, "set" | "clear">,
|
|
201
|
-
): TelegramSessionLifecycleHooks {
|
|
202
|
-
return {
|
|
203
|
-
onSessionStart: async (_event, ctx) => {
|
|
204
|
-
store.set(ctx);
|
|
205
|
-
},
|
|
206
|
-
onSessionShutdown: async (_event, ctx) => {
|
|
207
|
-
store.clear(ctx);
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
199
|
export function createTelegramSessionGenerationFence(
|
|
213
200
|
store: TelegramSessionContextStore<ExtensionContext>,
|
|
214
201
|
hooks: TelegramSessionLifecycleHooks,
|