@llblab/pi-telegram 0.50.0 → 0.51.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/AGENTS.md +9 -7
- package/BACKLOG.md +13 -32
- package/CHANGELOG.md +18 -0
- package/README.md +5 -5
- package/dist/lib/bindings.d.ts +3 -5
- package/dist/lib/bindings.js +4 -11
- package/dist/lib/bus-follower.d.ts +1 -1
- package/dist/lib/bus-follower.js +93 -9
- package/dist/lib/bus-leader.d.ts +9 -3
- package/dist/lib/bus-leader.js +231 -71
- package/dist/lib/bus.d.ts +4 -0
- package/dist/lib/bus.js +34 -7
- package/dist/lib/commands.d.ts +1 -4
- package/dist/lib/commands.js +13 -33
- package/dist/lib/extension.js +64 -6
- package/dist/lib/journal.d.ts +3 -0
- package/dist/lib/journal.js +13 -0
- package/dist/lib/lifecycle.d.ts +2 -0
- package/dist/lib/lifecycle.js +25 -4
- package/dist/lib/locks.d.ts +1 -0
- package/dist/lib/locks.js +32 -12
- package/dist/lib/menu-queue.d.ts +1 -0
- package/dist/lib/menu-queue.js +72 -1
- package/dist/lib/paths.d.ts +10 -0
- package/dist/lib/paths.js +22 -1
- package/dist/lib/polling.js +2 -2
- package/dist/lib/queue.d.ts +8 -1
- package/dist/lib/queue.js +28 -2
- package/dist/lib/replies.d.ts +3 -0
- package/dist/lib/replies.js +17 -0
- package/dist/lib/sync.d.ts +9 -0
- package/dist/lib/sync.js +49 -2
- package/dist/lib/telegram-api.d.ts +9 -1
- package/dist/lib/telegram-api.js +23 -3
- package/dist/lib/thread-cleanup-manager.js +7 -2
- package/dist/lib/threads.d.ts +2 -0
- package/dist/lib/threads.js +68 -37
- package/dist/lib/updates.d.ts +6 -4
- package/dist/lib/updates.js +134 -61
- package/dist/lib/workspace-admission.d.ts +9 -0
- package/dist/lib/workspace-admission.js +49 -3
- package/dist/lib/workspace-retirement.d.ts +77 -6
- package/dist/lib/workspace-retirement.js +384 -8
- package/dist/lib/workspace-slots.d.ts +3 -0
- package/dist/lib/workspace-slots.js +6 -0
- package/dist/package.json +1 -1
- package/docs/architecture.md +23 -14
- package/docs/multi-instance-bus.md +32 -11
- package/docs/public-api.md +1 -1
- package/docs/updates.md +2 -0
- package/lib/bindings.ts +5 -17
- package/lib/bus-follower.ts +69 -12
- package/lib/bus-leader.ts +236 -94
- package/lib/bus.ts +39 -9
- package/lib/commands.ts +18 -41
- package/lib/extension.ts +70 -10
- package/lib/journal.ts +14 -0
- package/lib/lifecycle.ts +23 -4
- package/lib/locks.ts +32 -13
- package/lib/menu-queue.ts +126 -1
- package/lib/paths.ts +29 -1
- package/lib/polling.ts +2 -2
- package/lib/queue.ts +48 -3
- package/lib/replies.ts +19 -0
- package/lib/sync.ts +53 -2
- package/lib/telegram-api.ts +30 -2
- package/lib/thread-cleanup-manager.ts +11 -3
- package/lib/threads.ts +73 -36
- package/lib/updates.ts +145 -78
- package/lib/workspace-admission.ts +67 -4
- package/lib/workspace-retirement.ts +465 -7
- package/lib/workspace-slots.ts +7 -0
- package/package.json +1 -1
package/dist/lib/paths.js
CHANGED
|
@@ -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) {
|
package/dist/lib/polling.js
CHANGED
|
@@ -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",
|
package/dist/lib/queue.d.ts
CHANGED
|
@@ -242,6 +242,11 @@ export declare function removeTelegramQueueItemsByMessageIds<TContext = unknown>
|
|
|
242
242
|
removedItems: TelegramQueueItem<TContext>[];
|
|
243
243
|
removedCount: number;
|
|
244
244
|
};
|
|
245
|
+
export declare function removeTelegramQueuedGuestPromptByOrder<TContext = unknown>(items: TelegramQueueItem<TContext>[], queueOrder: number): {
|
|
246
|
+
items: TelegramQueueItem<TContext>[];
|
|
247
|
+
removedItems: PendingTelegramTurn[];
|
|
248
|
+
removedCount: number;
|
|
249
|
+
};
|
|
245
250
|
export declare function applyTelegramQueuePromptReactionDisposition<TContext = unknown>(items: TelegramQueueItem<TContext>[], messageId: number, disposition: TelegramQueueReactionDisposition, destinationLaneOrder?: number, scope?: TelegramQueueMessageScope): {
|
|
246
251
|
items: TelegramQueueItem<TContext>[];
|
|
247
252
|
changed: boolean;
|
|
@@ -469,7 +474,7 @@ export interface TelegramAgentEndHookRuntimeDeps<TTurn extends PendingTelegramTu
|
|
|
469
474
|
getActiveTurn: () => TTurn | undefined;
|
|
470
475
|
loadConfig?: () => Promise<void>;
|
|
471
476
|
extractAssistant: (messages: readonly TMessage[]) => TelegramAgentEndAssistantResult;
|
|
472
|
-
|
|
477
|
+
isAssistantAlreadyPublished?: (assistant: TelegramAgentEndAssistantResult) => boolean;
|
|
473
478
|
getFoldQueuedPromptsIntoHistory: () => boolean;
|
|
474
479
|
resetRuntimeState: () => void;
|
|
475
480
|
isSessionActive?: (ctx: TContext) => boolean;
|
|
@@ -624,6 +629,7 @@ export interface TelegramQueueMutationController<TContext> {
|
|
|
624
629
|
reorder: (ctx: TContext) => void;
|
|
625
630
|
clear: (ctx: TContext) => number;
|
|
626
631
|
removeByMessageIds: (messageIds: number[], ctx: TContext, scope?: TelegramQueueMessageScope) => number;
|
|
632
|
+
removeGuestPromptByQueueOrder?: (queueOrder: number, ctx: TContext) => boolean;
|
|
627
633
|
applyReactionByMessageId: (messageId: number, disposition: TelegramQueueReactionDisposition, ctx: TContext, scope?: TelegramQueueMessageScope) => boolean;
|
|
628
634
|
}
|
|
629
635
|
export interface TelegramControlQueueControllerDeps<TContext> {
|
|
@@ -673,6 +679,7 @@ export declare function createTelegramQueueMutationController<TContext>(deps: Te
|
|
|
673
679
|
export declare function reorderTelegramQueueItemsRuntime<TContext>(deps: TelegramQueueMutationRuntimeDeps<TContext>): void;
|
|
674
680
|
export declare function clearTelegramQueueItemsRuntime<TContext>(deps: TelegramQueueMutationRuntimeDeps<TContext>): number;
|
|
675
681
|
export declare function removeTelegramQueueItemsByMessageIdsRuntime<TContext>(messageIds: number[], deps: TelegramQueueMutationRuntimeDeps<TContext>, scope?: TelegramQueueMessageScope): number;
|
|
682
|
+
export declare function removeTelegramQueuedGuestPromptByOrderRuntime<TContext>(queueOrder: number, deps: TelegramQueueMutationRuntimeDeps<TContext>): boolean;
|
|
676
683
|
export declare function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(messageId: number, disposition: TelegramQueueReactionDisposition, deps: TelegramQueueMutationRuntimeDeps<TContext>, scope?: TelegramQueueMessageScope): boolean;
|
|
677
684
|
export declare function enqueueTelegramPromptTurnRuntime<TMessage, TContext = unknown>(messages: TMessage[], deps: TelegramPromptEnqueueRuntimeDeps<TMessage, TContext>): Promise<PendingTelegramTurn>;
|
|
678
685
|
export declare function createTelegramPromptEnqueueController<TMessage, TContext = unknown>(deps: TelegramPromptEnqueueControllerDeps<TMessage, TContext>): TelegramPromptEnqueueController<TMessage, TContext>;
|
package/dist/lib/queue.js
CHANGED
|
@@ -491,6 +491,21 @@ export function removeTelegramQueueItemsByMessageIds(items, messageIds, scope) {
|
|
|
491
491
|
removedCount: removedItems.length,
|
|
492
492
|
};
|
|
493
493
|
}
|
|
494
|
+
export function removeTelegramQueuedGuestPromptByOrder(items, queueOrder) {
|
|
495
|
+
const index = items.findIndex((item) => {
|
|
496
|
+
return isPendingTelegramTurn(item) &&
|
|
497
|
+
item.guestQueryId !== undefined &&
|
|
498
|
+
item.queueOrder === queueOrder;
|
|
499
|
+
});
|
|
500
|
+
if (index < 0)
|
|
501
|
+
return { items, removedItems: [], removedCount: 0 };
|
|
502
|
+
const removedItem = items[index];
|
|
503
|
+
return {
|
|
504
|
+
items: [...items.slice(0, index), ...items.slice(index + 1)],
|
|
505
|
+
removedItems: [removedItem],
|
|
506
|
+
removedCount: 1,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
494
509
|
export function applyTelegramQueuePromptReactionDisposition(items, messageId, disposition, destinationLaneOrder, scope) {
|
|
495
510
|
let nextItems = items;
|
|
496
511
|
for (const [index, item] of items.entries()) {
|
|
@@ -854,8 +869,7 @@ export function createTelegramAgentEndHook(deps) {
|
|
|
854
869
|
return;
|
|
855
870
|
const turn = deps.getActiveTurn();
|
|
856
871
|
const extractedAssistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
|
|
857
|
-
const assistant = extractedAssistant
|
|
858
|
-
deps.isRecoveredAssistantAlreadyPublished?.(extractedAssistant)
|
|
872
|
+
const assistant = deps.isAssistantAlreadyPublished?.(extractedAssistant)
|
|
859
873
|
? { stopReason: extractedAssistant.stopReason }
|
|
860
874
|
: extractedAssistant;
|
|
861
875
|
const hasPublication = !!assistant.text || assistant.stopReason === "error" || !!turn?.queuedAttachments.length;
|
|
@@ -1440,6 +1454,7 @@ export function createTelegramQueueMutationController(deps) {
|
|
|
1440
1454
|
reorder: (ctx) => reorderTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
|
|
1441
1455
|
clear: (ctx) => clearTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
|
|
1442
1456
|
removeByMessageIds: (messageIds, ctx, scope) => removeTelegramQueueItemsByMessageIdsRuntime(messageIds, buildRuntimeDeps(ctx), scope),
|
|
1457
|
+
removeGuestPromptByQueueOrder: (queueOrder, ctx) => removeTelegramQueuedGuestPromptByOrderRuntime(queueOrder, buildRuntimeDeps(ctx)),
|
|
1443
1458
|
applyReactionByMessageId: (messageId, disposition, ctx, scope) => applyTelegramQueuePromptReactionDispositionRuntime(messageId, disposition, buildRuntimeDeps(ctx), scope),
|
|
1444
1459
|
};
|
|
1445
1460
|
}
|
|
@@ -1492,6 +1507,17 @@ export function removeTelegramQueueItemsByMessageIdsRuntime(messageIds, deps, sc
|
|
|
1492
1507
|
updateTelegramQueueStatusRuntime(deps);
|
|
1493
1508
|
return removedCount;
|
|
1494
1509
|
}
|
|
1510
|
+
export function removeTelegramQueuedGuestPromptByOrderRuntime(queueOrder, deps) {
|
|
1511
|
+
const { items, removedItems, removedCount } = removeTelegramQueuedGuestPromptByOrder(deps.getQueuedItems(), queueOrder);
|
|
1512
|
+
if (removedCount === 0)
|
|
1513
|
+
return false;
|
|
1514
|
+
// A menu Skip means permanent discard, so settle durable admission before
|
|
1515
|
+
// removing the live item. A failed settlement keeps the prompt retryable.
|
|
1516
|
+
deps.onItemsDiscarded?.(removedItems, deps.ctx);
|
|
1517
|
+
deps.setQueuedItems(items);
|
|
1518
|
+
updateTelegramQueueStatusRuntime(deps);
|
|
1519
|
+
return true;
|
|
1520
|
+
}
|
|
1495
1521
|
export function applyTelegramQueuePromptReactionDispositionRuntime(messageId, disposition, deps, scope) {
|
|
1496
1522
|
const queuedItems = deps.getQueuedItems();
|
|
1497
1523
|
const changesLane = queuedItems.some((item) => {
|
package/dist/lib/replies.d.ts
CHANGED
|
@@ -190,6 +190,7 @@ export declare const TELEGRAM_GUEST_PLACEHOLDER_FRAME_MS = 1000;
|
|
|
190
190
|
export declare const TELEGRAM_GUEST_PLACEHOLDER_MIN_MS = 20000;
|
|
191
191
|
export declare const TELEGRAM_GUEST_PLACEHOLDER_MAX_MS = 26000;
|
|
192
192
|
export declare const TELEGRAM_GUEST_PLACEHOLDER_FRAMES: readonly ["<b>🌎 Working on it.</b>", "<b>🌍 Working on it.</b>", "<b>🌏 Working on it..</b>", "<b>🌎 Working on it..</b>", "<b>🌍 Working on it...</b>", "<b>🌏 Working on it...</b>"];
|
|
193
|
+
export declare const TELEGRAM_DISMISSED_GUEST_PLACEHOLDER_TEXT = "\u2063";
|
|
193
194
|
export declare function buildTelegramGuestPlaceholderFrame(step: number): string;
|
|
194
195
|
export interface TelegramGuestPlaceholderRuntimeDeps {
|
|
195
196
|
editGuestInlineMessage: (inlineMessageId: string, content: {
|
|
@@ -211,5 +212,7 @@ export interface TelegramGuestPlaceholderRuntime {
|
|
|
211
212
|
stop: (inlineMessageId: string) => Promise<void>;
|
|
212
213
|
/** Cancels every loop without waiting for in-flight edits (session shutdown). */
|
|
213
214
|
stopAll: () => void;
|
|
215
|
+
/** Stops rotation and visually clears the inline placeholder. */
|
|
216
|
+
dismiss: (inlineMessageId: string) => Promise<void>;
|
|
214
217
|
}
|
|
215
218
|
export declare function createTelegramGuestPlaceholderRuntime(deps: TelegramGuestPlaceholderRuntimeDeps): TelegramGuestPlaceholderRuntime;
|
package/dist/lib/replies.js
CHANGED
|
@@ -612,6 +612,9 @@ export const TELEGRAM_GUEST_PLACEHOLDER_FRAMES = [
|
|
|
612
612
|
"<b>🌍 Working on it...</b>",
|
|
613
613
|
"<b>🌏 Working on it...</b>",
|
|
614
614
|
];
|
|
615
|
+
// Telegram does not expose deletion for inline messages. This non-printing,
|
|
616
|
+
// non-empty edit visually clears a guest placeholder after its query is skipped.
|
|
617
|
+
export const TELEGRAM_DISMISSED_GUEST_PLACEHOLDER_TEXT = "\u2063";
|
|
615
618
|
export function buildTelegramGuestPlaceholderFrame(step) {
|
|
616
619
|
const frames = TELEGRAM_GUEST_PLACEHOLDER_FRAMES;
|
|
617
620
|
const index = ((step % frames.length) + frames.length) % frames.length;
|
|
@@ -734,5 +737,19 @@ export function createTelegramGuestPlaceholderRuntime(deps) {
|
|
|
734
737
|
}
|
|
735
738
|
sessions.clear();
|
|
736
739
|
},
|
|
740
|
+
async dismiss(inlineMessageId) {
|
|
741
|
+
await stop(inlineMessageId);
|
|
742
|
+
try {
|
|
743
|
+
await deps.editGuestInlineMessage(inlineMessageId, {
|
|
744
|
+
text: TELEGRAM_DISMISSED_GUEST_PLACEHOLDER_TEXT,
|
|
745
|
+
parseMode: "HTML",
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
deps.recordRuntimeEvent?.("guest", error, {
|
|
750
|
+
phase: "guest-placeholder-dismiss",
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
},
|
|
737
754
|
};
|
|
738
755
|
}
|
package/dist/lib/sync.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/lib/sync.js
CHANGED
|
@@ -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) => {
|
|
@@ -381,7 +421,14 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
|
|
|
381
421
|
assertLeaderEpoch("before-workspace-persist");
|
|
382
422
|
await deps.topicTargetStore.persist();
|
|
383
423
|
assertLeaderEpoch("after-workspace-persist");
|
|
384
|
-
|
|
424
|
+
const { target: _target, slot: _slot, threadName: _threadName, displayTitle: _displayTitle, ...rest } = result;
|
|
425
|
+
return {
|
|
426
|
+
...rest,
|
|
427
|
+
target: { ...committed.target },
|
|
428
|
+
slot: committed.slot ?? result.slot,
|
|
429
|
+
...(committed.threadName ? { threadName: committed.threadName } : {}),
|
|
430
|
+
...(committed.displayTitle ? { displayTitle: committed.displayTitle } : {}),
|
|
431
|
+
};
|
|
385
432
|
};
|
|
386
433
|
try {
|
|
387
434
|
const unavailableTargetKeys = new Set([
|
|
@@ -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.
|
package/dist/lib/telegram-api.js
CHANGED
|
@@ -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;
|
package/dist/lib/threads.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/threads.js
CHANGED
|
@@ -11,7 +11,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
11
11
|
import { isTelegramApiCommitUnknownError, TelegramApiCommitUnknownError, } from "./telegram-api.js";
|
|
12
12
|
import { withTelegramFileTransaction } from "./locks.js";
|
|
13
13
|
import * as ThreadReconciler from "./thread-reconciler.js";
|
|
14
|
-
import { planTelegramWorkspaceSlotAllocation, TELEGRAM_WORKSPACE_SLOTS, } from "./workspace-slots.js";
|
|
14
|
+
import { planTelegramWorkspaceSlotAllocation, TELEGRAM_WORKSPACE_SLOTS, TelegramWorkspaceSlotUnavailableError, } from "./workspace-slots.js";
|
|
15
15
|
import { resolveAgentDir, resolveTelegramProfileTempFilePath, } from "./paths.js";
|
|
16
16
|
const TELEGRAM_WORKSPACE_KEY_MAX_LENGTH = 180;
|
|
17
17
|
const TELEGRAM_SESSION_ID_MAX_LENGTH = 256;
|
|
@@ -1301,19 +1301,31 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1301
1301
|
dirty = true;
|
|
1302
1302
|
mutationRevision += 1;
|
|
1303
1303
|
};
|
|
1304
|
-
const
|
|
1304
|
+
const markWorkspaceBindingInactiveByTarget = (target, inactiveSinceMs = getNowMs()) => {
|
|
1305
|
+
if (!Number.isFinite(inactiveSinceMs) || inactiveSinceMs < 0)
|
|
1306
|
+
return false;
|
|
1307
|
+
for (const [key, binding] of workspaceBindings) {
|
|
1308
|
+
if (!targetMatches(binding.target, target) || binding.inactiveSinceMs !== undefined)
|
|
1309
|
+
continue;
|
|
1310
|
+
workspaceBindings.set(key, { ...binding, inactiveSinceMs });
|
|
1311
|
+
markDirty();
|
|
1312
|
+
return true;
|
|
1313
|
+
}
|
|
1314
|
+
return false;
|
|
1315
|
+
};
|
|
1316
|
+
const persistSnapshot = (transition) => {
|
|
1305
1317
|
const persist = persistQueue.then(async () => {
|
|
1306
1318
|
const path = getPath();
|
|
1307
1319
|
if (loadedPath !== path && !dirty)
|
|
1308
1320
|
resetForPath(path);
|
|
1309
1321
|
if (options.canPersist && !options.canPersist()) {
|
|
1310
|
-
if (!
|
|
1322
|
+
if (!transition)
|
|
1311
1323
|
await loadFromDisk();
|
|
1312
1324
|
return false;
|
|
1313
1325
|
}
|
|
1314
1326
|
if (!dirty || !loaded)
|
|
1315
1327
|
await loadFromDisk();
|
|
1316
|
-
if (
|
|
1328
|
+
if (transition && !transition.isCurrent())
|
|
1317
1329
|
return false;
|
|
1318
1330
|
const nowMs = getNowMs();
|
|
1319
1331
|
reservations = reservations.filter((reservation) => reservation.expiresAtMs === undefined ||
|
|
@@ -1322,6 +1334,11 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1322
1334
|
const currentRecords = Array.from(records.values())
|
|
1323
1335
|
.filter(isPersistedThreadRecord)
|
|
1324
1336
|
.map(cloneRecord);
|
|
1337
|
+
if (transition?.kind === "detach") {
|
|
1338
|
+
const owners = currentRecords.filter((record) => targetMatches(record.target, transition.target));
|
|
1339
|
+
if (owners.length !== 1 || !isDeepStrictEqual(normalizeRecord(owners[0]), transition.owner))
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1325
1342
|
records = new Map(currentRecords.map((record) => [
|
|
1326
1343
|
getRecordOwnerKey(record),
|
|
1327
1344
|
cloneRecord(record),
|
|
@@ -1358,21 +1375,34 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1358
1375
|
return serialized;
|
|
1359
1376
|
}),
|
|
1360
1377
|
};
|
|
1361
|
-
if (
|
|
1362
|
-
const record = file.threads.find((record) => targetMatches(record.target,
|
|
1378
|
+
if (transition) {
|
|
1379
|
+
const record = file.threads.find((record) => targetMatches(record.target, transition.target));
|
|
1363
1380
|
if (!record)
|
|
1364
1381
|
return false;
|
|
1382
|
+
if (transition.kind === "detach") {
|
|
1383
|
+
const bindings = file.workspaceBindings.filter((binding) => targetMatches(binding.target, record.target));
|
|
1384
|
+
if (bindings.length !== 1 || !/^[A-Z]$/.test(record.slot ?? "") ||
|
|
1385
|
+
bindings[0].slot !== record.slot)
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1365
1388
|
file.threads = file.threads.filter((candidate) => candidate !== record);
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1389
|
+
for (const binding of file.workspaceBindings) {
|
|
1390
|
+
if (targetMatches(binding.target, transition.target) && binding.inactiveSinceMs === undefined) {
|
|
1391
|
+
binding.inactiveSinceMs = nowMs;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (transition.kind === "invalidate") {
|
|
1395
|
+
file.syncObservations = file.syncObservations.filter((observation) => !targetMatches(observation.target, record.target));
|
|
1396
|
+
file.syncObservations.push({
|
|
1397
|
+
target: { ...record.target },
|
|
1398
|
+
syncStatus: "deleted",
|
|
1399
|
+
observedAtMs: nowMs,
|
|
1400
|
+
...(record.instanceId ? { instanceId: record.instanceId } : {}),
|
|
1401
|
+
...(record.slot ? { slot: record.slot } : {}),
|
|
1402
|
+
lastSyncError: transition.lastSyncError,
|
|
1403
|
+
lastReconcileAction: "mark-stale",
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1376
1406
|
}
|
|
1377
1407
|
let persistedSemanticSnapshot;
|
|
1378
1408
|
try {
|
|
@@ -1382,7 +1412,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1382
1412
|
/* missing or invalid snapshots must be replaced */
|
|
1383
1413
|
}
|
|
1384
1414
|
// Normalize optional fields to wire JSON; object key order is not a state change.
|
|
1385
|
-
if (isDeepStrictEqual(persistedSemanticSnapshot, getTelegramStateSemanticSnapshot(JSON.parse(JSON.stringify(file)))) &&
|
|
1415
|
+
if (!transition && isDeepStrictEqual(persistedSemanticSnapshot, getTelegramStateSemanticSnapshot(JSON.parse(JSON.stringify(file)))) &&
|
|
1386
1416
|
mutationRevision === persistedRevision &&
|
|
1387
1417
|
statusRevision === persistedStatusRevision) {
|
|
1388
1418
|
loadedPath = path;
|
|
@@ -1398,18 +1428,21 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1398
1428
|
});
|
|
1399
1429
|
await chmod(tempPath, 0o600);
|
|
1400
1430
|
try {
|
|
1401
|
-
if (
|
|
1431
|
+
if (transition) {
|
|
1402
1432
|
let applied = false;
|
|
1403
1433
|
const commit = () => {
|
|
1404
1434
|
if (getPath() !== path || mutationRevision !== persistedRevision ||
|
|
1405
|
-
statusRevision !== persistedStatusRevision || !
|
|
1435
|
+
statusRevision !== persistedStatusRevision || !transition.isCurrent())
|
|
1406
1436
|
return;
|
|
1407
1437
|
// Fence and rename share one synchronous commit boundary. No stale
|
|
1408
|
-
//
|
|
1438
|
+
// transition enters the live projection before durable commit.
|
|
1409
1439
|
renameSync(tempPath, path);
|
|
1410
1440
|
loadedPath = path;
|
|
1411
|
-
records = new Map(Array.from(records).filter(([, record]) => !targetMatches(record.target,
|
|
1441
|
+
records = new Map(Array.from(records).filter(([, record]) => !targetMatches(record.target, transition.target)));
|
|
1412
1442
|
syncObservations = file.syncObservations;
|
|
1443
|
+
workspaceBindings = new Map(file.workspaceBindings.map((binding) => [
|
|
1444
|
+
getWorkspaceBindingMapKey(binding), cloneWorkspaceBinding(binding),
|
|
1445
|
+
]));
|
|
1413
1446
|
mutationRevision += 1;
|
|
1414
1447
|
dirty = false;
|
|
1415
1448
|
applied = true;
|
|
@@ -1465,7 +1498,13 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1465
1498
|
await persistSnapshot();
|
|
1466
1499
|
},
|
|
1467
1500
|
invalidateTarget(target, isCurrent, lastSyncError) {
|
|
1468
|
-
return persistSnapshot({ target, isCurrent, lastSyncError });
|
|
1501
|
+
return persistSnapshot({ kind: "invalidate", target, isCurrent, lastSyncError });
|
|
1502
|
+
},
|
|
1503
|
+
detachTargetOwner(expected, isCurrent) {
|
|
1504
|
+
const owner = normalizeRecord(expected);
|
|
1505
|
+
if (!owner)
|
|
1506
|
+
return Promise.resolve(false);
|
|
1507
|
+
return persistSnapshot({ kind: "detach", owner, target: owner.target, isCurrent });
|
|
1469
1508
|
},
|
|
1470
1509
|
list() {
|
|
1471
1510
|
return Array.from(records.values()).map(cloneRecord);
|
|
@@ -1994,18 +2033,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1994
2033
|
markDirty();
|
|
1995
2034
|
return true;
|
|
1996
2035
|
},
|
|
1997
|
-
markWorkspaceBindingInactiveByTarget
|
|
1998
|
-
if (!Number.isFinite(inactiveSinceMs) || inactiveSinceMs < 0)
|
|
1999
|
-
return false;
|
|
2000
|
-
for (const [key, binding] of workspaceBindings) {
|
|
2001
|
-
if (!targetMatches(binding.target, target) || binding.inactiveSinceMs !== undefined)
|
|
2002
|
-
continue;
|
|
2003
|
-
workspaceBindings.set(key, { ...binding, inactiveSinceMs });
|
|
2004
|
-
markDirty();
|
|
2005
|
-
return true;
|
|
2006
|
-
}
|
|
2007
|
-
return false;
|
|
2008
|
-
},
|
|
2036
|
+
markWorkspaceBindingInactiveByTarget,
|
|
2009
2037
|
markWorkspaceBindingActiveByTarget(target) {
|
|
2010
2038
|
if (hasWorkspaceRetirementConflict({ target }))
|
|
2011
2039
|
return false;
|
|
@@ -2419,8 +2447,11 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2419
2447
|
const record = Array.from(records.values()).find((entry) => targetMatches(entry.target, target));
|
|
2420
2448
|
const pending = syncStatus === "deleted" ? pendingProvisions.find((entry) => entry.target && targetMatches(entry.target, target)) : undefined;
|
|
2421
2449
|
const source = record ?? pending;
|
|
2450
|
+
// Confirmed absence ends this binding's active target lifetime, even after
|
|
2451
|
+
// an earlier stale observation already removed its live record.
|
|
2452
|
+
const inactive = syncStatus === "deleted" && markWorkspaceBindingInactiveByTarget(target);
|
|
2422
2453
|
if (!source?.target)
|
|
2423
|
-
return
|
|
2454
|
+
return inactive;
|
|
2424
2455
|
syncObservations = syncObservations.filter((entry) => !targetMatches(entry.target, target));
|
|
2425
2456
|
syncObservations.push({
|
|
2426
2457
|
target: { ...source.target }, syncStatus, observedAtMs: getNowMs(),
|
|
@@ -3576,7 +3607,7 @@ export function createTelegramTopicTargetProvisioner(deps) {
|
|
|
3576
3607
|
if (existing && isCurrentThreadRecord(existing)) {
|
|
3577
3608
|
const slot = existing.slot ?? deps.store.allocateSlot(request.profileKey);
|
|
3578
3609
|
if (!slot) {
|
|
3579
|
-
throw new
|
|
3610
|
+
throw new TelegramWorkspaceSlotUnavailableError();
|
|
3580
3611
|
}
|
|
3581
3612
|
const occupied = listOccupiedTelegramThreadIdentities({
|
|
3582
3613
|
records: deps.store.list(),
|
|
@@ -3691,7 +3722,7 @@ export function createTelegramTopicTargetProvisioner(deps) {
|
|
|
3691
3722
|
(candidateThreadName ? undefined : identity?.slot) ??
|
|
3692
3723
|
preferredNameSlot), request.workspaceBindingKey);
|
|
3693
3724
|
if (!slot) {
|
|
3694
|
-
throw new
|
|
3725
|
+
throw new TelegramWorkspaceSlotUnavailableError();
|
|
3695
3726
|
}
|
|
3696
3727
|
const uniqueCandidate = candidateThreadName &&
|
|
3697
3728
|
isTelegramTopicThreadNameValidForSlot(candidateThreadName, slot) &&
|