@llblab/pi-kit 0.19.1 → 0.20.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 +12 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +4 -4
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-state-flow/README.md +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +45 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +9 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +19 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +52 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +0 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +36 -15
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +5 -5
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +2 -1
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +48 -5
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +9 -9
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +65 -11
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -2
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +35 -15
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +9 -7
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +13 -32
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +11 -0
- package/node_modules/@llblab/pi-telegram/README.md +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +3 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +93 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +9 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +231 -71
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +4 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +34 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +63 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +13 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +25 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +32 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +10 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +22 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +41 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +9 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +23 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +7 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +68 -37
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +6 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +134 -61
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +49 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +77 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +384 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.d.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +6 -0
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +21 -14
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +31 -10
- package/node_modules/@llblab/pi-telegram/docs/updates.md +2 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +5 -7
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +69 -12
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +236 -94
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +39 -9
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +69 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +14 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +23 -4
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +32 -13
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -1
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +2 -3
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +45 -1
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +30 -2
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +11 -3
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +73 -36
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +145 -78
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +67 -4
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +465 -7
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +7 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
|
@@ -610,6 +610,9 @@ export interface TelegramUpdateJournalRuntimeBinding {
|
|
|
610
610
|
runtimeKey: string;
|
|
611
611
|
recoveryKey: string;
|
|
612
612
|
journal: TelegramUpdateJournalStore;
|
|
613
|
+
readForProtection?: () => {
|
|
614
|
+
entries: readonly TelegramUpdateJournalEntry[];
|
|
615
|
+
};
|
|
613
616
|
}
|
|
614
617
|
export interface TelegramUpdateJournalRuntimeBindingResolverDeps {
|
|
615
618
|
getProfileName: () => string | undefined;
|
|
@@ -1443,6 +1443,19 @@ export function createTelegramUpdateJournalRuntimeBindingResolver(deps) {
|
|
|
1443
1443
|
profileName,
|
|
1444
1444
|
botIdentity,
|
|
1445
1445
|
}),
|
|
1446
|
+
readForProtection() {
|
|
1447
|
+
// Protection must never turn corruption/recovery into empty-work evidence.
|
|
1448
|
+
const evidence = inspectTelegramUpdateJournalFamily({
|
|
1449
|
+
directory: dirname(path), path, profile: profileName, botIdentity,
|
|
1450
|
+
limits: {
|
|
1451
|
+
maxFiles: 1024,
|
|
1452
|
+
maxBytes: TELEGRAM_UPDATE_JOURNAL_MAX_BYTES * 2,
|
|
1453
|
+
maxEntries: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES,
|
|
1454
|
+
maxWork: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES * 1024,
|
|
1455
|
+
},
|
|
1456
|
+
});
|
|
1457
|
+
return { entries: evidence.kind === "present" ? evidence.file.entries : [] };
|
|
1458
|
+
},
|
|
1446
1459
|
journal: createTelegramUpdateJournalStore({
|
|
1447
1460
|
path,
|
|
1448
1461
|
profileName,
|
|
@@ -85,6 +85,7 @@ export interface TelegramBridgeSessionServiceRuntime {
|
|
|
85
85
|
guestPlaceholder?: {
|
|
86
86
|
stopAll(): void;
|
|
87
87
|
};
|
|
88
|
+
prepareThreadPreservationOnQuit?: (isSessionCurrent: () => boolean) => (() => Promise<void>) | undefined;
|
|
88
89
|
}
|
|
89
90
|
export interface TelegramBridgeSessionLifecycleAssemblyDeps<TQueueItem, TModel = unknown> {
|
|
90
91
|
contextStore: TelegramSessionContextStore<ExtensionContext>;
|
|
@@ -111,6 +112,7 @@ export interface TelegramBridgeSessionLifecyclePorts<TQueueItem, TModel = unknow
|
|
|
111
112
|
capabilityMonitor: TelegramBridgeSessionServiceRuntime["capabilityMonitor"];
|
|
112
113
|
queueWatchdog: TelegramBridgeSessionServiceRuntime["queueWatchdog"];
|
|
113
114
|
guestPlaceholder?: TelegramBridgeSessionServiceRuntime["guestPlaceholder"];
|
|
115
|
+
prepareThreadPreservationOnQuit?: TelegramBridgeSessionServiceRuntime["prepareThreadPreservationOnQuit"];
|
|
114
116
|
};
|
|
115
117
|
}
|
|
116
118
|
export declare function createTelegramBridgeSessionLifecycleDeps<TQueueItem, TModel = unknown>(ports: TelegramBridgeSessionLifecyclePorts<TQueueItem, TModel>): TelegramBridgeSessionLifecycleAssemblyDeps<TQueueItem, TModel>;
|
|
@@ -91,7 +91,8 @@ export function createTelegramSessionGenerationFence(store, hooks) {
|
|
|
91
91
|
if (!store.isCurrent(ctx, generation))
|
|
92
92
|
return;
|
|
93
93
|
await hooks.onSessionShutdown(event, ctx);
|
|
94
|
-
store.
|
|
94
|
+
if (store.isCurrent(ctx, generation))
|
|
95
|
+
store.clear(ctx);
|
|
95
96
|
},
|
|
96
97
|
};
|
|
97
98
|
}
|
|
@@ -115,6 +116,7 @@ export function createTelegramBridgeSessionLifecycleDeps(ports) {
|
|
|
115
116
|
capabilityMonitor: ports.services.capabilityMonitor,
|
|
116
117
|
queueWatchdog: ports.services.queueWatchdog,
|
|
117
118
|
guestPlaceholder: ports.services.guestPlaceholder,
|
|
119
|
+
prepareThreadPreservationOnQuit: ports.services.prepareThreadPreservationOnQuit,
|
|
118
120
|
},
|
|
119
121
|
};
|
|
120
122
|
}
|
|
@@ -144,18 +146,37 @@ export function createTelegramBridgeSessionLifecycleAssembly(deps) {
|
|
|
144
146
|
deps.services.queueWatchdog.start(ctx);
|
|
145
147
|
},
|
|
146
148
|
async onSessionShutdown(event, ctx) {
|
|
147
|
-
|
|
149
|
+
const generation = deps.contextStore.getGeneration();
|
|
150
|
+
const isCurrent = () => deps.contextStore.isCurrent(ctx, generation);
|
|
151
|
+
if (!isCurrent())
|
|
148
152
|
return;
|
|
153
|
+
let preserveThread;
|
|
154
|
+
if (event.reason === "quit") {
|
|
155
|
+
try {
|
|
156
|
+
preserveThread = deps.services.prepareThreadPreservationOnQuit?.(isCurrent);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
deps.follower.recordRuntimeEvent("session", error, { phase: "preserve-thread-on-quit" });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
149
162
|
deps.services.guestPlaceholder?.stopAll();
|
|
150
163
|
await deps.services.delivery.onSessionShutdown();
|
|
151
|
-
if (!
|
|
164
|
+
if (!isCurrent())
|
|
152
165
|
return;
|
|
153
166
|
deps.services.queueWatchdog.stop();
|
|
154
167
|
deps.services.capabilityMonitor.stop();
|
|
155
168
|
await queueLifecycle.onSessionShutdown(event, ctx);
|
|
156
|
-
if (!
|
|
169
|
+
if (!isCurrent())
|
|
157
170
|
return;
|
|
158
171
|
await deps.services.inboundWorker.onSessionShutdown();
|
|
172
|
+
if (!isCurrent())
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
await preserveThread?.();
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
deps.follower.recordRuntimeEvent("session", error, { phase: "preserve-thread-on-quit" });
|
|
179
|
+
}
|
|
159
180
|
},
|
|
160
181
|
};
|
|
161
182
|
const followerLifecycle = appendTelegramLifecycleHooks(servicesLifecycle, {
|
|
@@ -133,6 +133,7 @@ export interface TelegramLockedPollingRuntime<TContext extends TelegramLockConte
|
|
|
133
133
|
start: (ctx: TContext, options?: TelegramLockedPollingStartOptions) => Promise<TelegramLockedPollingStartResult>;
|
|
134
134
|
stop: () => Promise<string>;
|
|
135
135
|
suspend: () => Promise<void>;
|
|
136
|
+
isSuspended: () => boolean;
|
|
136
137
|
onPersistentConflict: (ctx: TContext, count: number) => Promise<void>;
|
|
137
138
|
onSessionStart: (_event: unknown, ctx: TContext) => Promise<void>;
|
|
138
139
|
registerFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry) => boolean | undefined | Promise<boolean | undefined>;
|
|
@@ -551,7 +551,8 @@ export function isProcessAlive(pid) {
|
|
|
551
551
|
return true;
|
|
552
552
|
}
|
|
553
553
|
catch (error) {
|
|
554
|
-
|
|
554
|
+
// Only an absent PID proves death; permission and unexpected failures do not.
|
|
555
|
+
return error.code !== "ESRCH";
|
|
555
556
|
}
|
|
556
557
|
}
|
|
557
558
|
export function formatTelegramLockEntry(lock) {
|
|
@@ -835,6 +836,9 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
835
836
|
let takeoverCandidate;
|
|
836
837
|
let sessionAutoStartRun;
|
|
837
838
|
let pollingGeneration = 0;
|
|
839
|
+
let suspendedGeneration;
|
|
840
|
+
let suspensionsInFlight = 0;
|
|
841
|
+
let startupsInFlight = 0;
|
|
838
842
|
const ownershipCheckMs = deps.ownershipCheckMs ?? TELEGRAM_OWNERSHIP_CHECK_MS;
|
|
839
843
|
const ownershipRefreshMs = deps.ownershipRefreshMs ?? TELEGRAM_OWNERSHIP_REFRESH_MS;
|
|
840
844
|
const stopOwnershipWatcher = () => {
|
|
@@ -846,20 +850,30 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
846
850
|
ownershipRefreshInterval = undefined;
|
|
847
851
|
};
|
|
848
852
|
const suspendPolling = async () => {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
if (sessionAutoStartRun) {
|
|
855
|
-
await sessionAutoStartRun;
|
|
853
|
+
const generation = ++pollingGeneration;
|
|
854
|
+
suspensionsInFlight += 1;
|
|
855
|
+
try {
|
|
856
|
+
activeContext = undefined;
|
|
857
|
+
deps.transportMonitor?.stop();
|
|
856
858
|
deps.stopFollowerRegistration?.();
|
|
859
|
+
stopOwnershipWatcher();
|
|
860
|
+
if (sessionAutoStartRun) {
|
|
861
|
+
await sessionAutoStartRun;
|
|
862
|
+
deps.stopFollowerRegistration?.();
|
|
863
|
+
}
|
|
864
|
+
if (ownershipStop) {
|
|
865
|
+
await ownershipStop;
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
await deps.stopPolling();
|
|
869
|
+
// Unsettled starts, overlapping stops or stale completion cannot certify quiescence.
|
|
870
|
+
if (generation === pollingGeneration && suspensionsInFlight === 1 && startupsInFlight === 0) {
|
|
871
|
+
suspendedGeneration = generation;
|
|
872
|
+
}
|
|
857
873
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
return;
|
|
874
|
+
finally {
|
|
875
|
+
suspensionsInFlight -= 1;
|
|
861
876
|
}
|
|
862
|
-
await deps.stopPolling();
|
|
863
877
|
};
|
|
864
878
|
const stopAfterOwnershipLoss = () => {
|
|
865
879
|
if (ownershipStop)
|
|
@@ -906,6 +920,7 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
906
920
|
return false;
|
|
907
921
|
activeContext = ctx;
|
|
908
922
|
startOwnershipWatcher(ctx);
|
|
923
|
+
startupsInFlight += 1;
|
|
909
924
|
try {
|
|
910
925
|
if (!deps.lock.refresh(snapshotLockContext(ctx))) {
|
|
911
926
|
stopOwnershipWatcher();
|
|
@@ -934,6 +949,9 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
934
949
|
deps.onTransportAvailabilityChanged?.();
|
|
935
950
|
throw error;
|
|
936
951
|
}
|
|
952
|
+
finally {
|
|
953
|
+
startupsInFlight -= 1;
|
|
954
|
+
}
|
|
937
955
|
if (!isCurrent())
|
|
938
956
|
return false;
|
|
939
957
|
if (deps.lock.owns(ctx)) {
|
|
@@ -1075,6 +1093,8 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
1075
1093
|
return "Telegram bridge disconnected.";
|
|
1076
1094
|
},
|
|
1077
1095
|
suspend: suspendPolling,
|
|
1096
|
+
isSuspended: () => suspendedGeneration === pollingGeneration &&
|
|
1097
|
+
suspensionsInFlight === 0 && startupsInFlight === 0 && !sessionAutoStartRun && !ownershipStop,
|
|
1078
1098
|
onPersistentConflict: async (ctx, count) => {
|
|
1079
1099
|
if (activeContext === undefined || ownershipStop)
|
|
1080
1100
|
return;
|
|
@@ -14,6 +14,12 @@ export interface TelegramAgentDirResolutionInput {
|
|
|
14
14
|
* 3. Fallback: `~/.pi/agent`.
|
|
15
15
|
*/
|
|
16
16
|
export declare function resolveAgentDir(input?: TelegramAgentDirResolutionInput): string;
|
|
17
|
+
/**
|
|
18
|
+
* Pure reference preflight against an independently approved resource path.
|
|
19
|
+
* Reject aliases; never repair relative historical references or redirect storage.
|
|
20
|
+
* Equality proves spelling only, not file identity, consumer closure or migration readiness.
|
|
21
|
+
*/
|
|
22
|
+
export declare function requireTelegramStoragePathReference(path: string, expectedPath: string): string;
|
|
17
23
|
/** Telegram bridge configuration file (<agentDir>/telegram.json). */
|
|
18
24
|
export declare function resolveTelegramConfigPath(): string;
|
|
19
25
|
/** Telegram bridge temporary directory (<agentDir>/tmp/telegram). */
|
|
@@ -28,12 +34,16 @@ export declare function getTelegramDiagnosticsDisplayPaths(profileName?: string)
|
|
|
28
34
|
};
|
|
29
35
|
/** Durable Workspace admission ledger (<agentDir>/tmp/telegram/workspace-admission[.<profile>].json). */
|
|
30
36
|
export declare function resolveTelegramWorkspaceAdmissionPath(agentDir?: string, profileName?: string): string;
|
|
37
|
+
/** Profile-only callback shape; binds storage to the configured agent directory. */
|
|
38
|
+
export declare function resolveTelegramWorkspaceAdmissionPathForProfile(profileName?: string): string;
|
|
31
39
|
/** Durable inactive Thread cleanup work-set journal. */
|
|
32
40
|
export declare function resolveTelegramThreadCleanupWorkPath(agentDir?: string, profileName?: string): string;
|
|
33
41
|
/** Durable agent-authored channel post journal. */
|
|
34
42
|
export declare function resolveTelegramChannelPostJournalPath(agentDir?: string, profileName?: string): string;
|
|
35
43
|
/** Durable inbound update journal (<agentDir>/tmp/telegram/inbox[.<profile>].json). */
|
|
36
44
|
export declare function resolveTelegramUpdateJournalPath(agentDir?: string, profileName?: string): string;
|
|
45
|
+
/** Profile-only callback shape; binds storage to the configured agent directory. */
|
|
46
|
+
export declare function resolveTelegramUpdateJournalPathForProfile(profileName?: string): string;
|
|
37
47
|
/** Durable follower delivery journal, isolated by stable recipient binding. */
|
|
38
48
|
export declare function resolveTelegramFollowerJournalPath(recipientBindingKey: string, agentDir?: string, profileName?: string): string;
|
|
39
49
|
/** Runtime event log (<agentDir>/tmp/telegram/logs.jsonl). */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
-
import { join, resolve } from "node:path";
|
|
12
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
13
13
|
export const TELEGRAM_DEFAULT_PROFILE_NAME = "default";
|
|
14
14
|
/**
|
|
15
15
|
* Resolve the agent data directory for the current Pi-compatible runtime.
|
|
@@ -33,6 +33,19 @@ export function resolveAgentDir(input = {}) {
|
|
|
33
33
|
}
|
|
34
34
|
return join(homedir(), ".pi", "agent");
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Pure reference preflight against an independently approved resource path.
|
|
38
|
+
* Reject aliases; never repair relative historical references or redirect storage.
|
|
39
|
+
* Equality proves spelling only, not file identity, consumer closure or migration readiness.
|
|
40
|
+
*/
|
|
41
|
+
export function requireTelegramStoragePathReference(path, expectedPath) {
|
|
42
|
+
if (typeof path !== "string" || typeof expectedPath !== "string" ||
|
|
43
|
+
!isAbsolute(path) || resolve(path) !== path ||
|
|
44
|
+
!isAbsolute(expectedPath) || resolve(expectedPath) !== expectedPath || path !== expectedPath) {
|
|
45
|
+
throw new Error("Telegram storage reference does not match its approved absolute path.");
|
|
46
|
+
}
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
36
49
|
/** Telegram bridge configuration file (<agentDir>/telegram.json). */
|
|
37
50
|
export function resolveTelegramConfigPath() {
|
|
38
51
|
return join(resolveAgentDir(), "telegram.json");
|
|
@@ -65,6 +78,10 @@ export function getTelegramDiagnosticsDisplayPaths(profileName) {
|
|
|
65
78
|
export function resolveTelegramWorkspaceAdmissionPath(agentDir = resolveAgentDir(), profileName) {
|
|
66
79
|
return resolveTelegramProfileTempFilePath("workspace-admission", "json", agentDir, profileName);
|
|
67
80
|
}
|
|
81
|
+
/** Profile-only callback shape; binds storage to the configured agent directory. */
|
|
82
|
+
export function resolveTelegramWorkspaceAdmissionPathForProfile(profileName) {
|
|
83
|
+
return resolveTelegramWorkspaceAdmissionPath(resolveAgentDir(), profileName);
|
|
84
|
+
}
|
|
68
85
|
/** Durable inactive Thread cleanup work-set journal. */
|
|
69
86
|
export function resolveTelegramThreadCleanupWorkPath(agentDir = resolveAgentDir(), profileName) {
|
|
70
87
|
return resolveTelegramProfileTempFilePath("thread-cleanup", "json", agentDir, profileName);
|
|
@@ -77,6 +94,10 @@ export function resolveTelegramChannelPostJournalPath(agentDir = resolveAgentDir
|
|
|
77
94
|
export function resolveTelegramUpdateJournalPath(agentDir = resolveAgentDir(), profileName) {
|
|
78
95
|
return resolveTelegramProfileTempFilePath("inbox", "json", agentDir, profileName);
|
|
79
96
|
}
|
|
97
|
+
/** Profile-only callback shape; binds storage to the configured agent directory. */
|
|
98
|
+
export function resolveTelegramUpdateJournalPathForProfile(profileName) {
|
|
99
|
+
return resolveTelegramUpdateJournalPath(resolveAgentDir(), profileName);
|
|
100
|
+
}
|
|
80
101
|
/** Durable follower delivery journal, isolated by stable recipient binding. */
|
|
81
102
|
export function resolveTelegramFollowerJournalPath(recipientBindingKey, agentDir = resolveAgentDir(), profileName) {
|
|
82
103
|
if (!recipientBindingKey) {
|
|
@@ -16,8 +16,8 @@ const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_MS = 1_000;
|
|
|
16
16
|
const TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS = 3_000;
|
|
17
17
|
const TELEGRAM_POLLING_RETRY_MS = 3_000;
|
|
18
18
|
export const TELEGRAM_GET_UPDATES_GRACE_MS = 10_000;
|
|
19
|
-
// Standard Telegram DM polling does not expose ordinary message-deletion events
|
|
20
|
-
//
|
|
19
|
+
// Standard Telegram DM polling does not expose ordinary message-deletion events.
|
|
20
|
+
// Business deletions belong to a separate namespace and default routing ignores them.
|
|
21
21
|
export const TELEGRAM_ALLOWED_UPDATES = [
|
|
22
22
|
"message",
|
|
23
23
|
"edited_message",
|
|
@@ -474,7 +474,7 @@ export interface TelegramAgentEndHookRuntimeDeps<TTurn extends PendingTelegramTu
|
|
|
474
474
|
getActiveTurn: () => TTurn | undefined;
|
|
475
475
|
loadConfig?: () => Promise<void>;
|
|
476
476
|
extractAssistant: (messages: readonly TMessage[]) => TelegramAgentEndAssistantResult;
|
|
477
|
-
|
|
477
|
+
isAssistantAlreadyPublished?: (assistant: TelegramAgentEndAssistantResult) => boolean;
|
|
478
478
|
getFoldQueuedPromptsIntoHistory: () => boolean;
|
|
479
479
|
resetRuntimeState: () => void;
|
|
480
480
|
isSessionActive?: (ctx: TContext) => boolean;
|
|
@@ -869,8 +869,7 @@ export function createTelegramAgentEndHook(deps) {
|
|
|
869
869
|
return;
|
|
870
870
|
const turn = deps.getActiveTurn();
|
|
871
871
|
const extractedAssistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
|
|
872
|
-
const assistant = extractedAssistant
|
|
873
|
-
deps.isRecoveredAssistantAlreadyPublished?.(extractedAssistant)
|
|
872
|
+
const assistant = deps.isAssistantAlreadyPublished?.(extractedAssistant)
|
|
874
873
|
? { stopReason: extractedAssistant.stopReason }
|
|
875
874
|
: extractedAssistant;
|
|
876
875
|
const hasPublication = !!assistant.text || assistant.stopReason === "error" || !!turn?.queuedAttachments.length;
|
|
@@ -97,6 +97,15 @@ export interface TelegramManualThreadDisconnectDeps<TSyncState> {
|
|
|
97
97
|
export declare function markTelegramConfigSyncChange<TSyncState extends TelegramSyncState>(state: TSyncState, action: string, options?: {
|
|
98
98
|
nowMs?: number;
|
|
99
99
|
}): TSyncState;
|
|
100
|
+
export declare function createTelegramPreservedLeaderQuitHandler(deps: {
|
|
101
|
+
instanceId: string;
|
|
102
|
+
topicTargetStore: Pick<TelegramTopicTargetStore, "load" | "list" | "listPendingCleanups" | "detachTargetOwner">;
|
|
103
|
+
getCurrentLeaderEpoch: () => number | string | undefined;
|
|
104
|
+
getProfileName: () => string | undefined;
|
|
105
|
+
isPollingSuspended: () => boolean;
|
|
106
|
+
resolveAutomaticThreadCleanupEnabled: () => boolean | Promise<boolean>;
|
|
107
|
+
runWorkspaceOperation: TelegramSyncWorkspaceOperationRunner;
|
|
108
|
+
}): (isSessionCurrent: () => boolean) => (() => Promise<void>) | undefined;
|
|
100
109
|
export interface TelegramSessionRestartThreadCleanupDeps<TSyncState extends TelegramSyncState> extends Omit<TelegramManualThreadDisconnectDeps<TSyncState>, "stopPolling"> {
|
|
101
110
|
suspendPolling: () => Promise<void>;
|
|
102
111
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError } from "./telegram-api.js";
|
|
7
7
|
import { getTelegramTargetKey } from "./target.js";
|
|
8
8
|
import * as ThreadReconciler from "./thread-reconciler.js";
|
|
9
|
+
import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.js";
|
|
9
10
|
import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
|
|
10
11
|
import { createTelegramCleanupTargetProtection, commitTelegramWorkspaceProvisionBinding, getTelegramTargetFromApiBody, getTelegramThreadOwnerKey, isSameTelegramProcessInstance, isTelegramTopicTargetStaleError, normalizeTelegramWorkspacePath, provisionOwnBusTopic, } from "./threads.js";
|
|
11
12
|
export function markTelegramConfigSyncChange(state, action, options) {
|
|
@@ -24,6 +25,42 @@ export function markTelegramConfigSyncChange(state, action, options) {
|
|
|
24
25
|
});
|
|
25
26
|
return nextState;
|
|
26
27
|
}
|
|
28
|
+
export function createTelegramPreservedLeaderQuitHandler(deps) {
|
|
29
|
+
return (isSessionCurrent) => {
|
|
30
|
+
const epoch = deps.getCurrentLeaderEpoch();
|
|
31
|
+
const profile = deps.getProfileName();
|
|
32
|
+
if (epoch === undefined || !isSessionCurrent())
|
|
33
|
+
return undefined;
|
|
34
|
+
const isCurrent = () => isSessionCurrent() && deps.getCurrentLeaderEpoch() === epoch &&
|
|
35
|
+
deps.getProfileName() === profile && deps.isPollingSuspended();
|
|
36
|
+
return async () => {
|
|
37
|
+
if (!isCurrent() || await deps.resolveAutomaticThreadCleanupEnabled() !== false)
|
|
38
|
+
return;
|
|
39
|
+
if (!isCurrent())
|
|
40
|
+
return;
|
|
41
|
+
await deps.runWorkspaceOperation({
|
|
42
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
43
|
+
operationKind: "workspace.preserve-leader-quit",
|
|
44
|
+
scopes: [{ kind: "profile" }],
|
|
45
|
+
}, async () => {
|
|
46
|
+
if (!isCurrent())
|
|
47
|
+
return;
|
|
48
|
+
await deps.topicTargetStore.load();
|
|
49
|
+
if (!isCurrent())
|
|
50
|
+
return;
|
|
51
|
+
const records = deps.topicTargetStore.list().filter((record) => record.instanceId === deps.instanceId && (record.status === "active" || record.status === "starting"));
|
|
52
|
+
if (records.length !== 1)
|
|
53
|
+
return;
|
|
54
|
+
const record = records[0];
|
|
55
|
+
if (deps.topicTargetStore.listPendingCleanups().some((intent) => intent.target.chatId === record.target.chatId && intent.target.threadId === record.target.threadId))
|
|
56
|
+
return;
|
|
57
|
+
if (!await deps.topicTargetStore.detachTargetOwner(record, isCurrent) && isCurrent()) {
|
|
58
|
+
throw new Error("Telegram preserved leader detachment was not committed.");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
}
|
|
27
64
|
export function createTelegramSessionRestartThreadCleanupHandler(deps) {
|
|
28
65
|
return createTelegramManualThreadDisconnectHandler({
|
|
29
66
|
...deps,
|
|
@@ -353,10 +390,13 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
|
|
|
353
390
|
};
|
|
354
391
|
const leaderProfileKey = getTelegramThreadOwnerKey(leaderOwner);
|
|
355
392
|
const legacyLeaderRecord = deps.topicTargetStore.getByProfileKey(leaderProfileKey);
|
|
393
|
+
let capacityUnavailable = false;
|
|
356
394
|
const workspaceIdentity = normalizedLeaderCwd
|
|
357
|
-
? deps.topicTargetStore.claimWorkspaceIdentity(normalizedLeaderCwd, deps.instanceId, legacyLeaderRecord?.instanceId, { sessionId: deps.sessionId })
|
|
395
|
+
? deps.topicTargetStore.claimWorkspaceIdentity(normalizedLeaderCwd, deps.instanceId, legacyLeaderRecord?.instanceId, { sessionId: deps.sessionId, onCapacityUnavailable() { capacityUnavailable = true; } })
|
|
358
396
|
: undefined;
|
|
359
397
|
if (deps.cwd && !workspaceIdentity) {
|
|
398
|
+
if (capacityUnavailable)
|
|
399
|
+
throw new TelegramWorkspaceSlotUnavailableError();
|
|
360
400
|
throw new Error("Telegram Workspace identity is already claimed.");
|
|
361
401
|
}
|
|
362
402
|
const commitWorkspaceBinding = async (result) => {
|
|
@@ -484,6 +484,8 @@ export declare function getTelegramApiErrorRequestTarget(error: unknown): {
|
|
|
484
484
|
threadId: number;
|
|
485
485
|
} | undefined;
|
|
486
486
|
export declare function isTelegramStaleTargetHttpError(error: unknown): boolean;
|
|
487
|
+
/** Only a parsed Telegram rejection of this method proves a request had no effect. */
|
|
488
|
+
export declare function isTelegramApiRequestRejected(error: unknown, method: string): boolean;
|
|
487
489
|
export declare function isTelegramMessageNotModifiedError(error: unknown): boolean;
|
|
488
490
|
export declare function isTelegramApiMethodRetrySafe(method: string): boolean;
|
|
489
491
|
export declare function isRetryableTelegramApiError(error: unknown): boolean;
|
|
@@ -522,13 +524,19 @@ export declare function createTelegramAssistantDraftSender(deps: {
|
|
|
522
524
|
sendRichMessageDraft: TelegramBridgeApiRuntime["sendRichMessageDraft"];
|
|
523
525
|
}): TelegramBridgeApiRuntime["sendMessageDraft"];
|
|
524
526
|
export declare function buildTelegramAnswerGuestQueryBody(guestQueryId: string, text?: string, options?: TelegramAnswerGuestQueryOptions): Record<string, unknown>;
|
|
527
|
+
export type TelegramWorkspaceThreadDeletionTransport = (authorize: () => {
|
|
528
|
+
chatId: number;
|
|
529
|
+
threadId: number;
|
|
530
|
+
}) => Promise<void>;
|
|
525
531
|
export declare function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
526
532
|
getBotToken: () => string | undefined;
|
|
527
533
|
recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
|
|
528
534
|
captureRequestErrorHandler?: TelegramBridgeApiRuntimeDeps["captureRequestErrorHandler"];
|
|
529
535
|
targetActivity?: TelegramApiTargetActivityRuntime;
|
|
530
536
|
workspaceAdmission?: TelegramApiWorkspaceAdmissionPort | (() => TelegramApiWorkspaceAdmissionPort | undefined);
|
|
531
|
-
}): TelegramBridgeApiRuntime
|
|
537
|
+
}): TelegramBridgeApiRuntime & {
|
|
538
|
+
deleteWorkspaceThread: TelegramWorkspaceThreadDeletionTransport;
|
|
539
|
+
};
|
|
532
540
|
export declare function createTelegramBridgeApiRuntime(deps: TelegramBridgeApiRuntimeDeps): TelegramBridgeApiRuntime;
|
|
533
541
|
/**
|
|
534
542
|
* Creates a low-level Telegram Bot API client.
|
|
@@ -231,11 +231,13 @@ export function isTelegramApiCommitUnknownError(error) {
|
|
|
231
231
|
class TelegramApiHttpError extends Error {
|
|
232
232
|
status;
|
|
233
233
|
retryAfterSeconds;
|
|
234
|
+
rejectedRequestMethod;
|
|
234
235
|
requestTarget;
|
|
235
|
-
constructor(message, status, retryAfterSeconds) {
|
|
236
|
+
constructor(message, status, retryAfterSeconds, rejectedRequestMethod) {
|
|
236
237
|
super(message);
|
|
237
238
|
this.status = status;
|
|
238
239
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
240
|
+
this.rejectedRequestMethod = rejectedRequestMethod;
|
|
239
241
|
}
|
|
240
242
|
}
|
|
241
243
|
function attachTelegramApiRequestTarget(error, body) {
|
|
@@ -267,6 +269,11 @@ export function isTelegramStaleTargetHttpError(error) {
|
|
|
267
269
|
return false;
|
|
268
270
|
return /^Telegram API \w+ failed: HTTP 400: Bad Request: (message thread not found|thread not found|topic not found|topic deleted|topic closed|thread closed|forum topic closed|message thread closed|topic_id_invalid|topic_closed)$/i.test(error.message);
|
|
269
271
|
}
|
|
272
|
+
/** Only a parsed Telegram rejection of this method proves a request had no effect. */
|
|
273
|
+
export function isTelegramApiRequestRejected(error, method) {
|
|
274
|
+
return error instanceof TelegramApiHttpError && error.rejectedRequestMethod !== undefined &&
|
|
275
|
+
error.rejectedRequestMethod === method;
|
|
276
|
+
}
|
|
270
277
|
export function isTelegramMessageNotModifiedError(error) {
|
|
271
278
|
return (error instanceof Error && error.message.includes("message is not modified"));
|
|
272
279
|
}
|
|
@@ -398,7 +405,8 @@ async function parseTelegramApiResponse(response, method) {
|
|
|
398
405
|
const retryAfterHeader = response.headers?.get("retry-after");
|
|
399
406
|
const retryAfterSeconds = data?.parameters?.retry_after ??
|
|
400
407
|
(retryAfterHeader ? Number.parseInt(retryAfterHeader, 10) : undefined);
|
|
401
|
-
throw new TelegramApiHttpError(`Telegram API ${method} failed: ${status}${description}`, response.status, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined
|
|
408
|
+
throw new TelegramApiHttpError(`Telegram API ${method} failed: ${status}${description}`, response.status, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined, data?.ok === false && data.error_code === response.status &&
|
|
409
|
+
[400, 401, 403, 404, 429].includes(response.status) ? method : undefined);
|
|
402
410
|
}
|
|
403
411
|
if (!data) {
|
|
404
412
|
throw new TelegramApiMalformedSuccessError(method, "returned invalid JSON");
|
|
@@ -856,7 +864,7 @@ export function createDefaultTelegramBridgeApiRuntime(deps) {
|
|
|
856
864
|
},
|
|
857
865
|
})
|
|
858
866
|
: client;
|
|
859
|
-
|
|
867
|
+
const runtime = createTelegramBridgeApiRuntime({
|
|
860
868
|
client: deps.targetActivity
|
|
861
869
|
? createTelegramApiTargetTrackingClient(admittedClient, deps.targetActivity)
|
|
862
870
|
: admittedClient,
|
|
@@ -866,6 +874,18 @@ export function createDefaultTelegramBridgeApiRuntime(deps) {
|
|
|
866
874
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
867
875
|
captureRequestErrorHandler: deps.captureRequestErrorHandler,
|
|
868
876
|
});
|
|
877
|
+
return {
|
|
878
|
+
...runtime,
|
|
879
|
+
async deleteWorkspaceThread(authorize) {
|
|
880
|
+
// The exclusive deletion permit replaces ordinary target admission.
|
|
881
|
+
const target = authorize();
|
|
882
|
+
const deleted = await client.call("deleteForumTopic", {
|
|
883
|
+
chat_id: target.chatId, message_thread_id: target.threadId,
|
|
884
|
+
}, { maxAttempts: 1, retrySafety: "non-idempotent" });
|
|
885
|
+
if (deleted !== true)
|
|
886
|
+
throw new Error("Telegram Workspace Thread deletion was not confirmed.");
|
|
887
|
+
},
|
|
888
|
+
};
|
|
869
889
|
}
|
|
870
890
|
export function createTelegramBridgeApiRuntime(deps) {
|
|
871
891
|
const recoverRequestError = async (handler, error) => {
|
|
@@ -342,7 +342,9 @@ export function createTelegramThreadCleanupWorkStore(options) {
|
|
|
342
342
|
return { confirmed: true, entry: structuredClone(entry) };
|
|
343
343
|
});
|
|
344
344
|
},
|
|
345
|
-
list() {
|
|
345
|
+
list() {
|
|
346
|
+
return structuredClone(withTelegramFileTransaction(`${options.path}.transaction`, () => read().workSets));
|
|
347
|
+
},
|
|
346
348
|
};
|
|
347
349
|
}
|
|
348
350
|
export async function commitTelegramInactiveThreadCleanup(input) {
|
|
@@ -401,7 +403,10 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
|
|
|
401
403
|
if (!deps.canAdoptFence(fence))
|
|
402
404
|
return undefined;
|
|
403
405
|
try {
|
|
404
|
-
|
|
406
|
+
const adopted = deps.ledger.adoptThreadCleanupFence(fence, { owner, leaderEpoch });
|
|
407
|
+
if (adopted.destructiveKind !== "manual-thread-cleanup")
|
|
408
|
+
return undefined;
|
|
409
|
+
fence = adopted;
|
|
405
410
|
}
|
|
406
411
|
catch {
|
|
407
412
|
return undefined;
|
|
@@ -212,6 +212,8 @@ export interface TelegramTopicTargetStore {
|
|
|
212
212
|
refresh?: () => Promise<void>;
|
|
213
213
|
persist: () => Promise<void>;
|
|
214
214
|
invalidateTarget: (target: TelegramTarget, isCurrent: () => boolean, lastSyncError: string) => Promise<boolean>;
|
|
215
|
+
/** Caller proves owner detachment; this does not assert Telegram Thread absence. */
|
|
216
|
+
detachTargetOwner: (expected: TelegramTopicTargetRecord, isCurrent: () => boolean) => Promise<boolean>;
|
|
215
217
|
list: () => TelegramTopicTargetRecord[];
|
|
216
218
|
getFollowerRecoveryHintByTarget?: (target: TelegramTarget) => {
|
|
217
219
|
slot?: string;
|