@llblab/pi-kit 0.19.2 → 0.21.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 +13 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +6 -6
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +9 -0
- package/node_modules/@llblab/pi-state-flow/README.md +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/index.js +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +7 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +41 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +94 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +2 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +4 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +8 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +6 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +19 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +27 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +10 -8
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +4 -3
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +5 -5
- package/node_modules/@llblab/pi-state-flow/index.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -3
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +41 -9
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +83 -1
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +2 -4
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +10 -4
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +23 -6
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +29 -3
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/package.json +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
|
@@ -104,7 +104,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
104
104
|
WorkspaceAdmission.createTelegramWorkspaceAdmissionRuntimeBinding({
|
|
105
105
|
getProfileName: configStore.getActiveProfileName,
|
|
106
106
|
getBotToken: configStore.getBotToken,
|
|
107
|
-
getPath: Paths.
|
|
107
|
+
getPath: Paths.resolveTelegramWorkspaceAdmissionPathForProfile,
|
|
108
108
|
owner: {
|
|
109
109
|
processId: telegramProcessId,
|
|
110
110
|
processBirthId: telegramQueueProcessBirthId,
|
|
@@ -187,7 +187,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
187
187
|
},
|
|
188
188
|
getWorkspaceAdmission: workspaceAdmissionRuntime.resolve,
|
|
189
189
|
},
|
|
190
|
-
getLeaderJournalPath: Paths.
|
|
190
|
+
getLeaderJournalPath: Paths.resolveTelegramUpdateJournalPathForProfile,
|
|
191
191
|
getFollowerJournalPath(bindingKey, profileName) {
|
|
192
192
|
return Paths.resolveTelegramFollowerJournalPath(
|
|
193
193
|
bindingKey,
|
|
@@ -387,6 +387,51 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
387
387
|
: "clear";
|
|
388
388
|
},
|
|
389
389
|
});
|
|
390
|
+
const reclaimTelegramWorkspaceDeadOwnerQueue =
|
|
391
|
+
WorkspaceRetirement.createTelegramWorkspaceDeadOwnerQueueReclaimer({
|
|
392
|
+
getExternalProtection: captureWorkspaceExternalProtection,
|
|
393
|
+
getActiveTurnTarget: activeTurnRuntime.getTarget,
|
|
394
|
+
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
395
|
+
resolveLeaderJournal: resolveTelegramUpdateJournalBinding,
|
|
396
|
+
createFollowerJournalResolver:
|
|
397
|
+
telegramJournalBindingRuntime.createRecipientResolver,
|
|
398
|
+
discoverFollowerJournals() {
|
|
399
|
+
return Journal.discoverTelegramFollowerJournalPaths({
|
|
400
|
+
directory: Paths.resolveTelegramTempDir(),
|
|
401
|
+
profileName: configStore.getActiveProfileName(),
|
|
402
|
+
});
|
|
403
|
+
},
|
|
404
|
+
createJournalPathResolver: telegramJournalBindingRuntime.createPathResolver,
|
|
405
|
+
withJournalReference(binding, operation) {
|
|
406
|
+
if (!binding.recoveryKey) throw new Error(
|
|
407
|
+
"Telegram workspace journal reference identity is unavailable.",
|
|
408
|
+
);
|
|
409
|
+
return telegramJournalReferenceRegistry.withReference({
|
|
410
|
+
referenceClass: "workspace-retirement",
|
|
411
|
+
recoveryKey: binding.recoveryKey,
|
|
412
|
+
}, operation);
|
|
413
|
+
},
|
|
414
|
+
getRecoveryOwner() {
|
|
415
|
+
return {
|
|
416
|
+
instanceId: telegramInstanceId,
|
|
417
|
+
processId: telegramProcessId,
|
|
418
|
+
processBirthId: telegramQueueProcessBirthId,
|
|
419
|
+
sessionGeneration: telegramSessionContextStore.getGeneration(),
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
getQueueOwnerLiveness: Bus.getTelegramProcessLiveness,
|
|
423
|
+
onMutationError(error) {
|
|
424
|
+
recordRuntimeEvent("Telegram dead-owner queue reclamation was refused.", {
|
|
425
|
+
error: error instanceof Error ? error.message : String(error),
|
|
426
|
+
});
|
|
427
|
+
},
|
|
428
|
+
isBindingCurrent(binding) {
|
|
429
|
+
return WorkspaceRetirement.isCurrentTelegramWorkspaceBinding(
|
|
430
|
+
threadStore,
|
|
431
|
+
binding,
|
|
432
|
+
);
|
|
433
|
+
},
|
|
434
|
+
});
|
|
390
435
|
const inactiveThreadCleanupReviewRuntime =
|
|
391
436
|
ThreadCleanupManager.createTelegramInactiveThreadCleanupReviewRuntime({
|
|
392
437
|
getProfileName() { return configStore.getActiveProfileName() ?? "default"; },
|
|
@@ -1410,6 +1455,18 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1410
1455
|
getWorkspaceAdmission: workspaceAdmissionRuntime.resolve,
|
|
1411
1456
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1412
1457
|
captureWorkspaceExternalProtection,
|
|
1458
|
+
workspaceRotation: {
|
|
1459
|
+
getAdmission: workspaceAdmissionRuntime.resolve,
|
|
1460
|
+
runExclusive: telegramWorkspaceOperationRuntime.runExclusive,
|
|
1461
|
+
deleteThread: directTelegramApiRuntime.deleteWorkspaceThread,
|
|
1462
|
+
reclaimDeadOwnerQueuedWork(binding, isCurrent) {
|
|
1463
|
+
return telegramWorkspaceOperationRuntime.run({
|
|
1464
|
+
operationId: WorkspaceAdmission.createTelegramWorkspaceAdmissionOperationId(),
|
|
1465
|
+
operationKind: "workspace.reclaim-dead-owner-queue",
|
|
1466
|
+
scopes: [{ kind: "target", target: binding.target }],
|
|
1467
|
+
}, reclaimTelegramWorkspaceDeadOwnerQueue.bind(undefined, binding, isCurrent));
|
|
1468
|
+
},
|
|
1469
|
+
},
|
|
1413
1470
|
callApi(method, body, options) {
|
|
1414
1471
|
return directTelegramApiRuntime.call(method, body, options);
|
|
1415
1472
|
},
|
|
@@ -1545,6 +1602,15 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1545
1602
|
recordRuntimeEvent,
|
|
1546
1603
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1547
1604
|
});
|
|
1605
|
+
const prepareThreadPreservationOnQuit = Sync.createTelegramPreservedLeaderQuitHandler({
|
|
1606
|
+
instanceId: telegramInstanceId,
|
|
1607
|
+
topicTargetStore: threadStore,
|
|
1608
|
+
getCurrentLeaderEpoch,
|
|
1609
|
+
getProfileName: configStore.getActiveProfileName,
|
|
1610
|
+
isPollingSuspended: lockedPollingRuntime.isSuspended,
|
|
1611
|
+
resolveAutomaticThreadCleanupEnabled: configControls.resolveAutomaticThreadCleanupEnabled,
|
|
1612
|
+
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1613
|
+
});
|
|
1548
1614
|
const telegramBridgeSessionLifecycleDeps =
|
|
1549
1615
|
Lifecycle.createTelegramBridgeSessionLifecycleDeps({
|
|
1550
1616
|
contextStore: telegramSessionContextStore,
|
|
@@ -1599,6 +1665,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1599
1665
|
capabilityMonitor: telegramThreadCapabilityMonitor,
|
|
1600
1666
|
queueWatchdog: queueDispatchWatchdogRuntime,
|
|
1601
1667
|
guestPlaceholder: { stopAll: guestPlaceholderRuntime.stopAll },
|
|
1668
|
+
prepareThreadPreservationOnQuit,
|
|
1602
1669
|
},
|
|
1603
1670
|
});
|
|
1604
1671
|
const sessionLifecycleRuntime =
|
|
@@ -2428,6 +2428,7 @@ export interface TelegramUpdateJournalRuntimeBinding {
|
|
|
2428
2428
|
runtimeKey: string;
|
|
2429
2429
|
recoveryKey: string;
|
|
2430
2430
|
journal: TelegramUpdateJournalStore;
|
|
2431
|
+
readForProtection?: () => { entries: readonly TelegramUpdateJournalEntry[] };
|
|
2431
2432
|
}
|
|
2432
2433
|
|
|
2433
2434
|
export interface TelegramUpdateJournalRuntimeBindingResolverDeps {
|
|
@@ -2470,6 +2471,19 @@ export function createTelegramUpdateJournalRuntimeBindingResolver(
|
|
|
2470
2471
|
profileName,
|
|
2471
2472
|
botIdentity,
|
|
2472
2473
|
}),
|
|
2474
|
+
readForProtection() {
|
|
2475
|
+
// Protection must never turn corruption/recovery into empty-work evidence.
|
|
2476
|
+
const evidence = inspectTelegramUpdateJournalFamily({
|
|
2477
|
+
directory: dirname(path), path, profile: profileName, botIdentity,
|
|
2478
|
+
limits: {
|
|
2479
|
+
maxFiles: 1024,
|
|
2480
|
+
maxBytes: TELEGRAM_UPDATE_JOURNAL_MAX_BYTES * 2,
|
|
2481
|
+
maxEntries: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES,
|
|
2482
|
+
maxWork: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES * 1024,
|
|
2483
|
+
},
|
|
2484
|
+
});
|
|
2485
|
+
return { entries: evidence.kind === "present" ? evidence.file.entries : [] };
|
|
2486
|
+
},
|
|
2473
2487
|
journal: createTelegramUpdateJournalStore({
|
|
2474
2488
|
path,
|
|
2475
2489
|
profileName,
|
|
@@ -244,7 +244,7 @@ export function createTelegramSessionGenerationFence(
|
|
|
244
244
|
const generation = store.getGeneration();
|
|
245
245
|
if (!store.isCurrent(ctx, generation)) return;
|
|
246
246
|
await hooks.onSessionShutdown(event, ctx);
|
|
247
|
-
store.clear(ctx);
|
|
247
|
+
if (store.isCurrent(ctx, generation)) store.clear(ctx);
|
|
248
248
|
},
|
|
249
249
|
};
|
|
250
250
|
}
|
|
@@ -266,6 +266,7 @@ export interface TelegramBridgeSessionServiceRuntime {
|
|
|
266
266
|
capabilityMonitor: { start(ctx: ExtensionContext): void; stop(): void };
|
|
267
267
|
queueWatchdog: { start(ctx: ExtensionContext): void; stop(): void };
|
|
268
268
|
guestPlaceholder?: { stopAll(): void };
|
|
269
|
+
prepareThreadPreservationOnQuit?: (isSessionCurrent: () => boolean) => (() => Promise<void>) | undefined;
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
export interface TelegramBridgeSessionLifecycleAssemblyDeps<
|
|
@@ -322,6 +323,7 @@ export interface TelegramBridgeSessionLifecyclePorts<
|
|
|
322
323
|
capabilityMonitor: TelegramBridgeSessionServiceRuntime["capabilityMonitor"];
|
|
323
324
|
queueWatchdog: TelegramBridgeSessionServiceRuntime["queueWatchdog"];
|
|
324
325
|
guestPlaceholder?: TelegramBridgeSessionServiceRuntime["guestPlaceholder"];
|
|
326
|
+
prepareThreadPreservationOnQuit?: TelegramBridgeSessionServiceRuntime["prepareThreadPreservationOnQuit"];
|
|
325
327
|
};
|
|
326
328
|
}
|
|
327
329
|
|
|
@@ -350,6 +352,7 @@ export function createTelegramBridgeSessionLifecycleDeps<
|
|
|
350
352
|
capabilityMonitor: ports.services.capabilityMonitor,
|
|
351
353
|
queueWatchdog: ports.services.queueWatchdog,
|
|
352
354
|
guestPlaceholder: ports.services.guestPlaceholder,
|
|
355
|
+
prepareThreadPreservationOnQuit: ports.services.prepareThreadPreservationOnQuit,
|
|
353
356
|
},
|
|
354
357
|
};
|
|
355
358
|
}
|
|
@@ -385,15 +388,31 @@ export function createTelegramBridgeSessionLifecycleAssembly<
|
|
|
385
388
|
deps.services.queueWatchdog.start(ctx);
|
|
386
389
|
},
|
|
387
390
|
async onSessionShutdown(event, ctx) {
|
|
388
|
-
|
|
391
|
+
const generation = deps.contextStore.getGeneration();
|
|
392
|
+
const isCurrent = () => deps.contextStore.isCurrent(ctx, generation);
|
|
393
|
+
if (!isCurrent()) return;
|
|
394
|
+
let preserveThread: (() => Promise<void>) | undefined;
|
|
395
|
+
if (event.reason === "quit") {
|
|
396
|
+
try {
|
|
397
|
+
preserveThread = deps.services.prepareThreadPreservationOnQuit?.(isCurrent);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
deps.follower.recordRuntimeEvent("session", error, { phase: "preserve-thread-on-quit" });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
389
402
|
deps.services.guestPlaceholder?.stopAll();
|
|
390
403
|
await deps.services.delivery.onSessionShutdown();
|
|
391
|
-
if (!
|
|
404
|
+
if (!isCurrent()) return;
|
|
392
405
|
deps.services.queueWatchdog.stop();
|
|
393
406
|
deps.services.capabilityMonitor.stop();
|
|
394
407
|
await queueLifecycle.onSessionShutdown(event, ctx);
|
|
395
|
-
if (!
|
|
408
|
+
if (!isCurrent()) return;
|
|
396
409
|
await deps.services.inboundWorker.onSessionShutdown();
|
|
410
|
+
if (!isCurrent()) return;
|
|
411
|
+
try {
|
|
412
|
+
await preserveThread?.();
|
|
413
|
+
} catch (error) {
|
|
414
|
+
deps.follower.recordRuntimeEvent("session", error, { phase: "preserve-thread-on-quit" });
|
|
415
|
+
}
|
|
397
416
|
},
|
|
398
417
|
};
|
|
399
418
|
const followerLifecycle = appendTelegramLifecycleHooks(
|
|
@@ -739,7 +739,8 @@ export function isProcessAlive(pid: number): boolean {
|
|
|
739
739
|
process.kill(pid, 0);
|
|
740
740
|
return true;
|
|
741
741
|
} catch (error) {
|
|
742
|
-
|
|
742
|
+
// Only an absent PID proves death; permission and unexpected failures do not.
|
|
743
|
+
return (error as { code?: string }).code !== "ESRCH";
|
|
743
744
|
}
|
|
744
745
|
}
|
|
745
746
|
|
|
@@ -1126,6 +1127,7 @@ export interface TelegramLockedPollingRuntime<
|
|
|
1126
1127
|
) => Promise<TelegramLockedPollingStartResult>;
|
|
1127
1128
|
stop: () => Promise<string>;
|
|
1128
1129
|
suspend: () => Promise<void>;
|
|
1130
|
+
isSuspended: () => boolean;
|
|
1129
1131
|
onPersistentConflict: (ctx: TContext, count: number) => Promise<void>;
|
|
1130
1132
|
onSessionStart: (_event: unknown, ctx: TContext) => Promise<void>;
|
|
1131
1133
|
registerFollowerWithOwner?: (
|
|
@@ -1190,6 +1192,9 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1190
1192
|
let takeoverCandidate: TelegramLockEntry | undefined;
|
|
1191
1193
|
let sessionAutoStartRun: Promise<void> | undefined;
|
|
1192
1194
|
let pollingGeneration = 0;
|
|
1195
|
+
let suspendedGeneration: number | undefined;
|
|
1196
|
+
let suspensionsInFlight = 0;
|
|
1197
|
+
let startupsInFlight = 0;
|
|
1193
1198
|
const ownershipCheckMs =
|
|
1194
1199
|
deps.ownershipCheckMs ?? TELEGRAM_OWNERSHIP_CHECK_MS;
|
|
1195
1200
|
const ownershipRefreshMs =
|
|
@@ -1201,20 +1206,29 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1201
1206
|
ownershipRefreshInterval = undefined;
|
|
1202
1207
|
};
|
|
1203
1208
|
const suspendPolling = async () => {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
if (sessionAutoStartRun) {
|
|
1210
|
-
await sessionAutoStartRun;
|
|
1209
|
+
const generation = ++pollingGeneration;
|
|
1210
|
+
suspensionsInFlight += 1;
|
|
1211
|
+
try {
|
|
1212
|
+
activeContext = undefined;
|
|
1213
|
+
deps.transportMonitor?.stop();
|
|
1211
1214
|
deps.stopFollowerRegistration?.();
|
|
1215
|
+
stopOwnershipWatcher();
|
|
1216
|
+
if (sessionAutoStartRun) {
|
|
1217
|
+
await sessionAutoStartRun;
|
|
1218
|
+
deps.stopFollowerRegistration?.();
|
|
1219
|
+
}
|
|
1220
|
+
if (ownershipStop) {
|
|
1221
|
+
await ownershipStop;
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
await deps.stopPolling();
|
|
1225
|
+
// Unsettled starts, overlapping stops or stale completion cannot certify quiescence.
|
|
1226
|
+
if (generation === pollingGeneration && suspensionsInFlight === 1 && startupsInFlight === 0) {
|
|
1227
|
+
suspendedGeneration = generation;
|
|
1228
|
+
}
|
|
1229
|
+
} finally {
|
|
1230
|
+
suspensionsInFlight -= 1;
|
|
1212
1231
|
}
|
|
1213
|
-
if (ownershipStop) {
|
|
1214
|
-
await ownershipStop;
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
await deps.stopPolling();
|
|
1218
1232
|
};
|
|
1219
1233
|
const stopAfterOwnershipLoss = () => {
|
|
1220
1234
|
if (ownershipStop) return;
|
|
@@ -1261,6 +1275,7 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1261
1275
|
if (!isCurrent()) return false;
|
|
1262
1276
|
activeContext = ctx;
|
|
1263
1277
|
startOwnershipWatcher(ctx);
|
|
1278
|
+
startupsInFlight += 1;
|
|
1264
1279
|
try {
|
|
1265
1280
|
if (!deps.lock.refresh(snapshotLockContext(ctx))) {
|
|
1266
1281
|
stopOwnershipWatcher();
|
|
@@ -1283,6 +1298,8 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1283
1298
|
deps.lock.release();
|
|
1284
1299
|
deps.onTransportAvailabilityChanged?.();
|
|
1285
1300
|
throw error;
|
|
1301
|
+
} finally {
|
|
1302
|
+
startupsInFlight -= 1;
|
|
1286
1303
|
}
|
|
1287
1304
|
if (!isCurrent()) return false;
|
|
1288
1305
|
if (deps.lock.owns(ctx)) {
|
|
@@ -1421,6 +1438,8 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1421
1438
|
return "Telegram bridge disconnected.";
|
|
1422
1439
|
},
|
|
1423
1440
|
suspend: suspendPolling,
|
|
1441
|
+
isSuspended: () => suspendedGeneration === pollingGeneration &&
|
|
1442
|
+
suspensionsInFlight === 0 && startupsInFlight === 0 && !sessionAutoStartRun && !ownershipStop,
|
|
1424
1443
|
onPersistentConflict: async (ctx, count) => {
|
|
1425
1444
|
if (activeContext === undefined || ownershipStop) return;
|
|
1426
1445
|
if (!(deps.isContextCurrent?.(ctx) ?? activeContext === ctx)) return;
|
|
@@ -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
|
|
|
14
14
|
export const TELEGRAM_DEFAULT_PROFILE_NAME = "default";
|
|
15
15
|
|
|
@@ -43,6 +43,20 @@ export function resolveAgentDir(
|
|
|
43
43
|
return join(homedir(), ".pi", "agent");
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Pure reference preflight against an independently approved resource path.
|
|
48
|
+
* Reject aliases; never repair relative historical references or redirect storage.
|
|
49
|
+
* Equality proves spelling only, not file identity, consumer closure or migration readiness.
|
|
50
|
+
*/
|
|
51
|
+
export function requireTelegramStoragePathReference(path: string, expectedPath: string): string {
|
|
52
|
+
if (typeof path !== "string" || typeof expectedPath !== "string" ||
|
|
53
|
+
!isAbsolute(path) || resolve(path) !== path ||
|
|
54
|
+
!isAbsolute(expectedPath) || resolve(expectedPath) !== expectedPath || path !== expectedPath) {
|
|
55
|
+
throw new Error("Telegram storage reference does not match its approved absolute path.");
|
|
56
|
+
}
|
|
57
|
+
return path;
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
/** Telegram bridge configuration file (<agentDir>/telegram.json). */
|
|
47
61
|
export function resolveTelegramConfigPath(): string {
|
|
48
62
|
return join(resolveAgentDir(), "telegram.json");
|
|
@@ -100,6 +114,13 @@ export function resolveTelegramWorkspaceAdmissionPath(
|
|
|
100
114
|
);
|
|
101
115
|
}
|
|
102
116
|
|
|
117
|
+
/** Profile-only callback shape; binds storage to the configured agent directory. */
|
|
118
|
+
export function resolveTelegramWorkspaceAdmissionPathForProfile(
|
|
119
|
+
profileName?: string,
|
|
120
|
+
): string {
|
|
121
|
+
return resolveTelegramWorkspaceAdmissionPath(resolveAgentDir(), profileName);
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
/** Durable inactive Thread cleanup work-set journal. */
|
|
104
125
|
export function resolveTelegramThreadCleanupWorkPath(
|
|
105
126
|
agentDir = resolveAgentDir(),
|
|
@@ -129,6 +150,13 @@ export function resolveTelegramUpdateJournalPath(
|
|
|
129
150
|
);
|
|
130
151
|
}
|
|
131
152
|
|
|
153
|
+
/** Profile-only callback shape; binds storage to the configured agent directory. */
|
|
154
|
+
export function resolveTelegramUpdateJournalPathForProfile(
|
|
155
|
+
profileName?: string,
|
|
156
|
+
): string {
|
|
157
|
+
return resolveTelegramUpdateJournalPath(resolveAgentDir(), profileName);
|
|
158
|
+
}
|
|
159
|
+
|
|
132
160
|
/** Durable follower delivery journal, isolated by stable recipient binding. */
|
|
133
161
|
export function resolveTelegramFollowerJournalPath(
|
|
134
162
|
recipientBindingKey: string,
|
|
@@ -28,8 +28,8 @@ const TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS = 3_000;
|
|
|
28
28
|
const TELEGRAM_POLLING_RETRY_MS = 3_000;
|
|
29
29
|
export const TELEGRAM_GET_UPDATES_GRACE_MS = 10_000;
|
|
30
30
|
|
|
31
|
-
// Standard Telegram DM polling does not expose ordinary message-deletion events
|
|
32
|
-
//
|
|
31
|
+
// Standard Telegram DM polling does not expose ordinary message-deletion events.
|
|
32
|
+
// Business deletions belong to a separate namespace and default routing ignores them.
|
|
33
33
|
export const TELEGRAM_ALLOWED_UPDATES = [
|
|
34
34
|
"message",
|
|
35
35
|
"edited_message",
|
|
@@ -1615,7 +1615,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
1615
1615
|
extractAssistant: (
|
|
1616
1616
|
messages: readonly TMessage[],
|
|
1617
1617
|
) => TelegramAgentEndAssistantResult;
|
|
1618
|
-
|
|
1618
|
+
isAssistantAlreadyPublished?: (assistant: TelegramAgentEndAssistantResult) => boolean;
|
|
1619
1619
|
getFoldQueuedPromptsIntoHistory: () => boolean;
|
|
1620
1620
|
resetRuntimeState: () => void;
|
|
1621
1621
|
isSessionActive?: (ctx: TContext) => boolean;
|
|
@@ -1761,8 +1761,7 @@ export function createTelegramAgentEndHook<
|
|
|
1761
1761
|
if (deps.isSessionActive?.(ctx) === false) return;
|
|
1762
1762
|
const turn = deps.getActiveTurn();
|
|
1763
1763
|
const extractedAssistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
|
|
1764
|
-
const assistant = extractedAssistant
|
|
1765
|
-
deps.isRecoveredAssistantAlreadyPublished?.(extractedAssistant)
|
|
1764
|
+
const assistant = deps.isAssistantAlreadyPublished?.(extractedAssistant)
|
|
1766
1765
|
? { stopReason: extractedAssistant.stopReason }
|
|
1767
1766
|
: extractedAssistant;
|
|
1768
1767
|
const hasPublication = !!assistant.text || assistant.stopReason === "error" || !!turn?.queuedAttachments.length;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError } from "./telegram-api.ts";
|
|
8
8
|
import { getTelegramTargetKey, type TelegramTarget } from "./target.ts";
|
|
9
9
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
10
|
+
import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.ts";
|
|
10
11
|
import {
|
|
11
12
|
createTelegramWorkspaceAdmissionOperationId,
|
|
12
13
|
runWithTelegramWorkspaceAdmissionsAsync,
|
|
@@ -197,6 +198,47 @@ export function markTelegramConfigSyncChange<
|
|
|
197
198
|
return nextState;
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
export function createTelegramPreservedLeaderQuitHandler(deps: {
|
|
202
|
+
instanceId: string;
|
|
203
|
+
topicTargetStore: Pick<TelegramTopicTargetStore,
|
|
204
|
+
"load" | "list" | "listPendingCleanups" | "detachTargetOwner">;
|
|
205
|
+
getCurrentLeaderEpoch: () => number | string | undefined;
|
|
206
|
+
getProfileName: () => string | undefined;
|
|
207
|
+
isPollingSuspended: () => boolean;
|
|
208
|
+
resolveAutomaticThreadCleanupEnabled: () => boolean | Promise<boolean>;
|
|
209
|
+
runWorkspaceOperation: TelegramSyncWorkspaceOperationRunner;
|
|
210
|
+
}): (isSessionCurrent: () => boolean) => (() => Promise<void>) | undefined {
|
|
211
|
+
return (isSessionCurrent) => {
|
|
212
|
+
const epoch = deps.getCurrentLeaderEpoch();
|
|
213
|
+
const profile = deps.getProfileName();
|
|
214
|
+
if (epoch === undefined || !isSessionCurrent()) return undefined;
|
|
215
|
+
const isCurrent = () => isSessionCurrent() && deps.getCurrentLeaderEpoch() === epoch &&
|
|
216
|
+
deps.getProfileName() === profile && deps.isPollingSuspended();
|
|
217
|
+
return async () => {
|
|
218
|
+
if (!isCurrent() || await deps.resolveAutomaticThreadCleanupEnabled() !== false) return;
|
|
219
|
+
if (!isCurrent()) return;
|
|
220
|
+
await deps.runWorkspaceOperation({
|
|
221
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
222
|
+
operationKind: "workspace.preserve-leader-quit",
|
|
223
|
+
scopes: [{ kind: "profile" }],
|
|
224
|
+
}, async () => {
|
|
225
|
+
if (!isCurrent()) return;
|
|
226
|
+
await deps.topicTargetStore.load();
|
|
227
|
+
if (!isCurrent()) return;
|
|
228
|
+
const records = deps.topicTargetStore.list().filter((record) =>
|
|
229
|
+
record.instanceId === deps.instanceId && (record.status === "active" || record.status === "starting"));
|
|
230
|
+
if (records.length !== 1) return;
|
|
231
|
+
const record = records[0]!;
|
|
232
|
+
if (deps.topicTargetStore.listPendingCleanups().some((intent) =>
|
|
233
|
+
intent.target.chatId === record.target.chatId && intent.target.threadId === record.target.threadId)) return;
|
|
234
|
+
if (!await deps.topicTargetStore.detachTargetOwner(record, isCurrent) && isCurrent()) {
|
|
235
|
+
throw new Error("Telegram preserved leader detachment was not committed.");
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
200
242
|
export interface TelegramSessionRestartThreadCleanupDeps<
|
|
201
243
|
TSyncState extends TelegramSyncState,
|
|
202
244
|
> extends Omit<TelegramManualThreadDisconnectDeps<TSyncState>, "stopPolling"> {
|
|
@@ -650,15 +692,17 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
650
692
|
const leaderProfileKey = getTelegramThreadOwnerKey(leaderOwner);
|
|
651
693
|
const legacyLeaderRecord =
|
|
652
694
|
deps.topicTargetStore.getByProfileKey(leaderProfileKey);
|
|
695
|
+
let capacityUnavailable = false;
|
|
653
696
|
const workspaceIdentity = normalizedLeaderCwd
|
|
654
697
|
? deps.topicTargetStore.claimWorkspaceIdentity(
|
|
655
698
|
normalizedLeaderCwd,
|
|
656
699
|
deps.instanceId,
|
|
657
700
|
legacyLeaderRecord?.instanceId,
|
|
658
|
-
{ sessionId: deps.sessionId },
|
|
701
|
+
{ sessionId: deps.sessionId, onCapacityUnavailable() { capacityUnavailable = true; } },
|
|
659
702
|
)
|
|
660
703
|
: undefined;
|
|
661
704
|
if (deps.cwd && !workspaceIdentity) {
|
|
705
|
+
if (capacityUnavailable) throw new TelegramWorkspaceSlotUnavailableError();
|
|
662
706
|
throw new Error("Telegram Workspace identity is already claimed.");
|
|
663
707
|
}
|
|
664
708
|
const commitWorkspaceBinding = async (
|
|
@@ -882,15 +882,18 @@ export function isTelegramApiCommitUnknownError(
|
|
|
882
882
|
class TelegramApiHttpError extends Error {
|
|
883
883
|
readonly status: number | undefined;
|
|
884
884
|
readonly retryAfterSeconds: number | undefined;
|
|
885
|
+
readonly rejectedRequestMethod: string | undefined;
|
|
885
886
|
requestTarget?: { chatId: number; threadId: number };
|
|
886
887
|
constructor(
|
|
887
888
|
message: string,
|
|
888
889
|
status: number | undefined,
|
|
889
890
|
retryAfterSeconds: number | undefined,
|
|
891
|
+
rejectedRequestMethod?: string,
|
|
890
892
|
) {
|
|
891
893
|
super(message);
|
|
892
894
|
this.status = status;
|
|
893
895
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
896
|
+
this.rejectedRequestMethod = rejectedRequestMethod;
|
|
894
897
|
}
|
|
895
898
|
}
|
|
896
899
|
|
|
@@ -934,6 +937,12 @@ export function isTelegramStaleTargetHttpError(error: unknown): boolean {
|
|
|
934
937
|
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);
|
|
935
938
|
}
|
|
936
939
|
|
|
940
|
+
/** Only a parsed Telegram rejection of this method proves a request had no effect. */
|
|
941
|
+
export function isTelegramApiRequestRejected(error: unknown, method: string): boolean {
|
|
942
|
+
return error instanceof TelegramApiHttpError && error.rejectedRequestMethod !== undefined &&
|
|
943
|
+
error.rejectedRequestMethod === method;
|
|
944
|
+
}
|
|
945
|
+
|
|
937
946
|
export function isTelegramMessageNotModifiedError(error: unknown): boolean {
|
|
938
947
|
return (
|
|
939
948
|
error instanceof Error && error.message.includes("message is not modified")
|
|
@@ -1106,6 +1115,8 @@ async function parseTelegramApiResponse<TResponse>(
|
|
|
1106
1115
|
`Telegram API ${method} failed: ${status}${description}`,
|
|
1107
1116
|
response.status,
|
|
1108
1117
|
Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined,
|
|
1118
|
+
data?.ok === false && data.error_code === response.status &&
|
|
1119
|
+
[400, 401, 403, 404, 429].includes(response.status) ? method : undefined,
|
|
1109
1120
|
);
|
|
1110
1121
|
}
|
|
1111
1122
|
if (!data) {
|
|
@@ -1785,6 +1796,10 @@ export function buildTelegramAnswerGuestQueryBody(
|
|
|
1785
1796
|
return body;
|
|
1786
1797
|
}
|
|
1787
1798
|
|
|
1799
|
+
export type TelegramWorkspaceThreadDeletionTransport = (
|
|
1800
|
+
authorize: () => { chatId: number; threadId: number },
|
|
1801
|
+
) => Promise<void>;
|
|
1802
|
+
|
|
1788
1803
|
export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
1789
1804
|
getBotToken: () => string | undefined;
|
|
1790
1805
|
recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
|
|
@@ -1793,7 +1808,9 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
|
1793
1808
|
workspaceAdmission?:
|
|
1794
1809
|
| TelegramApiWorkspaceAdmissionPort
|
|
1795
1810
|
| (() => TelegramApiWorkspaceAdmissionPort | undefined);
|
|
1796
|
-
}): TelegramBridgeApiRuntime {
|
|
1811
|
+
}): TelegramBridgeApiRuntime & {
|
|
1812
|
+
deleteWorkspaceThread: TelegramWorkspaceThreadDeletionTransport;
|
|
1813
|
+
} {
|
|
1797
1814
|
const client = createTelegramApiClient(deps.getBotToken, {
|
|
1798
1815
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1799
1816
|
});
|
|
@@ -1807,7 +1824,7 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
|
1807
1824
|
},
|
|
1808
1825
|
})
|
|
1809
1826
|
: client;
|
|
1810
|
-
|
|
1827
|
+
const runtime = createTelegramBridgeApiRuntime({
|
|
1811
1828
|
client: deps.targetActivity
|
|
1812
1829
|
? createTelegramApiTargetTrackingClient(admittedClient, deps.targetActivity)
|
|
1813
1830
|
: admittedClient,
|
|
@@ -1817,6 +1834,17 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
|
1817
1834
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1818
1835
|
captureRequestErrorHandler: deps.captureRequestErrorHandler,
|
|
1819
1836
|
});
|
|
1837
|
+
return {
|
|
1838
|
+
...runtime,
|
|
1839
|
+
async deleteWorkspaceThread(authorize) {
|
|
1840
|
+
// The exclusive deletion permit replaces ordinary target admission.
|
|
1841
|
+
const target = authorize();
|
|
1842
|
+
const deleted = await client.call<boolean>("deleteForumTopic", {
|
|
1843
|
+
chat_id: target.chatId, message_thread_id: target.threadId,
|
|
1844
|
+
}, { maxAttempts: 1, retrySafety: "non-idempotent" });
|
|
1845
|
+
if (deleted !== true) throw new Error("Telegram Workspace Thread deletion was not confirmed.");
|
|
1846
|
+
},
|
|
1847
|
+
};
|
|
1820
1848
|
}
|
|
1821
1849
|
|
|
1822
1850
|
export function createTelegramBridgeApiRuntime(
|
|
@@ -433,7 +433,12 @@ export function createTelegramThreadCleanupWorkStore(options: {
|
|
|
433
433
|
publish(file); return { confirmed: true, entry: structuredClone(entry) };
|
|
434
434
|
});
|
|
435
435
|
},
|
|
436
|
-
list() {
|
|
436
|
+
list() {
|
|
437
|
+
return structuredClone(withTelegramFileTransaction(
|
|
438
|
+
`${options.path}.transaction`,
|
|
439
|
+
() => read().workSets,
|
|
440
|
+
));
|
|
441
|
+
},
|
|
437
442
|
};
|
|
438
443
|
}
|
|
439
444
|
|
|
@@ -542,8 +547,11 @@ export function createTelegramThreadCleanupPermitRuntime(deps: {
|
|
|
542
547
|
if (fence.leaderEpoch !== leaderEpoch || fence.owner.processId !== owner.processId ||
|
|
543
548
|
fence.owner.processBirthId !== owner.processBirthId) {
|
|
544
549
|
if (!deps.canAdoptFence(fence)) return undefined;
|
|
545
|
-
try {
|
|
546
|
-
{ owner, leaderEpoch });
|
|
550
|
+
try {
|
|
551
|
+
const adopted = deps.ledger.adoptThreadCleanupFence(fence, { owner, leaderEpoch });
|
|
552
|
+
if (adopted.destructiveKind !== "manual-thread-cleanup") return undefined;
|
|
553
|
+
fence = adopted;
|
|
554
|
+
} catch { return undefined; }
|
|
547
555
|
}
|
|
548
556
|
return deps.getLeaderEpoch() === leaderEpoch ? fence : undefined;
|
|
549
557
|
};
|