@llblab/pi-kit 0.36.0 → 0.36.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BACKLOG.md +0 -4
- package/CHANGELOG.md +10 -0
- package/README.md +5 -5
- 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-state-flow/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +5 -4
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +1 -0
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +5 -4
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +17 -5
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +23 -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 +12 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +17 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +42 -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.js +3 -2
- 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.js +119 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +9 -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.d.ts +15 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +22 -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 +9 -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 +16 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +15 -5
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +3 -0
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +43 -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 +3 -2
- 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 +137 -24
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +14 -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 +33 -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/package.json +5 -5
|
@@ -7,6 +7,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
7
7
|
import { publishTelegramPrivateFile, readTelegramPrivateFile, TelegramPrivateFileError, withTelegramFileTransaction, } from "./locks.js";
|
|
8
8
|
import { getTelegramJournalPublicationPaths } from "./paths.js";
|
|
9
9
|
import { isWireRecord as isObject, hasOnlyWireKeys as onlyKeys, isNonNegativeWireInteger as safeTime, } from "./wire.js";
|
|
10
|
+
import { isTelegramWorkspaceRetirementFence } from "./workspace-admission.js";
|
|
10
11
|
function targetKey(target) {
|
|
11
12
|
return `${target.chatId}:${target.threadId}`;
|
|
12
13
|
}
|
|
@@ -42,8 +43,7 @@ export function planTelegramInactiveThreadCleanup(input) {
|
|
|
42
43
|
!binding.cwd ||
|
|
43
44
|
!binding.workspaceKey ||
|
|
44
45
|
!binding.instanceSlot ||
|
|
45
|
-
typeof slot !== "string" ||
|
|
46
|
-
!slot ||
|
|
46
|
+
(slot !== undefined && (typeof slot !== "string" || !/^[A-Z]$/u.test(slot))) ||
|
|
47
47
|
!validSessionIdentity(binding) ||
|
|
48
48
|
!validTarget(binding.target) ||
|
|
49
49
|
bindingKeys.has(binding.bindingKey) ||
|
|
@@ -72,9 +72,7 @@ export function planTelegramInactiveThreadCleanup(input) {
|
|
|
72
72
|
for (const binding of input.bindings) {
|
|
73
73
|
const inactiveSinceMs = binding.inactiveSinceMs;
|
|
74
74
|
const slot = binding.slot;
|
|
75
|
-
if (
|
|
76
|
-
!slot ||
|
|
77
|
-
!Number.isSafeInteger(inactiveSinceMs) ||
|
|
75
|
+
if (!Number.isSafeInteger(inactiveSinceMs) ||
|
|
78
76
|
inactiveSinceMs === undefined ||
|
|
79
77
|
inactiveSinceMs < 0 ||
|
|
80
78
|
!Number.isSafeInteger(binding.updatedAtMs) ||
|
|
@@ -97,7 +95,7 @@ export function planTelegramInactiveThreadCleanup(input) {
|
|
|
97
95
|
? { sessionId: binding.sessionId, sessionKey: binding.sessionKey }
|
|
98
96
|
: {}),
|
|
99
97
|
instanceSlot: binding.instanceSlot,
|
|
100
|
-
slot,
|
|
98
|
+
slot: slot ?? null,
|
|
101
99
|
target: { ...binding.target },
|
|
102
100
|
inactiveSinceMs,
|
|
103
101
|
bindingUpdatedAtMs: binding.updatedAtMs,
|
|
@@ -228,8 +226,7 @@ function validateWorkSet(value, profileName) {
|
|
|
228
226
|
!validSessionIdentity(raw) ||
|
|
229
227
|
typeof raw.instanceSlot !== "string" ||
|
|
230
228
|
!raw.instanceSlot ||
|
|
231
|
-
typeof raw.slot !== "string" ||
|
|
232
|
-
!raw.slot ||
|
|
229
|
+
(raw.slot !== null && (typeof raw.slot !== "string" || !/^[A-Z]$/u.test(raw.slot))) ||
|
|
233
230
|
!isObject(raw.target) ||
|
|
234
231
|
!onlyKeys(raw.target, ["chatId", "threadId"]) ||
|
|
235
232
|
!validTarget(raw.target) ||
|
|
@@ -311,7 +308,11 @@ function validateWorkSet(value, profileName) {
|
|
|
311
308
|
return { operationId: value.operationId, createdAtMs, entries };
|
|
312
309
|
}
|
|
313
310
|
function sameCandidates(left, right) {
|
|
314
|
-
|
|
311
|
+
const keys = ["profileName", "bindingKey", "cwd", "workspaceKey", "sessionId", "sessionKey", "instanceSlot", "slot", "inactiveSinceMs", "bindingUpdatedAtMs"];
|
|
312
|
+
return left.length === right.length && left.every((candidate, index) => {
|
|
313
|
+
const other = right[index];
|
|
314
|
+
return keys.every(key => candidate[key] === other[key]) && targetKey(candidate.target) === targetKey(other.target);
|
|
315
|
+
});
|
|
315
316
|
}
|
|
316
317
|
function parseTelegramThreadCleanupWorkFile(value, profileName, tokenSha256) {
|
|
317
318
|
if (!isObject(value) ||
|
|
@@ -411,12 +412,12 @@ export function createTelegramThreadCleanupWorkStore(options) {
|
|
|
411
412
|
!entry ||
|
|
412
413
|
entry.bindingUpdatedAtMs !== input.bindingUpdatedAtMs ||
|
|
413
414
|
permit.destructiveKind !== "manual-thread-cleanup" ||
|
|
414
|
-
permit.profileKey !== entry.profileName ||
|
|
415
|
+
permit.profileKey !== (options.admissionProfileKey ?? entry.profileName) ||
|
|
415
416
|
permit.bindingKey !== entry.bindingKey ||
|
|
416
417
|
permit.slot !== entry.slot ||
|
|
417
418
|
targetKey(permit.target) !== targetKey(entry.target) ||
|
|
418
419
|
!permit.operationId ||
|
|
419
|
-
|
|
420
|
+
permit.retirementIntentId !== workSet.operationId ||
|
|
420
421
|
!safeTime(permit.issuedAtMs))
|
|
421
422
|
throw new Error("Telegram Thread cleanup entry or deletion permit is stale or mismatched.");
|
|
422
423
|
if (entry.state !== "prepared") {
|
|
@@ -522,7 +523,7 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
|
|
|
522
523
|
const fence = deps.ledger.read().fence;
|
|
523
524
|
return fence?.operationId === operationId &&
|
|
524
525
|
fence.destructiveKind === "manual-thread-cleanup" &&
|
|
525
|
-
fence.profileKey ===
|
|
526
|
+
fence.profileKey === deps.ledger.getProfileKey() &&
|
|
526
527
|
fence.retirementIntentId === workSetId &&
|
|
527
528
|
fence.bindingKey === candidate.bindingKey &&
|
|
528
529
|
fence.slot === candidate.slot &&
|
|
@@ -629,14 +630,132 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
|
|
|
629
630
|
: { kind: "already-issued" };
|
|
630
631
|
},
|
|
631
632
|
async settleDeleted(fence, commit) {
|
|
633
|
+
const assertAuthority = () => {
|
|
634
|
+
const owner = deps.getOwner();
|
|
635
|
+
if (deps.getLeaderEpoch() !== fence.leaderEpoch ||
|
|
636
|
+
owner.processId !== fence.owner.processId || owner.processBirthId !== fence.owner.processBirthId)
|
|
637
|
+
throw new Error("Telegram Thread cleanup settlement authority changed.");
|
|
638
|
+
};
|
|
639
|
+
assertAuthority();
|
|
632
640
|
const ready = deps.ledger.confirmThreadCleanupAbsence(fence);
|
|
633
|
-
|
|
641
|
+
const committed = await commit();
|
|
642
|
+
assertAuthority();
|
|
643
|
+
if (!committed)
|
|
634
644
|
return "commit-pending";
|
|
635
645
|
deps.ledger.completeThreadCleanupFence(ready);
|
|
636
646
|
return "completed";
|
|
637
647
|
},
|
|
638
648
|
};
|
|
639
649
|
}
|
|
650
|
+
/** Review owns ordinary admission; confirmed cleanup uses the shared gate and its exclusive manual fence instead. */
|
|
651
|
+
export function createTelegramInactiveThreadCleanupRuntime(deps) {
|
|
652
|
+
const capture = () => {
|
|
653
|
+
const authority = deps.captureAuthority();
|
|
654
|
+
if (!authority || !authority.isCurrent())
|
|
655
|
+
throw new Error("Inactive Thread cleanup requires current leader authority.");
|
|
656
|
+
const ledgerKey = authority.ledger.getProfileKey();
|
|
657
|
+
const owner = authority.ledger.getOwner();
|
|
658
|
+
const assertCurrent = () => {
|
|
659
|
+
if (!authority.isCurrent() || authority.ledger.getProfileKey() !== ledgerKey)
|
|
660
|
+
throw new Error("Inactive Thread cleanup authority changed.");
|
|
661
|
+
};
|
|
662
|
+
const evidence = () => {
|
|
663
|
+
assertCurrent();
|
|
664
|
+
return captureTelegramInactiveThreadCleanupEvidence({
|
|
665
|
+
profileName: authority.profileName,
|
|
666
|
+
listBindings: deps.store.listWorkspaceBindings,
|
|
667
|
+
getProtection: deps.getProtection,
|
|
668
|
+
listReservations: deps.store.listReservations,
|
|
669
|
+
listPendingProvisions: deps.store.listPendingProvisions,
|
|
670
|
+
listPendingCleanups: deps.store.listPendingCleanups,
|
|
671
|
+
});
|
|
672
|
+
};
|
|
673
|
+
return { authority, owner, assertCurrent, evidence };
|
|
674
|
+
};
|
|
675
|
+
return {
|
|
676
|
+
async review() {
|
|
677
|
+
const scope = capture();
|
|
678
|
+
return createTelegramInactiveThreadCleanupReviewRuntime({
|
|
679
|
+
getProfileName: () => scope.authority.profileName,
|
|
680
|
+
listBindings: deps.store.listWorkspaceBindings,
|
|
681
|
+
getProtection: deps.getProtection,
|
|
682
|
+
listReservations: deps.store.listReservations,
|
|
683
|
+
listPendingProvisions: deps.store.listPendingProvisions,
|
|
684
|
+
listPendingCleanups: deps.store.listPendingCleanups,
|
|
685
|
+
getWorkStore() { scope.assertCurrent(); return deps.getWorkStore(scope.authority); },
|
|
686
|
+
runWorkspaceOperation(metadata, operation) {
|
|
687
|
+
return deps.runWorkspaceOperation(metadata, async () => {
|
|
688
|
+
scope.assertCurrent();
|
|
689
|
+
await deps.store.load();
|
|
690
|
+
scope.assertCurrent();
|
|
691
|
+
return operation();
|
|
692
|
+
});
|
|
693
|
+
},
|
|
694
|
+
}).review();
|
|
695
|
+
},
|
|
696
|
+
async clean(operationId) {
|
|
697
|
+
const scope = capture();
|
|
698
|
+
return deps.runExclusive(async () => {
|
|
699
|
+
scope.assertCurrent();
|
|
700
|
+
const { authority } = scope;
|
|
701
|
+
const work = deps.getWorkStore(authority);
|
|
702
|
+
const exactBinding = (candidate) => {
|
|
703
|
+
const fresh = planTelegramInactiveThreadCleanup(scope.evidence()).find(binding => binding.bindingKey === candidate.bindingKey);
|
|
704
|
+
if (!fresh || !sameCandidates([fresh], [candidate]))
|
|
705
|
+
return undefined;
|
|
706
|
+
return deps.store.listWorkspaceBindings().find(binding => binding.bindingKey === fresh.bindingKey);
|
|
707
|
+
};
|
|
708
|
+
const loadExactBinding = async (candidate) => {
|
|
709
|
+
scope.assertCurrent();
|
|
710
|
+
await deps.store.load();
|
|
711
|
+
scope.assertCurrent();
|
|
712
|
+
return exactBinding(candidate);
|
|
713
|
+
};
|
|
714
|
+
const permitRuntime = createTelegramThreadCleanupPermitRuntime({
|
|
715
|
+
ledger: authority.ledger,
|
|
716
|
+
getLeaderEpoch: () => authority.isCurrent() ? authority.leaderEpoch : undefined,
|
|
717
|
+
getProfileName: () => authority.profileName,
|
|
718
|
+
getOwner: () => scope.owner,
|
|
719
|
+
canAdoptFence: fence => authority.isCurrent() && deps.getProcessLiveness(fence.owner) === "dead",
|
|
720
|
+
revalidateUnderFence: async (candidate) => !!(await loadExactBinding(candidate)),
|
|
721
|
+
});
|
|
722
|
+
return cleanReviewedInactiveThreads({
|
|
723
|
+
store: work, operationId, permitRuntime,
|
|
724
|
+
resolveFullBinding: loadExactBinding,
|
|
725
|
+
async deleteWithPermit(permit, candidate) {
|
|
726
|
+
try {
|
|
727
|
+
await deps.deleteThread(() => {
|
|
728
|
+
scope.assertCurrent();
|
|
729
|
+
const fence = authority.ledger.read().fence;
|
|
730
|
+
if (!fence || !isTelegramWorkspaceRetirementFence(fence) ||
|
|
731
|
+
fence.destructiveKind !== "manual-thread-cleanup" || fence.phase !== "deletion-issued" ||
|
|
732
|
+
fence.operationId !== permit.operationId || fence.retirementIntentId !== operationId ||
|
|
733
|
+
fence.profileKey !== permit.profileKey || fence.bindingKey !== candidate.bindingKey ||
|
|
734
|
+
fence.slot !== candidate.slot || fence.deletionIssuedAtMs !== permit.issuedAtMs ||
|
|
735
|
+
fence.leaderEpoch !== authority.leaderEpoch || fence.owner.processId !== scope.owner.processId ||
|
|
736
|
+
fence.owner.processBirthId !== scope.owner.processBirthId ||
|
|
737
|
+
targetKey(fence.target) !== targetKey(permit.target) || !exactBinding(candidate))
|
|
738
|
+
throw new Error("Inactive Thread cleanup deletion permit is stale.");
|
|
739
|
+
return { ...permit.target };
|
|
740
|
+
});
|
|
741
|
+
scope.assertCurrent();
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
deps.onError?.(error);
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
async commitBinding(candidate) {
|
|
749
|
+
scope.assertCurrent();
|
|
750
|
+
await deps.store.load();
|
|
751
|
+
scope.assertCurrent();
|
|
752
|
+
return deps.store.commitInactiveWorkspaceCleanup(candidate, authority.isCurrent);
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
}
|
|
640
759
|
export function createTelegramInactiveThreadCleanupSettingsPort(deps) {
|
|
641
760
|
return (operationId) => cleanReviewedInactiveThreads({ ...deps, operationId });
|
|
642
761
|
}
|
|
@@ -654,9 +773,18 @@ export async function cleanReviewedInactiveThreads(input) {
|
|
|
654
773
|
for (const entry of workSet.entries) {
|
|
655
774
|
if (entry.state === "deleted") {
|
|
656
775
|
const ready = input.permitRuntime.findCommitReady(entry, input.operationId);
|
|
657
|
-
if (ready)
|
|
658
|
-
await input.permitRuntime.settleDeleted(ready, async () => true);
|
|
659
|
-
|
|
776
|
+
if (ready) {
|
|
777
|
+
const settlement = await input.permitRuntime.settleDeleted(ready, async () => true);
|
|
778
|
+
if (settlement === "completed")
|
|
779
|
+
deleted += 1;
|
|
780
|
+
else
|
|
781
|
+
outcomeUnknown += 1;
|
|
782
|
+
}
|
|
783
|
+
else if (input.permitRuntime.diagnoseRecovery(entry, input.operationId) !== "none") {
|
|
784
|
+
outcomeUnknown += 1;
|
|
785
|
+
}
|
|
786
|
+
else
|
|
787
|
+
deleted += 1;
|
|
660
788
|
continue;
|
|
661
789
|
}
|
|
662
790
|
if (entry.state === "outcome-unknown") {
|
|
@@ -62,7 +62,7 @@ export declare function createTelegramThreadDisplaySettingsRuntime(deps: {
|
|
|
62
62
|
};
|
|
63
63
|
/** Caller owns triggering and reporting; no timer or background retry is created. */
|
|
64
64
|
export declare function createTelegramThreadDisplayReconciler(deps: TelegramThreadDisplayReconcilerDeps): {
|
|
65
|
-
reconcile(): Promise<{
|
|
65
|
+
reconcile(bindingKey?: string): Promise<{
|
|
66
66
|
changed: number;
|
|
67
67
|
}>;
|
|
68
68
|
};
|
|
@@ -123,9 +123,14 @@ export function resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBinding
|
|
|
123
123
|
if (manualName) {
|
|
124
124
|
labels.set(binding.bindingKey, boundedLabel(manualName));
|
|
125
125
|
}
|
|
126
|
+
else if (!slot) {
|
|
127
|
+
// A displaced binding retains its generated identity, not the letter now owned elsewhere.
|
|
128
|
+
const name = binding.threadName ? labelText(binding.threadName) : undefined;
|
|
129
|
+
if (name)
|
|
130
|
+
labels.set(binding.bindingKey, boundedLabel(name));
|
|
131
|
+
}
|
|
126
132
|
else if (mode === "letters") {
|
|
127
|
-
|
|
128
|
-
labels.set(binding.bindingKey, slot);
|
|
133
|
+
labels.set(binding.bindingKey, slot);
|
|
129
134
|
}
|
|
130
135
|
else if (mode === "names") {
|
|
131
136
|
const name = binding.threadName ? labelText(binding.threadName) : slot;
|
|
@@ -142,8 +147,6 @@ export function resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBinding
|
|
|
142
147
|
bindings.filter((candidate) => candidate.cwd === binding.cwd)
|
|
143
148
|
.length > 1
|
|
144
149
|
: (liveDirectoryCounts.get(binding.cwd) ?? 0) > 1;
|
|
145
|
-
if (showSuffix && !slot)
|
|
146
|
-
continue;
|
|
147
150
|
const suffix = !showSuffix
|
|
148
151
|
? ""
|
|
149
152
|
: mode === "directory-title"
|
|
@@ -239,7 +242,7 @@ export function createTelegramThreadDisplaySettingsRuntime(deps) {
|
|
|
239
242
|
/** Caller owns triggering and reporting; no timer or background retry is created. */
|
|
240
243
|
export function createTelegramThreadDisplayReconciler(deps) {
|
|
241
244
|
let tail = Promise.resolve();
|
|
242
|
-
const apply = async () => {
|
|
245
|
+
const apply = async (bindingKey) => {
|
|
243
246
|
const epoch = deps.getLeaderEpoch();
|
|
244
247
|
const profile = deps.getProfileKey();
|
|
245
248
|
const mode = deps.getMode();
|
|
@@ -257,6 +260,8 @@ export function createTelegramThreadDisplayReconciler(deps) {
|
|
|
257
260
|
const titles = resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBindingKeys);
|
|
258
261
|
let changed = 0;
|
|
259
262
|
for (const binding of bindings) {
|
|
263
|
+
if (bindingKey !== undefined && binding.bindingKey !== bindingKey)
|
|
264
|
+
continue;
|
|
260
265
|
const isBindingCurrent = deps.captureBindingAuthority(binding);
|
|
261
266
|
if (!isBindingCurrent)
|
|
262
267
|
continue;
|
|
@@ -275,12 +280,14 @@ export function createTelegramThreadDisplayReconciler(deps) {
|
|
|
275
280
|
assertBinding(binding);
|
|
276
281
|
if ((binding.displayTitle ?? binding.threadName) === title)
|
|
277
282
|
continue;
|
|
278
|
-
await deps.callApi("editForumTopic", {
|
|
283
|
+
const acknowledged = await deps.callApi("editForumTopic", {
|
|
279
284
|
chat_id: binding.target.chatId,
|
|
280
285
|
message_thread_id: binding.target.threadId,
|
|
281
286
|
name: title,
|
|
282
287
|
}, { maxAttempts: 1 });
|
|
283
288
|
assertBinding(binding);
|
|
289
|
+
if (acknowledged !== true)
|
|
290
|
+
throw new Error("Telegram Thread display update was not acknowledged.");
|
|
284
291
|
if (!deps.store.setWorkspaceDisplayTitle(binding, title)) {
|
|
285
292
|
throw new Error("Telegram Thread display binding changed before title commit.");
|
|
286
293
|
}
|
|
@@ -296,8 +303,8 @@ export function createTelegramThreadDisplayReconciler(deps) {
|
|
|
296
303
|
return { changed };
|
|
297
304
|
};
|
|
298
305
|
return {
|
|
299
|
-
reconcile() {
|
|
300
|
-
const run = tail.then(apply);
|
|
306
|
+
reconcile(bindingKey) {
|
|
307
|
+
const run = tail.then(() => apply(bindingKey));
|
|
301
308
|
tail = run.catch(() => undefined);
|
|
302
309
|
return run;
|
|
303
310
|
},
|
|
@@ -543,7 +543,7 @@ export interface TelegramTopicTargetStore {
|
|
|
543
543
|
sessionId?: string;
|
|
544
544
|
sessionKey?: string;
|
|
545
545
|
instanceSlot: string;
|
|
546
|
-
slot: string;
|
|
546
|
+
slot: string | null;
|
|
547
547
|
bindingKey: string;
|
|
548
548
|
target: {
|
|
549
549
|
chatId: number;
|
|
@@ -556,6 +556,19 @@ export interface TelegramTopicTargetStore {
|
|
|
556
556
|
captureWorkspaceSlotOccupancy: (getExternalProtection: TelegramWorkspaceExternalProtectionCapture, options?: {
|
|
557
557
|
expectedRetirement?: TelegramWorkspaceRetirementIntent;
|
|
558
558
|
}) => TelegramWorkspaceSlotOccupancySnapshot;
|
|
559
|
+
/**
|
|
560
|
+
* Fallback for a full A-Z pool: strips the display letter from the least recently used
|
|
561
|
+
* binding (and its current Thread record) and persists, so a fresh allocation can take it.
|
|
562
|
+
* Metadata only: the Thread, journals and accepted work stay untouched, and a binding or
|
|
563
|
+
* letter held by a claim, reservation, provision, cleanup or retirement is never chosen.
|
|
564
|
+
*/
|
|
565
|
+
reuseLeastRecentlyUsedWorkspaceSlot: (isCurrent: () => boolean) => Promise<{
|
|
566
|
+
kind: "free";
|
|
567
|
+
} | {
|
|
568
|
+
kind: "reused";
|
|
569
|
+
bindingKey: string;
|
|
570
|
+
slot: string;
|
|
571
|
+
} | undefined>;
|
|
559
572
|
hasWorkspaceBinding: (cwd: string, sessionId?: string) => boolean;
|
|
560
573
|
setWorkspaceDisplayTitle: (expected: TelegramWorkspaceThreadBinding, title: string) => boolean;
|
|
561
574
|
markWorkspaceBindingInactiveByTarget: (target: TelegramTarget, inactiveSinceMs?: number) => boolean;
|
|
@@ -17,7 +17,7 @@ import { resolveTelegramProfileTempFilePath } from "./paths.js";
|
|
|
17
17
|
import { createTelegramRuntimeProjectionStore } from "./status.js";
|
|
18
18
|
import * as ThreadReconciler from "./thread-reconciler.js";
|
|
19
19
|
import { createTelegramSessionKey, createTelegramWorkspaceBindingIdentityWithKey, createTelegramWorkspaceDirectoryKey, normalizeTelegramSessionId, normalizeTelegramWorkspacePath, TELEGRAM_WORKSPACE_KEY_MAX_LENGTH, } from "./workspace-identity.js";
|
|
20
|
-
import { planTelegramWorkspaceSlotAllocation, TELEGRAM_WORKSPACE_SLOTS, TelegramWorkspaceSlotUnavailableError, } from "./workspace-slots.js";
|
|
20
|
+
import { planTelegramWorkspaceSlotAllocation, selectTelegramWorkspaceReusableSlot, TELEGRAM_WORKSPACE_SLOTS, TelegramWorkspaceSlotUnavailableError, } from "./workspace-slots.js";
|
|
21
21
|
export { createTelegramWorkspaceBindingIdentity, createTelegramWorkspaceDirectoryKey, normalizeTelegramSessionId, normalizeTelegramWorkspacePath, } from "./workspace-identity.js";
|
|
22
22
|
const TELEGRAM_THREAD_RESERVATION_TTL_MS = 15 * 60 * 1000;
|
|
23
23
|
function getNextMonotonicSlot(records, reservations, pendingProvisions, nowMs, lastSlot) {
|
|
@@ -1871,7 +1871,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1871
1871
|
};
|
|
1872
1872
|
/** Current records, claims, reservations, provisions, cleanups or other retirements still holding a binding's Thread or slot. */
|
|
1873
1873
|
const isWorkspaceBindingLocallyProtected = (binding, nowMs, options = {}) => {
|
|
1874
|
-
const slotMatches = (slot) =>
|
|
1874
|
+
const slotMatches = (slot) => !!binding.slot && slot === binding.slot;
|
|
1875
1875
|
const targetOrSlotMatches = (candidate) => (!!candidate.target && targetMatches(candidate.target, binding.target)) ||
|
|
1876
1876
|
slotMatches(candidate.slot);
|
|
1877
1877
|
return (Array.from(records.values()).some((record) => ThreadReconciler.isCurrentThreadRecord(record) &&
|
|
@@ -4572,7 +4572,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4572
4572
|
(!cleanupSnapshot.cwd ||
|
|
4573
4573
|
!cleanupSnapshot.workspaceKey ||
|
|
4574
4574
|
!cleanupSnapshot.instanceSlot ||
|
|
4575
|
-
!cleanupSnapshot.slot ||
|
|
4575
|
+
!(cleanupSnapshot.slot === null || (typeof cleanupSnapshot.slot === "string" && /^[A-Z]$/u.test(cleanupSnapshot.slot))) ||
|
|
4576
4576
|
!cleanupSnapshot.bindingKey ||
|
|
4577
4577
|
((cleanupSnapshot.sessionId !== undefined ||
|
|
4578
4578
|
cleanupSnapshot.sessionKey !== undefined) &&
|
|
@@ -4585,7 +4585,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4585
4585
|
cleanupSnapshot.target.threadId <= 0))
|
|
4586
4586
|
return false;
|
|
4587
4587
|
if (!cleanupSnapshot &&
|
|
4588
|
-
(!normalized
|
|
4588
|
+
(!normalized || normalized.inactiveSinceMs === undefined))
|
|
4589
4589
|
return false;
|
|
4590
4590
|
const mapKey = getWorkspaceBindingMapKey((cleanupSnapshot ?? normalized));
|
|
4591
4591
|
const binding = workspaceBindings.get(mapKey);
|
|
@@ -4597,7 +4597,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4597
4597
|
binding.sessionId === cleanupSessionId &&
|
|
4598
4598
|
binding.sessionKey === cleanupSessionKey &&
|
|
4599
4599
|
binding.instanceSlot === cleanupSnapshot.instanceSlot &&
|
|
4600
|
-
binding.slot === cleanupSnapshot.slot &&
|
|
4600
|
+
(binding.slot ?? null) === cleanupSnapshot.slot &&
|
|
4601
4601
|
binding.bindingKey === cleanupSnapshot.bindingKey &&
|
|
4602
4602
|
targetMatches(binding.target, cleanupSnapshot.target) &&
|
|
4603
4603
|
binding.inactiveSinceMs === cleanupSnapshot.inactiveSinceMs &&
|
|
@@ -4710,10 +4710,10 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4710
4710
|
captureWorkspaceSlotOccupancy(getExternalProtection, options) {
|
|
4711
4711
|
const nowMs = getNowMs();
|
|
4712
4712
|
const inSnapshot = getExternalProtection.withSnapshot ?? ((capture) => capture());
|
|
4713
|
-
|
|
4714
|
-
|
|
4713
|
+
// A slotless binding (legacy or reassigned) holds no letter, so it can neither block planning nor be retired for capacity.
|
|
4714
|
+
const bindings = inSnapshot(() => Array.from(workspaceBindings.values()).filter((binding) => binding.slot).map((binding) => {
|
|
4715
|
+
const slot = binding.slot.toLowerCase();
|
|
4715
4716
|
const locallyProtected = isWorkspaceBindingLocallyProtected(binding, nowMs, {
|
|
4716
|
-
requireBindingSlot: true,
|
|
4717
4717
|
isRetirementExcluded: (intent) => isDeepStrictEqual(intent, options?.expectedRetirement),
|
|
4718
4718
|
});
|
|
4719
4719
|
const externalProtection = getExternalProtection(cloneWorkspaceBinding(binding));
|
|
@@ -4765,6 +4765,120 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4765
4765
|
: [...localReservedSlots, "invalid"];
|
|
4766
4766
|
return { bindings, reservedSlots };
|
|
4767
4767
|
},
|
|
4768
|
+
async reuseLeastRecentlyUsedWorkspaceSlot(isCurrent) {
|
|
4769
|
+
if (workspaceRetirementCommitInFlight || !isCurrent())
|
|
4770
|
+
return undefined;
|
|
4771
|
+
const external = captureExternalReservedSlots();
|
|
4772
|
+
if (!external)
|
|
4773
|
+
return undefined;
|
|
4774
|
+
const nowMs = getNowMs();
|
|
4775
|
+
const currentRecords = Array.from(records.values()).filter(ThreadReconciler.isCurrentThreadRecord);
|
|
4776
|
+
const held = new Set([
|
|
4777
|
+
...external,
|
|
4778
|
+
...Array.from(workspaceClaims.values()).map((claim) => claim.identity.slot),
|
|
4779
|
+
...reservations
|
|
4780
|
+
.filter((reservation) => reservation.expiresAtMs === undefined ||
|
|
4781
|
+
reservation.expiresAtMs > nowMs)
|
|
4782
|
+
.map((reservation) => reservation.slot),
|
|
4783
|
+
...pendingProvisions
|
|
4784
|
+
.filter((provision) => isPendingProvisionLiveOrTargeted(provision, nowMs))
|
|
4785
|
+
.map((provision) => provision.slot),
|
|
4786
|
+
].filter((slot) => !!slot));
|
|
4787
|
+
const occupied = new Set([
|
|
4788
|
+
...held,
|
|
4789
|
+
...Array.from(workspaceBindings.values()).flatMap((binding) => binding.slot ? [binding.slot] : []),
|
|
4790
|
+
...currentRecords.flatMap((record) => (record.slot ? [record.slot] : [])),
|
|
4791
|
+
]);
|
|
4792
|
+
if (Array.from(TELEGRAM_WORKSPACE_SLOTS, (slot) => slot.toUpperCase()).some((slot) => !occupied.has(slot)))
|
|
4793
|
+
return { kind: "free" };
|
|
4794
|
+
const claimedKeys = new Set(Array.from(workspaceClaims.values()).map((claim) => claim.identity.bindingKey));
|
|
4795
|
+
const candidates = Array.from(workspaceBindings.values()).flatMap((binding) => {
|
|
4796
|
+
const slot = binding.slot;
|
|
4797
|
+
if (!slot ||
|
|
4798
|
+
!/^[A-Z]$/u.test(slot) ||
|
|
4799
|
+
held.has(slot) ||
|
|
4800
|
+
claimedKeys.has(binding.bindingKey) ||
|
|
4801
|
+
hasWorkspaceRetirementConflict({
|
|
4802
|
+
bindingKey: binding.bindingKey,
|
|
4803
|
+
target: binding.target,
|
|
4804
|
+
slot,
|
|
4805
|
+
}) ||
|
|
4806
|
+
pendingCleanups.some((intent) => targetMatches(intent.target, binding.target)) ||
|
|
4807
|
+
// Another Thread already holding the letter is unresolved duplication, not reusable metadata.
|
|
4808
|
+
currentRecords.some((record) => record.slot === slot && !targetMatches(record.target, binding.target)))
|
|
4809
|
+
return [];
|
|
4810
|
+
const live = currentRecords.some((record) => targetMatches(record.target, binding.target));
|
|
4811
|
+
return [
|
|
4812
|
+
{
|
|
4813
|
+
bindingKey: binding.bindingKey,
|
|
4814
|
+
slot,
|
|
4815
|
+
live,
|
|
4816
|
+
lastUsedMs: !live && binding.inactiveSinceMs !== undefined
|
|
4817
|
+
? binding.inactiveSinceMs
|
|
4818
|
+
: binding.updatedAtMs,
|
|
4819
|
+
},
|
|
4820
|
+
];
|
|
4821
|
+
});
|
|
4822
|
+
const selected = selectTelegramWorkspaceReusableSlot(candidates);
|
|
4823
|
+
if (!selected)
|
|
4824
|
+
return undefined;
|
|
4825
|
+
const mapKey = Array.from(workspaceBindings.entries()).find(([, binding]) => binding.bindingKey === selected.bindingKey)?.[0];
|
|
4826
|
+
const binding = mapKey ? workspaceBindings.get(mapKey) : undefined;
|
|
4827
|
+
if (!mapKey || !binding)
|
|
4828
|
+
return undefined;
|
|
4829
|
+
const affected = currentRecords.filter((record) => record.slot === selected.slot && targetMatches(record.target, binding.target));
|
|
4830
|
+
const previousBinding = cloneWorkspaceBinding(binding);
|
|
4831
|
+
const previousRecords = affected.map((record) => ({
|
|
4832
|
+
key: getRecordOwnerKey(record),
|
|
4833
|
+
record: cloneRecord(record),
|
|
4834
|
+
identity: identities.get(getRecordOwnerKey(record)),
|
|
4835
|
+
}));
|
|
4836
|
+
const { slot: _slot, ...slotless } = previousBinding;
|
|
4837
|
+
workspaceRetirementCommitInFlight = true;
|
|
4838
|
+
workspaceBindings.set(mapKey, slotless);
|
|
4839
|
+
for (const record of affected) {
|
|
4840
|
+
delete record.slot;
|
|
4841
|
+
const key = getRecordOwnerKey(record);
|
|
4842
|
+
const identity = identities.get(key);
|
|
4843
|
+
if (identity?.slot === selected.slot) {
|
|
4844
|
+
const { slot: _identitySlot, ...rest } = identity;
|
|
4845
|
+
identities.set(key, rest);
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4848
|
+
markDirty();
|
|
4849
|
+
const restoreInMemory = () => {
|
|
4850
|
+
workspaceBindings.set(mapKey, cloneWorkspaceBinding(previousBinding));
|
|
4851
|
+
for (const previous of previousRecords) {
|
|
4852
|
+
records.set(previous.key, cloneRecord(previous.record));
|
|
4853
|
+
if (previous.identity)
|
|
4854
|
+
identities.set(previous.key, previous.identity);
|
|
4855
|
+
}
|
|
4856
|
+
markDirty();
|
|
4857
|
+
};
|
|
4858
|
+
try {
|
|
4859
|
+
if (!isCurrent() || !(await persistSnapshot())) {
|
|
4860
|
+
restoreInMemory();
|
|
4861
|
+
return undefined;
|
|
4862
|
+
}
|
|
4863
|
+
return {
|
|
4864
|
+
kind: "reused",
|
|
4865
|
+
bindingKey: selected.bindingKey,
|
|
4866
|
+
slot: selected.slot,
|
|
4867
|
+
};
|
|
4868
|
+
}
|
|
4869
|
+
catch (error) {
|
|
4870
|
+
try {
|
|
4871
|
+
await loadFromDisk();
|
|
4872
|
+
}
|
|
4873
|
+
catch {
|
|
4874
|
+
restoreInMemory();
|
|
4875
|
+
}
|
|
4876
|
+
throw error;
|
|
4877
|
+
}
|
|
4878
|
+
finally {
|
|
4879
|
+
workspaceRetirementCommitInFlight = false;
|
|
4880
|
+
}
|
|
4881
|
+
},
|
|
4768
4882
|
hasWorkspaceBinding(cwd, sessionId) {
|
|
4769
4883
|
const normalizedCwd = normalizeTelegramWorkspacePath(cwd);
|
|
4770
4884
|
const normalizedSessionId = sessionId === undefined
|
|
@@ -1487,7 +1487,7 @@ export interface TelegramUpdateAdmissionLifecycleAssemblyDeps<TUpdate extends Te
|
|
|
1487
1487
|
runtimeBinding: TelegramUpdateAdmissionRuntimeBinding<TContext>;
|
|
1488
1488
|
inputCustodyBus?: TelegramInputCustodyBusBindingRuntime<TContext>;
|
|
1489
1489
|
acquireSourceReference?: (role: "leader" | "follower", binding: TelegramUpdateAdmissionLifecycleJournalBinding) => () => void;
|
|
1490
|
-
worker: Omit<TelegramUpdateAdmissionWorkerRuntimeDeps<TUpdate, TContext>, "journal" | "getJournalBindingKey" | "getRecipientBindingKey" | "hasAuthority" | "prepareUpdateForExecution">;
|
|
1490
|
+
worker: Omit<TelegramUpdateAdmissionWorkerRuntimeDeps<TUpdate, TContext>, "journal" | "getJournalBindingKey" | "getRecipientBindingKey" | "hasAuthority" | "prepareUpdateForExecution" | "recoverRejectedRecipientTarget">;
|
|
1491
1491
|
leader: {
|
|
1492
1492
|
resolveBinding: () => TelegramUpdateAdmissionLifecycleJournalBinding | undefined;
|
|
1493
1493
|
hasAuthority: (ctx: TContext) => boolean;
|
|
@@ -1497,6 +1497,7 @@ export interface TelegramUpdateAdmissionLifecycleAssemblyDeps<TUpdate extends Te
|
|
|
1497
1497
|
isRegistered: () => boolean;
|
|
1498
1498
|
getGeneration: () => string | undefined;
|
|
1499
1499
|
prepareUpdateForExecution: (update: TUpdate) => TUpdate;
|
|
1500
|
+
recoverRejectedRecipientTarget?: NonNullable<TelegramUpdateAdmissionWorkerRuntimeDeps<TUpdate, TContext>["recoverRejectedRecipientTarget"]>;
|
|
1500
1501
|
/** Session succession before worker creation, under the exact generation-fenced binding. */
|
|
1501
1502
|
prepareBinding?: (binding: TelegramUpdateAdmissionLifecycleJournalBinding & {
|
|
1502
1503
|
hasAuthority?: () => boolean;
|
|
@@ -1508,6 +1509,19 @@ export type TelegramUpdateAdmissionWorkerRuntimeDeps<TUpdate extends TelegramJou
|
|
|
1508
1509
|
inputCustody?: TelegramCustodyExecutionJournal;
|
|
1509
1510
|
defaultHandle: (update: TUpdate, ctx: TContext, execution?: TelegramUpdateExecutionFence) => Promise<void>;
|
|
1510
1511
|
prepareUpdateForExecution?: (update: TUpdate) => TUpdate;
|
|
1512
|
+
/**
|
|
1513
|
+
* Recipient-only recovery for a journaled message whose own Thread changed (for example after its Thread was re-created):
|
|
1514
|
+
* classifies a leader refusal of a reply to the original Thread and returns that Thread plus the owner's current one.
|
|
1515
|
+
*/
|
|
1516
|
+
recoverRejectedRecipientTarget?: (error: unknown) => {
|
|
1517
|
+
rejected: {
|
|
1518
|
+
chatId: number;
|
|
1519
|
+
threadId: number;
|
|
1520
|
+
};
|
|
1521
|
+
current: TelegramTarget & {
|
|
1522
|
+
threadId: number;
|
|
1523
|
+
};
|
|
1524
|
+
} | undefined;
|
|
1511
1525
|
registry?: TelegramUpdateHandlerRegistry;
|
|
1512
1526
|
};
|
|
1513
1527
|
/** Compose source-bound routing and late grouped settlement under one worker. */
|
|
@@ -6359,12 +6359,14 @@ export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
|
|
|
6359
6359
|
const typedUpdate = update;
|
|
6360
6360
|
const prepared = deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate;
|
|
6361
6361
|
// Journal/source evidence remains the original; only the warm released handler projection changes destination.
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6362
|
+
// Journal/source evidence remains the original; only the warm released handler projection changes destination.
|
|
6363
|
+
const projectTo = (destination) => ({ ...prepared,
|
|
6364
|
+
...(prepared.message ? { message: prepared.message.message_thread_id === destination.threadId ? { ...prepared.message }
|
|
6365
|
+
: withoutTelegramTurnReplyAnchor({ ...prepared.message, message_thread_id: destination.threadId, reply_to_message: undefined }) } : {}),
|
|
6366
|
+
...(prepared.edited_message ? { edited_message: prepared.edited_message.message_thread_id === destination.threadId ? { ...prepared.edited_message }
|
|
6367
|
+
: withoutTelegramTurnReplyAnchor({ ...prepared.edited_message, message_thread_id: destination.threadId, reply_to_message: undefined }) } : {}),
|
|
6368
|
+
});
|
|
6369
|
+
const routed = target ? projectTo(target) : prepared;
|
|
6368
6370
|
try {
|
|
6369
6371
|
return await executeUpdate(routed, ctx, signal);
|
|
6370
6372
|
}
|
|
@@ -6372,6 +6374,17 @@ export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
|
|
|
6372
6374
|
if (await deps.settleTerminalExecutionFailure?.(error)) {
|
|
6373
6375
|
return { kind: "complete" };
|
|
6374
6376
|
}
|
|
6377
|
+
// The leader refused a reply to a Thread this recipient no longer owns (re-created after the input was journaled).
|
|
6378
|
+
// Re-run once against its current Thread; any other failure, or a second refusal, keeps the ordinary retry state.
|
|
6379
|
+
const recovery = !target ? deps.recoverRejectedRecipientTarget?.(error) : undefined;
|
|
6380
|
+
const message = prepared.message ?? prepared.edited_message;
|
|
6381
|
+
if (recovery && message && message.message_thread_id === recovery.rejected.threadId &&
|
|
6382
|
+
message.chat?.id === recovery.rejected.chatId && recovery.current.chatId === recovery.rejected.chatId &&
|
|
6383
|
+
recovery.current.threadId !== recovery.rejected.threadId) {
|
|
6384
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "recipient-target-recovery", updateId: update.update_id,
|
|
6385
|
+
chatId: recovery.current.chatId, staleThreadId: recovery.rejected.threadId, threadId: recovery.current.threadId });
|
|
6386
|
+
return await executeUpdate(projectTo(recovery.current), ctx, signal);
|
|
6387
|
+
}
|
|
6375
6388
|
throw error;
|
|
6376
6389
|
}
|
|
6377
6390
|
},
|
|
@@ -6444,6 +6457,9 @@ export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
|
|
|
6444
6457
|
spendHistoricalInput: undefined,
|
|
6445
6458
|
onHeldSourcesPrepared: undefined,
|
|
6446
6459
|
prepareUpdateForExecution: deps.follower.prepareUpdateForExecution,
|
|
6460
|
+
...(deps.follower.recoverRejectedRecipientTarget
|
|
6461
|
+
? { recoverRejectedRecipientTarget: deps.follower.recoverRejectedRecipientTarget }
|
|
6462
|
+
: {}),
|
|
6447
6463
|
});
|
|
6448
6464
|
},
|
|
6449
6465
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
@@ -61,7 +61,8 @@ interface TelegramWorkspaceRetirementFenceBase {
|
|
|
61
61
|
retirementIntentId: string;
|
|
62
62
|
profileKey: string;
|
|
63
63
|
bindingKey: string;
|
|
64
|
-
|
|
64
|
+
/** Null is admitted only for explicit manual cleanup of a slotless binding. */
|
|
65
|
+
slot: string | null;
|
|
65
66
|
target: TelegramTarget & {
|
|
66
67
|
threadId: number;
|
|
67
68
|
};
|
|
@@ -93,7 +94,7 @@ export interface TelegramWorkspaceDeletionPermit {
|
|
|
93
94
|
readonly retirementIntentId: string;
|
|
94
95
|
readonly profileKey: string;
|
|
95
96
|
readonly bindingKey: string;
|
|
96
|
-
readonly slot: string;
|
|
97
|
+
readonly slot: string | null;
|
|
97
98
|
readonly target: TelegramTarget & {
|
|
98
99
|
threadId: number;
|
|
99
100
|
};
|
|
@@ -205,7 +206,7 @@ export interface TelegramWorkspaceAdmissionLedger {
|
|
|
205
206
|
operationId: string;
|
|
206
207
|
cleanupWorkSetId: string;
|
|
207
208
|
bindingKey: string;
|
|
208
|
-
slot: string;
|
|
209
|
+
slot: string | null;
|
|
209
210
|
target: TelegramTarget & {
|
|
210
211
|
threadId: number;
|
|
211
212
|
};
|