@llblab/pi-kit 0.33.1 → 0.34.1
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 +2 -1
- package/CHANGELOG.md +9 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-telegram/AGENTS.md +6 -5
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +6 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +20 -0
- package/node_modules/@llblab/pi-telegram/README.md +28 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +8 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +41 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +10 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +34 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +8 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +21 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +33 -28
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +18 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +28 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +100 -99
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +57 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +313 -25
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +4 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +62 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +6 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +11 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +11 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +57 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +13 -32
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +44 -15
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +25 -14
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +4 -0
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +8 -6
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +17 -17
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +52 -3
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +13 -5
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +37 -23
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +23 -7
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +52 -47
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +18 -2
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +34 -28
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +114 -162
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +8 -0
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +7 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +328 -33
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +51 -1
- package/node_modules/@llblab/pi-telegram/lib/status.ts +7 -7
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +57 -5
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +44 -21
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
|
@@ -916,6 +916,16 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
916
916
|
}
|
|
917
917
|
: undefined;
|
|
918
918
|
},
|
|
919
|
+
/** Observe only an already-issued leader command; never prepares, relocates or executes input. */
|
|
920
|
+
observeCommandCompletion(): "unknown" | "released" | undefined {
|
|
921
|
+
if (!continuationIssued || !leader?.complete) return undefined;
|
|
922
|
+
if (!current()) return "unknown";
|
|
923
|
+
try { return observeCommandCompletion(); }
|
|
924
|
+
catch (error) {
|
|
925
|
+
input.recordRuntimeEvent?.("telegram", error, { phase: "live-rebind-command-observation" });
|
|
926
|
+
return "unknown";
|
|
927
|
+
}
|
|
928
|
+
},
|
|
919
929
|
async advance(): Promise<"protected" | "unknown" | "released"> {
|
|
920
930
|
if (advancing || !current()) return "protected";
|
|
921
931
|
if (released) return "released";
|
|
@@ -1561,6 +1571,8 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
1561
1571
|
/** Quiet period after the last cancelled input before one cleanup attempt; defaults to 0 ms. */
|
|
1562
1572
|
temporaryThreadCleanupDelayMs?: number;
|
|
1563
1573
|
/** Live-rebind cleanup attempt pacing; the bounded window ends in `not-issued`, never a time-based deletion. */
|
|
1574
|
+
/** Bounded ACK observation inside the original chooser admission, independent from menu/reply delivery. */
|
|
1575
|
+
liveRebindCommandCompletionTimeoutMs?: number;
|
|
1564
1576
|
liveRebindCleanupSchedule?: {
|
|
1565
1577
|
delaysMs: readonly number[];
|
|
1566
1578
|
intervalMs: number;
|
|
@@ -1937,6 +1949,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1937
1949
|
extensionCommand?: Commands.ParsedTelegramCommand;
|
|
1938
1950
|
extensionKind?: Commands.PreparedSelectedCommand["kind"];
|
|
1939
1951
|
coordinator?: ReturnType<typeof createTelegramLiveRebindCoordinator>;
|
|
1952
|
+
commandCompletionWaited?: true;
|
|
1940
1953
|
isCurrent?: () => boolean;
|
|
1941
1954
|
};
|
|
1942
1955
|
/** The acknowledged tab presenting an All source; `sourceTarget` is this tab, not the original's location. */
|
|
@@ -4870,7 +4883,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
4870
4883
|
Bus.isSameTelegramBusFollowerRegistration(live, follower!) &&
|
|
4871
4884
|
[
|
|
4872
4885
|
...Bus.TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
|
|
4873
|
-
...(heldSelected ? Bus.
|
|
4886
|
+
...(heldSelected ? Bus.getTelegramBusHeldCommandCapabilities(heldSelected.name) : []),
|
|
4874
4887
|
].every((cap) => Bus.hasTelegramBusCapability(live.protocol, cap)) &&
|
|
4875
4888
|
!!live.target &&
|
|
4876
4889
|
live.target.chatId === binding.target.chatId &&
|
|
@@ -5642,6 +5655,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5642
5655
|
return {
|
|
5643
5656
|
operationId: request.operationId,
|
|
5644
5657
|
selectedCommandReference: () => coordinator?.selectedCommandReference(),
|
|
5658
|
+
observeCommandCompletion: () => coordinator?.observeCommandCompletion(),
|
|
5645
5659
|
async advance() {
|
|
5646
5660
|
if (!coordinator) {
|
|
5647
5661
|
if (!current() || !getRecipient()) return "protected";
|
|
@@ -9058,6 +9072,42 @@ export function createTelegramInboundRouteRuntime<
|
|
|
9058
9072
|
if (!(await selectPendingDestination())) return true;
|
|
9059
9073
|
const operationId = selection.coordinator.operationId;
|
|
9060
9074
|
if (current()) outcome = await selection.coordinator.advance();
|
|
9075
|
+
const completionTimeoutMs = deps.liveRebindCommandCompletionTimeoutMs ?? 0;
|
|
9076
|
+
if (outcome === "unknown" && !selection.commandCompletionWaited && selection.record.owner?.kind === "leader" &&
|
|
9077
|
+
(selection.helpCommand || selection.menuCommand || selection.abortCommand || selection.nextCommand || selection.stopCommand) &&
|
|
9078
|
+
Number.isFinite(completionTimeoutMs) && completionTimeoutMs > 0 &&
|
|
9079
|
+
selection.coordinator.observeCommandCompletion() !== undefined) {
|
|
9080
|
+
selection.commandCompletionWaited = true;
|
|
9081
|
+
const coordinator = selection.coordinator;
|
|
9082
|
+
const signal = Updates.getTelegramUpdateExecutionFence(query)?.signal;
|
|
9083
|
+
outcome = await new Promise<"unknown" | "released">((resolve) => {
|
|
9084
|
+
let settled = false;
|
|
9085
|
+
const subscriptions: (() => void)[] = [];
|
|
9086
|
+
const finish = (result: "unknown" | "released") => {
|
|
9087
|
+
if (settled) return;
|
|
9088
|
+
settled = true;
|
|
9089
|
+
clearTimeout(timer);
|
|
9090
|
+
for (const unsubscribe of subscriptions) unsubscribe();
|
|
9091
|
+
signal?.removeEventListener("abort", abort);
|
|
9092
|
+
resolve(result);
|
|
9093
|
+
};
|
|
9094
|
+
const abort = () => finish("unknown");
|
|
9095
|
+
const observe = () => {
|
|
9096
|
+
if (settled) return;
|
|
9097
|
+
if (!current()) { finish("unknown"); return; }
|
|
9098
|
+
if (coordinator.observeCommandCompletion() === "released") finish("released");
|
|
9099
|
+
};
|
|
9100
|
+
const timer = setTimeout(abort, completionTimeoutMs);
|
|
9101
|
+
timer.unref?.();
|
|
9102
|
+
for (const message of selection.messages) {
|
|
9103
|
+
const unsubscribe = Updates.subscribeTelegramDeferredSourceCompletion(message, observe);
|
|
9104
|
+
if (unsubscribe) subscriptions.push(unsubscribe);
|
|
9105
|
+
}
|
|
9106
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
9107
|
+
if (signal?.aborted || !current()) abort();
|
|
9108
|
+
else if (coordinator.observeCommandCompletion() === "released") finish("released");
|
|
9109
|
+
});
|
|
9110
|
+
}
|
|
9061
9111
|
// Cleanup readiness follows the canonical released row, not source disposal: a detached command ACK may
|
|
9062
9112
|
// still be pending while every attempt samples current work afresh.
|
|
9063
9113
|
const row =
|
|
@@ -66,7 +66,7 @@ export function formatTelegramConnectionFailure(error: unknown): string {
|
|
|
66
66
|
|
|
67
67
|
const TELEGRAM_STATUS_DEFAULT_PROFILE_NAME = "default";
|
|
68
68
|
|
|
69
|
-
export type TelegramStatusQueueLane = "control" | "priority" | "default";
|
|
69
|
+
export type TelegramStatusQueueLane = "control" | "steer" | "priority" | "default";
|
|
70
70
|
|
|
71
71
|
export interface TelegramUsageStats {
|
|
72
72
|
totalInput: number;
|
|
@@ -441,7 +441,6 @@ export interface TelegramBridgeStatusRuntimeDeps<
|
|
|
441
441
|
hasPendingModelSwitch: () => boolean;
|
|
442
442
|
getQueuedItems: () => TQueueItem[];
|
|
443
443
|
getQueuedItemCount?: (items: TQueueItem[]) => number;
|
|
444
|
-
formatQueuedStatus: (items: TQueueItem[]) => string;
|
|
445
444
|
getRecentRuntimeEvents: () => TelegramRuntimeEvent[];
|
|
446
445
|
getRuntimeLockState?: () => string;
|
|
447
446
|
getBusRole?: () => TelegramBridgeBusRole | undefined;
|
|
@@ -1200,9 +1199,7 @@ export function buildTelegramStatusBarText(
|
|
|
1200
1199
|
state: TelegramStatusBarState,
|
|
1201
1200
|
): string {
|
|
1202
1201
|
const label = theme.fg("accent", getTelegramStatusBarLabel(state));
|
|
1203
|
-
const queued = state.queuedStatus
|
|
1204
|
-
? theme.fg("warning", state.queuedStatus)
|
|
1205
|
-
: "";
|
|
1202
|
+
const queued = state.queuedStatus ? theme.fg("warning", state.queuedStatus) : "";
|
|
1206
1203
|
if (!state.hasBotToken)
|
|
1207
1204
|
return `${label} ${theme.fg("muted", "not configured")}${queued}`;
|
|
1208
1205
|
if (
|
|
@@ -1612,6 +1609,7 @@ function countTelegramQueueLanes(
|
|
|
1612
1609
|
): Record<TelegramStatusQueueLane, number> {
|
|
1613
1610
|
const counts: Record<TelegramStatusQueueLane, number> = {
|
|
1614
1611
|
control: 0,
|
|
1612
|
+
steer: 0,
|
|
1615
1613
|
priority: 0,
|
|
1616
1614
|
default: 0,
|
|
1617
1615
|
};
|
|
@@ -1626,10 +1624,11 @@ function buildTelegramBridgeCompactStatusLines(
|
|
|
1626
1624
|
control: controlQueueCount,
|
|
1627
1625
|
priority: priorityQueueCount,
|
|
1628
1626
|
default: defaultQueueCount,
|
|
1627
|
+
steer: steerQueueCount,
|
|
1629
1628
|
} = countTelegramQueueLanes(state.queuedItems);
|
|
1630
1629
|
const queueLine = `- queued turns: ${state.queuedItems.length}${
|
|
1631
1630
|
state.queuedItems.length > 0
|
|
1632
|
-
? ` (control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount})`
|
|
1631
|
+
? ` (control=${controlQueueCount}${steerQueueCount > 0 ? `, steer=${steerQueueCount}` : ""}, priority=${priorityQueueCount}, default=${defaultQueueCount})`
|
|
1633
1632
|
: ""
|
|
1634
1633
|
}`;
|
|
1635
1634
|
const executionState = state.pendingDispatch
|
|
@@ -1698,6 +1697,7 @@ function buildTelegramBridgeDiagnosticStatusLines(
|
|
|
1698
1697
|
control: controlQueueCount,
|
|
1699
1698
|
priority: priorityQueueCount,
|
|
1700
1699
|
default: defaultQueueCount,
|
|
1700
|
+
steer: steerQueueCount,
|
|
1701
1701
|
} = countTelegramQueueLanes(state.queuedItems);
|
|
1702
1702
|
return [
|
|
1703
1703
|
"connection:",
|
|
@@ -1744,7 +1744,7 @@ function buildTelegramBridgeDiagnosticStatusLines(
|
|
|
1744
1744
|
"",
|
|
1745
1745
|
"queue:",
|
|
1746
1746
|
`- queued turns: ${state.queuedItems.length}`,
|
|
1747
|
-
`- lanes: control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount}`,
|
|
1747
|
+
`- lanes: control=${controlQueueCount}${steerQueueCount > 0 ? `, steer=${steerQueueCount}` : ""}, priority=${priorityQueueCount}, default=${defaultQueueCount}`,
|
|
1748
1748
|
...buildTelegramBusFollowerLines(state),
|
|
1749
1749
|
...buildTelegramLocalBusLines(state, { verbose: true }),
|
|
1750
1750
|
...buildTelegramTopicTargetDiagnosticLines(state),
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
isSameTelegramProcessInstance,
|
|
19
19
|
isTelegramTopicTargetStaleError,
|
|
20
20
|
provisionOwnBusTopic,
|
|
21
|
+
resetTelegramConnectionProvisioning,
|
|
21
22
|
type TelegramOwnTopicProvisionResult,
|
|
22
23
|
type TelegramTopicTargetStore,
|
|
23
24
|
type TelegramWorkspaceDisplayBinding,
|
|
@@ -826,6 +827,15 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
826
827
|
...(deps.telegramProfile ? { telegramProfile: deps.telegramProfile } : {}),
|
|
827
828
|
};
|
|
828
829
|
const leaderProfileKey = getTelegramThreadOwnerKey(leaderOwner);
|
|
830
|
+
if (deps.forceFreshUnnamed) {
|
|
831
|
+
await resetTelegramConnectionProvisioning({
|
|
832
|
+
store: deps.topicTargetStore, owner: "leader", instanceId: deps.instanceId,
|
|
833
|
+
profileKey: leaderProfileKey, isCurrent: () => {
|
|
834
|
+
assertLeaderEpoch("connect-recovery");
|
|
835
|
+
return true;
|
|
836
|
+
}, recordEvent: deps.recordEvent,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
829
839
|
const legacyLeaderRecord =
|
|
830
840
|
deps.topicTargetStore.getByProfileKey(leaderProfileKey);
|
|
831
841
|
let capacityUnavailable = false;
|
|
@@ -836,6 +846,7 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
836
846
|
legacyLeaderRecord?.instanceId,
|
|
837
847
|
{
|
|
838
848
|
sessionId: deps.sessionId,
|
|
849
|
+
provisionProfileKey: leaderProfileKey,
|
|
839
850
|
onCapacityUnavailable() {
|
|
840
851
|
capacityUnavailable = true;
|
|
841
852
|
},
|
|
@@ -783,6 +783,7 @@ export interface TelegramTopicTargetStore {
|
|
|
783
783
|
) => { slot?: string; threadName?: string } | undefined;
|
|
784
784
|
listReservations: () => TelegramThreadReservation[];
|
|
785
785
|
listPendingProvisions: () => TelegramThreadPendingProvision[];
|
|
786
|
+
resetPendingProvisionForConnect: (expected: TelegramThreadPendingProvision, isCurrent: () => boolean) => Promise<boolean>;
|
|
786
787
|
listPendingCleanups: () => TelegramThreadCleanupIntent[];
|
|
787
788
|
listSyncObservations: () => TelegramTopicSyncObservation[];
|
|
788
789
|
reserveThread: (reservation: TelegramThreadReservation) => void;
|
|
@@ -902,6 +903,7 @@ export interface TelegramTopicTargetStore {
|
|
|
902
903
|
options?: {
|
|
903
904
|
existingBindingOnly?: boolean;
|
|
904
905
|
sessionId?: string;
|
|
906
|
+
provisionProfileKey?: string;
|
|
905
907
|
onCapacityUnavailable?: () => void;
|
|
906
908
|
},
|
|
907
909
|
) => TelegramWorkspaceBindingIdentity | undefined;
|
|
@@ -4168,6 +4170,7 @@ export function createTelegramTopicTargetStore(
|
|
|
4168
4170
|
}
|
|
4169
4171
|
),
|
|
4170
4172
|
isCurrentPublication?: () => boolean,
|
|
4173
|
+
resetProvisions?: readonly TelegramThreadPendingProvision[],
|
|
4171
4174
|
): Promise<boolean> => {
|
|
4172
4175
|
const submittedPath = getPath(),
|
|
4173
4176
|
submittedProfile = activeProfile();
|
|
@@ -4367,6 +4370,13 @@ export function createTelegramTopicTargetStore(
|
|
|
4367
4370
|
!!expectedRestore,
|
|
4368
4371
|
),
|
|
4369
4372
|
);
|
|
4373
|
+
if (resetProvisions) {
|
|
4374
|
+
const recoveries = readProvisionRecoveries(path, true);
|
|
4375
|
+
if (mutationRevision !== persistedRevision || resetProvisions.some(expected =>
|
|
4376
|
+
expected.target || Object.hasOwn(recoveries, expected.id) ||
|
|
4377
|
+
!isDeepStrictEqual(disk.pendingProvisions?.find(value => value.id === expected.id), expected)))
|
|
4378
|
+
throw new Error("Telegram connection recovery provisioning evidence changed.");
|
|
4379
|
+
}
|
|
4370
4380
|
if (!isDeepStrictEqual(disk.workspaceRestore, expectedRestore))
|
|
4371
4381
|
throw new Error(
|
|
4372
4382
|
"Workspace Restore evidence changed before publication.",
|
|
@@ -4429,7 +4439,7 @@ export function createTelegramTopicTargetStore(
|
|
|
4429
4439
|
}
|
|
4430
4440
|
: {}),
|
|
4431
4441
|
reservations: reservations.map((reservation) => ({ ...reservation })),
|
|
4432
|
-
pendingProvisions: pendingProvisions.map((provision) => ({
|
|
4442
|
+
pendingProvisions: pendingProvisions.filter(provision => !resetProvisions?.some(expected => expected.id === provision.id)).map((provision) => ({
|
|
4433
4443
|
...provision,
|
|
4434
4444
|
...(provision.target ? { target: { ...provision.target } } : {}),
|
|
4435
4445
|
})),
|
|
@@ -4636,6 +4646,7 @@ export function createTelegramTopicTargetStore(
|
|
|
4636
4646
|
loadedProfile = submittedProfile;
|
|
4637
4647
|
loaded = true;
|
|
4638
4648
|
observedWorkspaceSemantic = getTelegramStateSemanticSnapshot(wire);
|
|
4649
|
+
if (resetProvisions) { pendingProvisions = file.pendingProvisions; mutationRevision += 1; }
|
|
4639
4650
|
if (transition) {
|
|
4640
4651
|
records = new Map(
|
|
4641
4652
|
file.threads.map((record) => {
|
|
@@ -4808,6 +4819,10 @@ export function createTelegramTopicTargetStore(
|
|
|
4808
4819
|
if (isCurrentPublication?.() === false) return false;
|
|
4809
4820
|
loadedPath = path;
|
|
4810
4821
|
loaded = true;
|
|
4822
|
+
if (resetProvisions) {
|
|
4823
|
+
if (mutationRevision !== persistedRevision) return false;
|
|
4824
|
+
pendingProvisions = file.pendingProvisions;
|
|
4825
|
+
}
|
|
4811
4826
|
if (mutationRevision === persistedRevision) dirty = false;
|
|
4812
4827
|
return true;
|
|
4813
4828
|
});
|
|
@@ -6814,6 +6829,11 @@ export function createTelegramTopicTargetStore(
|
|
|
6814
6829
|
current.status = "ambiguous";
|
|
6815
6830
|
return true;
|
|
6816
6831
|
},
|
|
6832
|
+
resetPendingProvisionForConnect(expected, isCurrent) {
|
|
6833
|
+
// Stage no in-memory deletion: refusal or a stale publication must keep its protective hold.
|
|
6834
|
+
if (expected.target || !isCurrent()) return Promise.resolve(false);
|
|
6835
|
+
return persistSnapshot(undefined, isCurrent, [structuredClone(expected)]);
|
|
6836
|
+
},
|
|
6817
6837
|
removePendingProvision(id) {
|
|
6818
6838
|
const before = pendingProvisions.length;
|
|
6819
6839
|
pendingProvisions = pendingProvisions.filter(
|
|
@@ -7608,8 +7628,14 @@ export function createTelegramTopicTargetStore(
|
|
|
7608
7628
|
) {
|
|
7609
7629
|
return undefined;
|
|
7610
7630
|
}
|
|
7611
|
-
const
|
|
7612
|
-
|
|
7631
|
+
const ownedProvisions = pendingProvisions.filter(value =>
|
|
7632
|
+
value.workspaceBindingKey === identity.bindingKey &&
|
|
7633
|
+
(value.instanceId === instanceId || value.instanceId === effectivePreviousInstanceId ||
|
|
7634
|
+
(options?.provisionProfileKey !== undefined && value.profileKey === options.provisionProfileKey)));
|
|
7635
|
+
if (ownedProvisions.length > 1) return undefined;
|
|
7636
|
+
const ownProvision = ownedProvisions[0];
|
|
7637
|
+
const retainedTarget = ownProvision?.target ?? binding?.target ?? legacyRecord?.target;
|
|
7638
|
+
const retainedSlot = ownProvision?.slot ?? binding?.slot ?? legacyRecord?.slot;
|
|
7613
7639
|
const otherBindings = Array.from(workspaceBindings.values())
|
|
7614
7640
|
.filter(
|
|
7615
7641
|
(other) => other.bindingKey !== identity.bindingKey && other.slot,
|
|
@@ -7641,7 +7667,7 @@ export function createTelegramTopicTargetStore(
|
|
|
7641
7667
|
)
|
|
7642
7668
|
.map((reservation) => reservation.slot),
|
|
7643
7669
|
...pendingProvisions
|
|
7644
|
-
.filter((provision) =>
|
|
7670
|
+
.filter((provision) => provision !== ownProvision &&
|
|
7645
7671
|
isPendingProvisionLiveOrTargeted(provision, nowMs),
|
|
7646
7672
|
)
|
|
7647
7673
|
.map((provision) => provision.slot),
|
|
@@ -9661,6 +9687,32 @@ export function createTelegramTopicTargetRenamer(
|
|
|
9661
9687
|
};
|
|
9662
9688
|
}
|
|
9663
9689
|
|
|
9690
|
+
/** Caller holds profile Workspace admission; only an explicit connect grants a fresh creation attempt. */
|
|
9691
|
+
export async function resetTelegramConnectionProvisioning(input: {
|
|
9692
|
+
store: TelegramTopicTargetStore;
|
|
9693
|
+
owner: TelegramThreadPendingProvision["owner"];
|
|
9694
|
+
instanceId: string;
|
|
9695
|
+
profileKey: string;
|
|
9696
|
+
isCurrent: () => boolean;
|
|
9697
|
+
recordEvent: (category: string, message: unknown, details?: Record<string, unknown>) => void;
|
|
9698
|
+
}): Promise<void> {
|
|
9699
|
+
if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
|
|
9700
|
+
const removed: TelegramThreadPendingProvision[] = [];
|
|
9701
|
+
for (const pending of input.store.listPendingProvisions()) {
|
|
9702
|
+
if (pending.owner !== input.owner || pending.target ||
|
|
9703
|
+
(pending.profileKey !== input.profileKey && pending.instanceId !== input.instanceId)) continue;
|
|
9704
|
+
if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
|
|
9705
|
+
if (!await input.store.resetPendingProvisionForConnect(pending, input.isCurrent))
|
|
9706
|
+
throw new Error("Telegram connection recovery lost publication authority.");
|
|
9707
|
+
if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
|
|
9708
|
+
removed.push(pending);
|
|
9709
|
+
}
|
|
9710
|
+
if (removed.length === 0) return;
|
|
9711
|
+
input.recordEvent("connection", "Explicit connect reset unfinished Thread creation; an unconfirmed Telegram Thread may remain.", {
|
|
9712
|
+
phase: "connect-provision-reset", count: removed.length, owner: input.owner,
|
|
9713
|
+
});
|
|
9714
|
+
}
|
|
9715
|
+
|
|
9664
9716
|
export function createTelegramTopicTargetProvisioner(
|
|
9665
9717
|
deps: TelegramTopicTargetProvisionerDeps,
|
|
9666
9718
|
): (
|
|
@@ -9923,7 +9975,7 @@ export function createTelegramTopicTargetProvisioner(
|
|
|
9923
9975
|
slot,
|
|
9924
9976
|
);
|
|
9925
9977
|
}
|
|
9926
|
-
const pendingId = `provision:${request.instanceId}:${slot}:${nowMs}`;
|
|
9978
|
+
const pendingId = `provision:${request.instanceId}:${slot}:${nowMs}:${randomUUID()}`;
|
|
9927
9979
|
const pendingOwner =
|
|
9928
9980
|
request.owner?.kind === "leader" ? "leader" : "manual-follower";
|
|
9929
9981
|
assertLeaderEpoch("before-pending-intent");
|
|
@@ -108,17 +108,10 @@ export type TelegramReactionType =
|
|
|
108
108
|
|
|
109
109
|
export const TELEGRAM_PRIORITY_REACTIONS = [
|
|
110
110
|
{ id: 10, name: "like", emoji: "👍" },
|
|
111
|
-
{ id: 11, name: "lightning", emoji: "⚡" },
|
|
112
|
-
{ id: 12, name: "heart", emoji: "❤" },
|
|
113
|
-
{ id: 13, name: "dove", emoji: "🕊" },
|
|
114
|
-
{ id: 14, name: "fire", emoji: "🔥" },
|
|
115
111
|
] as const;
|
|
112
|
+
export const TELEGRAM_STEER_REACTIONS = [{ id: 14, name: "fire", emoji: "🔥" }] as const;
|
|
116
113
|
export const TELEGRAM_REMOVAL_REACTIONS = [
|
|
117
114
|
{ id: 20, name: "dislike", emoji: "👎" },
|
|
118
|
-
{ id: 21, name: "ghost", emoji: "👻" },
|
|
119
|
-
{ id: 22, name: "broken-heart", emoji: "💔" },
|
|
120
|
-
{ id: 23, name: "poop", emoji: "💩" },
|
|
121
|
-
{ id: 24, name: "wastebasket", emoji: "🗑" },
|
|
122
115
|
] as const;
|
|
123
116
|
export const TELEGRAM_PRIORITY_REACTION_EMOJIS =
|
|
124
117
|
TELEGRAM_PRIORITY_REACTIONS.map((reaction) => reaction.emoji);
|
|
@@ -160,14 +153,10 @@ function getTelegramQueueReactionTransition(
|
|
|
160
153
|
): TelegramQueueReactionDisposition | undefined {
|
|
161
154
|
const oldEmojis = collectTelegramReactionEmojis(oldReactions);
|
|
162
155
|
const newEmojis = collectTelegramReactionEmojis(newReactions);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
);
|
|
167
|
-
const newPriorityEmoji = getTelegramReactionEmoji(
|
|
168
|
-
newEmojis,
|
|
169
|
-
TELEGRAM_PRIORITY_REACTION_EMOJIS,
|
|
170
|
-
);
|
|
156
|
+
// Fire selects Steer; thumbs-up beneath it does not reorder the lane. Other emoji carry no queue intent.
|
|
157
|
+
const steerEmoji = TELEGRAM_STEER_REACTIONS[0].emoji;
|
|
158
|
+
const oldPriorityEmoji = oldEmojis.has(steerEmoji) ? steerEmoji : getTelegramReactionEmoji(oldEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
|
|
159
|
+
const newPriorityEmoji = newEmojis.has(steerEmoji) ? steerEmoji : getTelegramReactionEmoji(newEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
|
|
171
160
|
const oldSuppressionEmoji = getTelegramReactionEmoji(
|
|
172
161
|
oldEmojis,
|
|
173
162
|
TELEGRAM_REMOVAL_REACTION_EMOJIS,
|
|
@@ -188,6 +177,7 @@ function getTelegramQueueReactionTransition(
|
|
|
188
177
|
> = { kind: "reaction-transition" };
|
|
189
178
|
if (oldPriorityEmoji !== newPriorityEmoji) {
|
|
190
179
|
transition.priorityEmoji = newPriorityEmoji ?? null;
|
|
180
|
+
if (newPriorityEmoji === steerEmoji) transition.promptLane = "steer";
|
|
191
181
|
}
|
|
192
182
|
if (oldSuppressionEmoji !== newSuppressionEmoji) {
|
|
193
183
|
transition.suppressionEmoji = newSuppressionEmoji ?? null;
|
|
@@ -551,6 +541,7 @@ interface TelegramUpdateAdmissionBinding {
|
|
|
551
541
|
inspectSource?: () => TelegramDeferredSourceEvidence | undefined;
|
|
552
542
|
inspectSourceSnapshot?: () => TelegramDeferredSourceSnapshot | undefined;
|
|
553
543
|
inspectCompletion?: () => TelegramDeferredSourceEvidence | undefined;
|
|
544
|
+
subscribeCompletion?: (notify: () => void) => () => void;
|
|
554
545
|
isSourceUnsettled?: () => boolean;
|
|
555
546
|
prepareLiveInput?: (
|
|
556
547
|
sourceUpdateIds: readonly number[],
|
|
@@ -619,6 +610,7 @@ export function bindTelegramUpdateAdmissionSource<
|
|
|
619
610
|
| "inspectSource"
|
|
620
611
|
| "inspectSourceSnapshot"
|
|
621
612
|
| "inspectCompletion"
|
|
613
|
+
| "subscribeCompletion"
|
|
622
614
|
| "isSourceUnsettled"
|
|
623
615
|
| "acknowledgeTransferred"
|
|
624
616
|
| "prepareLiveInput"
|
|
@@ -747,6 +739,15 @@ export function inspectTelegramDeferredSourceCompletion(
|
|
|
747
739
|
return execution.isCurrent() && source ? { ...source } : undefined;
|
|
748
740
|
}
|
|
749
741
|
|
|
742
|
+
/** Warm ACK notification only; subscribers must independently recheck exact completion and recipient authority. */
|
|
743
|
+
export function subscribeTelegramDeferredSourceCompletion(value: unknown, notify: () => void): (() => void) | undefined {
|
|
744
|
+
const execution = getTelegramUpdateExecutionFence(value);
|
|
745
|
+
if (execution?.isCurrent() !== true) return undefined;
|
|
746
|
+
return getTelegramUpdateAdmissionBinding(value)?.subscribeCompletion?.(() => {
|
|
747
|
+
if (execution.isCurrent()) notify();
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
750
751
|
/** Reuse only live bound deferred originals; raw IDs or cold/adopted input cannot mint this carrier. */
|
|
751
752
|
export function prepareTelegramLiveDeferredInput(
|
|
752
753
|
values: readonly unknown[],
|
|
@@ -8725,6 +8726,7 @@ function createTelegramUpdateSourceAdmissionHandle<
|
|
|
8725
8726
|
let outcome: TelegramUpdateAdmissionOutcome | undefined;
|
|
8726
8727
|
let completionAttempted = false;
|
|
8727
8728
|
let completedSource: TelegramDeferredSourceCompletion | undefined;
|
|
8729
|
+
const completionObservers = new Set<() => void>();
|
|
8728
8730
|
const boundUpdate = bindTelegramUpdateExecutionFence(
|
|
8729
8731
|
bindTelegramUpdateAdmissionSource(
|
|
8730
8732
|
update,
|
|
@@ -8781,11 +8783,12 @@ function createTelegramUpdateSourceAdmissionHandle<
|
|
|
8781
8783
|
execution.isCurrent() &&
|
|
8782
8784
|
completion.isCurrent() &&
|
|
8783
8785
|
isDeepStrictEqual(completion.source, expectedSource)
|
|
8784
|
-
)
|
|
8785
|
-
completedSource = {
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
}
|
|
8786
|
+
) {
|
|
8787
|
+
completedSource = { ...completion, source: { ...expectedSource } };
|
|
8788
|
+
for (const notify of completionObservers) {
|
|
8789
|
+
try { notify(); } catch { /* Observation cannot invalidate an exact removal ACK. */ }
|
|
8790
|
+
}
|
|
8791
|
+
}
|
|
8789
8792
|
})
|
|
8790
8793
|
.catch((error) => {
|
|
8791
8794
|
try {
|
|
@@ -8846,6 +8849,10 @@ function createTelegramUpdateSourceAdmissionHandle<
|
|
|
8846
8849
|
signal,
|
|
8847
8850
|
})
|
|
8848
8851
|
: undefined,
|
|
8852
|
+
subscribeCompletion: (notify) => {
|
|
8853
|
+
completionObservers.add(notify);
|
|
8854
|
+
return () => { completionObservers.delete(notify); };
|
|
8855
|
+
},
|
|
8849
8856
|
inspectCompletion: () =>
|
|
8850
8857
|
execution.isCurrent() && completedSource?.isCurrent()
|
|
8851
8858
|
? { ...completedSource.source }
|
|
@@ -9322,6 +9329,7 @@ export function createTelegramQueueHandoffControlExecutionFactory<TContext>(
|
|
|
9322
9329
|
|
|
9323
9330
|
export interface TelegramQueueHandoffCoordinatorInput<TContext> {
|
|
9324
9331
|
item: TelegramQueueItem<TContext>;
|
|
9332
|
+
steerSupported?: boolean;
|
|
9325
9333
|
expectedOwner: TelegramUpdateJournalQueueOwner;
|
|
9326
9334
|
recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
|
|
9327
9335
|
handoffToken: string;
|
|
@@ -9377,6 +9385,7 @@ export async function coordinateTelegramQueueHandoff<TContext>(
|
|
|
9377
9385
|
const handoff = createTelegramQueueHandoff({
|
|
9378
9386
|
handoffToken: input.handoffToken,
|
|
9379
9387
|
item: input.item,
|
|
9388
|
+
steerSupported: input.steerSupported,
|
|
9380
9389
|
});
|
|
9381
9390
|
const receipt = handoff.payload.admissionReceipts[0];
|
|
9382
9391
|
if (!receipt || handoff.payload.admissionReceipts.length !== 1) {
|
|
@@ -9553,6 +9562,8 @@ export function createTelegramQueueHandoffRecipientRuntime<TContext>(
|
|
|
9553
9562
|
}
|
|
9554
9563
|
|
|
9555
9564
|
export interface TelegramQueueHandoffReconcilerDeps<TContext> {
|
|
9565
|
+
canHandoffSteerWithRecipient?: (recipient: TelegramBusFollowerView) => boolean;
|
|
9566
|
+
hasPendingDispatch?: () => boolean;
|
|
9556
9567
|
ownsDirect: () => boolean;
|
|
9557
9568
|
isFollowerRegistered: () => boolean;
|
|
9558
9569
|
isBusEnabled: () => boolean;
|
|
@@ -9591,6 +9602,8 @@ export interface TelegramQueueHandoffReconcilerDeps<TContext> {
|
|
|
9591
9602
|
export interface TelegramQueueHandoffReconciliationRuntimeAssemblyDeps<
|
|
9592
9603
|
TContext,
|
|
9593
9604
|
> {
|
|
9605
|
+
canHandoffSteerWithRecipient?: (recipient: TelegramBusFollowerView) => boolean;
|
|
9606
|
+
hasPendingDispatch?: () => boolean;
|
|
9594
9607
|
ownsDirect: () => boolean;
|
|
9595
9608
|
isFollowerRegistered: () => boolean;
|
|
9596
9609
|
isBusEnabled: () => boolean;
|
|
@@ -9630,6 +9643,8 @@ export function createTelegramQueueHandoffReconciliationRuntimeAssembly<
|
|
|
9630
9643
|
deps: TelegramQueueHandoffReconciliationRuntimeAssemblyDeps<TContext>,
|
|
9631
9644
|
): (ctx: TContext) => Promise<void> {
|
|
9632
9645
|
return createTelegramQueueHandoffReconciler({
|
|
9646
|
+
canHandoffSteerWithRecipient: deps.canHandoffSteerWithRecipient,
|
|
9647
|
+
hasPendingDispatch: deps.hasPendingDispatch,
|
|
9633
9648
|
ownsDirect: deps.ownsDirect,
|
|
9634
9649
|
isFollowerRegistered: deps.isFollowerRegistered,
|
|
9635
9650
|
isBusEnabled: deps.isBusEnabled,
|
|
@@ -9706,6 +9721,8 @@ export function createTelegramQueueHandoffReconciler<TContext>(
|
|
|
9706
9721
|
.filter((follower) => follower.instanceId !== deps.donorInstanceId);
|
|
9707
9722
|
if (followers.length === 0) return;
|
|
9708
9723
|
for (const item of [...deps.getQueuedItems()]) {
|
|
9724
|
+
if ((deps.hasPendingDispatch?.() && deps.getQueuedItems()[0] === item) ||
|
|
9725
|
+
(item.kind === "prompt" && (item.steerAttempted || item.steerPacketTail !== undefined || item.steerPacketSourceOrders !== undefined))) continue;
|
|
9709
9726
|
const target = item.target;
|
|
9710
9727
|
const recipient = target
|
|
9711
9728
|
? followers.find(
|
|
@@ -9715,6 +9732,8 @@ export function createTelegramQueueHandoffReconciler<TContext>(
|
|
|
9715
9732
|
)
|
|
9716
9733
|
: undefined;
|
|
9717
9734
|
if (!recipient) continue;
|
|
9735
|
+
const steerSupported = item.queueLane === "steer" && deps.canHandoffSteerWithRecipient?.(recipient) === true;
|
|
9736
|
+
if (item.queueLane === "steer" && !steerSupported) continue;
|
|
9718
9737
|
const receipt = item.admissionReceipts?.[0];
|
|
9719
9738
|
if (!receipt || item.admissionReceipts?.length !== 1) continue;
|
|
9720
9739
|
if (
|
|
@@ -9741,6 +9760,7 @@ export function createTelegramQueueHandoffReconciler<TContext>(
|
|
|
9741
9760
|
const handoffToken = deps.createHandoffToken();
|
|
9742
9761
|
const result = await coordinateTelegramQueueHandoff({
|
|
9743
9762
|
item,
|
|
9763
|
+
steerSupported,
|
|
9744
9764
|
expectedOwner,
|
|
9745
9765
|
recipientOwner: {
|
|
9746
9766
|
instanceId: recipient.instanceId,
|
|
@@ -9751,6 +9771,9 @@ export function createTelegramQueueHandoffReconciler<TContext>(
|
|
|
9751
9771
|
handoffToken,
|
|
9752
9772
|
lifecycle,
|
|
9753
9773
|
stageRemote: async ({ payload: sourcePayload }) => {
|
|
9774
|
+
if (sourcePayload.queueLane === "steer" && deps.canHandoffSteerWithRecipient?.(recipient) !== true) {
|
|
9775
|
+
throw new Error("Steer queue handoff capability changed before staging.");
|
|
9776
|
+
}
|
|
9754
9777
|
const payload = {
|
|
9755
9778
|
...sourcePayload,
|
|
9756
9779
|
admissionReceipts: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@llblab/pi-codex-usage": "0.13.0",
|
|
48
48
|
"@llblab/pi-grow-loop": "0.9.0",
|
|
49
49
|
"@llblab/pi-state-flow": "0.26.4",
|
|
50
|
-
"@llblab/pi-telegram": "0.
|
|
50
|
+
"@llblab/pi-telegram": "0.56.2",
|
|
51
51
|
"@llblab/skills": "1.15.0"
|
|
52
52
|
},
|
|
53
53
|
"bundledDependencies": [
|