@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
|
@@ -505,6 +505,19 @@ export declare class TelegramApiStaleTargetError extends Error {
|
|
|
505
505
|
threadId: number;
|
|
506
506
|
});
|
|
507
507
|
}
|
|
508
|
+
/** A follower API refusal; legacy text alone does not prove the request was rejected before execution. */
|
|
509
|
+
export declare class TelegramApiFollowerNotAllowedError extends Error {
|
|
510
|
+
readonly requestTarget: {
|
|
511
|
+
chatId: number;
|
|
512
|
+
threadId: number;
|
|
513
|
+
} | undefined;
|
|
514
|
+
/** Present only after a structured pre-execution refusal matches the requested API method. */
|
|
515
|
+
readonly rejectedRequestMethod: string | undefined;
|
|
516
|
+
constructor(message: string, requestTarget?: {
|
|
517
|
+
chatId: number;
|
|
518
|
+
threadId: number;
|
|
519
|
+
}, rejectedRequestMethod?: string);
|
|
520
|
+
}
|
|
508
521
|
export declare function getTelegramApiErrorRequestTarget(error: unknown): {
|
|
509
522
|
chatId: number;
|
|
510
523
|
threadId: number;
|
|
@@ -338,6 +338,18 @@ export class TelegramApiStaleTargetError extends Error {
|
|
|
338
338
|
this.requestTarget = { ...requestTarget };
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
|
+
/** A follower API refusal; legacy text alone does not prove the request was rejected before execution. */
|
|
342
|
+
export class TelegramApiFollowerNotAllowedError extends Error {
|
|
343
|
+
requestTarget;
|
|
344
|
+
/** Present only after a structured pre-execution refusal matches the requested API method. */
|
|
345
|
+
rejectedRequestMethod;
|
|
346
|
+
constructor(message, requestTarget, rejectedRequestMethod) {
|
|
347
|
+
super(message);
|
|
348
|
+
this.name = "TelegramApiFollowerNotAllowedError";
|
|
349
|
+
this.requestTarget = requestTarget ? { ...requestTarget } : undefined;
|
|
350
|
+
this.rejectedRequestMethod = rejectedRequestMethod;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
341
353
|
export function getTelegramApiErrorRequestTarget(error) {
|
|
342
354
|
const target = error instanceof TelegramApiHttpError ||
|
|
343
355
|
error instanceof TelegramApiStaleTargetError
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
* Zones: telegram threads, workspace lifecycle
|
|
4
4
|
* Owns fail-closed cleanup eligibility projection without persistence or Bot API effects
|
|
5
5
|
*/
|
|
6
|
-
import type { TelegramWorkspaceDeletionPermit, TelegramWorkspaceDestructiveFence, TelegramWorkspaceRetirementFence } from "./workspace-admission.ts";
|
|
6
|
+
import type { TelegramWorkspaceDeletionPermit, TelegramWorkspaceDestructiveFence, TelegramWorkspaceRetirementFence, TelegramWorkspaceAdmissionLedger, TelegramWorkspaceAdmissionOwner } from "./workspace-admission.ts";
|
|
7
|
+
import type { TelegramTopicTargetStore, TelegramWorkspaceThreadBinding } from "./threads.ts";
|
|
8
|
+
import type { TelegramWorkspaceOperationRunner } from "./workspace-retirement.ts";
|
|
9
|
+
import type { TelegramWorkspaceThreadDeletionTransport } from "./telegram-api.ts";
|
|
10
|
+
import type { TelegramProcessLiveness } from "./process-identity.ts";
|
|
7
11
|
export interface TelegramThreadCleanupBindingSnapshot {
|
|
8
12
|
cwd: string;
|
|
9
13
|
workspaceKey: string;
|
|
@@ -38,7 +42,8 @@ export interface TelegramThreadCleanupCandidate {
|
|
|
38
42
|
sessionId?: string;
|
|
39
43
|
sessionKey?: string;
|
|
40
44
|
instanceSlot: string;
|
|
41
|
-
|
|
45
|
+
/** Null is explicit absence in reviewed work; it never denotes a reusable letter. */
|
|
46
|
+
slot: string | null;
|
|
42
47
|
target: {
|
|
43
48
|
chatId: number;
|
|
44
49
|
threadId: number;
|
|
@@ -155,6 +160,8 @@ export declare function createTelegramThreadCleanupWorkStore(options: {
|
|
|
155
160
|
runtimeDir?: string;
|
|
156
161
|
profileName: string;
|
|
157
162
|
tokenSha256: string;
|
|
163
|
+
/** Exact captured ledger identity; standalone fixtures may use the plain profile identity. */
|
|
164
|
+
admissionProfileKey?: string;
|
|
158
165
|
maxWorkSets?: number;
|
|
159
166
|
maxBytes?: number;
|
|
160
167
|
getNowMs?: () => number;
|
|
@@ -185,6 +192,7 @@ export declare function executeTelegramInactiveThreadCleanup(input: {
|
|
|
185
192
|
}>;
|
|
186
193
|
export declare function createTelegramThreadCleanupPermitRuntime(deps: {
|
|
187
194
|
ledger: {
|
|
195
|
+
getProfileKey(): string;
|
|
188
196
|
read(): {
|
|
189
197
|
fence?: TelegramWorkspaceDestructiveFence;
|
|
190
198
|
};
|
|
@@ -192,7 +200,7 @@ export declare function createTelegramThreadCleanupPermitRuntime(deps: {
|
|
|
192
200
|
operationId: string;
|
|
193
201
|
cleanupWorkSetId: string;
|
|
194
202
|
bindingKey: string;
|
|
195
|
-
slot: string;
|
|
203
|
+
slot: string | null;
|
|
196
204
|
target: {
|
|
197
205
|
chatId: number;
|
|
198
206
|
threadId: number;
|
|
@@ -256,6 +264,34 @@ export interface TelegramInactiveThreadCleanupCoordinatorDeps<TBinding> {
|
|
|
256
264
|
deleteWithPermit(permit: TelegramThreadCleanupDeletionPermit, candidate: TelegramThreadCleanupCandidate): Promise<void>;
|
|
257
265
|
commitBinding(candidate: TelegramThreadCleanupCandidate, currentBinding?: TBinding): Promise<boolean>;
|
|
258
266
|
}
|
|
267
|
+
export interface TelegramInactiveThreadCleanupAuthority {
|
|
268
|
+
profileName: string;
|
|
269
|
+
leaderEpoch: number | string;
|
|
270
|
+
ledger: TelegramWorkspaceAdmissionLedger;
|
|
271
|
+
isCurrent(): boolean;
|
|
272
|
+
}
|
|
273
|
+
/** Review owns ordinary admission; confirmed cleanup uses the shared gate and its exclusive manual fence instead. */
|
|
274
|
+
export declare function createTelegramInactiveThreadCleanupRuntime(deps: {
|
|
275
|
+
store: Pick<TelegramTopicTargetStore, "load" | "listWorkspaceBindings" | "listReservations" | "listPendingProvisions" | "listPendingCleanups" | "commitInactiveWorkspaceCleanup">;
|
|
276
|
+
captureAuthority(): TelegramInactiveThreadCleanupAuthority | undefined;
|
|
277
|
+
getWorkStore(authority: TelegramInactiveThreadCleanupAuthority): TelegramThreadCleanupWorkStore;
|
|
278
|
+
getProtection(binding: TelegramWorkspaceThreadBinding): {
|
|
279
|
+
liveOwner: TelegramThreadCleanupProtectionState;
|
|
280
|
+
acceptedWork: TelegramThreadCleanupProtectionState;
|
|
281
|
+
deliveryAuthority: TelegramThreadCleanupProtectionState;
|
|
282
|
+
};
|
|
283
|
+
getProcessLiveness(owner: TelegramWorkspaceAdmissionOwner): TelegramProcessLiveness;
|
|
284
|
+
runExclusive<T>(operation: () => Promise<T>): Promise<T>;
|
|
285
|
+
runWorkspaceOperation: TelegramWorkspaceOperationRunner;
|
|
286
|
+
deleteThread: TelegramWorkspaceThreadDeletionTransport;
|
|
287
|
+
onError?: (error: unknown) => void;
|
|
288
|
+
}): {
|
|
289
|
+
review(): Promise<{
|
|
290
|
+
count: number;
|
|
291
|
+
operationId?: string;
|
|
292
|
+
}>;
|
|
293
|
+
clean(operationId: string): Promise<TelegramInactiveThreadCleanupResult>;
|
|
294
|
+
};
|
|
259
295
|
export declare function createTelegramInactiveThreadCleanupSettingsPort<TBinding>(deps: TelegramInactiveThreadCleanupCoordinatorDeps<TBinding>): (operationId: string) => Promise<TelegramInactiveThreadCleanupResult>;
|
|
260
296
|
export interface TelegramInactiveThreadCleanupResult {
|
|
261
297
|
deleted: number;
|
|
@@ -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
|