@llblab/pi-kit 0.6.0 → 0.7.1
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/CHANGELOG.md +8 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-actors/README.md +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
- package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
- package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
- package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
- package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
- package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
- package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
- package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
- package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
- package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
- package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
- package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
- package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
- package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
- package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
- package/node_modules/@llblab/pi-state-flow/README.md +203 -107
- package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
- package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
- package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
- package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
- package/node_modules/@llblab/pi-state-flow/package.json +8 -2
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
- package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +23 -5
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
- package/node_modules/@llblab/pi-telegram/README.md +13 -9
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +227 -23
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -6
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -6
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
- package/node_modules/@llblab/pi-telegram/index.ts +4 -1418
- package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +46 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +32 -19
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +355 -26
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +718 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +237 -11
- package/node_modules/@llblab/pi-telegram/lib/config.ts +242 -26
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1851 -0
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +44 -2
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +97 -10
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +17 -0
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +6 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +118 -26
- package/node_modules/@llblab/pi-telegram/lib/rendering.ts +4 -1
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +21 -2
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +344 -112
- package/node_modules/@llblab/pi-telegram/lib/setup.ts +44 -4
- package/node_modules/@llblab/pi-telegram/lib/status.ts +51 -4
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +353 -22
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
- package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
- package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
- package/node_modules/@llblab/skills/package.json +2 -3
- package/package.json +6 -5
- /package/node_modules/@llblab/pi-telegram/lib/{logs.ts → logging.ts} +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Owns persisted bot/session pairing state, local config storage, live config controls, authorization policy, and first-user pairing side effects
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
import {
|
|
9
9
|
chmodSync,
|
|
10
10
|
existsSync,
|
|
@@ -62,6 +62,59 @@ function getConfigPath(): string {
|
|
|
62
62
|
return resolveTelegramConfigPath();
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
const TELEGRAM_BOT_TOKEN_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
66
|
+
|
|
67
|
+
/** Parsed stored bot-token form: a literal secret or one environment-variable reference. */
|
|
68
|
+
export type TelegramBotTokenReference =
|
|
69
|
+
| { kind: "literal"; token: string }
|
|
70
|
+
| { kind: "environment"; variable: string }
|
|
71
|
+
| { kind: "malformed" };
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse a persisted bot-token value. `$NAME` and `${NAME}` are exact
|
|
75
|
+
* environment-variable references. Any other `$`-prefixed value is malformed
|
|
76
|
+
* rather than a literal secret so a broken reference fails closed.
|
|
77
|
+
*/
|
|
78
|
+
export function getTelegramBotTokenReference(
|
|
79
|
+
value: string | undefined,
|
|
80
|
+
): TelegramBotTokenReference | undefined {
|
|
81
|
+
const trimmed = value?.trim();
|
|
82
|
+
if (!trimmed) return undefined;
|
|
83
|
+
if (!trimmed.startsWith("$")) return { kind: "literal", token: trimmed };
|
|
84
|
+
const body =
|
|
85
|
+
trimmed.startsWith("${") && trimmed.endsWith("}")
|
|
86
|
+
? trimmed.slice(2, -1)
|
|
87
|
+
: trimmed.slice(1);
|
|
88
|
+
return TELEGRAM_BOT_TOKEN_ENV_NAME_PATTERN.test(body)
|
|
89
|
+
? { kind: "environment", variable: body }
|
|
90
|
+
: { kind: "malformed" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Resolve a persisted token at a validation/activation boundary. */
|
|
94
|
+
export function resolveTelegramBotToken(
|
|
95
|
+
value: string | undefined,
|
|
96
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
97
|
+
): string | undefined {
|
|
98
|
+
const reference = getTelegramBotTokenReference(value);
|
|
99
|
+
if (reference?.kind === "literal") return reference.token;
|
|
100
|
+
if (reference?.kind !== "environment") return undefined;
|
|
101
|
+
return env[reference.variable]?.trim() || undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Redacted diagnostic for an unresolved or malformed token reference. */
|
|
105
|
+
export function getTelegramBotTokenDiagnostic(
|
|
106
|
+
value: string | undefined,
|
|
107
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
108
|
+
): string | undefined {
|
|
109
|
+
const reference = getTelegramBotTokenReference(value);
|
|
110
|
+
if (reference?.kind === "malformed") {
|
|
111
|
+
return "Telegram bot token environment reference is malformed; use $NAME or ${NAME}.";
|
|
112
|
+
}
|
|
113
|
+
if (reference?.kind !== "environment") return undefined;
|
|
114
|
+
if (resolveTelegramBotToken(value, env)) return undefined;
|
|
115
|
+
return `Telegram bot token environment variable ${reference.variable} is not set.`;
|
|
116
|
+
}
|
|
117
|
+
|
|
65
118
|
export type TelegramOutboundCommandTemplateConfig =
|
|
66
119
|
string | CommandTemplateObjectConfig;
|
|
67
120
|
export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConfig {
|
|
@@ -83,6 +136,35 @@ export interface ResolvedTelegramTimeConfig {
|
|
|
83
136
|
timezone: string;
|
|
84
137
|
}
|
|
85
138
|
|
|
139
|
+
export type TelegramThreadDisplayMode = "letters" | "names" | "directories";
|
|
140
|
+
|
|
141
|
+
export function resolveTelegramThreadDisplayMode(
|
|
142
|
+
config: Pick<TelegramConfig, "threadDisplayMode">,
|
|
143
|
+
): TelegramThreadDisplayMode {
|
|
144
|
+
return config.threadDisplayMode === "directories"
|
|
145
|
+
? "directories"
|
|
146
|
+
: "letters";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function setTelegramThreadDisplayMode(
|
|
150
|
+
store: TelegramConfigStore,
|
|
151
|
+
mode: TelegramThreadDisplayMode,
|
|
152
|
+
isCurrent: () => boolean,
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
if (!["letters", "directories"].includes(mode)) {
|
|
155
|
+
throw new Error("Invalid Telegram Thread display mode.");
|
|
156
|
+
}
|
|
157
|
+
const profile = store.getActiveProfileName();
|
|
158
|
+
const current = () => isCurrent() && store.getActiveProfileName() === profile;
|
|
159
|
+
if (!current()) throw new Error("Telegram Thread display setting lost authority.");
|
|
160
|
+
await store.load();
|
|
161
|
+
if (!current() || !store.hasBotToken()) {
|
|
162
|
+
throw new Error("Telegram Thread display setting lost its configured profile.");
|
|
163
|
+
}
|
|
164
|
+
await store.persist({ ...store.get(), threadDisplayMode: mode }, { isCurrent: current });
|
|
165
|
+
if (!current()) throw new Error("Telegram Thread display setting changed during persistence.");
|
|
166
|
+
}
|
|
167
|
+
|
|
86
168
|
export type TelegramAssistantRenderingMode = "rich" | "html";
|
|
87
169
|
export type TelegramActivityVerbosity =
|
|
88
170
|
| "quiet"
|
|
@@ -99,6 +181,8 @@ export interface TelegramConfig {
|
|
|
99
181
|
botId?: number;
|
|
100
182
|
/** @deprecated persisted identity belongs in profiles.default; retained for effective/legacy views */
|
|
101
183
|
allowedUserId?: number;
|
|
184
|
+
/** Effective view; persisted under profiles.<name>. */
|
|
185
|
+
threadDisplayMode?: TelegramThreadDisplayMode;
|
|
102
186
|
inboundHandlers?: TelegramInboundHandlerConfig[];
|
|
103
187
|
attachmentHandlers?: TelegramInboundHandlerConfig[];
|
|
104
188
|
outboundHandlers?: TelegramOutboundHandlerConfig[];
|
|
@@ -130,7 +214,7 @@ export interface TelegramConfig {
|
|
|
130
214
|
}
|
|
131
215
|
|
|
132
216
|
/**
|
|
133
|
-
* Per-profile bot/session identity
|
|
217
|
+
* Per-profile bot/session identity and Thread display preference.
|
|
134
218
|
* Stored under `profiles.<name>` in telegram.json.
|
|
135
219
|
* Shared bridge settings (inboundHandlers, outboundHandlers, voice, time,
|
|
136
220
|
* assistant) stay at the top level.
|
|
@@ -140,6 +224,7 @@ export interface TelegramBotProfile {
|
|
|
140
224
|
botUsername?: string;
|
|
141
225
|
botId?: number;
|
|
142
226
|
allowedUserId?: number;
|
|
227
|
+
threadDisplayMode?: TelegramThreadDisplayMode;
|
|
143
228
|
}
|
|
144
229
|
|
|
145
230
|
interface TelegramLegacyCursorCarrier {
|
|
@@ -174,6 +259,7 @@ export interface TelegramConfigStore {
|
|
|
174
259
|
activateProfile: (profileName: string | undefined) => boolean;
|
|
175
260
|
getActiveProfileName: () => string | undefined;
|
|
176
261
|
getBotToken: () => string | undefined;
|
|
262
|
+
getBotTokenDiagnostic: () => string | undefined;
|
|
177
263
|
hasBotToken: () => boolean;
|
|
178
264
|
getAllowedUserId: () => number | undefined;
|
|
179
265
|
getLegacyPollingCursor: () => number | undefined;
|
|
@@ -182,9 +268,35 @@ export interface TelegramConfigStore {
|
|
|
182
268
|
getAttachmentHandlers: () => TelegramInboundHandlerConfig[] | undefined;
|
|
183
269
|
getOutboundHandlers: () => TelegramOutboundHandlerConfig[] | undefined;
|
|
184
270
|
setAllowedUserId: (userId: number) => void;
|
|
271
|
+
/** Publish an unpaired profile owner atomically; true only for the resulting exact owner. */
|
|
272
|
+
persistAllowedUserId: (
|
|
273
|
+
userId: number,
|
|
274
|
+
assertExecutionCurrent?: () => void,
|
|
275
|
+
commitIfOwned?: (commit: () => void) => boolean,
|
|
276
|
+
) => Promise<boolean>;
|
|
277
|
+
/** Lock-only serialization for trusted synchronous source operations, not authorization.
|
|
278
|
+
* Does not read/adopt config. Acquire required Workspace admission first; never
|
|
279
|
+
* acquire owners or nest config admission here. Do not pass async callbacks:
|
|
280
|
+
* returned promises are not protected after their synchronous prefix.
|
|
281
|
+
*/
|
|
282
|
+
withSourceSerialization: <T>(operation: () => T) => T;
|
|
283
|
+
/** Trusted synchronous publication only; caller acquires Workspace admission before this config transaction. */
|
|
284
|
+
withPairingAdmission: <T>(
|
|
285
|
+
profileName: string,
|
|
286
|
+
tokenSha256: string,
|
|
287
|
+
publish: (preApprovalExcluded: boolean) => T,
|
|
288
|
+
) => T;
|
|
289
|
+
/** Observe an existing exact owner and refresh an unpaired cache; never create an owner. Callback must be synchronous. */
|
|
290
|
+
withPairedUserAdmission: <T>(
|
|
291
|
+
profileName: string,
|
|
292
|
+
tokenSha256: string,
|
|
293
|
+
userId: number,
|
|
294
|
+
publish: () => T,
|
|
295
|
+
assertExecutionCurrent?: () => void,
|
|
296
|
+
) => { admitted: false } | { admitted: true; value: T };
|
|
185
297
|
load: () => Promise<void>;
|
|
186
298
|
didLastLoadRecoverInvalidConfig: () => boolean;
|
|
187
|
-
persist: (config?: TelegramConfig) => Promise<void>;
|
|
299
|
+
persist: (config?: TelegramConfig, options?: { isCurrent?: () => boolean }) => Promise<void>;
|
|
188
300
|
}
|
|
189
301
|
|
|
190
302
|
export function createTelegramConfigBotIdGetter(
|
|
@@ -203,6 +315,8 @@ export interface TelegramConfigStoreOptions {
|
|
|
203
315
|
initialConfig?: TelegramConfig;
|
|
204
316
|
agentDir?: string;
|
|
205
317
|
configPath?: string;
|
|
318
|
+
/** Environment used to resolve `$NAME` token references; defaults to process.env. */
|
|
319
|
+
env?: NodeJS.ProcessEnv;
|
|
206
320
|
recordRuntimeEvent?: (
|
|
207
321
|
category: string,
|
|
208
322
|
error: unknown,
|
|
@@ -437,6 +551,9 @@ export function getTelegramProfileFields(
|
|
|
437
551
|
...(config.allowedUserId !== undefined
|
|
438
552
|
? { allowedUserId: config.allowedUserId }
|
|
439
553
|
: {}),
|
|
554
|
+
...(config.threadDisplayMode !== undefined
|
|
555
|
+
? { threadDisplayMode: config.threadDisplayMode }
|
|
556
|
+
: {}),
|
|
440
557
|
...(legacyCursor !== undefined ? { lastUpdateId: legacyCursor } : {}),
|
|
441
558
|
};
|
|
442
559
|
}
|
|
@@ -447,6 +564,7 @@ function omitTelegramRootProfileFields(config: TelegramConfig): TelegramConfig {
|
|
|
447
564
|
botUsername: _botUsername,
|
|
448
565
|
botId: _botId,
|
|
449
566
|
allowedUserId: _allowedUserId,
|
|
567
|
+
threadDisplayMode: _threadDisplayMode,
|
|
450
568
|
lastUpdateId: _lastUpdateId,
|
|
451
569
|
...sharedConfig
|
|
452
570
|
} = config as TelegramConfig & TelegramLegacyCursorCarrier;
|
|
@@ -484,6 +602,7 @@ export function normalizeTelegramDefaultProfileConfig(config: TelegramConfig): {
|
|
|
484
602
|
"botUsername",
|
|
485
603
|
"botId",
|
|
486
604
|
"allowedUserId",
|
|
605
|
+
"threadDisplayMode",
|
|
487
606
|
"lastUpdateId",
|
|
488
607
|
].some((field) => Object.hasOwn(config, field));
|
|
489
608
|
if (!hasLegacyRootProfile) {
|
|
@@ -504,6 +623,9 @@ export function normalizeTelegramDefaultProfileConfig(config: TelegramConfig): {
|
|
|
504
623
|
...(config.allowedUserId !== undefined
|
|
505
624
|
? { allowedUserId: config.allowedUserId }
|
|
506
625
|
: {}),
|
|
626
|
+
...(config.threadDisplayMode !== undefined
|
|
627
|
+
? { threadDisplayMode: config.threadDisplayMode }
|
|
628
|
+
: {}),
|
|
507
629
|
...((config as TelegramConfig & TelegramLegacyCursorCarrier)
|
|
508
630
|
.lastUpdateId !== undefined
|
|
509
631
|
? {
|
|
@@ -587,6 +709,7 @@ export function createTelegramConfigStore(
|
|
|
587
709
|
let lastLoadRecoveredInvalidConfig = false;
|
|
588
710
|
const agentDir = options.agentDir ?? resolveAgentDir();
|
|
589
711
|
const configPath = options.configPath ?? getConfigPath();
|
|
712
|
+
const env = options.env ?? process.env;
|
|
590
713
|
const getEffectiveConfig = () =>
|
|
591
714
|
applyTelegramProfile(config, activeProfileName);
|
|
592
715
|
const setEffectiveConfig = (nextConfig: TelegramConfig) => {
|
|
@@ -597,6 +720,39 @@ export function createTelegramConfigStore(
|
|
|
597
720
|
);
|
|
598
721
|
mutationVersion += 1;
|
|
599
722
|
};
|
|
723
|
+
const adoptPersistedConfig = (merged: TelegramConfig, preserveLocalChanges: boolean) => {
|
|
724
|
+
// Local edits are relative to the latest observation, not a queued write's older request baseline.
|
|
725
|
+
const nextConfig = preserveLocalChanges
|
|
726
|
+
? mergeTelegramConfigDelta(persistedConfig as Record<string, unknown>, config as Record<string, unknown>,
|
|
727
|
+
merged as Record<string, unknown>) as TelegramConfig
|
|
728
|
+
: cloneTelegramConfig(merged);
|
|
729
|
+
persistedConfig = cloneTelegramConfig(merged);
|
|
730
|
+
config = nextConfig;
|
|
731
|
+
};
|
|
732
|
+
const withPersistedPairingProfile = <T>(
|
|
733
|
+
profileName: string, tokenSha256: string,
|
|
734
|
+
observe: (latest: TelegramConfig, profile: TelegramBotProfile) => T,
|
|
735
|
+
): T => {
|
|
736
|
+
if ((profileName !== TELEGRAM_DEFAULT_PROFILE_NAME && !isValidTelegramProfileName(profileName)) ||
|
|
737
|
+
!/^[a-f0-9]{64}$/u.test(tokenSha256)) {
|
|
738
|
+
throw new Error("Invalid Telegram pairing admission identity.");
|
|
739
|
+
}
|
|
740
|
+
return withTelegramFileTransaction(`${configPath}.transaction`, () => {
|
|
741
|
+
const latest = readTelegramConfigForTransaction(configPath);
|
|
742
|
+
const profile = latest.profiles?.[profileName];
|
|
743
|
+
const resolvedToken =
|
|
744
|
+
typeof profile?.botToken === "string"
|
|
745
|
+
? resolveTelegramBotToken(profile.botToken, env)
|
|
746
|
+
: undefined;
|
|
747
|
+
if (!profile || !resolvedToken ||
|
|
748
|
+
createHash("sha256").update(resolvedToken).digest("hex") !== tokenSha256 ||
|
|
749
|
+
(profile.allowedUserId !== undefined &&
|
|
750
|
+
(!Number.isSafeInteger(profile.allowedUserId) || profile.allowedUserId <= 0))) {
|
|
751
|
+
throw new Error("Telegram pairing admission authority is unavailable or changed.");
|
|
752
|
+
}
|
|
753
|
+
return observe(latest, profile);
|
|
754
|
+
});
|
|
755
|
+
};
|
|
600
756
|
return {
|
|
601
757
|
get: getEffectiveConfig,
|
|
602
758
|
getStoredConfig: () => config,
|
|
@@ -628,8 +784,10 @@ export function createTelegramConfigStore(
|
|
|
628
784
|
return true;
|
|
629
785
|
},
|
|
630
786
|
getActiveProfileName: () => activeProfileName,
|
|
631
|
-
getBotToken: () => getEffectiveConfig().botToken,
|
|
632
|
-
|
|
787
|
+
getBotToken: () => resolveTelegramBotToken(getEffectiveConfig().botToken, env),
|
|
788
|
+
getBotTokenDiagnostic: () =>
|
|
789
|
+
getTelegramBotTokenDiagnostic(getEffectiveConfig().botToken, env),
|
|
790
|
+
hasBotToken: () => !!resolveTelegramBotToken(getEffectiveConfig().botToken, env),
|
|
633
791
|
getAllowedUserId: () => getEffectiveConfig().allowedUserId,
|
|
634
792
|
getLegacyPollingCursor: () =>
|
|
635
793
|
(getEffectiveConfig() as TelegramConfig & TelegramLegacyCursorCarrier)
|
|
@@ -652,6 +810,73 @@ export function createTelegramConfigStore(
|
|
|
652
810
|
nextConfig.allowedUserId = userId;
|
|
653
811
|
setEffectiveConfig(nextConfig);
|
|
654
812
|
},
|
|
813
|
+
withSourceSerialization: (operation) =>
|
|
814
|
+
withTelegramFileTransaction(`${configPath}.transaction`, operation),
|
|
815
|
+
withPairingAdmission: (profileName, tokenSha256, publish) =>
|
|
816
|
+
withPersistedPairingProfile(profileName, tokenSha256, (_latest, profile) =>
|
|
817
|
+
publish(profile.allowedUserId === undefined)),
|
|
818
|
+
withPairedUserAdmission: (profileName, tokenSha256, userId, publish, assertExecutionCurrent) => {
|
|
819
|
+
if (!Number.isSafeInteger(userId) || userId <= 0) return { admitted: false };
|
|
820
|
+
assertExecutionCurrent?.();
|
|
821
|
+
return withPersistedPairingProfile(profileName, tokenSha256, (latest, profile) => {
|
|
822
|
+
if (profile.allowedUserId !== userId) return { admitted: false };
|
|
823
|
+
assertExecutionCurrent?.();
|
|
824
|
+
const current = getEffectiveConfig();
|
|
825
|
+
const previousOwner = persistedConfig.profiles?.[profileName]?.allowedUserId;
|
|
826
|
+
if ((activeProfileName ?? TELEGRAM_DEFAULT_PROFILE_NAME) !== profileName ||
|
|
827
|
+
current.botToken !== profile.botToken ||
|
|
828
|
+
(current.allowedUserId !== undefined && current.allowedUserId !== userId) ||
|
|
829
|
+
(current.allowedUserId === undefined && previousOwner !== undefined)) {
|
|
830
|
+
throw new Error("Telegram paired admission lost local profile authority.");
|
|
831
|
+
}
|
|
832
|
+
// Observation is not a local edit; queued persistence still adopts its own fresh disk result.
|
|
833
|
+
adoptPersistedConfig(latest, true);
|
|
834
|
+
return { admitted: true, value: publish() };
|
|
835
|
+
});
|
|
836
|
+
},
|
|
837
|
+
persistAllowedUserId: (userId, assertExecutionCurrent, commitIfOwned) => {
|
|
838
|
+
const profileName = activeProfileName;
|
|
839
|
+
const profileKey = profileName ?? TELEGRAM_DEFAULT_PROFILE_NAME;
|
|
840
|
+
const botToken = getEffectiveConfig().botToken;
|
|
841
|
+
const previousOwner = getEffectiveConfig().allowedUserId;
|
|
842
|
+
const assertCurrent = () => {
|
|
843
|
+
assertExecutionCurrent?.();
|
|
844
|
+
if (activeProfileName !== profileName || getEffectiveConfig().botToken !== botToken ||
|
|
845
|
+
getEffectiveConfig().allowedUserId !== previousOwner) {
|
|
846
|
+
throw new Error("Telegram pairing lost its originating profile authority.");
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
const pairing = persistQueue.then(() => {
|
|
850
|
+
assertCurrent();
|
|
851
|
+
if (!Number.isSafeInteger(userId) || userId <= 0) throw new Error("Invalid Telegram pairing user ID.");
|
|
852
|
+
let merged: TelegramConfig | undefined;
|
|
853
|
+
const publish = () => {
|
|
854
|
+
merged = withTelegramFileTransaction(`${configPath}.transaction`, () => {
|
|
855
|
+
const latest = readTelegramConfigForTransaction(configPath);
|
|
856
|
+
const profile = latest.profiles?.[profileKey];
|
|
857
|
+
if (!botToken || profile?.botToken !== botToken) {
|
|
858
|
+
throw new Error("Telegram pairing profile is unavailable or changed.");
|
|
859
|
+
}
|
|
860
|
+
assertCurrent();
|
|
861
|
+
if (profile.allowedUserId !== undefined) return latest;
|
|
862
|
+
const next = { ...latest, profiles: { ...latest.profiles,
|
|
863
|
+
[profileKey]: { ...profile, allowedUserId: userId } } };
|
|
864
|
+
writeTelegramConfigInTransaction(agentDir, configPath, next);
|
|
865
|
+
return next;
|
|
866
|
+
});
|
|
867
|
+
};
|
|
868
|
+
if (commitIfOwned) {
|
|
869
|
+
if (!commitIfOwned(publish)) throw new Error("Telegram pairing lost transport ownership before publication.");
|
|
870
|
+
} else {
|
|
871
|
+
publish();
|
|
872
|
+
}
|
|
873
|
+
if (!merged) throw new Error("Telegram pairing publication did not execute.");
|
|
874
|
+
adoptPersistedConfig(merged, true);
|
|
875
|
+
return merged.profiles?.[profileKey]?.allowedUserId === userId;
|
|
876
|
+
});
|
|
877
|
+
persistQueue = pairing.then(() => undefined, () => undefined);
|
|
878
|
+
return pairing;
|
|
879
|
+
},
|
|
655
880
|
load: async () => {
|
|
656
881
|
lastLoadRecoveredInvalidConfig = false;
|
|
657
882
|
const loadedConfig = await readTelegramConfig(configPath, {
|
|
@@ -693,7 +918,7 @@ export function createTelegramConfigStore(
|
|
|
693
918
|
mutationVersion += 1;
|
|
694
919
|
},
|
|
695
920
|
didLastLoadRecoverInvalidConfig: () => lastLoadRecoveredInvalidConfig,
|
|
696
|
-
persist: (nextConfig = getEffectiveConfig()) => {
|
|
921
|
+
persist: (nextConfig = getEffectiveConfig(), options) => {
|
|
697
922
|
const profileName = activeProfileName;
|
|
698
923
|
const desiredConfig = storeTelegramEffectiveConfig(
|
|
699
924
|
config,
|
|
@@ -706,6 +931,9 @@ export function createTelegramConfigStore(
|
|
|
706
931
|
const mergedConfig = withTelegramFileTransaction(
|
|
707
932
|
`${configPath}.transaction`,
|
|
708
933
|
() => {
|
|
934
|
+
if (options?.isCurrent && !options.isCurrent()) {
|
|
935
|
+
throw new Error("Telegram config update lost its originating authority.");
|
|
936
|
+
}
|
|
709
937
|
const latestConfig = readTelegramConfigForTransaction(configPath);
|
|
710
938
|
const merged = mergeTelegramConfigDelta(
|
|
711
939
|
baseConfig as Record<string, unknown>,
|
|
@@ -718,16 +946,7 @@ export function createTelegramConfigStore(
|
|
|
718
946
|
return merged;
|
|
719
947
|
},
|
|
720
948
|
);
|
|
721
|
-
|
|
722
|
-
if (mutationVersion === capturedMutationVersion) {
|
|
723
|
-
config = cloneTelegramConfig(mergedConfig);
|
|
724
|
-
} else {
|
|
725
|
-
config = mergeTelegramConfigDelta(
|
|
726
|
-
baseConfig as Record<string, unknown>,
|
|
727
|
-
config as Record<string, unknown>,
|
|
728
|
-
mergedConfig as Record<string, unknown>,
|
|
729
|
-
) as TelegramConfig;
|
|
730
|
-
}
|
|
949
|
+
adoptPersistedConfig(mergedConfig, mutationVersion !== capturedMutationVersion);
|
|
731
950
|
});
|
|
732
951
|
persistQueue = persist.catch(() => undefined);
|
|
733
952
|
return persist;
|
|
@@ -1054,20 +1273,19 @@ export type TelegramAuthorizationState =
|
|
|
1054
1273
|
export interface TelegramUserPairingDeps<TContext> {
|
|
1055
1274
|
allowedUserId?: number;
|
|
1056
1275
|
ctx: TContext;
|
|
1057
|
-
|
|
1058
|
-
persistConfig: () => Promise<void>;
|
|
1276
|
+
persistAllowedUserId: TelegramConfigStore["persistAllowedUserId"];
|
|
1059
1277
|
updateStatus: (ctx: TContext) => void;
|
|
1060
1278
|
assertExecutionCurrent?: () => void;
|
|
1061
1279
|
}
|
|
1062
1280
|
|
|
1063
1281
|
export interface TelegramUserPairingRuntimeDeps<TContext> {
|
|
1064
1282
|
getAllowedUserId: () => number | undefined;
|
|
1065
|
-
|
|
1066
|
-
persistConfig: () => Promise<void>;
|
|
1283
|
+
persistAllowedUserId: TelegramConfigStore["persistAllowedUserId"];
|
|
1067
1284
|
updateStatus: (ctx: TContext) => void;
|
|
1068
1285
|
}
|
|
1069
1286
|
|
|
1070
1287
|
export interface TelegramUserPairingRuntime<TContext> {
|
|
1288
|
+
/** True means this user is authorized, whether newly paired or already configured. */
|
|
1071
1289
|
pairIfNeeded: (
|
|
1072
1290
|
userId: number,
|
|
1073
1291
|
ctx: TContext,
|
|
@@ -1104,12 +1322,11 @@ export async function pairTelegramUserIfNeeded<TContext>(
|
|
|
1104
1322
|
userId,
|
|
1105
1323
|
deps.allowedUserId,
|
|
1106
1324
|
);
|
|
1107
|
-
if (authorization.kind !== "pair") return
|
|
1108
|
-
deps.assertExecutionCurrent?.();
|
|
1109
|
-
deps.setAllowedUserId(authorization.userId);
|
|
1325
|
+
if (authorization.kind !== "pair") return authorization.kind === "allow";
|
|
1110
1326
|
deps.assertExecutionCurrent?.();
|
|
1111
|
-
await deps.
|
|
1327
|
+
const allowed = await deps.persistAllowedUserId(authorization.userId, deps.assertExecutionCurrent);
|
|
1112
1328
|
deps.assertExecutionCurrent?.();
|
|
1329
|
+
if (!allowed) return false;
|
|
1113
1330
|
try {
|
|
1114
1331
|
deps.updateStatus(deps.ctx);
|
|
1115
1332
|
} catch (error) {
|
|
@@ -1126,8 +1343,7 @@ export function createTelegramUserPairingRuntime<TContext>(
|
|
|
1126
1343
|
pairTelegramUserIfNeeded(userId, {
|
|
1127
1344
|
allowedUserId: deps.getAllowedUserId(),
|
|
1128
1345
|
ctx,
|
|
1129
|
-
|
|
1130
|
-
persistConfig: deps.persistConfig,
|
|
1346
|
+
persistAllowedUserId: deps.persistAllowedUserId,
|
|
1131
1347
|
updateStatus: deps.updateStatus,
|
|
1132
1348
|
assertExecutionCurrent,
|
|
1133
1349
|
}),
|