@llblab/pi-kit 0.6.0 → 0.7.0
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 +4 -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 +13 -8
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -3
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
- package/node_modules/@llblab/pi-telegram/README.md +12 -8
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
- 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 +12 -5
- 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 +28 -1
- 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 +350 -26
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
- package/node_modules/@llblab/pi-telegram/lib/config.ts +177 -24
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -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 +38 -1
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +51 -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/prompts.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +52 -18
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
- package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +279 -4
- 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/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
|
@@ -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,
|
|
@@ -83,6 +83,35 @@ export interface ResolvedTelegramTimeConfig {
|
|
|
83
83
|
timezone: string;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
export type TelegramThreadDisplayMode = "letters" | "names" | "directories";
|
|
87
|
+
|
|
88
|
+
export function resolveTelegramThreadDisplayMode(
|
|
89
|
+
config: Pick<TelegramConfig, "threadDisplayMode">,
|
|
90
|
+
): TelegramThreadDisplayMode {
|
|
91
|
+
return config.threadDisplayMode === "directories"
|
|
92
|
+
? "directories"
|
|
93
|
+
: "letters";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function setTelegramThreadDisplayMode(
|
|
97
|
+
store: TelegramConfigStore,
|
|
98
|
+
mode: TelegramThreadDisplayMode,
|
|
99
|
+
isCurrent: () => boolean,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
if (!["letters", "directories"].includes(mode)) {
|
|
102
|
+
throw new Error("Invalid Telegram Thread display mode.");
|
|
103
|
+
}
|
|
104
|
+
const profile = store.getActiveProfileName();
|
|
105
|
+
const current = () => isCurrent() && store.getActiveProfileName() === profile;
|
|
106
|
+
if (!current()) throw new Error("Telegram Thread display setting lost authority.");
|
|
107
|
+
await store.load();
|
|
108
|
+
if (!current() || !store.hasBotToken()) {
|
|
109
|
+
throw new Error("Telegram Thread display setting lost its configured profile.");
|
|
110
|
+
}
|
|
111
|
+
await store.persist({ ...store.get(), threadDisplayMode: mode }, { isCurrent: current });
|
|
112
|
+
if (!current()) throw new Error("Telegram Thread display setting changed during persistence.");
|
|
113
|
+
}
|
|
114
|
+
|
|
86
115
|
export type TelegramAssistantRenderingMode = "rich" | "html";
|
|
87
116
|
export type TelegramActivityVerbosity =
|
|
88
117
|
| "quiet"
|
|
@@ -99,6 +128,8 @@ export interface TelegramConfig {
|
|
|
99
128
|
botId?: number;
|
|
100
129
|
/** @deprecated persisted identity belongs in profiles.default; retained for effective/legacy views */
|
|
101
130
|
allowedUserId?: number;
|
|
131
|
+
/** Effective view; persisted under profiles.<name>. */
|
|
132
|
+
threadDisplayMode?: TelegramThreadDisplayMode;
|
|
102
133
|
inboundHandlers?: TelegramInboundHandlerConfig[];
|
|
103
134
|
attachmentHandlers?: TelegramInboundHandlerConfig[];
|
|
104
135
|
outboundHandlers?: TelegramOutboundHandlerConfig[];
|
|
@@ -130,7 +161,7 @@ export interface TelegramConfig {
|
|
|
130
161
|
}
|
|
131
162
|
|
|
132
163
|
/**
|
|
133
|
-
* Per-profile bot/session identity
|
|
164
|
+
* Per-profile bot/session identity and Thread display preference.
|
|
134
165
|
* Stored under `profiles.<name>` in telegram.json.
|
|
135
166
|
* Shared bridge settings (inboundHandlers, outboundHandlers, voice, time,
|
|
136
167
|
* assistant) stay at the top level.
|
|
@@ -140,6 +171,7 @@ export interface TelegramBotProfile {
|
|
|
140
171
|
botUsername?: string;
|
|
141
172
|
botId?: number;
|
|
142
173
|
allowedUserId?: number;
|
|
174
|
+
threadDisplayMode?: TelegramThreadDisplayMode;
|
|
143
175
|
}
|
|
144
176
|
|
|
145
177
|
interface TelegramLegacyCursorCarrier {
|
|
@@ -182,9 +214,35 @@ export interface TelegramConfigStore {
|
|
|
182
214
|
getAttachmentHandlers: () => TelegramInboundHandlerConfig[] | undefined;
|
|
183
215
|
getOutboundHandlers: () => TelegramOutboundHandlerConfig[] | undefined;
|
|
184
216
|
setAllowedUserId: (userId: number) => void;
|
|
217
|
+
/** Publish an unpaired profile owner atomically; true only for the resulting exact owner. */
|
|
218
|
+
persistAllowedUserId: (
|
|
219
|
+
userId: number,
|
|
220
|
+
assertExecutionCurrent?: () => void,
|
|
221
|
+
commitIfOwned?: (commit: () => void) => boolean,
|
|
222
|
+
) => Promise<boolean>;
|
|
223
|
+
/** Lock-only serialization for trusted synchronous source operations, not authorization.
|
|
224
|
+
* Does not read/adopt config. Acquire required Workspace admission first; never
|
|
225
|
+
* acquire owners or nest config admission here. Do not pass async callbacks:
|
|
226
|
+
* returned promises are not protected after their synchronous prefix.
|
|
227
|
+
*/
|
|
228
|
+
withSourceSerialization: <T>(operation: () => T) => T;
|
|
229
|
+
/** Trusted synchronous publication only; caller acquires Workspace admission before this config transaction. */
|
|
230
|
+
withPairingAdmission: <T>(
|
|
231
|
+
profileName: string,
|
|
232
|
+
tokenSha256: string,
|
|
233
|
+
publish: (preApprovalExcluded: boolean) => T,
|
|
234
|
+
) => T;
|
|
235
|
+
/** Observe an existing exact owner and refresh an unpaired cache; never create an owner. Callback must be synchronous. */
|
|
236
|
+
withPairedUserAdmission: <T>(
|
|
237
|
+
profileName: string,
|
|
238
|
+
tokenSha256: string,
|
|
239
|
+
userId: number,
|
|
240
|
+
publish: () => T,
|
|
241
|
+
assertExecutionCurrent?: () => void,
|
|
242
|
+
) => { admitted: false } | { admitted: true; value: T };
|
|
185
243
|
load: () => Promise<void>;
|
|
186
244
|
didLastLoadRecoverInvalidConfig: () => boolean;
|
|
187
|
-
persist: (config?: TelegramConfig) => Promise<void>;
|
|
245
|
+
persist: (config?: TelegramConfig, options?: { isCurrent?: () => boolean }) => Promise<void>;
|
|
188
246
|
}
|
|
189
247
|
|
|
190
248
|
export function createTelegramConfigBotIdGetter(
|
|
@@ -437,6 +495,9 @@ export function getTelegramProfileFields(
|
|
|
437
495
|
...(config.allowedUserId !== undefined
|
|
438
496
|
? { allowedUserId: config.allowedUserId }
|
|
439
497
|
: {}),
|
|
498
|
+
...(config.threadDisplayMode !== undefined
|
|
499
|
+
? { threadDisplayMode: config.threadDisplayMode }
|
|
500
|
+
: {}),
|
|
440
501
|
...(legacyCursor !== undefined ? { lastUpdateId: legacyCursor } : {}),
|
|
441
502
|
};
|
|
442
503
|
}
|
|
@@ -447,6 +508,7 @@ function omitTelegramRootProfileFields(config: TelegramConfig): TelegramConfig {
|
|
|
447
508
|
botUsername: _botUsername,
|
|
448
509
|
botId: _botId,
|
|
449
510
|
allowedUserId: _allowedUserId,
|
|
511
|
+
threadDisplayMode: _threadDisplayMode,
|
|
450
512
|
lastUpdateId: _lastUpdateId,
|
|
451
513
|
...sharedConfig
|
|
452
514
|
} = config as TelegramConfig & TelegramLegacyCursorCarrier;
|
|
@@ -484,6 +546,7 @@ export function normalizeTelegramDefaultProfileConfig(config: TelegramConfig): {
|
|
|
484
546
|
"botUsername",
|
|
485
547
|
"botId",
|
|
486
548
|
"allowedUserId",
|
|
549
|
+
"threadDisplayMode",
|
|
487
550
|
"lastUpdateId",
|
|
488
551
|
].some((field) => Object.hasOwn(config, field));
|
|
489
552
|
if (!hasLegacyRootProfile) {
|
|
@@ -504,6 +567,9 @@ export function normalizeTelegramDefaultProfileConfig(config: TelegramConfig): {
|
|
|
504
567
|
...(config.allowedUserId !== undefined
|
|
505
568
|
? { allowedUserId: config.allowedUserId }
|
|
506
569
|
: {}),
|
|
570
|
+
...(config.threadDisplayMode !== undefined
|
|
571
|
+
? { threadDisplayMode: config.threadDisplayMode }
|
|
572
|
+
: {}),
|
|
507
573
|
...((config as TelegramConfig & TelegramLegacyCursorCarrier)
|
|
508
574
|
.lastUpdateId !== undefined
|
|
509
575
|
? {
|
|
@@ -597,6 +663,35 @@ export function createTelegramConfigStore(
|
|
|
597
663
|
);
|
|
598
664
|
mutationVersion += 1;
|
|
599
665
|
};
|
|
666
|
+
const adoptPersistedConfig = (merged: TelegramConfig, preserveLocalChanges: boolean) => {
|
|
667
|
+
// Local edits are relative to the latest observation, not a queued write's older request baseline.
|
|
668
|
+
const nextConfig = preserveLocalChanges
|
|
669
|
+
? mergeTelegramConfigDelta(persistedConfig as Record<string, unknown>, config as Record<string, unknown>,
|
|
670
|
+
merged as Record<string, unknown>) as TelegramConfig
|
|
671
|
+
: cloneTelegramConfig(merged);
|
|
672
|
+
persistedConfig = cloneTelegramConfig(merged);
|
|
673
|
+
config = nextConfig;
|
|
674
|
+
};
|
|
675
|
+
const withPersistedPairingProfile = <T>(
|
|
676
|
+
profileName: string, tokenSha256: string,
|
|
677
|
+
observe: (latest: TelegramConfig, profile: TelegramBotProfile) => T,
|
|
678
|
+
): T => {
|
|
679
|
+
if ((profileName !== TELEGRAM_DEFAULT_PROFILE_NAME && !isValidTelegramProfileName(profileName)) ||
|
|
680
|
+
!/^[a-f0-9]{64}$/u.test(tokenSha256)) {
|
|
681
|
+
throw new Error("Invalid Telegram pairing admission identity.");
|
|
682
|
+
}
|
|
683
|
+
return withTelegramFileTransaction(`${configPath}.transaction`, () => {
|
|
684
|
+
const latest = readTelegramConfigForTransaction(configPath);
|
|
685
|
+
const profile = latest.profiles?.[profileName];
|
|
686
|
+
if (typeof profile?.botToken !== "string" || !profile.botToken ||
|
|
687
|
+
createHash("sha256").update(profile.botToken).digest("hex") !== tokenSha256 ||
|
|
688
|
+
(profile.allowedUserId !== undefined &&
|
|
689
|
+
(!Number.isSafeInteger(profile.allowedUserId) || profile.allowedUserId <= 0))) {
|
|
690
|
+
throw new Error("Telegram pairing admission authority is unavailable or changed.");
|
|
691
|
+
}
|
|
692
|
+
return observe(latest, profile);
|
|
693
|
+
});
|
|
694
|
+
};
|
|
600
695
|
return {
|
|
601
696
|
get: getEffectiveConfig,
|
|
602
697
|
getStoredConfig: () => config,
|
|
@@ -652,6 +747,73 @@ export function createTelegramConfigStore(
|
|
|
652
747
|
nextConfig.allowedUserId = userId;
|
|
653
748
|
setEffectiveConfig(nextConfig);
|
|
654
749
|
},
|
|
750
|
+
withSourceSerialization: (operation) =>
|
|
751
|
+
withTelegramFileTransaction(`${configPath}.transaction`, operation),
|
|
752
|
+
withPairingAdmission: (profileName, tokenSha256, publish) =>
|
|
753
|
+
withPersistedPairingProfile(profileName, tokenSha256, (_latest, profile) =>
|
|
754
|
+
publish(profile.allowedUserId === undefined)),
|
|
755
|
+
withPairedUserAdmission: (profileName, tokenSha256, userId, publish, assertExecutionCurrent) => {
|
|
756
|
+
if (!Number.isSafeInteger(userId) || userId <= 0) return { admitted: false };
|
|
757
|
+
assertExecutionCurrent?.();
|
|
758
|
+
return withPersistedPairingProfile(profileName, tokenSha256, (latest, profile) => {
|
|
759
|
+
if (profile.allowedUserId !== userId) return { admitted: false };
|
|
760
|
+
assertExecutionCurrent?.();
|
|
761
|
+
const current = getEffectiveConfig();
|
|
762
|
+
const previousOwner = persistedConfig.profiles?.[profileName]?.allowedUserId;
|
|
763
|
+
if ((activeProfileName ?? TELEGRAM_DEFAULT_PROFILE_NAME) !== profileName ||
|
|
764
|
+
current.botToken !== profile.botToken ||
|
|
765
|
+
(current.allowedUserId !== undefined && current.allowedUserId !== userId) ||
|
|
766
|
+
(current.allowedUserId === undefined && previousOwner !== undefined)) {
|
|
767
|
+
throw new Error("Telegram paired admission lost local profile authority.");
|
|
768
|
+
}
|
|
769
|
+
// Observation is not a local edit; queued persistence still adopts its own fresh disk result.
|
|
770
|
+
adoptPersistedConfig(latest, true);
|
|
771
|
+
return { admitted: true, value: publish() };
|
|
772
|
+
});
|
|
773
|
+
},
|
|
774
|
+
persistAllowedUserId: (userId, assertExecutionCurrent, commitIfOwned) => {
|
|
775
|
+
const profileName = activeProfileName;
|
|
776
|
+
const profileKey = profileName ?? TELEGRAM_DEFAULT_PROFILE_NAME;
|
|
777
|
+
const botToken = getEffectiveConfig().botToken;
|
|
778
|
+
const previousOwner = getEffectiveConfig().allowedUserId;
|
|
779
|
+
const assertCurrent = () => {
|
|
780
|
+
assertExecutionCurrent?.();
|
|
781
|
+
if (activeProfileName !== profileName || getEffectiveConfig().botToken !== botToken ||
|
|
782
|
+
getEffectiveConfig().allowedUserId !== previousOwner) {
|
|
783
|
+
throw new Error("Telegram pairing lost its originating profile authority.");
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
const pairing = persistQueue.then(() => {
|
|
787
|
+
assertCurrent();
|
|
788
|
+
if (!Number.isSafeInteger(userId) || userId <= 0) throw new Error("Invalid Telegram pairing user ID.");
|
|
789
|
+
let merged: TelegramConfig | undefined;
|
|
790
|
+
const publish = () => {
|
|
791
|
+
merged = withTelegramFileTransaction(`${configPath}.transaction`, () => {
|
|
792
|
+
const latest = readTelegramConfigForTransaction(configPath);
|
|
793
|
+
const profile = latest.profiles?.[profileKey];
|
|
794
|
+
if (!botToken || profile?.botToken !== botToken) {
|
|
795
|
+
throw new Error("Telegram pairing profile is unavailable or changed.");
|
|
796
|
+
}
|
|
797
|
+
assertCurrent();
|
|
798
|
+
if (profile.allowedUserId !== undefined) return latest;
|
|
799
|
+
const next = { ...latest, profiles: { ...latest.profiles,
|
|
800
|
+
[profileKey]: { ...profile, allowedUserId: userId } } };
|
|
801
|
+
writeTelegramConfigInTransaction(agentDir, configPath, next);
|
|
802
|
+
return next;
|
|
803
|
+
});
|
|
804
|
+
};
|
|
805
|
+
if (commitIfOwned) {
|
|
806
|
+
if (!commitIfOwned(publish)) throw new Error("Telegram pairing lost transport ownership before publication.");
|
|
807
|
+
} else {
|
|
808
|
+
publish();
|
|
809
|
+
}
|
|
810
|
+
if (!merged) throw new Error("Telegram pairing publication did not execute.");
|
|
811
|
+
adoptPersistedConfig(merged, true);
|
|
812
|
+
return merged.profiles?.[profileKey]?.allowedUserId === userId;
|
|
813
|
+
});
|
|
814
|
+
persistQueue = pairing.then(() => undefined, () => undefined);
|
|
815
|
+
return pairing;
|
|
816
|
+
},
|
|
655
817
|
load: async () => {
|
|
656
818
|
lastLoadRecoveredInvalidConfig = false;
|
|
657
819
|
const loadedConfig = await readTelegramConfig(configPath, {
|
|
@@ -693,7 +855,7 @@ export function createTelegramConfigStore(
|
|
|
693
855
|
mutationVersion += 1;
|
|
694
856
|
},
|
|
695
857
|
didLastLoadRecoverInvalidConfig: () => lastLoadRecoveredInvalidConfig,
|
|
696
|
-
persist: (nextConfig = getEffectiveConfig()) => {
|
|
858
|
+
persist: (nextConfig = getEffectiveConfig(), options) => {
|
|
697
859
|
const profileName = activeProfileName;
|
|
698
860
|
const desiredConfig = storeTelegramEffectiveConfig(
|
|
699
861
|
config,
|
|
@@ -706,6 +868,9 @@ export function createTelegramConfigStore(
|
|
|
706
868
|
const mergedConfig = withTelegramFileTransaction(
|
|
707
869
|
`${configPath}.transaction`,
|
|
708
870
|
() => {
|
|
871
|
+
if (options?.isCurrent && !options.isCurrent()) {
|
|
872
|
+
throw new Error("Telegram config update lost its originating authority.");
|
|
873
|
+
}
|
|
709
874
|
const latestConfig = readTelegramConfigForTransaction(configPath);
|
|
710
875
|
const merged = mergeTelegramConfigDelta(
|
|
711
876
|
baseConfig as Record<string, unknown>,
|
|
@@ -718,16 +883,7 @@ export function createTelegramConfigStore(
|
|
|
718
883
|
return merged;
|
|
719
884
|
},
|
|
720
885
|
);
|
|
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
|
-
}
|
|
886
|
+
adoptPersistedConfig(mergedConfig, mutationVersion !== capturedMutationVersion);
|
|
731
887
|
});
|
|
732
888
|
persistQueue = persist.catch(() => undefined);
|
|
733
889
|
return persist;
|
|
@@ -1054,20 +1210,19 @@ export type TelegramAuthorizationState =
|
|
|
1054
1210
|
export interface TelegramUserPairingDeps<TContext> {
|
|
1055
1211
|
allowedUserId?: number;
|
|
1056
1212
|
ctx: TContext;
|
|
1057
|
-
|
|
1058
|
-
persistConfig: () => Promise<void>;
|
|
1213
|
+
persistAllowedUserId: TelegramConfigStore["persistAllowedUserId"];
|
|
1059
1214
|
updateStatus: (ctx: TContext) => void;
|
|
1060
1215
|
assertExecutionCurrent?: () => void;
|
|
1061
1216
|
}
|
|
1062
1217
|
|
|
1063
1218
|
export interface TelegramUserPairingRuntimeDeps<TContext> {
|
|
1064
1219
|
getAllowedUserId: () => number | undefined;
|
|
1065
|
-
|
|
1066
|
-
persistConfig: () => Promise<void>;
|
|
1220
|
+
persistAllowedUserId: TelegramConfigStore["persistAllowedUserId"];
|
|
1067
1221
|
updateStatus: (ctx: TContext) => void;
|
|
1068
1222
|
}
|
|
1069
1223
|
|
|
1070
1224
|
export interface TelegramUserPairingRuntime<TContext> {
|
|
1225
|
+
/** True means this user is authorized, whether newly paired or already configured. */
|
|
1071
1226
|
pairIfNeeded: (
|
|
1072
1227
|
userId: number,
|
|
1073
1228
|
ctx: TContext,
|
|
@@ -1104,12 +1259,11 @@ export async function pairTelegramUserIfNeeded<TContext>(
|
|
|
1104
1259
|
userId,
|
|
1105
1260
|
deps.allowedUserId,
|
|
1106
1261
|
);
|
|
1107
|
-
if (authorization.kind !== "pair") return
|
|
1108
|
-
deps.assertExecutionCurrent?.();
|
|
1109
|
-
deps.setAllowedUserId(authorization.userId);
|
|
1262
|
+
if (authorization.kind !== "pair") return authorization.kind === "allow";
|
|
1110
1263
|
deps.assertExecutionCurrent?.();
|
|
1111
|
-
await deps.
|
|
1264
|
+
const allowed = await deps.persistAllowedUserId(authorization.userId, deps.assertExecutionCurrent);
|
|
1112
1265
|
deps.assertExecutionCurrent?.();
|
|
1266
|
+
if (!allowed) return false;
|
|
1113
1267
|
try {
|
|
1114
1268
|
deps.updateStatus(deps.ctx);
|
|
1115
1269
|
} catch (error) {
|
|
@@ -1126,8 +1280,7 @@ export function createTelegramUserPairingRuntime<TContext>(
|
|
|
1126
1280
|
pairTelegramUserIfNeeded(userId, {
|
|
1127
1281
|
allowedUserId: deps.getAllowedUserId(),
|
|
1128
1282
|
ctx,
|
|
1129
|
-
|
|
1130
|
-
persistConfig: deps.persistConfig,
|
|
1283
|
+
persistAllowedUserId: deps.persistAllowedUserId,
|
|
1131
1284
|
updateStatus: deps.updateStatus,
|
|
1132
1285
|
assertExecutionCurrent,
|
|
1133
1286
|
}),
|