@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
|
@@ -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 =
|
|
@@ -10495,6 +10495,7 @@ export interface TelegramUpdateAdmissionLifecycleAssemblyDeps<
|
|
|
10495
10495
|
| "getRecipientBindingKey"
|
|
10496
10496
|
| "hasAuthority"
|
|
10497
10497
|
| "prepareUpdateForExecution"
|
|
10498
|
+
| "recoverRejectedRecipientTarget"
|
|
10498
10499
|
>;
|
|
10499
10500
|
leader: {
|
|
10500
10501
|
resolveBinding: () =>
|
|
@@ -10507,6 +10508,9 @@ export interface TelegramUpdateAdmissionLifecycleAssemblyDeps<
|
|
|
10507
10508
|
isRegistered: () => boolean;
|
|
10508
10509
|
getGeneration: () => string | undefined;
|
|
10509
10510
|
prepareUpdateForExecution: (update: TUpdate) => TUpdate;
|
|
10511
|
+
recoverRejectedRecipientTarget?: NonNullable<
|
|
10512
|
+
TelegramUpdateAdmissionWorkerRuntimeDeps<TUpdate, TContext>["recoverRejectedRecipientTarget"]
|
|
10513
|
+
>;
|
|
10510
10514
|
/** Session succession before worker creation, under the exact generation-fenced binding. */
|
|
10511
10515
|
prepareBinding?: (
|
|
10512
10516
|
binding: TelegramUpdateAdmissionLifecycleJournalBinding & {
|
|
@@ -10531,6 +10535,13 @@ export type TelegramUpdateAdmissionWorkerRuntimeDeps<
|
|
|
10531
10535
|
execution?: TelegramUpdateExecutionFence,
|
|
10532
10536
|
) => Promise<void>;
|
|
10533
10537
|
prepareUpdateForExecution?: (update: TUpdate) => TUpdate;
|
|
10538
|
+
/**
|
|
10539
|
+
* Recipient-only recovery for a journaled message whose own Thread changed (for example after its Thread was re-created):
|
|
10540
|
+
* classifies a leader refusal of a reply to the original Thread and returns that Thread plus the owner's current one.
|
|
10541
|
+
*/
|
|
10542
|
+
recoverRejectedRecipientTarget?: (error: unknown) =>
|
|
10543
|
+
| { rejected: { chatId: number; threadId: number }; current: TelegramTarget & { threadId: number } }
|
|
10544
|
+
| undefined;
|
|
10534
10545
|
registry?: TelegramUpdateHandlerRegistry;
|
|
10535
10546
|
};
|
|
10536
10547
|
|
|
@@ -10657,12 +10668,14 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
|
|
|
10657
10668
|
const typedUpdate = update as TUpdate;
|
|
10658
10669
|
const prepared = deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate;
|
|
10659
10670
|
// Journal/source evidence remains the original; only the warm released handler projection changes destination.
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10671
|
+
// Journal/source evidence remains the original; only the warm released handler projection changes destination.
|
|
10672
|
+
const projectTo = (destination: TelegramTarget & { threadId: number }): TUpdate => ({ ...prepared,
|
|
10673
|
+
...(prepared.message ? { message: prepared.message.message_thread_id === destination.threadId ? { ...prepared.message }
|
|
10674
|
+
: withoutTelegramTurnReplyAnchor({ ...prepared.message, message_thread_id: destination.threadId, reply_to_message: undefined }) } : {}),
|
|
10675
|
+
...(prepared.edited_message ? { edited_message: prepared.edited_message.message_thread_id === destination.threadId ? { ...prepared.edited_message }
|
|
10676
|
+
: withoutTelegramTurnReplyAnchor({ ...prepared.edited_message, message_thread_id: destination.threadId, reply_to_message: undefined }) } : {}),
|
|
10677
|
+
} as TUpdate);
|
|
10678
|
+
const routed = target ? projectTo(target) : prepared;
|
|
10666
10679
|
try {
|
|
10667
10680
|
return await executeUpdate(
|
|
10668
10681
|
routed,
|
|
@@ -10673,6 +10686,17 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
|
|
|
10673
10686
|
if (await deps.settleTerminalExecutionFailure?.(error)) {
|
|
10674
10687
|
return { kind: "complete" };
|
|
10675
10688
|
}
|
|
10689
|
+
// The leader refused a reply to a Thread this recipient no longer owns (re-created after the input was journaled).
|
|
10690
|
+
// Re-run once against its current Thread; any other failure, or a second refusal, keeps the ordinary retry state.
|
|
10691
|
+
const recovery = !target ? deps.recoverRejectedRecipientTarget?.(error) : undefined;
|
|
10692
|
+
const message = prepared.message ?? prepared.edited_message;
|
|
10693
|
+
if (recovery && message && message.message_thread_id === recovery.rejected.threadId &&
|
|
10694
|
+
message.chat?.id === recovery.rejected.chatId && recovery.current.chatId === recovery.rejected.chatId &&
|
|
10695
|
+
recovery.current.threadId !== recovery.rejected.threadId) {
|
|
10696
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "recipient-target-recovery", updateId: update.update_id,
|
|
10697
|
+
chatId: recovery.current.chatId, staleThreadId: recovery.rejected.threadId, threadId: recovery.current.threadId });
|
|
10698
|
+
return await executeUpdate(projectTo(recovery.current), ctx, signal);
|
|
10699
|
+
}
|
|
10676
10700
|
throw error;
|
|
10677
10701
|
}
|
|
10678
10702
|
},
|
|
@@ -10753,6 +10777,9 @@ export function createTelegramUpdateAdmissionLifecycleAssembly<
|
|
|
10753
10777
|
spendHistoricalInput: undefined,
|
|
10754
10778
|
onHeldSourcesPrepared: undefined,
|
|
10755
10779
|
prepareUpdateForExecution: deps.follower.prepareUpdateForExecution,
|
|
10780
|
+
...(deps.follower.recoverRejectedRecipientTarget
|
|
10781
|
+
? { recoverRejectedRecipientTarget: deps.follower.recoverRejectedRecipientTarget }
|
|
10782
|
+
: {}),
|
|
10756
10783
|
});
|
|
10757
10784
|
},
|
|
10758
10785
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
@@ -187,7 +187,8 @@ interface TelegramWorkspaceRetirementFenceBase {
|
|
|
187
187
|
retirementIntentId: string;
|
|
188
188
|
profileKey: string;
|
|
189
189
|
bindingKey: string;
|
|
190
|
-
|
|
190
|
+
/** Null is admitted only for explicit manual cleanup of a slotless binding. */
|
|
191
|
+
slot: string | null;
|
|
191
192
|
target: TelegramTarget & { threadId: number };
|
|
192
193
|
leaderEpoch: number | string;
|
|
193
194
|
retirementRequestedAtMs: number;
|
|
@@ -228,7 +229,7 @@ export interface TelegramWorkspaceDeletionPermit {
|
|
|
228
229
|
readonly retirementIntentId: string;
|
|
229
230
|
readonly profileKey: string;
|
|
230
231
|
readonly bindingKey: string;
|
|
231
|
-
readonly slot: string;
|
|
232
|
+
readonly slot: string | null;
|
|
232
233
|
readonly target: TelegramTarget & { threadId: number };
|
|
233
234
|
readonly leaderEpoch: number | string;
|
|
234
235
|
readonly issuedAtMs: number;
|
|
@@ -376,7 +377,7 @@ export interface TelegramWorkspaceAdmissionLedger {
|
|
|
376
377
|
operationId: string;
|
|
377
378
|
cleanupWorkSetId: string;
|
|
378
379
|
bindingKey: string;
|
|
379
|
-
slot: string;
|
|
380
|
+
slot: string | null;
|
|
380
381
|
target: TelegramTarget & { threadId: number };
|
|
381
382
|
leaderEpoch: number | string;
|
|
382
383
|
cleanupRequestedAtMs: number;
|
|
@@ -538,8 +539,8 @@ function normalizeFence(
|
|
|
538
539
|
!isBoundedText(value.retirementIntentId) ||
|
|
539
540
|
value.profileKey !== profileKey ||
|
|
540
541
|
!isBoundedText(value.bindingKey) ||
|
|
541
|
-
typeof value.slot
|
|
542
|
-
|
|
542
|
+
!((typeof value.slot === "string" && /^[A-Z]$/u.test(value.slot)) ||
|
|
543
|
+
(value.destructiveKind === "manual-thread-cleanup" && value.slot === null)) ||
|
|
543
544
|
!target ||
|
|
544
545
|
!isLeaderEpoch(value.leaderEpoch) ||
|
|
545
546
|
!isSafeTimestamp(value.retirementRequestedAtMs) ||
|
|
@@ -1869,7 +1870,7 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1869
1870
|
operationId: string;
|
|
1870
1871
|
retirementIntentId: string;
|
|
1871
1872
|
bindingKey: string;
|
|
1872
|
-
slot: string;
|
|
1873
|
+
slot: string | null;
|
|
1873
1874
|
target: TelegramTarget & { threadId: number };
|
|
1874
1875
|
leaderEpoch: number | string;
|
|
1875
1876
|
retirementRequestedAtMs: number;
|
|
@@ -2307,7 +2308,7 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
2307
2308
|
}),
|
|
2308
2309
|
listReservedSlots: () => {
|
|
2309
2310
|
const fence = read().fence;
|
|
2310
|
-
return fence && isTelegramWorkspaceRetirementFence(fence)
|
|
2311
|
+
return fence && isTelegramWorkspaceRetirementFence(fence) && fence.slot !== null
|
|
2311
2312
|
? [fence.slot]
|
|
2312
2313
|
: [];
|
|
2313
2314
|
},
|
|
@@ -1764,6 +1764,8 @@ export async function runTelegramWorkspaceRetirementLifecycle(input: {
|
|
|
1764
1764
|
owner,
|
|
1765
1765
|
leaderEpoch: epoch,
|
|
1766
1766
|
});
|
|
1767
|
+
if (fence.slot === null)
|
|
1768
|
+
return { kind: "retained", reason: "stale-intent" };
|
|
1767
1769
|
input.admission.completeRetirementFence(fence);
|
|
1768
1770
|
return {
|
|
1769
1771
|
kind: "retired",
|
|
@@ -1892,6 +1894,7 @@ export function createTelegramWorkspaceSlotRotation(
|
|
|
1892
1894
|
binding: TelegramWorkspaceThreadBinding,
|
|
1893
1895
|
) => TelegramWorkspaceExternalProtectionEvidence;
|
|
1894
1896
|
recordEvent: (message: string, details: Record<string, unknown>) => void;
|
|
1897
|
+
onSlotReused?: (bindingKey: string, isCurrent: () => boolean) => Promise<void>;
|
|
1895
1898
|
},
|
|
1896
1899
|
): TelegramWorkspaceCapacityRunner {
|
|
1897
1900
|
const requests = createTelegramWorkspaceOperationGate();
|
|
@@ -1906,7 +1909,7 @@ export function createTelegramWorkspaceSlotRotation(
|
|
|
1906
1909
|
return requests.runExclusive(async () => {
|
|
1907
1910
|
if (!isCurrent())
|
|
1908
1911
|
throw new Error("Telegram Workspace allocation lost leader authority.");
|
|
1909
|
-
const rotate = async () => {
|
|
1912
|
+
const rotate = async (options?: { reuseWhenProtected?: boolean }) => {
|
|
1910
1913
|
const result = await input.runExclusive(async () => {
|
|
1911
1914
|
if (!isCurrent())
|
|
1912
1915
|
throw new Error(
|
|
@@ -1959,6 +1962,57 @@ export function createTelegramWorkspaceSlotRotation(
|
|
|
1959
1962
|
},
|
|
1960
1963
|
});
|
|
1961
1964
|
});
|
|
1965
|
+
if (
|
|
1966
|
+
options?.reuseWhenProtected &&
|
|
1967
|
+
result.kind === "blocked" &&
|
|
1968
|
+
result.stage === "preparation" &&
|
|
1969
|
+
result.reason === "protected-capacity"
|
|
1970
|
+
) {
|
|
1971
|
+
// Every letter is protected, so deleting a Thread is off the table: reassign the
|
|
1972
|
+
// least recently used letter as metadata and keep its Thread, journals and work.
|
|
1973
|
+
const reused = await input.runExclusive(async () => {
|
|
1974
|
+
if (!isCurrent())
|
|
1975
|
+
throw new Error(
|
|
1976
|
+
"Telegram Workspace slot reuse lost leader authority.",
|
|
1977
|
+
);
|
|
1978
|
+
await input.store.load();
|
|
1979
|
+
if (!isCurrent())
|
|
1980
|
+
throw new Error(
|
|
1981
|
+
"Telegram Workspace slot reuse lost leader authority.",
|
|
1982
|
+
);
|
|
1983
|
+
return input.store.reuseLeastRecentlyUsedWorkspaceSlot(isCurrent);
|
|
1984
|
+
});
|
|
1985
|
+
if (!isCurrent())
|
|
1986
|
+
throw new Error("Telegram Workspace rotation lost leader authority.");
|
|
1987
|
+
if (reused) {
|
|
1988
|
+
if (reused.kind === "reused") {
|
|
1989
|
+
// The store commit is final. Await display repair before the next allocation,
|
|
1990
|
+
// but never roll back or repeat a published reuse after a transport failure.
|
|
1991
|
+
try {
|
|
1992
|
+
await input.onSlotReused?.(reused.bindingKey, isCurrent);
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
try {
|
|
1995
|
+
input.recordEvent("Telegram Workspace slot display update failed.", {
|
|
1996
|
+
slot: reused.slot,
|
|
1997
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1998
|
+
});
|
|
1999
|
+
} catch {
|
|
2000
|
+
/* A diagnostic failure grants no rollback or another display attempt. */
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
if (!isCurrent())
|
|
2004
|
+
throw new Error("Telegram Workspace rotation lost leader authority.");
|
|
2005
|
+
try {
|
|
2006
|
+
input.recordEvent("Telegram Workspace slot reused.", {
|
|
2007
|
+
slot: reused.slot,
|
|
2008
|
+
});
|
|
2009
|
+
} catch {
|
|
2010
|
+
/* Diagnostics cannot turn a published reassignment into another attempt. */
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
return { kind: "reused" as const };
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
1962
2016
|
if (
|
|
1963
2017
|
result.kind !== "retired" &&
|
|
1964
2018
|
result.kind !== "not-needed" &&
|
|
@@ -2104,7 +2158,7 @@ export function createTelegramWorkspaceSlotRotation(
|
|
|
2104
2158
|
}
|
|
2105
2159
|
}
|
|
2106
2160
|
}
|
|
2107
|
-
await rotate();
|
|
2161
|
+
await rotate({ reuseWhenProtected: true });
|
|
2108
2162
|
return operation();
|
|
2109
2163
|
}
|
|
2110
2164
|
});
|
|
@@ -27,6 +27,50 @@ export type TelegramWorkspaceSlotAllocation =
|
|
|
27
27
|
| { kind: "reclaim"; candidate: TelegramWorkspaceSlotOccupancy }
|
|
28
28
|
| { kind: "blocked"; reason: "invalid-state" | "protected-capacity" };
|
|
29
29
|
|
|
30
|
+
/** A binding whose letter may be reassigned without touching its Thread, journals or accepted work. */
|
|
31
|
+
export interface TelegramWorkspaceReusableSlot {
|
|
32
|
+
bindingKey: string;
|
|
33
|
+
/** Uppercase display letter currently held by the binding. */
|
|
34
|
+
slot: string;
|
|
35
|
+
/** A live owner counts as most recently used, whatever its stored clocks say. */
|
|
36
|
+
live: boolean;
|
|
37
|
+
/** Last known use: inactivity start for detached bindings, otherwise the last binding update. */
|
|
38
|
+
lastUsedMs: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Metadata-only fallback for a full A-Z pool: pick the least recently used letter,
|
|
43
|
+
* preferring detached bindings over live ones. Unlike pressure retirement this
|
|
44
|
+
* ignores accepted-work protection because reuse deletes nothing.
|
|
45
|
+
* Returns undefined for an empty or malformed candidate set.
|
|
46
|
+
*/
|
|
47
|
+
export function selectTelegramWorkspaceReusableSlot(
|
|
48
|
+
candidates: readonly TelegramWorkspaceReusableSlot[],
|
|
49
|
+
): TelegramWorkspaceReusableSlot | undefined {
|
|
50
|
+
const slots = new Set<string>();
|
|
51
|
+
const keys = new Set<string>();
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
if (
|
|
54
|
+
!/^[A-Z]$/u.test(candidate.slot) ||
|
|
55
|
+
!candidate.bindingKey ||
|
|
56
|
+
!Number.isSafeInteger(candidate.lastUsedMs) ||
|
|
57
|
+
candidate.lastUsedMs < 0 ||
|
|
58
|
+
slots.has(candidate.slot) ||
|
|
59
|
+
keys.has(candidate.bindingKey)
|
|
60
|
+
)
|
|
61
|
+
return undefined;
|
|
62
|
+
slots.add(candidate.slot);
|
|
63
|
+
keys.add(candidate.bindingKey);
|
|
64
|
+
}
|
|
65
|
+
const [selected] = [...candidates].sort(
|
|
66
|
+
(left, right) =>
|
|
67
|
+
Number(left.live) - Number(right.live) ||
|
|
68
|
+
left.lastUsedMs - right.lastUsedMs ||
|
|
69
|
+
left.slot.localeCompare(right.slot),
|
|
70
|
+
);
|
|
71
|
+
return selected ? { ...selected } : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
30
74
|
function isSlot(slot: string): boolean {
|
|
31
75
|
return /^[a-z]$/u.test(slot);
|
|
32
76
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.57.
|
|
3
|
+
"version": "0.57.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"test": "node --experimental-strip-types --import ./tests/fixtures/canonical-tmpdir.ts --test --test-concurrency=4 --test-reporter=dot tests/*.test.ts",
|
|
31
|
+
"test:low-concurrency": "node --experimental-strip-types --import ./tests/fixtures/canonical-tmpdir.ts --test --test-concurrency=2 --test-reporter=dot tests/*.test.ts",
|
|
31
32
|
"test:verbose": "node --experimental-strip-types --import ./tests/fixtures/canonical-tmpdir.ts --test --test-reporter=spec tests/*.test.ts",
|
|
32
33
|
"typecheck": "tsc --noEmit",
|
|
33
34
|
"build": "node scripts/build-dist.mjs",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@llblab/pi-actors": "0.55.0",
|
|
48
|
-
"@llblab/pi-claude-usage": "0.2.
|
|
48
|
+
"@llblab/pi-claude-usage": "0.2.3",
|
|
49
49
|
"@llblab/pi-clean-room": "0.3.0",
|
|
50
|
-
"@llblab/pi-codex-usage": "0.13.
|
|
50
|
+
"@llblab/pi-codex-usage": "0.13.2",
|
|
51
51
|
"@llblab/pi-grow-loop": "0.9.0",
|
|
52
|
-
"@llblab/pi-state-flow": "0.26.
|
|
53
|
-
"@llblab/pi-telegram": "0.57.
|
|
52
|
+
"@llblab/pi-state-flow": "0.26.5",
|
|
53
|
+
"@llblab/pi-telegram": "0.57.7",
|
|
54
54
|
"@llblab/skills": "1.15.0"
|
|
55
55
|
},
|
|
56
56
|
"bundledDependencies": [
|