@llblab/pi-kit 0.34.3 → 0.35.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 -0
- package/CHANGELOG.md +10 -0
- package/README.md +3 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +3 -3
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +6 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +11 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +12 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +39 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +21 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +6 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/config.js +6 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +40 -45
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +7 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +80 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +33 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +46 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +137 -13
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +10 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +19 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +41 -14
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +5 -3
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +4 -2
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -3
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +14 -8
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +2 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +45 -24
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +21 -8
- package/node_modules/@llblab/pi-telegram/lib/config.ts +11 -17
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +43 -55
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +80 -0
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +36 -3
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +43 -4
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +3 -2
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +140 -18
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +22 -5
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +54 -18
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
|
@@ -1568,6 +1568,13 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
1568
1568
|
requiredJournalBindingKeys: readonly string[],
|
|
1569
1569
|
ownInputs?: readonly Threads.TelegramTemporaryThreadInput[],
|
|
1570
1570
|
) => readonly number[] | undefined;
|
|
1571
|
+
/** Explicit cleanup-only recovery; never called by a read-only protection observer. */
|
|
1572
|
+
recoverTemporaryThreadDeadQueueOwners?: (
|
|
1573
|
+
target: Queue.TelegramQueueTarget,
|
|
1574
|
+
requiredJournalBindingKeys: readonly string[],
|
|
1575
|
+
ownInputs: readonly Threads.TelegramTemporaryThreadInput[],
|
|
1576
|
+
isCurrent: () => boolean,
|
|
1577
|
+
) => number;
|
|
1571
1578
|
/** Quiet period after the last cancelled input before one cleanup attempt; defaults to 0 ms. */
|
|
1572
1579
|
temporaryThreadCleanupDelayMs?: number;
|
|
1573
1580
|
/** Live-rebind cleanup attempt pacing; the bounded window ends in `not-issued`, never a time-based deletion. */
|
|
@@ -6837,6 +6844,93 @@ export function createTelegramInboundRouteRuntime<
|
|
|
6837
6844
|
}
|
|
6838
6845
|
await promptEnqueue(messages, ctx);
|
|
6839
6846
|
};
|
|
6847
|
+
/** Local menu acceptance owns a one-shot effect; source removal and cleanup precede its release, never its replay. */
|
|
6848
|
+
const stageTemporaryRerouteMenu = async (
|
|
6849
|
+
pending: PendingUnboundReroute,
|
|
6850
|
+
command: Commands.ParsedTelegramCommand,
|
|
6851
|
+
messages: TMessage[],
|
|
6852
|
+
ctx: TContext,
|
|
6853
|
+
assertRecipientCurrent: () => void,
|
|
6854
|
+
completion: NonNullable<ReturnType<typeof Updates.prepareTelegramDeferredSourceCompletion>>,
|
|
6855
|
+
): Promise<(() => Promise<void>) | undefined> => {
|
|
6856
|
+
const original = pending.messages[0];
|
|
6857
|
+
const cap = captureTemporaryThreadAuthority(ctx);
|
|
6858
|
+
const temporary = pending.temporaryThread ?? findCreatedTemporaryThread(pending.sourceTarget);
|
|
6859
|
+
if (!cap?.isCurrent() || !temporary) return undefined;
|
|
6860
|
+
let accept!: (value: boolean) => void, resume!: (value: boolean) => void;
|
|
6861
|
+
const accepted = new Promise<boolean>(resolve => { accept = resolve; });
|
|
6862
|
+
const released = new Promise<boolean>(resolve => { resume = resolve; });
|
|
6863
|
+
const admission: Commands.TelegramSelectedCommandAdmission = {
|
|
6864
|
+
assertSourceCurrent() {
|
|
6865
|
+
if (!completion.isCurrent()) throw new Error("Rerouted menu source authority changed.");
|
|
6866
|
+
},
|
|
6867
|
+
assertRecipientCurrent,
|
|
6868
|
+
reportCompleted() {
|
|
6869
|
+
const result = completion.reportCompleted();
|
|
6870
|
+
accept(result);
|
|
6871
|
+
return result;
|
|
6872
|
+
},
|
|
6873
|
+
async beforeEffects() {
|
|
6874
|
+
if (!(await released)) throw new Error("Rerouted menu execution was not released.");
|
|
6875
|
+
},
|
|
6876
|
+
};
|
|
6877
|
+
const prepare = command.name === "start" ? commandHandler.prepareSelectedStartCommand
|
|
6878
|
+
: command.name === "help" ? commandHandler.prepareSelectedHelpCommand : commandHandler.prepareSelectedMenuCommand;
|
|
6879
|
+
const dispatch = prepare(command, messages, ctx, admission);
|
|
6880
|
+
if (!dispatch) return undefined;
|
|
6881
|
+
const execution = dispatch().catch(error => {
|
|
6882
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "reroute-menu-execution" });
|
|
6883
|
+
return false;
|
|
6884
|
+
});
|
|
6885
|
+
if (!(await Promise.race([accepted, execution.then(() => false)]))) {
|
|
6886
|
+
resume(false);
|
|
6887
|
+
await execution;
|
|
6888
|
+
return undefined;
|
|
6889
|
+
}
|
|
6890
|
+
return async () => {
|
|
6891
|
+
let allowEffects = false;
|
|
6892
|
+
try {
|
|
6893
|
+
const confirmed = await new Promise<boolean>(resolve => {
|
|
6894
|
+
let done = false, unsubscribe: (() => void) | undefined;
|
|
6895
|
+
const signal = Updates.getTelegramUpdateExecutionFence(original)?.signal;
|
|
6896
|
+
const finish = (result: boolean) => {
|
|
6897
|
+
if (done) return;
|
|
6898
|
+
done = true;
|
|
6899
|
+
clearTimeout(timer);
|
|
6900
|
+
unsubscribe?.();
|
|
6901
|
+
signal?.removeEventListener("abort", abort);
|
|
6902
|
+
resolve(result);
|
|
6903
|
+
};
|
|
6904
|
+
const abort = () => finish(false);
|
|
6905
|
+
const observe = () => {
|
|
6906
|
+
if (!cap.isCurrent() || !completion.isCurrent()) finish(false);
|
|
6907
|
+
else if (completion.inspectCompletion()) finish(true);
|
|
6908
|
+
};
|
|
6909
|
+
const timer = setTimeout(abort, deps.liveRebindCommandCompletionTimeoutMs ?? 8_000);
|
|
6910
|
+
timer.unref?.();
|
|
6911
|
+
unsubscribe = Updates.subscribeTelegramDeferredSourceCompletion(original, observe);
|
|
6912
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
6913
|
+
if (signal?.aborted) abort(); else observe();
|
|
6914
|
+
});
|
|
6915
|
+
if (!confirmed) throw new Error("Rerouted menu source removal is unconfirmed; execution remains suppressed.");
|
|
6916
|
+
assertRecipientCurrent();
|
|
6917
|
+
cancelTemporaryThreadCleanup(temporary.token);
|
|
6918
|
+
try {
|
|
6919
|
+
await runTemporaryThreadCleanup(temporary.token, cap, false);
|
|
6920
|
+
} catch (error) {
|
|
6921
|
+
// Cleanup uncertainty retains its canonical grant, not command execution authority or a retry license.
|
|
6922
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "reroute-menu-cleanup" });
|
|
6923
|
+
}
|
|
6924
|
+
assertRecipientCurrent();
|
|
6925
|
+
allowEffects = true;
|
|
6926
|
+
} catch (error) {
|
|
6927
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "reroute-menu-release" });
|
|
6928
|
+
} finally {
|
|
6929
|
+
resume(allowEffects);
|
|
6930
|
+
await execution;
|
|
6931
|
+
}
|
|
6932
|
+
};
|
|
6933
|
+
};
|
|
6840
6934
|
const finalizePendingReroute = async (
|
|
6841
6935
|
rerouteId: string,
|
|
6842
6936
|
pending: PendingUnboundReroute,
|
|
@@ -7258,12 +7352,20 @@ export function createTelegramInboundRouteRuntime<
|
|
|
7258
7352
|
});
|
|
7259
7353
|
if (!cancelledCurrent()) return;
|
|
7260
7354
|
entry = cap.adopt(entry);
|
|
7261
|
-
if (
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7355
|
+
if (!entry || !cap.isCurrent()) return;
|
|
7356
|
+
if (isRerouteTargetProtected(target, undefined, entry) && deps.recoverTemporaryThreadDeadQueueOwners &&
|
|
7357
|
+
store.inspectTemporaryThreadTarget(entry, cap.authority)?.kind === "temporary") {
|
|
7358
|
+
const inputs = Threads.getTelegramTemporaryThreadInputs(entry);
|
|
7359
|
+
const removed = deps.recoverTemporaryThreadDeadQueueOwners(
|
|
7360
|
+
target, [cap.journalBindingKey, ...inputs.map(input => input.journalBindingKey)],
|
|
7361
|
+
inputs, cap.isCurrent,
|
|
7362
|
+
);
|
|
7363
|
+
if (removed > 0) deps.recordRuntimeEvent?.("routing",
|
|
7364
|
+
"Discarded historical queue receipts from confirmed-dead owners before temporary Thread cleanup.",
|
|
7365
|
+
{ phase: "temporary-thread-dead-owner-recovery", removedUpdateCount: removed });
|
|
7366
|
+
}
|
|
7367
|
+
// Recovery is not deletion authority: recapture every ordinary protection after the journal CAS.
|
|
7368
|
+
if (!cap.isCurrent() || isRerouteTargetProtected(target, undefined, entry)) return;
|
|
7267
7369
|
const issued = store.issueTemporaryThreadCleanup(entry, cap.authority);
|
|
7268
7370
|
if (!issued || !cap.isCurrent()) return;
|
|
7269
7371
|
entry = issued.entry;
|
|
@@ -8658,6 +8760,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8658
8760
|
const executeUnboundRerouteCallbackOperation = async (
|
|
8659
8761
|
query: TCallbackQuery,
|
|
8660
8762
|
ctx: TContext,
|
|
8763
|
+
afterAdmission: (release: () => Promise<void>) => void,
|
|
8661
8764
|
): Promise<boolean> => {
|
|
8662
8765
|
const parsed = parseTelegramUnboundRerouteCallbackData(query.data);
|
|
8663
8766
|
if (!parsed) return false;
|
|
@@ -9349,7 +9452,20 @@ export function createTelegramInboundRouteRuntime<
|
|
|
9349
9452
|
const localMessages = pending.rerouteCopies
|
|
9350
9453
|
? cloneTelegramMessagesForThread(pending.messages, parsed.threadId, pending.rerouteCopies.bySourceUpdateId)
|
|
9351
9454
|
: reroutedMessages;
|
|
9352
|
-
|
|
9455
|
+
const command = pending.dispatchKind === "command"
|
|
9456
|
+
? getLiveRebindMenu(localMessages) ?? getLiveRebindHelp(localMessages) : undefined;
|
|
9457
|
+
const completion = pending.rerouteCopies && command
|
|
9458
|
+
? Updates.prepareTelegramDeferredSourceCompletion(pending.messages[0]) : undefined;
|
|
9459
|
+
if (command && completion) {
|
|
9460
|
+
const release = await stageTemporaryRerouteMenu(pending, command, localMessages, ctx, assertRerouteCurrent, completion);
|
|
9461
|
+
if (!release) {
|
|
9462
|
+
await deps.answerCallbackQuery(query.id, "Command acceptance unconfirmed; not resending");
|
|
9463
|
+
return true;
|
|
9464
|
+
}
|
|
9465
|
+
afterAdmission(release);
|
|
9466
|
+
} else {
|
|
9467
|
+
await dispatchPendingRerouteMessages(pending, localMessages, ctx);
|
|
9468
|
+
}
|
|
9353
9469
|
assertRerouteCurrent();
|
|
9354
9470
|
pending.messages = [];
|
|
9355
9471
|
return completeRoutedReroute();
|
|
@@ -9360,17 +9476,23 @@ export function createTelegramInboundRouteRuntime<
|
|
|
9360
9476
|
): Promise<boolean> => {
|
|
9361
9477
|
const parsed = parseTelegramUnboundRerouteCallbackData(query.data);
|
|
9362
9478
|
if (!parsed) return false;
|
|
9363
|
-
return withPendingRerouteSelection(parsed.rerouteId, query, () => {
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9479
|
+
return withPendingRerouteSelection(parsed.rerouteId, query, async () => {
|
|
9480
|
+
let release: (() => Promise<void>) | undefined;
|
|
9481
|
+
const operation = () => executeUnboundRerouteCallbackOperation(query, ctx, value => { release = value; });
|
|
9482
|
+
try {
|
|
9483
|
+
if (!deps.runWorkspaceOperation) return await operation();
|
|
9484
|
+
return await deps.runWorkspaceOperation(
|
|
9485
|
+
{
|
|
9486
|
+
operationId: `workspace-reroute:${query.id}`,
|
|
9487
|
+
operationKind: "workspace.route-unbound-thread",
|
|
9488
|
+
scopes: [{ kind: "profile" }],
|
|
9489
|
+
},
|
|
9490
|
+
operation,
|
|
9491
|
+
);
|
|
9492
|
+
} finally {
|
|
9493
|
+
// Keep the exact source references, but release the non-reentrant Workspace gate before cleanup.
|
|
9494
|
+
await release?.();
|
|
9495
|
+
}
|
|
9374
9496
|
});
|
|
9375
9497
|
};
|
|
9376
9498
|
const handleUnboundRerouteCallback = async (
|
|
@@ -473,6 +473,8 @@ export interface TelegramAnswerGuestQueryOptions {
|
|
|
473
473
|
}
|
|
474
474
|
|
|
475
475
|
export interface TelegramEditGuestInlineMessageContent {
|
|
476
|
+
/** Cached media only: inline edits cannot upload a new local file. */
|
|
477
|
+
media?: { type: "photo" | "document" | "audio"; media: string; caption?: string };
|
|
476
478
|
text?: string;
|
|
477
479
|
richMessage?: TelegramInputRichMessage;
|
|
478
480
|
parseMode?: "HTML";
|
|
@@ -917,10 +919,9 @@ export interface TelegramBridgeApiRuntime extends Pick<
|
|
|
917
919
|
options?: TelegramAnswerGuestQueryOptions,
|
|
918
920
|
) => Promise<void>;
|
|
919
921
|
/**
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
* guest answers, so this exists only to falsify that behavior live.
|
|
922
|
+
* Answers the guest query and retains the sent inline message id for final
|
|
923
|
+
* text or cached-media replacement. Requires direct transport ownership;
|
|
924
|
+
* actual guest-media rendering remains live-client gated.
|
|
924
925
|
*/
|
|
925
926
|
answerGuestQueryForInlineMessage: (
|
|
926
927
|
guestQueryId: string,
|
|
@@ -930,6 +931,7 @@ export interface TelegramBridgeApiRuntime extends Pick<
|
|
|
930
931
|
editGuestInlineMessage: (
|
|
931
932
|
inlineMessageId: string,
|
|
932
933
|
content: TelegramEditGuestInlineMessageContent,
|
|
934
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
933
935
|
) => Promise<void>;
|
|
934
936
|
deleteMessage: (chatId: number, messageId: number) => Promise<void>;
|
|
935
937
|
prepareTempDir: () => Promise<number>;
|
|
@@ -2404,14 +2406,29 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2404
2406
|
editGuestInlineMessage: async (
|
|
2405
2407
|
inlineMessageId: string,
|
|
2406
2408
|
content: TelegramEditGuestInlineMessageContent,
|
|
2409
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
2407
2410
|
) => {
|
|
2411
|
+
if (content.media) {
|
|
2412
|
+
try {
|
|
2413
|
+
const result = await callRecorded("editMessageMedia", {
|
|
2414
|
+
inline_message_id: inlineMessageId,
|
|
2415
|
+
media: content.media,
|
|
2416
|
+
}, { ...options, maxAttempts: 1, retryRateLimit: false });
|
|
2417
|
+
if (result !== true) throw new Error("Missing inline edit acknowledgement.");
|
|
2418
|
+
} catch (error) {
|
|
2419
|
+
// Only a parsed rejection permits replacing this attempt with a failure notice.
|
|
2420
|
+
if (isTelegramApiRequestRejected(error, "editMessageMedia")) throw error;
|
|
2421
|
+
throw new TelegramApiCommitUnknownError("editMessageMedia", error);
|
|
2422
|
+
}
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2408
2425
|
await callRecorded("editMessageText", {
|
|
2409
2426
|
inline_message_id: inlineMessageId,
|
|
2410
2427
|
...(content.richMessage
|
|
2411
2428
|
? { rich_message: content.richMessage }
|
|
2412
2429
|
: { text: content.text }),
|
|
2413
2430
|
...(content.parseMode ? { parse_mode: content.parseMode } : {}),
|
|
2414
|
-
});
|
|
2431
|
+
}, options);
|
|
2415
2432
|
},
|
|
2416
2433
|
prepareTempDir: () =>
|
|
2417
2434
|
prepareTelegramTempDir(deps.tempDir, deps.tempFileMaxAgeMs),
|
|
@@ -904,6 +904,8 @@ export interface TelegramTopicTargetStore {
|
|
|
904
904
|
existingBindingOnly?: boolean;
|
|
905
905
|
sessionId?: string;
|
|
906
906
|
provisionProfileKey?: string;
|
|
907
|
+
/** Explicit same-process session handoff from `previousInstanceId`; never inferred. */
|
|
908
|
+
sessionHandoff?: boolean;
|
|
907
909
|
onCapacityUnavailable?: () => void;
|
|
908
910
|
},
|
|
909
911
|
) => TelegramWorkspaceBindingIdentity | undefined;
|
|
@@ -2721,10 +2723,16 @@ function liveRebindOldTargetUnknown(value: TelegramWorkspaceLiveRebindIntent): b
|
|
|
2721
2723
|
function retainLiveRebind(value: TelegramWorkspaceLiveRebindIntent): boolean {
|
|
2722
2724
|
return liveRebindActive(value) || liveRebindOldTargetUnknown(value) || value.oldTargetProtected === true;
|
|
2723
2725
|
}
|
|
2724
|
-
function
|
|
2726
|
+
function provisioningProtectionIntents(snapshot: TelegramWorkspaceRestoreSnapshot): readonly Pick<TelegramWorkspaceRestoreIntent, "request">[] {
|
|
2727
|
+
return [...snapshot.operations, ...(snapshot.liveRebindings ?? []).filter(retainLiveRebind)];
|
|
2728
|
+
}
|
|
2729
|
+
function protectedLiveRebindOldTargetIntents(snapshot: TelegramWorkspaceRestoreSnapshot | undefined): TelegramWorkspaceLiveRebindIntent[] {
|
|
2725
2730
|
return (snapshot?.liveRebindings ?? []).filter(value =>
|
|
2726
2731
|
value.oldTargetProtected === true || liveRebindOldTargetUnknown(value) || value.inputRelease !== undefined && value.cleanup === "issued",
|
|
2727
|
-
)
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
function protectedLiveRebindOldTargets(snapshot: TelegramWorkspaceRestoreSnapshot | undefined): TelegramTarget[] {
|
|
2735
|
+
return protectedLiveRebindOldTargetIntents(snapshot).map(value => ({ ...value.request.binding.target }));
|
|
2728
2736
|
}
|
|
2729
2737
|
function pendingWorkspaceRelocationRequests(
|
|
2730
2738
|
snapshot: TelegramWorkspaceRestoreSnapshot | undefined,
|
|
@@ -2819,10 +2827,11 @@ function assertWorkspaceRestoreBindingProtection(
|
|
|
2819
2827
|
throw new Error("Protected Workspace Restore owner target changed.");
|
|
2820
2828
|
}
|
|
2821
2829
|
}
|
|
2822
|
-
for (const
|
|
2830
|
+
for (const { request } of protectedLiveRebindOldTargetIntents(file.workspaceRestore)) {
|
|
2831
|
+
const old = request.binding.target;
|
|
2823
2832
|
if (file.workspaceBindings?.some(value => targetMatches(value.target, old)) ||
|
|
2824
2833
|
file.threads?.some(value => targetMatches(value.target, old) && ["active", "starting", "pending", "probe-required"].includes(value.status)) ||
|
|
2825
|
-
file.pendingProvisions?.some(value =>
|
|
2834
|
+
file.pendingProvisions?.some(value => conflictsWithWorkspaceRestoreProvision(request, value)) ||
|
|
2826
2835
|
file.reservations?.some(value => targetMatches(value.target, old)))
|
|
2827
2836
|
throw new Error("Unknown live cleanup target remains protected.");
|
|
2828
2837
|
}
|
|
@@ -3838,7 +3847,7 @@ export function createTelegramTopicTargetStore(
|
|
|
3838
3847
|
};
|
|
3839
3848
|
const assertRestoreRecoveryProtection = (
|
|
3840
3849
|
path: string,
|
|
3841
|
-
operations: readonly TelegramWorkspaceRestoreIntent[],
|
|
3850
|
+
operations: readonly Pick<TelegramWorkspaceRestoreIntent, "request">[],
|
|
3842
3851
|
provisions: readonly TelegramThreadPendingProvision[],
|
|
3843
3852
|
requireKnownTargets = false,
|
|
3844
3853
|
recoveryEvidence?: TelegramProvisionRecoveryFile,
|
|
@@ -5059,7 +5068,7 @@ export function createTelegramTopicTargetStore(
|
|
|
5059
5068
|
assertWorkspaceRestoreBindingProtection(file);
|
|
5060
5069
|
assertRestoreRecoveryProtection(
|
|
5061
5070
|
path,
|
|
5062
|
-
file.workspaceRestore
|
|
5071
|
+
provisioningProtectionIntents(file.workspaceRestore!),
|
|
5063
5072
|
file.pendingProvisions ?? [],
|
|
5064
5073
|
);
|
|
5065
5074
|
observe({
|
|
@@ -5115,7 +5124,7 @@ export function createTelegramTopicTargetStore(
|
|
|
5115
5124
|
assertWorkspaceRestoreBindingProtection(file);
|
|
5116
5125
|
assertRestoreRecoveryProtection(
|
|
5117
5126
|
path,
|
|
5118
|
-
file.workspaceRestore
|
|
5127
|
+
provisioningProtectionIntents(file.workspaceRestore!),
|
|
5119
5128
|
file.pendingProvisions ?? [],
|
|
5120
5129
|
);
|
|
5121
5130
|
const canonical = file.workspaceBindings ?? [],
|
|
@@ -5173,9 +5182,9 @@ export function createTelegramTopicTargetStore(
|
|
|
5173
5182
|
(row) => currentRow(row) && targetMatches(row.target, old),
|
|
5174
5183
|
) ||
|
|
5175
5184
|
reserved.some((value) => targetMatches(value.target, old)) ||
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
) ||
|
|
5185
|
+
// Use the same binding/slot/owner/target conflicts as rebind admission. An unrelated unlocated
|
|
5186
|
+
// create must not globally veto cleanup of this exact former binding (nor its issued marker).
|
|
5187
|
+
provisions.some((value) => conflictsWithWorkspaceRestoreProvision(request, value)) ||
|
|
5179
5188
|
cleanups.some((value) => targetMatches(value.target, old));
|
|
5180
5189
|
if (
|
|
5181
5190
|
protectedTarget(
|
|
@@ -5388,8 +5397,11 @@ export function createTelegramTopicTargetStore(
|
|
|
5388
5397
|
file.reservations?.some((value) =>
|
|
5389
5398
|
targetMatches(value.target, target),
|
|
5390
5399
|
) ||
|
|
5400
|
+
// This exact created frame already proves a separately created routing tab (API ACK or authenticated
|
|
5401
|
+
// implicit creation). An unlocated Workspace create is not its owner; keep that intent unresolved,
|
|
5402
|
+
// but veto a concrete target collision. Generic unknown-target/Restore cleanup keeps its own guards.
|
|
5391
5403
|
file.pendingProvisions?.some(
|
|
5392
|
-
(value) =>
|
|
5404
|
+
(value) => !!value.target && targetMatches(value.target, target),
|
|
5393
5405
|
) ||
|
|
5394
5406
|
file.pendingCleanups?.some((value) =>
|
|
5395
5407
|
targetMatches(value.target, target),
|
|
@@ -5555,7 +5567,7 @@ export function createTelegramTopicTargetStore(
|
|
|
5555
5567
|
parseTopicTargetFile(before.file).pendingProvisions ?? [];
|
|
5556
5568
|
assertRestoreRecoveryProtection(
|
|
5557
5569
|
path,
|
|
5558
|
-
before.state
|
|
5570
|
+
provisioningProtectionIntents(before.state),
|
|
5559
5571
|
provisions,
|
|
5560
5572
|
);
|
|
5561
5573
|
const after = { ...before.file, workspaceRestore: next };
|
|
@@ -5571,7 +5583,7 @@ export function createTelegramTopicTargetStore(
|
|
|
5571
5583
|
return false;
|
|
5572
5584
|
assertRestoreRecoveryProtection(
|
|
5573
5585
|
path,
|
|
5574
|
-
[...before.state
|
|
5586
|
+
[...provisioningProtectionIntents(before.state), ...provisioningProtectionIntents(next)],
|
|
5575
5587
|
provisions,
|
|
5576
5588
|
);
|
|
5577
5589
|
committedBefore = before;
|
|
@@ -5596,7 +5608,7 @@ export function createTelegramTopicTargetStore(
|
|
|
5596
5608
|
);
|
|
5597
5609
|
assertRestoreRecoveryProtection(
|
|
5598
5610
|
path,
|
|
5599
|
-
[...before.state
|
|
5611
|
+
[...provisioningProtectionIntents(before.state), ...provisioningProtectionIntents(next)],
|
|
5600
5612
|
provisions,
|
|
5601
5613
|
);
|
|
5602
5614
|
if (!currentScope() || !isCurrent()) return;
|
|
@@ -6773,7 +6785,7 @@ export function createTelegramTopicTargetStore(
|
|
|
6773
6785
|
conflictsWithWorkspaceRestoreProvision(request, provision),
|
|
6774
6786
|
) ||
|
|
6775
6787
|
conflictsWithTemporaryThread(workspaceRestore, provision.target) ||
|
|
6776
|
-
|
|
6788
|
+
protectedLiveRebindOldTargetIntents(workspaceRestore).some(({ request }) => conflictsWithWorkspaceRestoreProvision(request, provision))
|
|
6777
6789
|
) {
|
|
6778
6790
|
throw new Error("Protected Workspace Restore provisioning conflict.");
|
|
6779
6791
|
}
|
|
@@ -7634,11 +7646,35 @@ export function createTelegramTopicTargetStore(
|
|
|
7634
7646
|
(options?.provisionProfileKey !== undefined && value.profileKey === options.provisionProfileKey)));
|
|
7635
7647
|
if (ownedProvisions.length > 1) return undefined;
|
|
7636
7648
|
const ownProvision = ownedProvisions[0];
|
|
7637
|
-
|
|
7638
|
-
const
|
|
7649
|
+
// A same-cwd session handoff keeps its predecessor's Thread slot; the Workspace commit re-keys that binding.
|
|
7650
|
+
const handoffBindings =
|
|
7651
|
+
binding ||
|
|
7652
|
+
ownProvision ||
|
|
7653
|
+
!options?.sessionHandoff ||
|
|
7654
|
+
!previousInstanceId ||
|
|
7655
|
+
!isSameTelegramProcessInstance(previousInstanceId, instanceId)
|
|
7656
|
+
? []
|
|
7657
|
+
: Array.from(workspaceBindings.values()).filter(
|
|
7658
|
+
(candidate) =>
|
|
7659
|
+
candidate.cwd === identity.cwd &&
|
|
7660
|
+
candidate.sessionId !== identity.sessionId &&
|
|
7661
|
+
Array.from(records.values()).some(
|
|
7662
|
+
(record) =>
|
|
7663
|
+
record.instanceId === previousInstanceId &&
|
|
7664
|
+
record.status === "active" &&
|
|
7665
|
+
targetMatches(record.target, candidate.target),
|
|
7666
|
+
),
|
|
7667
|
+
);
|
|
7668
|
+
const handoffBinding =
|
|
7669
|
+
handoffBindings.length === 1 ? handoffBindings[0] : undefined;
|
|
7670
|
+
const retainedTarget = ownProvision?.target ?? binding?.target ?? handoffBinding?.target ?? legacyRecord?.target;
|
|
7671
|
+
const retainedSlot = ownProvision?.slot ?? binding?.slot ?? handoffBinding?.slot ?? legacyRecord?.slot;
|
|
7639
7672
|
const otherBindings = Array.from(workspaceBindings.values())
|
|
7640
7673
|
.filter(
|
|
7641
|
-
(other) =>
|
|
7674
|
+
(other) =>
|
|
7675
|
+
other.bindingKey !== identity.bindingKey &&
|
|
7676
|
+
other.bindingKey !== handoffBinding?.bindingKey &&
|
|
7677
|
+
other.slot,
|
|
7642
7678
|
)
|
|
7643
7679
|
.map((other) => ({
|
|
7644
7680
|
bindingKey: other.bindingKey,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@llblab/pi-codex-usage": "0.13.1",
|
|
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.57.1",
|
|
51
51
|
"@llblab/skills": "1.15.0"
|
|
52
52
|
},
|
|
53
53
|
"bundledDependencies": [
|