@llblab/pi-kit 0.27.6 → 0.28.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/BACKLOG.md +2 -5
- package/CHANGELOG.md +5 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
- package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
- package/node_modules/@llblab/pi-telegram/README.md +15 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
- package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
- package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
- package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
- package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
- package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
- package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
- package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
- package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
- package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
- package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
- package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
- package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
- package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
- package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
- package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
- package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
- package/node_modules/@llblab/pi-telegram/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
- package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/package.json +2 -2
|
@@ -284,7 +284,7 @@ function buildTelegramSectionCallbackContext(
|
|
|
284
284
|
// --- GlobalThis Bridge ---
|
|
285
285
|
|
|
286
286
|
/** @internal */
|
|
287
|
-
|
|
287
|
+
function setGlobalTelegramSectionRegistry(
|
|
288
288
|
registry: TelegramSectionRegistry,
|
|
289
289
|
): void {
|
|
290
290
|
(globalThis as Record<string, unknown>)[SECTION_REGISTRY_KEY] = registry;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram status rendering helpers
|
|
3
3
|
* Zones: telegram ui, pi agent diagnostics, tui
|
|
4
|
-
* Owns status summaries, redacted
|
|
4
|
+
* Owns status summaries, redacted diagnostics, projection lifecycle and compact connection-failure copy
|
|
5
|
+
* Excludes canonical state, transport admission and filesystem policy
|
|
5
6
|
*/
|
|
6
7
|
|
|
8
|
+
import { isDeepStrictEqual } from "node:util";
|
|
9
|
+
|
|
7
10
|
/** UI copy is allowlisted; raw exception text belongs only in redacted diagnostics. */
|
|
8
11
|
export function formatTelegramConnectionFailure(error: unknown): string {
|
|
9
12
|
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
@@ -463,7 +466,7 @@ function truncateTelegramRuntimeEventText(
|
|
|
463
466
|
return `${text.slice(0, maxLength).trimEnd()}… [truncated ${text.length - maxLength} chars]`;
|
|
464
467
|
}
|
|
465
468
|
|
|
466
|
-
|
|
469
|
+
function redactTelegramRuntimeMessage(
|
|
467
470
|
message: string,
|
|
468
471
|
botToken: string | undefined,
|
|
469
472
|
): string {
|
|
@@ -579,7 +582,7 @@ export function registerTelegramStatusLineProvider(
|
|
|
579
582
|
};
|
|
580
583
|
}
|
|
581
584
|
|
|
582
|
-
|
|
585
|
+
function getTelegramStatusLineProviderResults(
|
|
583
586
|
ctx: TelegramStatusLineProviderContext,
|
|
584
587
|
): TelegramStatusLineProviderResult[] {
|
|
585
588
|
const results: TelegramStatusLineProviderResult[] = [];
|
|
@@ -842,13 +845,19 @@ export function createTelegramRuntimeLogScope(input: {
|
|
|
842
845
|
};
|
|
843
846
|
}
|
|
844
847
|
|
|
845
|
-
export
|
|
846
|
-
state: TelegramBridgeStatusLineState,
|
|
847
|
-
): {
|
|
848
|
+
export interface TelegramStatusSnapshot {
|
|
848
849
|
runtime: Record<string, unknown>;
|
|
849
850
|
liveRoster: Record<string, unknown>;
|
|
850
851
|
diagnostics: Record<string, unknown>;
|
|
851
|
-
}
|
|
852
|
+
}
|
|
853
|
+
export interface TelegramStoredRuntimeProjection extends TelegramStatusSnapshot {
|
|
854
|
+
version: 1;
|
|
855
|
+
source: "snapshot";
|
|
856
|
+
writtenAtMs: number;
|
|
857
|
+
}
|
|
858
|
+
export function createTelegramStatusSnapshot(
|
|
859
|
+
state: TelegramBridgeStatusLineState,
|
|
860
|
+
): TelegramStatusSnapshot {
|
|
852
861
|
return {
|
|
853
862
|
runtime: {
|
|
854
863
|
busRole: state.busRole,
|
|
@@ -869,7 +878,6 @@ export function createTelegramStatusSnapshot(
|
|
|
869
878
|
...(state.localBus ? { localBus: state.localBus } : {}),
|
|
870
879
|
topicTargets: state.topicTargets ?? [],
|
|
871
880
|
reservations: state.threadReservations ?? [],
|
|
872
|
-
syncObservations: state.topicSyncObservations ?? [],
|
|
873
881
|
},
|
|
874
882
|
diagnostics: {
|
|
875
883
|
pendingDispatch: state.pendingDispatch,
|
|
@@ -883,17 +891,94 @@ export function createTelegramStatusSnapshot(
|
|
|
883
891
|
};
|
|
884
892
|
}
|
|
885
893
|
|
|
894
|
+
function isRuntimeProjection(value: unknown): value is TelegramStoredRuntimeProjection {
|
|
895
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
896
|
+
const record = value as Record<string, unknown>;
|
|
897
|
+
return record.version === 1 && record.source === "snapshot" && Number.isSafeInteger(record.writtenAtMs) && (record.writtenAtMs as number) >= 0 &&
|
|
898
|
+
Object.keys(record).every(key => ["version", "source", "writtenAtMs", "runtime", "liveRoster", "diagnostics"].includes(key)) &&
|
|
899
|
+
["runtime", "liveRoster", "diagnostics"].every(key => record[key] !== null && typeof record[key] === "object" && !Array.isArray(record[key]));
|
|
900
|
+
}
|
|
901
|
+
export interface TelegramRuntimeProjectionScope {
|
|
902
|
+
path: string;
|
|
903
|
+
profile: string;
|
|
904
|
+
}
|
|
905
|
+
export interface TelegramRuntimeProjectionStorage {
|
|
906
|
+
read: (scope: TelegramRuntimeProjectionScope) => unknown;
|
|
907
|
+
/** The adapter must enforce exact owner plus the supplied physical/logical scope in one transaction. */
|
|
908
|
+
publish: (
|
|
909
|
+
scope: TelegramRuntimeProjectionScope,
|
|
910
|
+
mutate: (current: unknown) => { value: unknown; changed: boolean },
|
|
911
|
+
isCurrent: () => boolean,
|
|
912
|
+
) => boolean;
|
|
913
|
+
}
|
|
914
|
+
export interface TelegramRuntimeProjectionStoreOptions {
|
|
915
|
+
getPath: () => string;
|
|
916
|
+
getProfile: () => string | undefined;
|
|
917
|
+
/** Capture exact owner/context/session authority at submission, never renew it after the queued await. */
|
|
918
|
+
captureAuthority: () => (() => boolean) | undefined;
|
|
919
|
+
storage: TelegramRuntimeProjectionStorage;
|
|
920
|
+
getNowMs?: () => number;
|
|
921
|
+
}
|
|
922
|
+
/** Observational runtime storage only; Workspace, admission, transport, logs and recovery remain with their owners. */
|
|
923
|
+
export function createTelegramRuntimeProjectionStore(options: TelegramRuntimeProjectionStoreOptions): {
|
|
924
|
+
read: () => TelegramStoredRuntimeProjection | undefined;
|
|
925
|
+
persist: (snapshot: TelegramStatusSnapshot) => Promise<boolean>;
|
|
926
|
+
} {
|
|
927
|
+
let publicationQueue = Promise.resolve();
|
|
928
|
+
const { getPath, getProfile: readProfile, captureAuthority } = options;
|
|
929
|
+
const { read, publish } = options.storage;
|
|
930
|
+
const getNowMs = options.getNowMs ?? Date.now;
|
|
931
|
+
const getProfile = (): string => readProfile() || TELEGRAM_STATUS_DEFAULT_PROFILE_NAME;
|
|
932
|
+
return {
|
|
933
|
+
read() {
|
|
934
|
+
try {
|
|
935
|
+
const path = getPath(), profile = getProfile(), value = read({ path, profile });
|
|
936
|
+
return getPath() === path && getProfile() === profile && isRuntimeProjection(value) ? value : undefined;
|
|
937
|
+
} catch { return undefined; }
|
|
938
|
+
},
|
|
939
|
+
async persist(snapshot) {
|
|
940
|
+
const path = getPath(), profile = getProfile(), authority = captureAuthority();
|
|
941
|
+
if (!authority) return false;
|
|
942
|
+
// Freeze wire content at submission, but do not duplicate the event log in persistent runtime state.
|
|
943
|
+
const semantic = JSON.parse(JSON.stringify({ version: 1, source: "snapshot", runtime: snapshot.runtime,
|
|
944
|
+
liveRoster: snapshot.liveRoster, diagnostics: snapshot.diagnostics })) as Omit<TelegramStoredRuntimeProjection, "writtenAtMs">;
|
|
945
|
+
if (!isRuntimeProjection({ ...semantic, writtenAtMs: 0 })) throw new Error("Telegram runtime projection is malformed.");
|
|
946
|
+
delete semantic.diagnostics.recentRuntimeEvents;
|
|
947
|
+
const isCurrent = (): boolean => getPath() === path && getProfile() === profile && authority() === true;
|
|
948
|
+
const publication = publicationQueue.then(() => {
|
|
949
|
+
if (!isCurrent()) return false;
|
|
950
|
+
return publish({ path, profile }, current => {
|
|
951
|
+
if (isRuntimeProjection(current)) {
|
|
952
|
+
const { writtenAtMs: _writtenAtMs, ...existing } = current;
|
|
953
|
+
if (isDeepStrictEqual(existing, semantic)) return { value: current, changed: false };
|
|
954
|
+
}
|
|
955
|
+
const writtenAtMs = getNowMs();
|
|
956
|
+
if (!Number.isSafeInteger(writtenAtMs) || writtenAtMs < 0) throw new Error("Telegram runtime projection timestamp is invalid.");
|
|
957
|
+
return { value: { ...semantic, writtenAtMs }, changed: true };
|
|
958
|
+
}, isCurrent) === true;
|
|
959
|
+
});
|
|
960
|
+
publicationQueue = publication.then(() => undefined, () => undefined);
|
|
961
|
+
return publication;
|
|
962
|
+
},
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
886
966
|
const TELEGRAM_DIAGNOSTICS_SNAPSHOT_COALESCE_MS = 100;
|
|
887
967
|
|
|
888
968
|
export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps: {
|
|
889
|
-
persistSnapshot: () => Promise<void>;
|
|
969
|
+
persistSnapshot: (isCurrent: () => boolean) => Promise<void>;
|
|
890
970
|
recordError: (error: unknown) => void;
|
|
891
|
-
|
|
892
|
-
|
|
971
|
+
captureScope?: () => (() => boolean) | undefined;
|
|
972
|
+
setTimer?: (callback: () => void, ms: number) => { unref?: () => void } | number;
|
|
973
|
+
clearTimer?: (timer: { unref?: () => void } | number) => void;
|
|
974
|
+
}): (() => void) & { resume(): void; suspend(): Promise<void> } {
|
|
893
975
|
const setTimer = deps.setTimer ?? setTimeout;
|
|
894
|
-
|
|
976
|
+
const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle as ReturnType<typeof setTimeout>));
|
|
977
|
+
let generation = 0;
|
|
978
|
+
let enabled = true;
|
|
979
|
+
let timer: { token: object; handle: { unref?: () => void } | number } | undefined;
|
|
895
980
|
let persistPromise: Promise<void> | undefined;
|
|
896
|
-
let pending
|
|
981
|
+
let pending: (() => boolean) | undefined;
|
|
897
982
|
const recordError = (error: unknown): void => {
|
|
898
983
|
try {
|
|
899
984
|
deps.recordError(error);
|
|
@@ -901,30 +986,72 @@ export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps: {
|
|
|
901
986
|
// Snapshot diagnostics cannot create an unhandled scheduler rejection.
|
|
902
987
|
}
|
|
903
988
|
};
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
989
|
+
const cancelTimer = (): void => {
|
|
990
|
+
const previous = timer;
|
|
991
|
+
timer = undefined;
|
|
992
|
+
if (previous) {
|
|
993
|
+
try { clearTimer(previous.handle); } catch (error) { recordError(error); }
|
|
908
994
|
}
|
|
909
|
-
|
|
995
|
+
};
|
|
996
|
+
const capture = (): (() => boolean) | undefined => {
|
|
997
|
+
if (!enabled) return undefined;
|
|
998
|
+
const expectedGeneration = generation;
|
|
999
|
+
let scope: (() => boolean) | undefined;
|
|
1000
|
+
try { scope = deps.captureScope ? deps.captureScope() : () => true; }
|
|
1001
|
+
catch (error) { recordError(error); return undefined; }
|
|
1002
|
+
if (!scope) return undefined;
|
|
1003
|
+
return () => {
|
|
1004
|
+
if (!enabled || generation !== expectedGeneration) return false;
|
|
1005
|
+
try { return scope!(); } catch (error) { recordError(error); return false; }
|
|
1006
|
+
};
|
|
1007
|
+
};
|
|
1008
|
+
const arm = (): void => {
|
|
1009
|
+
if (timer || persistPromise || !pending) return;
|
|
1010
|
+
const authority = pending;
|
|
1011
|
+
if (!authority()) { pending = undefined; return; }
|
|
1012
|
+
const token = {};
|
|
1013
|
+
const handle = setTimer(() => {
|
|
1014
|
+
if (timer?.token !== token) return;
|
|
910
1015
|
timer = undefined;
|
|
1016
|
+
const publicationAuthority = pending;
|
|
1017
|
+
pending = undefined;
|
|
1018
|
+
if (!authority() || !publicationAuthority?.()) return;
|
|
911
1019
|
let tracked: Promise<void>;
|
|
912
1020
|
tracked = Promise.resolve()
|
|
913
|
-
.then(deps.persistSnapshot)
|
|
1021
|
+
.then(() => publicationAuthority() ? deps.persistSnapshot(publicationAuthority) : undefined)
|
|
914
1022
|
.catch(recordError)
|
|
915
1023
|
.finally(() => {
|
|
916
1024
|
if (persistPromise !== tracked) return;
|
|
917
1025
|
persistPromise = undefined;
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
request();
|
|
921
|
-
}
|
|
1026
|
+
// Only an explicitly captured new request can arm a successor publication.
|
|
1027
|
+
arm();
|
|
922
1028
|
});
|
|
923
1029
|
persistPromise = tracked;
|
|
924
1030
|
}, TELEGRAM_DIAGNOSTICS_SNAPSHOT_COALESCE_MS);
|
|
925
|
-
|
|
1031
|
+
timer = { token, handle };
|
|
1032
|
+
if (typeof handle !== "number") handle.unref?.();
|
|
1033
|
+
};
|
|
1034
|
+
const request = (): void => {
|
|
1035
|
+
const authority = capture();
|
|
1036
|
+
if (!authority) return;
|
|
1037
|
+
pending = authority;
|
|
1038
|
+
arm();
|
|
926
1039
|
};
|
|
927
|
-
return request
|
|
1040
|
+
return Object.assign(request, {
|
|
1041
|
+
resume() {
|
|
1042
|
+
generation += 1;
|
|
1043
|
+
cancelTimer();
|
|
1044
|
+
pending = undefined;
|
|
1045
|
+
enabled = true;
|
|
1046
|
+
},
|
|
1047
|
+
suspend() {
|
|
1048
|
+
enabled = false;
|
|
1049
|
+
generation += 1;
|
|
1050
|
+
cancelTimer();
|
|
1051
|
+
pending = undefined;
|
|
1052
|
+
return persistPromise ?? Promise.resolve();
|
|
1053
|
+
},
|
|
1054
|
+
});
|
|
928
1055
|
}
|
|
929
1056
|
|
|
930
1057
|
export function getTelegramStatusBarProcessingStatus(state: {
|
|
@@ -1382,16 +1509,9 @@ function buildTelegramBridgeCompactStatusLines(
|
|
|
1382
1509
|
: state.activeSourceMessageIds?.length
|
|
1383
1510
|
? "active"
|
|
1384
1511
|
: "idle";
|
|
1385
|
-
const diagnosticsProfileName =
|
|
1386
|
-
state.activeProfileName === TELEGRAM_STATUS_DEFAULT_PROFILE_NAME
|
|
1387
|
-
? undefined
|
|
1388
|
-
: state.activeProfileName;
|
|
1389
|
-
const profileSuffix = diagnosticsProfileName
|
|
1390
|
-
? `.${diagnosticsProfileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}`
|
|
1391
|
-
: "";
|
|
1392
1512
|
const diagnosticsPaths = state.diagnosticPaths ?? {
|
|
1393
|
-
state:
|
|
1394
|
-
logs:
|
|
1513
|
+
state: "~/.pi/agent/tmp/pi-telegram/state.json",
|
|
1514
|
+
logs: "~/.pi/agent/tmp/pi-telegram/logs.jsonl",
|
|
1395
1515
|
};
|
|
1396
1516
|
return [
|
|
1397
1517
|
"connection:",
|
|
@@ -1443,7 +1563,7 @@ export function buildTelegramBridgeStatusLines(
|
|
|
1443
1563
|
return buildTelegramBridgeCompactStatusLines(state);
|
|
1444
1564
|
}
|
|
1445
1565
|
|
|
1446
|
-
|
|
1566
|
+
function buildTelegramBridgeDiagnosticStatusLines(
|
|
1447
1567
|
state: TelegramBridgeStatusLineState,
|
|
1448
1568
|
): string[] {
|
|
1449
1569
|
const controlQueueCount = state.queuedItems.filter(
|
|
@@ -1620,7 +1740,7 @@ function buildTelegramStatusRoleSuffix(
|
|
|
1620
1740
|
return ` @${state.busRole}`;
|
|
1621
1741
|
}
|
|
1622
1742
|
|
|
1623
|
-
|
|
1743
|
+
function buildStatusHtml(
|
|
1624
1744
|
ctx: TelegramStatusContext,
|
|
1625
1745
|
activeModel: TelegramStatusActiveModel | undefined,
|
|
1626
1746
|
bridgeStatus?: TelegramBridgeStatusLineState,
|
|
@@ -8,6 +8,7 @@ import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError } from
|
|
|
8
8
|
import { getTelegramTargetKey, type TelegramTarget } from "./target.ts";
|
|
9
9
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
10
10
|
import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.ts";
|
|
11
|
+
import { normalizeTelegramWorkspacePath } from "./workspace-identity.ts";
|
|
11
12
|
import {
|
|
12
13
|
createTelegramWorkspaceAdmissionOperationId,
|
|
13
14
|
runWithTelegramWorkspaceAdmissionsAsync,
|
|
@@ -20,7 +21,6 @@ import {
|
|
|
20
21
|
getTelegramThreadOwnerKey,
|
|
21
22
|
isSameTelegramProcessInstance,
|
|
22
23
|
isTelegramTopicTargetStaleError,
|
|
23
|
-
normalizeTelegramWorkspacePath,
|
|
24
24
|
provisionOwnBusTopic,
|
|
25
25
|
type TelegramOwnTopicProvisionResult,
|
|
26
26
|
type TelegramTopicTargetStore,
|
|
@@ -179,7 +179,7 @@ export interface TelegramManualThreadDisconnectDeps<TSyncState> {
|
|
|
179
179
|
getNowMs?: () => number;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
function markTelegramConfigSyncChange<
|
|
183
183
|
TSyncState extends TelegramSyncState,
|
|
184
184
|
>(state: TSyncState, action: string, options?: { nowMs?: number }): TSyncState {
|
|
185
185
|
const nowMs = options?.nowMs ?? Date.now();
|
|
@@ -245,7 +245,7 @@ export interface TelegramSessionRestartThreadCleanupDeps<
|
|
|
245
245
|
suspendPolling: () => Promise<void>;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
function createTelegramSessionRestartThreadCleanupHandler<
|
|
249
249
|
TSyncState extends TelegramSyncState,
|
|
250
250
|
>(
|
|
251
251
|
deps: TelegramSessionRestartThreadCleanupDeps<TSyncState>,
|
|
@@ -997,7 +997,7 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
997
997
|
}
|
|
998
998
|
}
|
|
999
999
|
|
|
1000
|
-
|
|
1000
|
+
const TELEGRAM_SYNC_SLICE_TARGET_BINDINGS = "target-bindings";
|
|
1001
1001
|
|
|
1002
1002
|
export const TELEGRAM_SYNC_SLICES = [
|
|
1003
1003
|
"bot-identity",
|
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
import { createWriteStream, openAsBlob } from "node:fs";
|
|
11
|
-
import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
11
|
+
import { mkdir, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
12
12
|
import { request as requestHttps } from "node:https";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
import {
|
|
14
|
+
import type { TelegramAttachmentSource } from "./media.ts";
|
|
15
|
+
import { resolveTelegramAttachmentsDir } from "./paths.ts";
|
|
15
16
|
import { Readable, Transform } from "node:stream";
|
|
16
17
|
import { pipeline } from "node:stream/promises";
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
const TELEGRAM_API_BASE = "https://api.telegram.org";
|
|
19
20
|
|
|
20
21
|
export const TELEGRAM_FILE_MAX_BYTES = 50 * 1024 * 1024;
|
|
21
22
|
|
|
@@ -34,12 +35,10 @@ export function getTelegramInboundFileByteLimitFromEnv(
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function getTelegramApiTempDir(): string {
|
|
37
|
-
return
|
|
38
|
+
return resolveTelegramAttachmentsDir();
|
|
38
39
|
}
|
|
39
40
|
const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
40
41
|
const activeTelegramApiWorkspaceAdmissionOperationIds = new Set<string>();
|
|
41
|
-
const TELEGRAM_TEMP_SCRATCH_FILE_PATTERN =
|
|
42
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-/u;
|
|
43
42
|
const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(
|
|
44
43
|
process.env,
|
|
45
44
|
["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES", "TELEGRAM_MAX_FILE_SIZE_BYTES"],
|
|
@@ -750,6 +749,8 @@ export interface TelegramBridgeApiRuntimeDeps {
|
|
|
750
749
|
tempDir: string;
|
|
751
750
|
maxFileSizeBytes: number;
|
|
752
751
|
tempFileMaxAgeMs: number;
|
|
752
|
+
/** Bot `@username` (or numeric id) naming private-chat attachments. */
|
|
753
|
+
getBotScope?: () => string | undefined;
|
|
753
754
|
recordRuntimeEvent: (
|
|
754
755
|
kind: "api" | "multipart" | "download",
|
|
755
756
|
error: unknown,
|
|
@@ -774,7 +775,11 @@ export interface TelegramBridgeApiRuntime {
|
|
|
774
775
|
fileName: string,
|
|
775
776
|
options?: TelegramApiCallOptions,
|
|
776
777
|
) => Promise<TResponse>;
|
|
777
|
-
downloadFile: (
|
|
778
|
+
downloadFile: (
|
|
779
|
+
fileId: string,
|
|
780
|
+
suggestedName: string,
|
|
781
|
+
source?: TelegramAttachmentSource,
|
|
782
|
+
) => Promise<string>;
|
|
778
783
|
deleteWebhook: (signal?: AbortSignal) => Promise<boolean>;
|
|
779
784
|
getUpdates: (
|
|
780
785
|
body: Record<string, unknown>,
|
|
@@ -853,6 +858,28 @@ function sanitizeFileName(name: string): string {
|
|
|
853
858
|
return name.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
854
859
|
}
|
|
855
860
|
|
|
861
|
+
/** Names follow `<kind>-<scope>-<messageId>[-<index>][-<name>|.<ext>]`. */
|
|
862
|
+
/**
|
|
863
|
+
* `scope` is the bot `@username` (id fallback) in a private chat and the public chat `@username` (id fallback) elsewhere.
|
|
864
|
+
* A negative chat id loses its sign, so the name never contains two adjacent dashes.
|
|
865
|
+
*/
|
|
866
|
+
export function createTelegramAttachmentFileName(
|
|
867
|
+
source: TelegramAttachmentSource,
|
|
868
|
+
generatedName: string,
|
|
869
|
+
botScope?: string,
|
|
870
|
+
): string {
|
|
871
|
+
const chat = source.chat;
|
|
872
|
+
const clean = (value: string): string => sanitizeFileName(value.replace(/^@/u, "")).replace(/^[-.]+/u, "");
|
|
873
|
+
const scope = source.scope ? clean(source.scope) : chat?.type === "private" || !chat
|
|
874
|
+
? botScope ? clean(botScope) : chat ? String(Math.abs(chat.id)) : ""
|
|
875
|
+
: chat.username ? clean(chat.username) : String(Math.abs(chat.id));
|
|
876
|
+
const dot = generatedName.lastIndexOf(".");
|
|
877
|
+
const extension = dot > 0 ? sanitizeFileName(generatedName.slice(dot)) : "";
|
|
878
|
+
const tail = source.userFileName ? `-${clean(source.userFileName)}` : extension;
|
|
879
|
+
return [sanitizeFileName(source.kind), scope, String(source.messageId)].filter(Boolean).join("-") +
|
|
880
|
+
(source.index !== undefined ? `-${source.index}` : "") + tail;
|
|
881
|
+
}
|
|
882
|
+
|
|
856
883
|
class TelegramApiMalformedSuccessError extends Error {
|
|
857
884
|
constructor(method: string, detail: string) {
|
|
858
885
|
super(`Telegram API ${method} ${detail}`);
|
|
@@ -1490,9 +1517,8 @@ export async function cleanupTelegramTempFiles(
|
|
|
1490
1517
|
return 0;
|
|
1491
1518
|
}
|
|
1492
1519
|
for (const entry of entries) {
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
}
|
|
1520
|
+
// The directory holds only attachment scratch files, so age alone decides.
|
|
1521
|
+
if (!entry.isFile()) continue;
|
|
1496
1522
|
const path = join(tempDir, entry.name);
|
|
1497
1523
|
try {
|
|
1498
1524
|
const stats = await stat(path);
|
|
@@ -1643,10 +1669,9 @@ export async function downloadTelegramFile(
|
|
|
1643
1669
|
);
|
|
1644
1670
|
assertTelegramFileSizeWithinLimit(file.file_size, options?.maxFileSizeBytes);
|
|
1645
1671
|
await mkdir(tempDir, { recursive: true, mode: 0o700 });
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
);
|
|
1672
|
+
// Names carry scope and message id, so the same message maps to the same path; publish by rename.
|
|
1673
|
+
const targetPath = join(tempDir, sanitizeFileName(suggestedName));
|
|
1674
|
+
const partPath = `${targetPath}.${randomUUID()}.part`;
|
|
1650
1675
|
const response = await callTelegramTransportRequest((family) =>
|
|
1651
1676
|
telegramFetch(
|
|
1652
1677
|
`${TELEGRAM_API_BASE}/file/bot${configuredBotToken}/${file.file_path}`,
|
|
@@ -1665,11 +1690,12 @@ export async function downloadTelegramFile(
|
|
|
1665
1690
|
try {
|
|
1666
1691
|
await writeTelegramDownloadResponse(
|
|
1667
1692
|
response,
|
|
1668
|
-
|
|
1693
|
+
partPath,
|
|
1669
1694
|
options?.maxFileSizeBytes,
|
|
1670
1695
|
);
|
|
1696
|
+
await rename(partPath, targetPath);
|
|
1671
1697
|
} catch (error) {
|
|
1672
|
-
await removeTelegramPartialDownload(
|
|
1698
|
+
await removeTelegramPartialDownload(partPath);
|
|
1673
1699
|
throw error;
|
|
1674
1700
|
}
|
|
1675
1701
|
return targetPath;
|
|
@@ -1700,21 +1726,6 @@ export async function answerTelegramCallbackQuery(
|
|
|
1700
1726
|
}
|
|
1701
1727
|
}
|
|
1702
1728
|
|
|
1703
|
-
export async function deleteTelegramMessage(
|
|
1704
|
-
botToken: string | undefined,
|
|
1705
|
-
chatId: number,
|
|
1706
|
-
messageId: number,
|
|
1707
|
-
): Promise<void> {
|
|
1708
|
-
try {
|
|
1709
|
-
await callTelegram<boolean>(botToken, "deleteMessage", {
|
|
1710
|
-
chat_id: chatId,
|
|
1711
|
-
message_id: messageId,
|
|
1712
|
-
});
|
|
1713
|
-
} catch {
|
|
1714
|
-
// ignore
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
1729
|
export function createTelegramChatActionSender<TAction extends string>(
|
|
1719
1730
|
sendChatAction: (
|
|
1720
1731
|
chatId: number,
|
|
@@ -1802,6 +1813,7 @@ export type TelegramWorkspaceThreadDeletionTransport = (
|
|
|
1802
1813
|
|
|
1803
1814
|
export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
1804
1815
|
getBotToken: () => string | undefined;
|
|
1816
|
+
getBotScope?: () => string | undefined;
|
|
1805
1817
|
recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
|
|
1806
1818
|
captureRequestErrorHandler?: TelegramBridgeApiRuntimeDeps["captureRequestErrorHandler"];
|
|
1807
1819
|
targetActivity?: TelegramApiTargetActivityRuntime;
|
|
@@ -1831,6 +1843,7 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
|
1831
1843
|
tempDir: getTelegramApiTempDir(),
|
|
1832
1844
|
maxFileSizeBytes: TELEGRAM_INBOUND_FILE_MAX_BYTES,
|
|
1833
1845
|
tempFileMaxAgeMs: TELEGRAM_TEMP_FILE_MAX_AGE_MS,
|
|
1846
|
+
...(deps.getBotScope ? { getBotScope: deps.getBotScope } : {}),
|
|
1834
1847
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1835
1848
|
captureRequestErrorHandler: deps.captureRequestErrorHandler,
|
|
1836
1849
|
});
|
|
@@ -2040,11 +2053,13 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2040
2053
|
* Downloads a file from the Telegram servers into the local temp directory.
|
|
2041
2054
|
* Used for inbound voice messages, photos, documents, etc.
|
|
2042
2055
|
*/
|
|
2043
|
-
downloadFile: async (fileId, suggestedName) => {
|
|
2056
|
+
downloadFile: async (fileId, suggestedName, source) => {
|
|
2044
2057
|
try {
|
|
2045
2058
|
return await deps.client.downloadFile(
|
|
2046
2059
|
fileId,
|
|
2047
|
-
|
|
2060
|
+
source
|
|
2061
|
+
? createTelegramAttachmentFileName(source, suggestedName, deps.getBotScope?.())
|
|
2062
|
+
: suggestedName,
|
|
2048
2063
|
deps.tempDir,
|
|
2049
2064
|
{
|
|
2050
2065
|
maxFileSizeBytes: deps.maxFileSizeBytes,
|
|
@@ -10,6 +10,12 @@ import { dirname } from "node:path";
|
|
|
10
10
|
import { createHash, randomUUID } from "node:crypto";
|
|
11
11
|
|
|
12
12
|
import { renameTelegramPathWithRetry, withTelegramFileTransaction } from "./locks.ts";
|
|
13
|
+
import { getTelegramJournalPublicationPaths } from "./paths.ts";
|
|
14
|
+
import {
|
|
15
|
+
isWireRecord as isObject,
|
|
16
|
+
hasOnlyWireKeys as onlyKeys,
|
|
17
|
+
isNonNegativeWireInteger as safeTime,
|
|
18
|
+
} from "./wire.ts";
|
|
13
19
|
import type { TelegramWorkspaceDeletionPermit,
|
|
14
20
|
TelegramWorkspaceDestructiveFence,
|
|
15
21
|
TelegramWorkspaceRetirementFence } from "./workspace-admission.ts";
|
|
@@ -247,15 +253,6 @@ export interface TelegramThreadCleanupWorkStore {
|
|
|
247
253
|
list(): TelegramThreadCleanupWorkSet[];
|
|
248
254
|
}
|
|
249
255
|
|
|
250
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
251
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
252
|
-
}
|
|
253
|
-
function onlyKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
254
|
-
const allowed = new Set(keys); return Object.keys(value).every(key => allowed.has(key));
|
|
255
|
-
}
|
|
256
|
-
function safeTime(value: unknown): value is number {
|
|
257
|
-
return Number.isSafeInteger(value) && (value as number) >= 0;
|
|
258
|
-
}
|
|
259
256
|
function validateWorkSet(value: unknown, profileName: string): TelegramThreadCleanupWorkSet {
|
|
260
257
|
if (!isObject(value) || !onlyKeys(value, ["operationId", "createdAtMs", "entries"]) ||
|
|
261
258
|
typeof value.operationId !== "string" || !value.operationId || !safeTime(value.createdAtMs) ||
|
|
@@ -311,11 +308,23 @@ function sameCandidates(left: readonly TelegramThreadCleanupCandidate[], right:
|
|
|
311
308
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
312
309
|
}
|
|
313
310
|
|
|
311
|
+
function parseTelegramThreadCleanupWorkFile(value: unknown, profileName: string, tokenSha256: string): TelegramThreadCleanupWorkFile {
|
|
312
|
+
if (!isObject(value) || !onlyKeys(value, ["version", "profileName", "tokenSha256", "workSets"]) ||
|
|
313
|
+
value.version !== 1 || value.profileName !== profileName || value.tokenSha256 !== tokenSha256 || !Array.isArray(value.workSets))
|
|
314
|
+
throw new Error("Telegram Thread cleanup store identity or schema does not match.");
|
|
315
|
+
const workSets = value.workSets.map(workSet => validateWorkSet(workSet, profileName));
|
|
316
|
+
if (new Set(workSets.map(workSet => workSet.operationId)).size !== workSets.length)
|
|
317
|
+
throw new Error("Telegram Thread cleanup operation identity is ambiguous.");
|
|
318
|
+
return { version: 1, profileName, tokenSha256, workSets };
|
|
319
|
+
}
|
|
320
|
+
|
|
314
321
|
export function createTelegramThreadCleanupWorkStore(options: {
|
|
315
|
-
path: string; profileName: string; tokenSha256: string; maxWorkSets?: number;
|
|
322
|
+
path: string; runtimeDir?: string; profileName: string; tokenSha256: string; maxWorkSets?: number;
|
|
316
323
|
maxBytes?: number; getNowMs?: () => number;
|
|
317
324
|
onPublicationBoundary?: (boundary: "after-write-before-rename" | "after-rename") => void;
|
|
318
325
|
}): TelegramThreadCleanupWorkStore {
|
|
326
|
+
options = { ...options };
|
|
327
|
+
const publicationPaths = getTelegramJournalPublicationPaths(options.path, options.runtimeDir);
|
|
319
328
|
const maxWorkSets = options.maxWorkSets ?? 32;
|
|
320
329
|
const maxBytes = options.maxBytes ?? 1024 * 1024;
|
|
321
330
|
const now = options.getNowMs ?? Date.now;
|
|
@@ -344,21 +353,15 @@ export function createTelegramThreadCleanupWorkStore(options: {
|
|
|
344
353
|
opened.mtimeNs !== before.mtimeNs) throw new Error("Telegram Thread cleanup store changed during inspection.");
|
|
345
354
|
value = JSON.parse(readFileSync(fd, "utf8")) as unknown;
|
|
346
355
|
} finally { closeSync(fd); }
|
|
347
|
-
|
|
348
|
-
value.version !== 1 || value.profileName !== options.profileName ||
|
|
349
|
-
value.tokenSha256 !== options.tokenSha256 || !Array.isArray(value.workSets))
|
|
350
|
-
throw new Error("Telegram Thread cleanup store identity or schema does not match.");
|
|
351
|
-
const workSets = value.workSets.map(workSet => validateWorkSet(workSet, options.profileName));
|
|
352
|
-
if (new Set(workSets.map(workSet => workSet.operationId)).size !== workSets.length)
|
|
353
|
-
throw new Error("Telegram Thread cleanup operation identity is ambiguous.");
|
|
354
|
-
return { version: 1, profileName: options.profileName, tokenSha256: options.tokenSha256, workSets };
|
|
356
|
+
return parseTelegramThreadCleanupWorkFile(value, options.profileName, options.tokenSha256);
|
|
355
357
|
};
|
|
356
358
|
const publish = (file: TelegramThreadCleanupWorkFile): void => {
|
|
357
359
|
if (file.workSets.length > maxWorkSets) throw new Error("Telegram Thread cleanup work-set capacity reached.");
|
|
358
360
|
const serialized = `${JSON.stringify(file, null, 2)}\n`;
|
|
359
361
|
if (Buffer.byteLength(serialized) > maxBytes) throw new Error("Telegram Thread cleanup byte capacity reached.");
|
|
360
|
-
const temporaryPath = `${
|
|
362
|
+
const temporaryPath = `${publicationPaths.temporaryBasePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
361
363
|
mkdirSync(dirname(options.path), { recursive: true, mode: 0o700 });
|
|
364
|
+
mkdirSync(dirname(temporaryPath), { recursive: true, mode: 0o700 });
|
|
362
365
|
try {
|
|
363
366
|
writeFileSync(temporaryPath, serialized, { mode: 0o600 });
|
|
364
367
|
chmodSync(temporaryPath, 0o600);
|
|
@@ -370,7 +373,7 @@ export function createTelegramThreadCleanupWorkStore(options: {
|
|
|
370
373
|
} finally { try { unlinkSync(temporaryPath); } catch { /* rename consumed it */ } }
|
|
371
374
|
};
|
|
372
375
|
const mutate = <T>(operation: (file: TelegramThreadCleanupWorkFile) => T): T =>
|
|
373
|
-
withTelegramFileTransaction(
|
|
376
|
+
withTelegramFileTransaction(publicationPaths.transactionPath, () => operation(read()));
|
|
374
377
|
return {
|
|
375
378
|
prepare(operationId, candidates) {
|
|
376
379
|
if (!operationId || candidates.length === 0 || candidates.some(candidate => candidate.profileName !== options.profileName))
|
|
@@ -435,7 +438,7 @@ export function createTelegramThreadCleanupWorkStore(options: {
|
|
|
435
438
|
},
|
|
436
439
|
list() {
|
|
437
440
|
return structuredClone(withTelegramFileTransaction(
|
|
438
|
-
|
|
441
|
+
publicationPaths.transactionPath,
|
|
439
442
|
() => read().workSets,
|
|
440
443
|
));
|
|
441
444
|
},
|