@llblab/pi-kit 0.36.1 → 0.36.3
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 +10 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-claude-usage/README.md +2 -2
- package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +2 -3
- package/node_modules/@llblab/pi-claude-usage/package.json +1 -1
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +1 -1
- package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +2 -3
- package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +43 -5
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +45 -0
- package/node_modules/@llblab/pi-telegram/README.md +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +17 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +23 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +24 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +7 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +7 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +22 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +52 -10
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.js +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +6 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +50 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +120 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +13 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +39 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +144 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +15 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +14 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +122 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +9 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +52 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.d.ts +17 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +25 -0
- package/node_modules/@llblab/pi-telegram/dist/package.json +2 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +10 -10
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +23 -3
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +24 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +30 -13
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +25 -8
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +34 -8
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +54 -11
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +12 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +4 -3
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +79 -16
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +142 -26
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +18 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +173 -17
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +14 -8
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +166 -10
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +9 -6
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +8 -7
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +56 -2
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +44 -0
- package/node_modules/@llblab/pi-telegram/package.json +2 -1
- package/node_modules/@llblab/pi-telegram/scripts/run-macos-tests.mjs +43 -0
- package/package.json +4 -4
|
@@ -22,7 +22,14 @@ import type {
|
|
|
22
22
|
TelegramWorkspaceDeletionPermit,
|
|
23
23
|
TelegramWorkspaceDestructiveFence,
|
|
24
24
|
TelegramWorkspaceRetirementFence,
|
|
25
|
+
TelegramWorkspaceAdmissionLedger,
|
|
26
|
+
TelegramWorkspaceAdmissionOwner,
|
|
25
27
|
} from "./workspace-admission.ts";
|
|
28
|
+
import { isTelegramWorkspaceRetirementFence } from "./workspace-admission.ts";
|
|
29
|
+
import type { TelegramTopicTargetStore, TelegramWorkspaceThreadBinding } from "./threads.ts";
|
|
30
|
+
import type { TelegramWorkspaceOperationRunner } from "./workspace-retirement.ts";
|
|
31
|
+
import type { TelegramWorkspaceThreadDeletionTransport } from "./telegram-api.ts";
|
|
32
|
+
import type { TelegramProcessLiveness } from "./process-identity.ts";
|
|
26
33
|
|
|
27
34
|
export interface TelegramThreadCleanupBindingSnapshot {
|
|
28
35
|
cwd: string;
|
|
@@ -56,7 +63,8 @@ export interface TelegramThreadCleanupCandidate {
|
|
|
56
63
|
sessionId?: string;
|
|
57
64
|
sessionKey?: string;
|
|
58
65
|
instanceSlot: string;
|
|
59
|
-
|
|
66
|
+
/** Null is explicit absence in reviewed work; it never denotes a reusable letter. */
|
|
67
|
+
slot: string | null;
|
|
60
68
|
target: { chatId: number; threadId: number };
|
|
61
69
|
inactiveSinceMs: number;
|
|
62
70
|
bindingUpdatedAtMs: number;
|
|
@@ -116,8 +124,7 @@ export function planTelegramInactiveThreadCleanup(input: {
|
|
|
116
124
|
!binding.cwd ||
|
|
117
125
|
!binding.workspaceKey ||
|
|
118
126
|
!binding.instanceSlot ||
|
|
119
|
-
typeof slot !== "string" ||
|
|
120
|
-
!slot ||
|
|
127
|
+
(slot !== undefined && (typeof slot !== "string" || !/^[A-Z]$/u.test(slot))) ||
|
|
121
128
|
!validSessionIdentity(binding) ||
|
|
122
129
|
!validTarget(binding.target) ||
|
|
123
130
|
bindingKeys.has(binding.bindingKey) ||
|
|
@@ -152,8 +159,6 @@ export function planTelegramInactiveThreadCleanup(input: {
|
|
|
152
159
|
const inactiveSinceMs = binding.inactiveSinceMs;
|
|
153
160
|
const slot = binding.slot;
|
|
154
161
|
if (
|
|
155
|
-
typeof slot !== "string" ||
|
|
156
|
-
!slot ||
|
|
157
162
|
!Number.isSafeInteger(inactiveSinceMs) ||
|
|
158
163
|
inactiveSinceMs === undefined ||
|
|
159
164
|
inactiveSinceMs < 0 ||
|
|
@@ -180,7 +185,7 @@ export function planTelegramInactiveThreadCleanup(input: {
|
|
|
180
185
|
? { sessionId: binding.sessionId, sessionKey: binding.sessionKey }
|
|
181
186
|
: {}),
|
|
182
187
|
instanceSlot: binding.instanceSlot,
|
|
183
|
-
slot,
|
|
188
|
+
slot: slot ?? null,
|
|
184
189
|
target: { ...binding.target },
|
|
185
190
|
inactiveSinceMs,
|
|
186
191
|
bindingUpdatedAtMs: binding.updatedAtMs,
|
|
@@ -409,8 +414,7 @@ function validateWorkSet(
|
|
|
409
414
|
!validSessionIdentity(raw) ||
|
|
410
415
|
typeof raw.instanceSlot !== "string" ||
|
|
411
416
|
!raw.instanceSlot ||
|
|
412
|
-
typeof raw.slot !== "string" ||
|
|
413
|
-
!raw.slot ||
|
|
417
|
+
(raw.slot !== null && (typeof raw.slot !== "string" || !/^[A-Z]$/u.test(raw.slot))) ||
|
|
414
418
|
!isObject(raw.target) ||
|
|
415
419
|
!onlyKeys(raw.target, ["chatId", "threadId"]) ||
|
|
416
420
|
!validTarget(
|
|
@@ -509,7 +513,11 @@ function sameCandidates(
|
|
|
509
513
|
left: readonly TelegramThreadCleanupCandidate[],
|
|
510
514
|
right: readonly TelegramThreadCleanupCandidate[],
|
|
511
515
|
): boolean {
|
|
512
|
-
|
|
516
|
+
const keys = ["profileName", "bindingKey", "cwd", "workspaceKey", "sessionId", "sessionKey", "instanceSlot", "slot", "inactiveSinceMs", "bindingUpdatedAtMs"] as const;
|
|
517
|
+
return left.length === right.length && left.every((candidate, index) => {
|
|
518
|
+
const other = right[index]!;
|
|
519
|
+
return keys.every(key => candidate[key] === other[key]) && targetKey(candidate.target) === targetKey(other.target);
|
|
520
|
+
});
|
|
513
521
|
}
|
|
514
522
|
|
|
515
523
|
function parseTelegramThreadCleanupWorkFile(
|
|
@@ -544,6 +552,8 @@ export function createTelegramThreadCleanupWorkStore(options: {
|
|
|
544
552
|
runtimeDir?: string;
|
|
545
553
|
profileName: string;
|
|
546
554
|
tokenSha256: string;
|
|
555
|
+
/** Exact captured ledger identity; standalone fixtures may use the plain profile identity. */
|
|
556
|
+
admissionProfileKey?: string;
|
|
547
557
|
maxWorkSets?: number;
|
|
548
558
|
maxBytes?: number;
|
|
549
559
|
getNowMs?: () => number;
|
|
@@ -680,12 +690,12 @@ export function createTelegramThreadCleanupWorkStore(options: {
|
|
|
680
690
|
!entry ||
|
|
681
691
|
entry.bindingUpdatedAtMs !== input.bindingUpdatedAtMs ||
|
|
682
692
|
permit.destructiveKind !== "manual-thread-cleanup" ||
|
|
683
|
-
permit.profileKey !== entry.profileName ||
|
|
693
|
+
permit.profileKey !== (options.admissionProfileKey ?? entry.profileName) ||
|
|
684
694
|
permit.bindingKey !== entry.bindingKey ||
|
|
685
695
|
permit.slot !== entry.slot ||
|
|
686
696
|
targetKey(permit.target) !== targetKey(entry.target) ||
|
|
687
697
|
!permit.operationId ||
|
|
688
|
-
|
|
698
|
+
permit.retirementIntentId !== workSet.operationId ||
|
|
689
699
|
!safeTime(permit.issuedAtMs)
|
|
690
700
|
)
|
|
691
701
|
throw new Error(
|
|
@@ -842,12 +852,13 @@ export async function executeTelegramInactiveThreadCleanup(input: {
|
|
|
842
852
|
|
|
843
853
|
export function createTelegramThreadCleanupPermitRuntime(deps: {
|
|
844
854
|
ledger: {
|
|
855
|
+
getProfileKey(): string;
|
|
845
856
|
read(): { fence?: TelegramWorkspaceDestructiveFence };
|
|
846
857
|
acquireThreadCleanupFence(input: {
|
|
847
858
|
operationId: string;
|
|
848
859
|
cleanupWorkSetId: string;
|
|
849
860
|
bindingKey: string;
|
|
850
|
-
slot: string;
|
|
861
|
+
slot: string | null;
|
|
851
862
|
target: { chatId: number; threadId: number };
|
|
852
863
|
leaderEpoch: number | string;
|
|
853
864
|
cleanupRequestedAtMs: number;
|
|
@@ -930,7 +941,7 @@ export function createTelegramThreadCleanupPermitRuntime(deps: {
|
|
|
930
941
|
const fence = deps.ledger.read().fence;
|
|
931
942
|
return fence?.operationId === operationId &&
|
|
932
943
|
fence.destructiveKind === "manual-thread-cleanup" &&
|
|
933
|
-
fence.profileKey ===
|
|
944
|
+
fence.profileKey === deps.ledger.getProfileKey() &&
|
|
934
945
|
fence.retirementIntentId === workSetId &&
|
|
935
946
|
fence.bindingKey === candidate.bindingKey &&
|
|
936
947
|
fence.slot === candidate.slot &&
|
|
@@ -1045,8 +1056,17 @@ export function createTelegramThreadCleanupPermitRuntime(deps: {
|
|
|
1045
1056
|
: { kind: "already-issued" };
|
|
1046
1057
|
},
|
|
1047
1058
|
async settleDeleted(fence, commit) {
|
|
1059
|
+
const assertAuthority = () => {
|
|
1060
|
+
const owner = deps.getOwner();
|
|
1061
|
+
if (deps.getLeaderEpoch() !== fence.leaderEpoch ||
|
|
1062
|
+
owner.processId !== fence.owner.processId || owner.processBirthId !== fence.owner.processBirthId)
|
|
1063
|
+
throw new Error("Telegram Thread cleanup settlement authority changed.");
|
|
1064
|
+
};
|
|
1065
|
+
assertAuthority();
|
|
1048
1066
|
const ready = deps.ledger.confirmThreadCleanupAbsence(fence);
|
|
1049
|
-
|
|
1067
|
+
const committed = await commit();
|
|
1068
|
+
assertAuthority();
|
|
1069
|
+
if (!committed) return "commit-pending";
|
|
1050
1070
|
deps.ledger.completeThreadCleanupFence(ready);
|
|
1051
1071
|
return "completed";
|
|
1052
1072
|
},
|
|
@@ -1069,6 +1089,138 @@ export interface TelegramInactiveThreadCleanupCoordinatorDeps<TBinding> {
|
|
|
1069
1089
|
): Promise<boolean>;
|
|
1070
1090
|
}
|
|
1071
1091
|
|
|
1092
|
+
export interface TelegramInactiveThreadCleanupAuthority {
|
|
1093
|
+
profileName: string;
|
|
1094
|
+
leaderEpoch: number | string;
|
|
1095
|
+
ledger: TelegramWorkspaceAdmissionLedger;
|
|
1096
|
+
isCurrent(): boolean;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/** Review owns ordinary admission; confirmed cleanup uses the shared gate and its exclusive manual fence instead. */
|
|
1100
|
+
export function createTelegramInactiveThreadCleanupRuntime(deps: {
|
|
1101
|
+
store: Pick<TelegramTopicTargetStore, "load" | "listWorkspaceBindings" | "listReservations" | "listPendingProvisions" | "listPendingCleanups" | "commitInactiveWorkspaceCleanup">;
|
|
1102
|
+
captureAuthority(): TelegramInactiveThreadCleanupAuthority | undefined;
|
|
1103
|
+
getWorkStore(authority: TelegramInactiveThreadCleanupAuthority): TelegramThreadCleanupWorkStore;
|
|
1104
|
+
getProtection(binding: TelegramWorkspaceThreadBinding): {
|
|
1105
|
+
liveOwner: TelegramThreadCleanupProtectionState;
|
|
1106
|
+
acceptedWork: TelegramThreadCleanupProtectionState;
|
|
1107
|
+
deliveryAuthority: TelegramThreadCleanupProtectionState;
|
|
1108
|
+
};
|
|
1109
|
+
getProcessLiveness(owner: TelegramWorkspaceAdmissionOwner): TelegramProcessLiveness;
|
|
1110
|
+
runExclusive<T>(operation: () => Promise<T>): Promise<T>;
|
|
1111
|
+
runWorkspaceOperation: TelegramWorkspaceOperationRunner;
|
|
1112
|
+
deleteThread: TelegramWorkspaceThreadDeletionTransport;
|
|
1113
|
+
onError?: (error: unknown) => void;
|
|
1114
|
+
}): {
|
|
1115
|
+
review(): Promise<{ count: number; operationId?: string }>;
|
|
1116
|
+
clean(operationId: string): Promise<TelegramInactiveThreadCleanupResult>;
|
|
1117
|
+
} {
|
|
1118
|
+
const capture = () => {
|
|
1119
|
+
const authority = deps.captureAuthority();
|
|
1120
|
+
if (!authority || !authority.isCurrent())
|
|
1121
|
+
throw new Error("Inactive Thread cleanup requires current leader authority.");
|
|
1122
|
+
const ledgerKey = authority.ledger.getProfileKey();
|
|
1123
|
+
const owner = authority.ledger.getOwner();
|
|
1124
|
+
const assertCurrent = () => {
|
|
1125
|
+
if (!authority.isCurrent() || authority.ledger.getProfileKey() !== ledgerKey)
|
|
1126
|
+
throw new Error("Inactive Thread cleanup authority changed.");
|
|
1127
|
+
};
|
|
1128
|
+
const evidence = () => {
|
|
1129
|
+
assertCurrent();
|
|
1130
|
+
return captureTelegramInactiveThreadCleanupEvidence({
|
|
1131
|
+
profileName: authority.profileName,
|
|
1132
|
+
listBindings: deps.store.listWorkspaceBindings,
|
|
1133
|
+
getProtection: deps.getProtection,
|
|
1134
|
+
listReservations: deps.store.listReservations,
|
|
1135
|
+
listPendingProvisions: deps.store.listPendingProvisions,
|
|
1136
|
+
listPendingCleanups: deps.store.listPendingCleanups,
|
|
1137
|
+
});
|
|
1138
|
+
};
|
|
1139
|
+
return { authority, owner, assertCurrent, evidence };
|
|
1140
|
+
};
|
|
1141
|
+
return {
|
|
1142
|
+
async review() {
|
|
1143
|
+
const scope = capture();
|
|
1144
|
+
return createTelegramInactiveThreadCleanupReviewRuntime({
|
|
1145
|
+
getProfileName: () => scope.authority.profileName,
|
|
1146
|
+
listBindings: deps.store.listWorkspaceBindings,
|
|
1147
|
+
getProtection: deps.getProtection,
|
|
1148
|
+
listReservations: deps.store.listReservations,
|
|
1149
|
+
listPendingProvisions: deps.store.listPendingProvisions,
|
|
1150
|
+
listPendingCleanups: deps.store.listPendingCleanups,
|
|
1151
|
+
getWorkStore() { scope.assertCurrent(); return deps.getWorkStore(scope.authority); },
|
|
1152
|
+
runWorkspaceOperation(metadata, operation) {
|
|
1153
|
+
return deps.runWorkspaceOperation(metadata, async () => {
|
|
1154
|
+
scope.assertCurrent();
|
|
1155
|
+
await deps.store.load();
|
|
1156
|
+
scope.assertCurrent();
|
|
1157
|
+
return operation();
|
|
1158
|
+
});
|
|
1159
|
+
},
|
|
1160
|
+
}).review();
|
|
1161
|
+
},
|
|
1162
|
+
async clean(operationId) {
|
|
1163
|
+
const scope = capture();
|
|
1164
|
+
return deps.runExclusive(async () => {
|
|
1165
|
+
scope.assertCurrent();
|
|
1166
|
+
const { authority } = scope;
|
|
1167
|
+
const work = deps.getWorkStore(authority);
|
|
1168
|
+
const exactBinding = (candidate: TelegramThreadCleanupCandidate) => {
|
|
1169
|
+
const fresh = planTelegramInactiveThreadCleanup(scope.evidence()).find(binding => binding.bindingKey === candidate.bindingKey);
|
|
1170
|
+
if (!fresh || !sameCandidates([fresh], [candidate])) return undefined;
|
|
1171
|
+
return deps.store.listWorkspaceBindings().find(binding => binding.bindingKey === fresh.bindingKey);
|
|
1172
|
+
};
|
|
1173
|
+
const loadExactBinding = async (candidate: TelegramThreadCleanupCandidate) => {
|
|
1174
|
+
scope.assertCurrent();
|
|
1175
|
+
await deps.store.load();
|
|
1176
|
+
scope.assertCurrent();
|
|
1177
|
+
return exactBinding(candidate);
|
|
1178
|
+
};
|
|
1179
|
+
const permitRuntime = createTelegramThreadCleanupPermitRuntime({
|
|
1180
|
+
ledger: authority.ledger,
|
|
1181
|
+
getLeaderEpoch: () => authority.isCurrent() ? authority.leaderEpoch : undefined,
|
|
1182
|
+
getProfileName: () => authority.profileName,
|
|
1183
|
+
getOwner: () => scope.owner,
|
|
1184
|
+
canAdoptFence: fence => authority.isCurrent() && deps.getProcessLiveness(fence.owner) === "dead",
|
|
1185
|
+
revalidateUnderFence: async candidate => !!(await loadExactBinding(candidate)),
|
|
1186
|
+
});
|
|
1187
|
+
return cleanReviewedInactiveThreads({
|
|
1188
|
+
store: work, operationId, permitRuntime,
|
|
1189
|
+
resolveFullBinding: loadExactBinding,
|
|
1190
|
+
async deleteWithPermit(permit, candidate) {
|
|
1191
|
+
try {
|
|
1192
|
+
await deps.deleteThread(() => {
|
|
1193
|
+
scope.assertCurrent();
|
|
1194
|
+
const fence = authority.ledger.read().fence;
|
|
1195
|
+
if (!fence || !isTelegramWorkspaceRetirementFence(fence) ||
|
|
1196
|
+
fence.destructiveKind !== "manual-thread-cleanup" || fence.phase !== "deletion-issued" ||
|
|
1197
|
+
fence.operationId !== permit.operationId || fence.retirementIntentId !== operationId ||
|
|
1198
|
+
fence.profileKey !== permit.profileKey || fence.bindingKey !== candidate.bindingKey ||
|
|
1199
|
+
fence.slot !== candidate.slot || fence.deletionIssuedAtMs !== permit.issuedAtMs ||
|
|
1200
|
+
fence.leaderEpoch !== authority.leaderEpoch || fence.owner.processId !== scope.owner.processId ||
|
|
1201
|
+
fence.owner.processBirthId !== scope.owner.processBirthId ||
|
|
1202
|
+
targetKey(fence.target) !== targetKey(permit.target) || !exactBinding(candidate))
|
|
1203
|
+
throw new Error("Inactive Thread cleanup deletion permit is stale.");
|
|
1204
|
+
return { ...permit.target };
|
|
1205
|
+
});
|
|
1206
|
+
scope.assertCurrent();
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
deps.onError?.(error);
|
|
1209
|
+
throw error;
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1212
|
+
async commitBinding(candidate) {
|
|
1213
|
+
scope.assertCurrent();
|
|
1214
|
+
await deps.store.load();
|
|
1215
|
+
scope.assertCurrent();
|
|
1216
|
+
return deps.store.commitInactiveWorkspaceCleanup(candidate, authority.isCurrent);
|
|
1217
|
+
},
|
|
1218
|
+
});
|
|
1219
|
+
});
|
|
1220
|
+
},
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1072
1224
|
export function createTelegramInactiveThreadCleanupSettingsPort<TBinding>(
|
|
1073
1225
|
deps: TelegramInactiveThreadCleanupCoordinatorDeps<TBinding>,
|
|
1074
1226
|
): (operationId: string) => Promise<TelegramInactiveThreadCleanupResult> {
|
|
@@ -1105,9 +1257,13 @@ export async function cleanReviewedInactiveThreads<TBinding>(
|
|
|
1105
1257
|
entry,
|
|
1106
1258
|
input.operationId,
|
|
1107
1259
|
);
|
|
1108
|
-
if (ready)
|
|
1109
|
-
await input.permitRuntime.settleDeleted(ready, async () => true);
|
|
1110
|
-
|
|
1260
|
+
if (ready) {
|
|
1261
|
+
const settlement = await input.permitRuntime.settleDeleted(ready, async () => true);
|
|
1262
|
+
if (settlement === "completed") deleted += 1;
|
|
1263
|
+
else outcomeUnknown += 1;
|
|
1264
|
+
} else if (input.permitRuntime.diagnoseRecovery(entry, input.operationId) !== "none") {
|
|
1265
|
+
outcomeUnknown += 1;
|
|
1266
|
+
} else deleted += 1;
|
|
1111
1267
|
continue;
|
|
1112
1268
|
}
|
|
1113
1269
|
if (entry.state === "outcome-unknown") {
|
|
@@ -174,8 +174,12 @@ export function resolveTelegramWorkspaceDisplayNames(
|
|
|
174
174
|
: undefined;
|
|
175
175
|
if (manualName) {
|
|
176
176
|
labels.set(binding.bindingKey, boundedLabel(manualName));
|
|
177
|
+
} else if (!slot) {
|
|
178
|
+
// A displaced binding retains its generated identity, not the letter now owned elsewhere.
|
|
179
|
+
const name = binding.threadName ? labelText(binding.threadName) : undefined;
|
|
180
|
+
if (name) labels.set(binding.bindingKey, boundedLabel(name));
|
|
177
181
|
} else if (mode === "letters") {
|
|
178
|
-
|
|
182
|
+
labels.set(binding.bindingKey, slot);
|
|
179
183
|
} else if (mode === "names") {
|
|
180
184
|
const name = binding.threadName ? labelText(binding.threadName) : slot;
|
|
181
185
|
if (name) labels.set(binding.bindingKey, boundedLabel(name));
|
|
@@ -191,12 +195,11 @@ export function resolveTelegramWorkspaceDisplayNames(
|
|
|
191
195
|
bindings.filter((candidate) => candidate.cwd === binding.cwd)
|
|
192
196
|
.length > 1
|
|
193
197
|
: (liveDirectoryCounts.get(binding.cwd) ?? 0) > 1;
|
|
194
|
-
if (showSuffix && !slot) continue;
|
|
195
198
|
const suffix = !showSuffix
|
|
196
199
|
? ""
|
|
197
200
|
: mode === "directory-title"
|
|
198
201
|
? ` ${slot}`
|
|
199
|
-
: `_${slot
|
|
202
|
+
: `_${slot.toLowerCase()}`;
|
|
200
203
|
labels.set(binding.bindingKey, boundedLabel(base, suffix));
|
|
201
204
|
}
|
|
202
205
|
}
|
|
@@ -378,9 +381,9 @@ export function createTelegramThreadDisplaySettingsRuntime(deps: {
|
|
|
378
381
|
/** Caller owns triggering and reporting; no timer or background retry is created. */
|
|
379
382
|
export function createTelegramThreadDisplayReconciler(
|
|
380
383
|
deps: TelegramThreadDisplayReconcilerDeps,
|
|
381
|
-
): { reconcile(): Promise<{ changed: number }> } {
|
|
384
|
+
): { reconcile(bindingKey?: string): Promise<{ changed: number }> } {
|
|
382
385
|
let tail: Promise<unknown> = Promise.resolve();
|
|
383
|
-
const apply = async (): Promise<{ changed: number }> => {
|
|
386
|
+
const apply = async (bindingKey?: string): Promise<{ changed: number }> => {
|
|
384
387
|
const epoch = deps.getLeaderEpoch();
|
|
385
388
|
const profile = deps.getProfileKey();
|
|
386
389
|
const mode = deps.getMode();
|
|
@@ -406,6 +409,7 @@ export function createTelegramThreadDisplayReconciler(
|
|
|
406
409
|
);
|
|
407
410
|
let changed = 0;
|
|
408
411
|
for (const binding of bindings) {
|
|
412
|
+
if (bindingKey !== undefined && binding.bindingKey !== bindingKey) continue;
|
|
409
413
|
const isBindingCurrent = deps.captureBindingAuthority(binding);
|
|
410
414
|
if (!isBindingCurrent) continue;
|
|
411
415
|
const title = titles.get(binding.bindingKey);
|
|
@@ -426,7 +430,7 @@ export function createTelegramThreadDisplayReconciler(
|
|
|
426
430
|
};
|
|
427
431
|
assertBinding(binding);
|
|
428
432
|
if ((binding.displayTitle ?? binding.threadName) === title) continue;
|
|
429
|
-
await deps.callApi(
|
|
433
|
+
const acknowledged = await deps.callApi<boolean>(
|
|
430
434
|
"editForumTopic",
|
|
431
435
|
{
|
|
432
436
|
chat_id: binding.target.chatId,
|
|
@@ -436,6 +440,8 @@ export function createTelegramThreadDisplayReconciler(
|
|
|
436
440
|
{ maxAttempts: 1 },
|
|
437
441
|
);
|
|
438
442
|
assertBinding(binding);
|
|
443
|
+
if (acknowledged !== true)
|
|
444
|
+
throw new Error("Telegram Thread display update was not acknowledged.");
|
|
439
445
|
if (!deps.store.setWorkspaceDisplayTitle(binding, title)) {
|
|
440
446
|
throw new Error(
|
|
441
447
|
"Telegram Thread display binding changed before title commit.",
|
|
@@ -452,8 +458,8 @@ export function createTelegramThreadDisplayReconciler(
|
|
|
452
458
|
return { changed };
|
|
453
459
|
};
|
|
454
460
|
return {
|
|
455
|
-
reconcile() {
|
|
456
|
-
const run = tail.then(apply);
|
|
461
|
+
reconcile(bindingKey) {
|
|
462
|
+
const run = tail.then(() => apply(bindingKey));
|
|
457
463
|
tail = run.catch(() => undefined);
|
|
458
464
|
return run;
|
|
459
465
|
},
|
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
} from "./workspace-identity.ts";
|
|
75
75
|
import {
|
|
76
76
|
planTelegramWorkspaceSlotAllocation,
|
|
77
|
+
selectTelegramWorkspaceReusableSlot,
|
|
77
78
|
TELEGRAM_WORKSPACE_SLOTS,
|
|
78
79
|
TelegramWorkspaceSlotUnavailableError,
|
|
79
80
|
type TelegramWorkspaceSlotOccupancy,
|
|
@@ -873,7 +874,7 @@ export interface TelegramTopicTargetStore {
|
|
|
873
874
|
sessionId?: string;
|
|
874
875
|
sessionKey?: string;
|
|
875
876
|
instanceSlot: string;
|
|
876
|
-
slot: string;
|
|
877
|
+
slot: string | null;
|
|
877
878
|
bindingKey: string;
|
|
878
879
|
target: { chatId: number; threadId: number };
|
|
879
880
|
inactiveSinceMs: number;
|
|
@@ -886,6 +887,19 @@ export interface TelegramTopicTargetStore {
|
|
|
886
887
|
getExternalProtection: TelegramWorkspaceExternalProtectionCapture,
|
|
887
888
|
options?: { expectedRetirement?: TelegramWorkspaceRetirementIntent },
|
|
888
889
|
) => TelegramWorkspaceSlotOccupancySnapshot;
|
|
890
|
+
/**
|
|
891
|
+
* Fallback for a full A-Z pool: strips the display letter from the least recently used
|
|
892
|
+
* binding (and its current Thread record) and persists, so a fresh allocation can take it.
|
|
893
|
+
* Metadata only: the Thread, journals and accepted work stay untouched, and a binding or
|
|
894
|
+
* letter held by a claim, reservation, provision, cleanup or retirement is never chosen.
|
|
895
|
+
*/
|
|
896
|
+
reuseLeastRecentlyUsedWorkspaceSlot: (
|
|
897
|
+
isCurrent: () => boolean,
|
|
898
|
+
) => Promise<
|
|
899
|
+
| { kind: "free" }
|
|
900
|
+
| { kind: "reused"; bindingKey: string; slot: string }
|
|
901
|
+
| undefined
|
|
902
|
+
>;
|
|
889
903
|
hasWorkspaceBinding: (cwd: string, sessionId?: string) => boolean;
|
|
890
904
|
setWorkspaceDisplayTitle: (
|
|
891
905
|
expected: TelegramWorkspaceThreadBinding,
|
|
@@ -3556,8 +3570,6 @@ export function createTelegramTopicTargetStore(
|
|
|
3556
3570
|
>,
|
|
3557
3571
|
nowMs: number,
|
|
3558
3572
|
options: {
|
|
3559
|
-
/** Slot equality only counts when the binding itself holds a slot. */
|
|
3560
|
-
requireBindingSlot?: boolean;
|
|
3561
3573
|
isRetirementExcluded?: (
|
|
3562
3574
|
intent: TelegramWorkspaceRetirementIntent,
|
|
3563
3575
|
index: number,
|
|
@@ -3565,7 +3577,7 @@ export function createTelegramTopicTargetStore(
|
|
|
3565
3577
|
} = {},
|
|
3566
3578
|
): boolean => {
|
|
3567
3579
|
const slotMatches = (slot: string | undefined): boolean =>
|
|
3568
|
-
|
|
3580
|
+
!!binding.slot && slot === binding.slot;
|
|
3569
3581
|
const targetOrSlotMatches = (candidate: {
|
|
3570
3582
|
target?: TelegramTarget;
|
|
3571
3583
|
slot?: string;
|
|
@@ -7272,7 +7284,7 @@ export function createTelegramTopicTargetStore(
|
|
|
7272
7284
|
(!cleanupSnapshot.cwd ||
|
|
7273
7285
|
!cleanupSnapshot.workspaceKey ||
|
|
7274
7286
|
!cleanupSnapshot.instanceSlot ||
|
|
7275
|
-
!cleanupSnapshot.slot ||
|
|
7287
|
+
!(cleanupSnapshot.slot === null || (typeof cleanupSnapshot.slot === "string" && /^[A-Z]$/u.test(cleanupSnapshot.slot))) ||
|
|
7276
7288
|
!cleanupSnapshot.bindingKey ||
|
|
7277
7289
|
((cleanupSnapshot.sessionId !== undefined ||
|
|
7278
7290
|
cleanupSnapshot.sessionKey !== undefined) &&
|
|
@@ -7287,7 +7299,7 @@ export function createTelegramTopicTargetStore(
|
|
|
7287
7299
|
return false;
|
|
7288
7300
|
if (
|
|
7289
7301
|
!cleanupSnapshot &&
|
|
7290
|
-
(!normalized
|
|
7302
|
+
(!normalized || normalized.inactiveSinceMs === undefined)
|
|
7291
7303
|
)
|
|
7292
7304
|
return false;
|
|
7293
7305
|
const mapKey = getWorkspaceBindingMapKey(
|
|
@@ -7301,7 +7313,7 @@ export function createTelegramTopicTargetStore(
|
|
|
7301
7313
|
binding.sessionId === cleanupSessionId &&
|
|
7302
7314
|
binding.sessionKey === cleanupSessionKey &&
|
|
7303
7315
|
binding.instanceSlot === cleanupSnapshot.instanceSlot &&
|
|
7304
|
-
binding.slot === cleanupSnapshot.slot &&
|
|
7316
|
+
(binding.slot ?? null) === cleanupSnapshot.slot &&
|
|
7305
7317
|
binding.bindingKey === cleanupSnapshot.bindingKey &&
|
|
7306
7318
|
targetMatches(binding.target, cleanupSnapshot.target) &&
|
|
7307
7319
|
binding.inactiveSinceMs === cleanupSnapshot.inactiveSinceMs &&
|
|
@@ -7420,13 +7432,13 @@ export function createTelegramTopicTargetStore(
|
|
|
7420
7432
|
const nowMs = getNowMs();
|
|
7421
7433
|
const inSnapshot =
|
|
7422
7434
|
getExternalProtection.withSnapshot ?? ((capture) => capture());
|
|
7423
|
-
|
|
7424
|
-
|
|
7435
|
+
// A slotless binding (legacy or reassigned) holds no letter, so it can neither block planning nor be retired for capacity.
|
|
7436
|
+
const bindings = inSnapshot(() => Array.from(workspaceBindings.values()).filter((binding) => binding.slot).map((binding) => {
|
|
7437
|
+
const slot = binding.slot!.toLowerCase();
|
|
7425
7438
|
const locallyProtected = isWorkspaceBindingLocallyProtected(
|
|
7426
7439
|
binding,
|
|
7427
7440
|
nowMs,
|
|
7428
7441
|
{
|
|
7429
|
-
requireBindingSlot: true,
|
|
7430
7442
|
isRetirementExcluded: (intent) =>
|
|
7431
7443
|
isDeepStrictEqual(intent, options?.expectedRetirement),
|
|
7432
7444
|
},
|
|
@@ -7491,6 +7503,150 @@ export function createTelegramTopicTargetStore(
|
|
|
7491
7503
|
: [...localReservedSlots, "invalid"];
|
|
7492
7504
|
return { bindings, reservedSlots };
|
|
7493
7505
|
},
|
|
7506
|
+
async reuseLeastRecentlyUsedWorkspaceSlot(isCurrent) {
|
|
7507
|
+
if (workspaceRetirementCommitInFlight || !isCurrent()) return undefined;
|
|
7508
|
+
const external = captureExternalReservedSlots();
|
|
7509
|
+
if (!external) return undefined;
|
|
7510
|
+
const nowMs = getNowMs();
|
|
7511
|
+
const currentRecords = Array.from(records.values()).filter(
|
|
7512
|
+
ThreadReconciler.isCurrentThreadRecord,
|
|
7513
|
+
);
|
|
7514
|
+
const held = new Set<string>(
|
|
7515
|
+
[
|
|
7516
|
+
...external,
|
|
7517
|
+
...Array.from(workspaceClaims.values()).map(
|
|
7518
|
+
(claim) => claim.identity.slot,
|
|
7519
|
+
),
|
|
7520
|
+
...reservations
|
|
7521
|
+
.filter(
|
|
7522
|
+
(reservation) =>
|
|
7523
|
+
reservation.expiresAtMs === undefined ||
|
|
7524
|
+
reservation.expiresAtMs > nowMs,
|
|
7525
|
+
)
|
|
7526
|
+
.map((reservation) => reservation.slot),
|
|
7527
|
+
...pendingProvisions
|
|
7528
|
+
.filter((provision) =>
|
|
7529
|
+
isPendingProvisionLiveOrTargeted(provision, nowMs),
|
|
7530
|
+
)
|
|
7531
|
+
.map((provision) => provision.slot),
|
|
7532
|
+
].filter((slot): slot is string => !!slot),
|
|
7533
|
+
);
|
|
7534
|
+
const occupied = new Set<string>([
|
|
7535
|
+
...held,
|
|
7536
|
+
...Array.from(workspaceBindings.values()).flatMap((binding) =>
|
|
7537
|
+
binding.slot ? [binding.slot] : [],
|
|
7538
|
+
),
|
|
7539
|
+
...currentRecords.flatMap((record) => (record.slot ? [record.slot] : [])),
|
|
7540
|
+
]);
|
|
7541
|
+
if (
|
|
7542
|
+
Array.from(TELEGRAM_WORKSPACE_SLOTS, (slot) => slot.toUpperCase()).some(
|
|
7543
|
+
(slot) => !occupied.has(slot),
|
|
7544
|
+
)
|
|
7545
|
+
)
|
|
7546
|
+
return { kind: "free" };
|
|
7547
|
+
const claimedKeys = new Set(
|
|
7548
|
+
Array.from(workspaceClaims.values()).map(
|
|
7549
|
+
(claim) => claim.identity.bindingKey,
|
|
7550
|
+
),
|
|
7551
|
+
);
|
|
7552
|
+
const candidates = Array.from(workspaceBindings.values()).flatMap(
|
|
7553
|
+
(binding) => {
|
|
7554
|
+
const slot = binding.slot;
|
|
7555
|
+
if (
|
|
7556
|
+
!slot ||
|
|
7557
|
+
!/^[A-Z]$/u.test(slot) ||
|
|
7558
|
+
held.has(slot) ||
|
|
7559
|
+
claimedKeys.has(binding.bindingKey) ||
|
|
7560
|
+
hasWorkspaceRetirementConflict({
|
|
7561
|
+
bindingKey: binding.bindingKey,
|
|
7562
|
+
target: binding.target,
|
|
7563
|
+
slot,
|
|
7564
|
+
}) ||
|
|
7565
|
+
pendingCleanups.some((intent) =>
|
|
7566
|
+
targetMatches(intent.target, binding.target),
|
|
7567
|
+
) ||
|
|
7568
|
+
// Another Thread already holding the letter is unresolved duplication, not reusable metadata.
|
|
7569
|
+
currentRecords.some(
|
|
7570
|
+
(record) =>
|
|
7571
|
+
record.slot === slot && !targetMatches(record.target, binding.target),
|
|
7572
|
+
)
|
|
7573
|
+
)
|
|
7574
|
+
return [];
|
|
7575
|
+
const live = currentRecords.some((record) =>
|
|
7576
|
+
targetMatches(record.target, binding.target),
|
|
7577
|
+
);
|
|
7578
|
+
return [
|
|
7579
|
+
{
|
|
7580
|
+
bindingKey: binding.bindingKey,
|
|
7581
|
+
slot,
|
|
7582
|
+
live,
|
|
7583
|
+
lastUsedMs:
|
|
7584
|
+
!live && binding.inactiveSinceMs !== undefined
|
|
7585
|
+
? binding.inactiveSinceMs
|
|
7586
|
+
: binding.updatedAtMs,
|
|
7587
|
+
},
|
|
7588
|
+
];
|
|
7589
|
+
},
|
|
7590
|
+
);
|
|
7591
|
+
const selected = selectTelegramWorkspaceReusableSlot(candidates);
|
|
7592
|
+
if (!selected) return undefined;
|
|
7593
|
+
const mapKey = Array.from(workspaceBindings.entries()).find(
|
|
7594
|
+
([, binding]) => binding.bindingKey === selected.bindingKey,
|
|
7595
|
+
)?.[0];
|
|
7596
|
+
const binding = mapKey ? workspaceBindings.get(mapKey) : undefined;
|
|
7597
|
+
if (!mapKey || !binding) return undefined;
|
|
7598
|
+
const affected = currentRecords.filter(
|
|
7599
|
+
(record) =>
|
|
7600
|
+
record.slot === selected.slot && targetMatches(record.target, binding.target),
|
|
7601
|
+
);
|
|
7602
|
+
const previousBinding = cloneWorkspaceBinding(binding);
|
|
7603
|
+
const previousRecords = affected.map((record) => ({
|
|
7604
|
+
key: getRecordOwnerKey(record),
|
|
7605
|
+
record: cloneRecord(record),
|
|
7606
|
+
identity: identities.get(getRecordOwnerKey(record)),
|
|
7607
|
+
}));
|
|
7608
|
+
const { slot: _slot, ...slotless } = previousBinding;
|
|
7609
|
+
workspaceRetirementCommitInFlight = true;
|
|
7610
|
+
workspaceBindings.set(mapKey, slotless);
|
|
7611
|
+
for (const record of affected) {
|
|
7612
|
+
delete record.slot;
|
|
7613
|
+
const key = getRecordOwnerKey(record);
|
|
7614
|
+
const identity = identities.get(key);
|
|
7615
|
+
if (identity?.slot === selected.slot) {
|
|
7616
|
+
const { slot: _identitySlot, ...rest } = identity;
|
|
7617
|
+
identities.set(key, rest);
|
|
7618
|
+
}
|
|
7619
|
+
}
|
|
7620
|
+
markDirty();
|
|
7621
|
+
const restoreInMemory = () => {
|
|
7622
|
+
workspaceBindings.set(mapKey, cloneWorkspaceBinding(previousBinding));
|
|
7623
|
+
for (const previous of previousRecords) {
|
|
7624
|
+
records.set(previous.key, cloneRecord(previous.record));
|
|
7625
|
+
if (previous.identity) identities.set(previous.key, previous.identity);
|
|
7626
|
+
}
|
|
7627
|
+
markDirty();
|
|
7628
|
+
};
|
|
7629
|
+
try {
|
|
7630
|
+
if (!isCurrent() || !(await persistSnapshot())) {
|
|
7631
|
+
restoreInMemory();
|
|
7632
|
+
return undefined;
|
|
7633
|
+
}
|
|
7634
|
+
return {
|
|
7635
|
+
kind: "reused" as const,
|
|
7636
|
+
bindingKey: selected.bindingKey,
|
|
7637
|
+
slot: selected.slot,
|
|
7638
|
+
};
|
|
7639
|
+
} catch (error) {
|
|
7640
|
+
try {
|
|
7641
|
+
await loadFromDisk();
|
|
7642
|
+
} catch {
|
|
7643
|
+
restoreInMemory();
|
|
7644
|
+
}
|
|
7645
|
+
throw error;
|
|
7646
|
+
} finally {
|
|
7647
|
+
workspaceRetirementCommitInFlight = false;
|
|
7648
|
+
}
|
|
7649
|
+
},
|
|
7494
7650
|
hasWorkspaceBinding(cwd, sessionId) {
|
|
7495
7651
|
const normalizedCwd = normalizeTelegramWorkspacePath(cwd);
|
|
7496
7652
|
const normalizedSessionId =
|
|
@@ -10657,12 +10657,13 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
|
|
|
10657
10657
|
const typedUpdate = update as TUpdate;
|
|
10658
10658
|
const prepared = deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate;
|
|
10659
10659
|
// Journal/source evidence remains the original; only the warm released handler projection changes destination.
|
|
10660
|
-
const
|
|
10661
|
-
...(prepared.message ? { message: prepared.message.message_thread_id ===
|
|
10662
|
-
: withoutTelegramTurnReplyAnchor({ ...prepared.message, message_thread_id:
|
|
10663
|
-
...(prepared.edited_message ? { edited_message: prepared.edited_message.message_thread_id ===
|
|
10664
|
-
: withoutTelegramTurnReplyAnchor({ ...prepared.edited_message, message_thread_id:
|
|
10665
|
-
} as TUpdate
|
|
10660
|
+
const projectTo = (destination: TelegramTarget & { threadId: number }): TUpdate => ({ ...prepared,
|
|
10661
|
+
...(prepared.message ? { message: prepared.message.message_thread_id === destination.threadId ? { ...prepared.message }
|
|
10662
|
+
: withoutTelegramTurnReplyAnchor({ ...prepared.message, message_thread_id: destination.threadId, reply_to_message: undefined }) } : {}),
|
|
10663
|
+
...(prepared.edited_message ? { edited_message: prepared.edited_message.message_thread_id === destination.threadId ? { ...prepared.edited_message }
|
|
10664
|
+
: withoutTelegramTurnReplyAnchor({ ...prepared.edited_message, message_thread_id: destination.threadId, reply_to_message: undefined }) } : {}),
|
|
10665
|
+
} as TUpdate);
|
|
10666
|
+
const routed = target ? projectTo(target) : prepared;
|
|
10666
10667
|
try {
|
|
10667
10668
|
return await executeUpdate(
|
|
10668
10669
|
routed,
|
|
@@ -10673,6 +10674,8 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
|
|
|
10673
10674
|
if (await deps.settleTerminalExecutionFailure?.(error)) {
|
|
10674
10675
|
return { kind: "complete" };
|
|
10675
10676
|
}
|
|
10677
|
+
// A refused reply proves only that send was not issued, not that the handler made no prior effects.
|
|
10678
|
+
// Leave failure settlement to the worker's retry policy; never re-enter the handler to recover delivery here.
|
|
10676
10679
|
throw error;
|
|
10677
10680
|
}
|
|
10678
10681
|
},
|