@llblab/pi-kit 0.19.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +4 -4
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-state-flow/README.md +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +45 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +9 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +19 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +52 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +0 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +36 -15
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +5 -5
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +2 -1
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +48 -5
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +9 -9
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +65 -11
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -2
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +35 -15
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +9 -7
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +13 -32
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +11 -0
- package/node_modules/@llblab/pi-telegram/README.md +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +3 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +93 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +9 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +231 -71
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +4 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +34 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +63 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +13 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +25 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +32 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +10 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +22 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +41 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +9 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +23 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +7 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +68 -37
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +6 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +134 -61
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +49 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +77 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +384 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.d.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +6 -0
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +21 -14
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +31 -10
- package/node_modules/@llblab/pi-telegram/docs/updates.md +2 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +5 -7
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +69 -12
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +236 -94
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +39 -9
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +69 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +14 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +23 -4
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +32 -13
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -1
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +2 -3
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +45 -1
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +30 -2
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +11 -3
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +73 -36
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +145 -78
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +67 -4
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +465 -7
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +7 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workspace
|
|
2
|
+
* Workspace slot rotation
|
|
3
3
|
* Zones: telegram, workspace identity, lifecycle
|
|
4
|
-
* Owns fail-closed protection
|
|
5
|
-
* successor
|
|
4
|
+
* Owns fail-closed protection, demand-driven pressure retirement, exact-intent admission,
|
|
5
|
+
* successor recovery, and fenced one-shot deletion before durable slot reuse.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { isDeepStrictEqual } from "node:util";
|
|
9
9
|
|
|
10
|
+
import type {
|
|
11
|
+
TelegramUpdateJournalDeadQueueOwnerRecoveryInput,
|
|
12
|
+
TelegramUpdateJournalDeadQueueOwnerRecoveryResult,
|
|
13
|
+
TelegramUpdateJournalEntry,
|
|
14
|
+
TelegramUpdateJournalQueueOwnerIdentity,
|
|
15
|
+
} from "./journal.ts";
|
|
10
16
|
import type { TelegramTarget } from "./target.ts";
|
|
17
|
+
import {
|
|
18
|
+
getTelegramApiErrorRequestTarget,
|
|
19
|
+
isTelegramApiRequestRejected,
|
|
20
|
+
type TelegramWorkspaceThreadDeletionTransport,
|
|
21
|
+
} from "./telegram-api.ts";
|
|
11
22
|
import type {
|
|
12
23
|
TelegramTopicTargetStore,
|
|
13
24
|
TelegramWorkspaceExternalProtectionEvidence,
|
|
@@ -17,6 +28,7 @@ import type {
|
|
|
17
28
|
} from "./threads.ts";
|
|
18
29
|
import {
|
|
19
30
|
planTelegramWorkspaceSlotAllocation,
|
|
31
|
+
TelegramWorkspaceSlotUnavailableError,
|
|
20
32
|
type TelegramWorkspaceSlotOccupancy,
|
|
21
33
|
} from "./workspace-slots.ts";
|
|
22
34
|
import {
|
|
@@ -93,6 +105,7 @@ export interface TelegramWorkspaceJournalProtectionCapture {
|
|
|
93
105
|
interface TelegramWorkspaceJournalReader {
|
|
94
106
|
recoveryKey?: string;
|
|
95
107
|
journal: { read: () => { entries: readonly { update: unknown }[] } };
|
|
108
|
+
readForProtection?: () => { entries: readonly { update: unknown }[] };
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
export function captureTelegramWorkspaceJournalProtectionSources(input: {
|
|
@@ -123,9 +136,10 @@ export function captureTelegramWorkspaceJournalProtectionSources(input: {
|
|
|
123
136
|
sources.push({ kind: "unknown", scope });
|
|
124
137
|
return;
|
|
125
138
|
}
|
|
139
|
+
const read = binding.readForProtection ?? binding.journal.read;
|
|
126
140
|
const snapshot = input.withJournalReference
|
|
127
|
-
? input.withJournalReference(binding,
|
|
128
|
-
:
|
|
141
|
+
? input.withJournalReference(binding, read)
|
|
142
|
+
: read();
|
|
129
143
|
sources.push({ kind: "available", scope, entries: snapshot.entries });
|
|
130
144
|
} catch {
|
|
131
145
|
complete = false;
|
|
@@ -466,6 +480,244 @@ export function createTelegramWorkspaceExternalProtectionCapture(deps: {
|
|
|
466
480
|
};
|
|
467
481
|
}
|
|
468
482
|
|
|
483
|
+
export function isCurrentTelegramWorkspaceBinding(
|
|
484
|
+
store: Pick<TelegramTopicTargetStore, "listWorkspaceBindings">,
|
|
485
|
+
expected: TelegramWorkspaceThreadBinding,
|
|
486
|
+
): boolean {
|
|
487
|
+
return store.listWorkspaceBindings().some((binding) =>
|
|
488
|
+
isDeepStrictEqual(binding, expected));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export interface TelegramWorkspaceDeadQueueJournalBinding {
|
|
492
|
+
recoveryKey?: string;
|
|
493
|
+
readForProtection?: () => { entries: readonly TelegramUpdateJournalEntry[] };
|
|
494
|
+
journal: {
|
|
495
|
+
recoverDeadQueueOwner: (
|
|
496
|
+
input: TelegramUpdateJournalDeadQueueOwnerRecoveryInput,
|
|
497
|
+
) => TelegramUpdateJournalDeadQueueOwnerRecoveryResult;
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export type TelegramWorkspaceDeadQueueReclamation =
|
|
502
|
+
| { kind: "not-needed" }
|
|
503
|
+
| { kind: "recovered"; receipts: number; updateIds: number[] }
|
|
504
|
+
| {
|
|
505
|
+
kind: "blocked";
|
|
506
|
+
reason:
|
|
507
|
+
| "authority-changed"
|
|
508
|
+
| "live-owner"
|
|
509
|
+
| "local-work"
|
|
510
|
+
| "incomplete-source"
|
|
511
|
+
| "unsupported-custody"
|
|
512
|
+
| "owner-alive"
|
|
513
|
+
| "owner-unverifiable"
|
|
514
|
+
| "mutation-refused"
|
|
515
|
+
| "protection-retained";
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Demand-only preparation for pressure retirement. Every removed group is still
|
|
520
|
+
* owned by the journal's exact dead-owner CAS; this function never clears local
|
|
521
|
+
* queue memory or turns unknown evidence into deletion authority.
|
|
522
|
+
*/
|
|
523
|
+
export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
|
|
524
|
+
getExternalProtection: (
|
|
525
|
+
binding: TelegramWorkspaceThreadBinding,
|
|
526
|
+
) => TelegramWorkspaceExternalProtectionEvidence;
|
|
527
|
+
getActiveTurnTarget: () => TelegramTarget | undefined;
|
|
528
|
+
getQueuedItems: () => readonly { chatId: number; target?: TelegramTarget }[];
|
|
529
|
+
resolveLeaderJournal: () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
|
|
530
|
+
createFollowerJournalResolver: (
|
|
531
|
+
journalBindingKey: string,
|
|
532
|
+
) => () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
|
|
533
|
+
discoverFollowerJournals?: () => { paths: readonly string[]; complete: boolean };
|
|
534
|
+
createJournalPathResolver?: (
|
|
535
|
+
path: string,
|
|
536
|
+
) => () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
|
|
537
|
+
withJournalReference?: <T>(
|
|
538
|
+
binding: TelegramWorkspaceDeadQueueJournalBinding,
|
|
539
|
+
operation: () => T,
|
|
540
|
+
) => T;
|
|
541
|
+
getRecoveryOwner: () => TelegramUpdateJournalQueueOwnerIdentity;
|
|
542
|
+
getQueueOwnerLiveness: (
|
|
543
|
+
owner: { processId: number; processBirthId: string },
|
|
544
|
+
) => "alive" | "dead" | "unverifiable";
|
|
545
|
+
isBindingCurrent: (binding: TelegramWorkspaceThreadBinding) => boolean;
|
|
546
|
+
onMutationError?: (error: unknown) => void;
|
|
547
|
+
}): (
|
|
548
|
+
binding: TelegramWorkspaceThreadBinding,
|
|
549
|
+
isCurrent: () => boolean,
|
|
550
|
+
) => Promise<TelegramWorkspaceDeadQueueReclamation> {
|
|
551
|
+
const localProtection = (
|
|
552
|
+
binding: TelegramWorkspaceThreadBinding,
|
|
553
|
+
): "clear" | "protected" | "unknown" => {
|
|
554
|
+
const active = deps.getActiveTurnTarget();
|
|
555
|
+
if (active && sameTarget(active, binding.target)) return "protected";
|
|
556
|
+
let unknown = false;
|
|
557
|
+
for (const item of deps.getQueuedItems()) {
|
|
558
|
+
if (item.target && sameTarget(item.target, binding.target)) return "protected";
|
|
559
|
+
if (!item.target && item.chatId === binding.target.chatId) unknown = true;
|
|
560
|
+
}
|
|
561
|
+
return unknown ? "unknown" : "clear";
|
|
562
|
+
};
|
|
563
|
+
return async (binding, isCurrent) => {
|
|
564
|
+
if (!isCurrent() || !deps.isBindingCurrent(binding)) {
|
|
565
|
+
return { kind: "blocked", reason: "authority-changed" };
|
|
566
|
+
}
|
|
567
|
+
const initial = deps.getExternalProtection(binding);
|
|
568
|
+
if (initial.liveOwner !== "clear" || initial.deliveryAuthority !== "clear") {
|
|
569
|
+
return { kind: "blocked", reason: "live-owner" };
|
|
570
|
+
}
|
|
571
|
+
if (localProtection(binding) !== "clear") {
|
|
572
|
+
return { kind: "blocked", reason: "local-work" };
|
|
573
|
+
}
|
|
574
|
+
if (initial.acceptedWork === "clear") return { kind: "not-needed" };
|
|
575
|
+
|
|
576
|
+
const discovery = binding.journalBindingsComplete === true
|
|
577
|
+
? undefined
|
|
578
|
+
: deps.discoverFollowerJournals?.();
|
|
579
|
+
let complete = binding.journalBindingsComplete === true || discovery?.complete === true;
|
|
580
|
+
const sources: Array<{
|
|
581
|
+
scope: "shared" | "binding" | "discovered";
|
|
582
|
+
binding: TelegramWorkspaceDeadQueueJournalBinding;
|
|
583
|
+
entries: readonly TelegramUpdateJournalEntry[];
|
|
584
|
+
}> = [];
|
|
585
|
+
const recoveryKeys = new Set<string>();
|
|
586
|
+
const capture = (
|
|
587
|
+
scope: "shared" | "binding" | "discovered",
|
|
588
|
+
resolve: () => TelegramWorkspaceDeadQueueJournalBinding | undefined,
|
|
589
|
+
): void => {
|
|
590
|
+
try {
|
|
591
|
+
const source = resolve();
|
|
592
|
+
if (!source?.recoveryKey || !source.readForProtection) {
|
|
593
|
+
complete = false;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (recoveryKeys.has(source.recoveryKey)) return;
|
|
597
|
+
const snapshot = deps.withJournalReference
|
|
598
|
+
? deps.withJournalReference(source, source.readForProtection)
|
|
599
|
+
: source.readForProtection();
|
|
600
|
+
recoveryKeys.add(source.recoveryKey);
|
|
601
|
+
sources.push({ scope, binding: source, entries: snapshot.entries });
|
|
602
|
+
} catch {
|
|
603
|
+
complete = false;
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
capture("shared", deps.resolveLeaderJournal);
|
|
607
|
+
for (const key of binding.journalBindingKeys ?? []) {
|
|
608
|
+
capture("binding", deps.createFollowerJournalResolver(key));
|
|
609
|
+
}
|
|
610
|
+
for (const path of discovery?.paths ?? []) {
|
|
611
|
+
if (!deps.createJournalPathResolver) {
|
|
612
|
+
complete = false;
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
capture("discovered", deps.createJournalPathResolver(path));
|
|
616
|
+
}
|
|
617
|
+
if (!complete) return { kind: "blocked", reason: "incomplete-source" };
|
|
618
|
+
|
|
619
|
+
const plans: Array<{
|
|
620
|
+
source: TelegramWorkspaceDeadQueueJournalBinding;
|
|
621
|
+
recovery: TelegramUpdateJournalDeadQueueOwnerRecoveryInput;
|
|
622
|
+
}> = [];
|
|
623
|
+
for (const source of sources) {
|
|
624
|
+
const relevant = source.scope === "binding"
|
|
625
|
+
? [...source.entries]
|
|
626
|
+
: source.entries.filter((entry) => {
|
|
627
|
+
const target = getJournalUpdateTarget(entry.update);
|
|
628
|
+
return !!target && sameTarget(target, binding.target);
|
|
629
|
+
});
|
|
630
|
+
if (source.scope === "binding" && relevant.some((entry) => {
|
|
631
|
+
const target = getJournalUpdateTarget(entry.update);
|
|
632
|
+
return !target || !sameTarget(target, binding.target);
|
|
633
|
+
})) return { kind: "blocked", reason: "unsupported-custody" };
|
|
634
|
+
const seen = new Set<string>();
|
|
635
|
+
for (const entry of relevant) {
|
|
636
|
+
if (entry.state !== "queued" ||
|
|
637
|
+
(entry.queueKind !== "prompt" && entry.queueKind !== "control") ||
|
|
638
|
+
!entry.queueReceiptId || !entry.queueOwner || entry.queueHandoff) {
|
|
639
|
+
return { kind: "blocked", reason: "unsupported-custody" };
|
|
640
|
+
}
|
|
641
|
+
if (seen.has(entry.queueReceiptId)) continue;
|
|
642
|
+
const receiptEntries = source.entries.filter(
|
|
643
|
+
(candidate) => candidate.queueReceiptId === entry.queueReceiptId,
|
|
644
|
+
);
|
|
645
|
+
if (!receiptEntries.length || receiptEntries.some((candidate) => {
|
|
646
|
+
const target = getJournalUpdateTarget(candidate.update);
|
|
647
|
+
return candidate.state !== "queued" ||
|
|
648
|
+
candidate.queueKind !== entry.queueKind ||
|
|
649
|
+
!candidate.queueOwner || candidate.queueHandoff !== undefined ||
|
|
650
|
+
!isDeepStrictEqual(candidate.queueOwner, entry.queueOwner) ||
|
|
651
|
+
!target || !sameTarget(target, binding.target);
|
|
652
|
+
})) return { kind: "blocked", reason: "unsupported-custody" };
|
|
653
|
+
seen.add(entry.queueReceiptId);
|
|
654
|
+
plans.push({
|
|
655
|
+
source: source.binding,
|
|
656
|
+
recovery: {
|
|
657
|
+
queueKind: entry.queueKind,
|
|
658
|
+
receiptId: entry.queueReceiptId,
|
|
659
|
+
sourceUpdateIds: receiptEntries.map((candidate) => candidate.updateId).sort((a, b) => a - b),
|
|
660
|
+
deadOwner: entry.queueOwner,
|
|
661
|
+
recoveryOwner: deps.getRecoveryOwner(),
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (!plans.length) return { kind: "blocked", reason: "protection-retained" };
|
|
667
|
+
for (const plan of plans) {
|
|
668
|
+
let liveness: "alive" | "dead" | "unverifiable" = "unverifiable";
|
|
669
|
+
try { liveness = deps.getQueueOwnerLiveness(plan.recovery.deadOwner); }
|
|
670
|
+
catch { /* Unknown process evidence is never destructive authority. */ }
|
|
671
|
+
if (liveness === "alive") return { kind: "blocked", reason: "owner-alive" };
|
|
672
|
+
if (liveness !== "dead") return { kind: "blocked", reason: "owner-unverifiable" };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const recoveredIds: number[] = [];
|
|
676
|
+
for (const plan of plans) {
|
|
677
|
+
if (!isCurrent() || !deps.isBindingCurrent(binding)) {
|
|
678
|
+
return { kind: "blocked", reason: "authority-changed" };
|
|
679
|
+
}
|
|
680
|
+
const current = deps.getExternalProtection(binding);
|
|
681
|
+
if (current.liveOwner !== "clear" || current.deliveryAuthority !== "clear") {
|
|
682
|
+
return { kind: "blocked", reason: "live-owner" };
|
|
683
|
+
}
|
|
684
|
+
if (localProtection(binding) !== "clear") {
|
|
685
|
+
return { kind: "blocked", reason: "local-work" };
|
|
686
|
+
}
|
|
687
|
+
let result: TelegramUpdateJournalDeadQueueOwnerRecoveryResult;
|
|
688
|
+
try {
|
|
689
|
+
result = deps.withJournalReference
|
|
690
|
+
? deps.withJournalReference(plan.source, () =>
|
|
691
|
+
plan.source.journal.recoverDeadQueueOwner(plan.recovery))
|
|
692
|
+
: plan.source.journal.recoverDeadQueueOwner(plan.recovery);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
try { deps.onMutationError?.(error); } catch { /* Diagnostics are fail-open. */ }
|
|
695
|
+
return { kind: "blocked", reason: "mutation-refused" };
|
|
696
|
+
}
|
|
697
|
+
if (result.status === "owner-alive") {
|
|
698
|
+
return { kind: "blocked", reason: "owner-alive" };
|
|
699
|
+
}
|
|
700
|
+
if (result.status === "owner-unverifiable") {
|
|
701
|
+
return { kind: "blocked", reason: "owner-unverifiable" };
|
|
702
|
+
}
|
|
703
|
+
recoveredIds.push(...result.recoveredUpdateIds);
|
|
704
|
+
}
|
|
705
|
+
if (!isCurrent() || !deps.isBindingCurrent(binding)) {
|
|
706
|
+
return { kind: "blocked", reason: "authority-changed" };
|
|
707
|
+
}
|
|
708
|
+
const after = deps.getExternalProtection(binding);
|
|
709
|
+
if (after.liveOwner !== "clear" || after.acceptedWork !== "clear" ||
|
|
710
|
+
after.deliveryAuthority !== "clear") {
|
|
711
|
+
return { kind: "blocked", reason: "protection-retained" };
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
kind: "recovered",
|
|
715
|
+
receipts: plans.length,
|
|
716
|
+
updateIds: recoveredIds.sort((a, b) => a - b),
|
|
717
|
+
};
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
469
721
|
export type TelegramWorkspaceRetirementAdoption =
|
|
470
722
|
| { kind: "adopted"; intent: TelegramWorkspaceRetirementIntent }
|
|
471
723
|
| {
|
|
@@ -537,7 +789,7 @@ function isTelegramWorkspaceDeletionConfirmedAbsent(error: unknown): boolean {
|
|
|
537
789
|
if (!(error instanceof Error)) return false;
|
|
538
790
|
const status = "status" in error && typeof error.status === "number"
|
|
539
791
|
? error.status : undefined;
|
|
540
|
-
if (status !==
|
|
792
|
+
if (status !== 400) return false;
|
|
541
793
|
const message = error.message.toLowerCase();
|
|
542
794
|
return message.includes("topic_id_invalid") ||
|
|
543
795
|
message.includes("message thread not found") ||
|
|
@@ -548,6 +800,7 @@ function isTelegramWorkspaceDeletionConfirmedAbsent(error: unknown): boolean {
|
|
|
548
800
|
|
|
549
801
|
export type TelegramWorkspaceRetirementExecution =
|
|
550
802
|
| { kind: "retired"; bindingKey: string; slot: string }
|
|
803
|
+
| { kind: "cancelled"; reason: "delete-rejected" }
|
|
551
804
|
| {
|
|
552
805
|
kind: "retained";
|
|
553
806
|
reason:
|
|
@@ -556,6 +809,7 @@ export type TelegramWorkspaceRetirementExecution =
|
|
|
556
809
|
| "admission-active"
|
|
557
810
|
| "fence-conflict"
|
|
558
811
|
| "delete-unconfirmed"
|
|
812
|
+
| "delete-rejected"
|
|
559
813
|
| "authority-changed"
|
|
560
814
|
| "commit-rejected"
|
|
561
815
|
| "fence-release-unconfirmed";
|
|
@@ -581,6 +835,47 @@ function matchesTelegramWorkspaceRetirementFence(
|
|
|
581
835
|
);
|
|
582
836
|
}
|
|
583
837
|
|
|
838
|
+
async function cancelRejectedTelegramWorkspaceRetirement(
|
|
839
|
+
input: Pick<Parameters<typeof executeTelegramWorkspaceRetirement>[0],
|
|
840
|
+
"store" | "admission" | "getLeaderEpoch" | "getProfileKey" | "isCurrent">,
|
|
841
|
+
retained: TelegramWorkspaceRetirementFence,
|
|
842
|
+
): Promise<Exclude<TelegramWorkspaceRetirementExecution, { kind: "retired" }>> {
|
|
843
|
+
const epoch = input.getLeaderEpoch();
|
|
844
|
+
const isCurrent = () => epoch !== undefined && input.getLeaderEpoch() === epoch &&
|
|
845
|
+
input.getProfileKey() === retained.profileKey && input.isCurrent?.() !== false;
|
|
846
|
+
if (!isCurrent()) return { kind: "retained", reason: "authority-changed" };
|
|
847
|
+
if (retained.phase !== "deletion-rejected" ||
|
|
848
|
+
(retained.destructiveKind ?? "pressure-retirement") !== "pressure-retirement") {
|
|
849
|
+
return { kind: "retained", reason: "fence-conflict" };
|
|
850
|
+
}
|
|
851
|
+
const owner = input.admission.getOwner();
|
|
852
|
+
const fence = retained.leaderEpoch === epoch && isDeepStrictEqual(retained.owner, owner)
|
|
853
|
+
? retained : input.admission.adoptRetirementFence(retained, { owner, leaderEpoch: epoch! });
|
|
854
|
+
if (fence.phase !== "deletion-rejected") return { kind: "retained", reason: "fence-conflict" };
|
|
855
|
+
const bindingRetained = () => input.store.listWorkspaceBindings().some(binding =>
|
|
856
|
+
binding.bindingKey === fence.bindingKey && binding.slot === fence.slot && sameTarget(binding.target, fence.target));
|
|
857
|
+
const intents = input.store.listWorkspaceRetirementIntents();
|
|
858
|
+
if (!bindingRetained() || intents.length > 1 ||
|
|
859
|
+
(intents[0] && !matchesTelegramWorkspaceRetirementFence(fence, intents[0]))) {
|
|
860
|
+
return { kind: "retained", reason: "stale-intent" };
|
|
861
|
+
}
|
|
862
|
+
if (intents[0] && !input.store.removeWorkspaceRetirementIntent(intents[0])) {
|
|
863
|
+
return { kind: "retained", reason: "commit-rejected" };
|
|
864
|
+
}
|
|
865
|
+
// Publish even after a same-store retry whose failed write left only a dirty
|
|
866
|
+
// in-memory withdrawal. The rejection fence protects both commit prefixes.
|
|
867
|
+
await input.store.persist();
|
|
868
|
+
if (!isCurrent()) return { kind: "retained", reason: "authority-changed" };
|
|
869
|
+
if (input.store.listWorkspaceRetirementIntents().length || !bindingRetained()) {
|
|
870
|
+
return { kind: "retained", reason: "commit-rejected" };
|
|
871
|
+
}
|
|
872
|
+
try { input.admission.completeRejectedRetirementFence(fence); }
|
|
873
|
+
catch {
|
|
874
|
+
if (input.admission.read().fence) return { kind: "retained", reason: "fence-release-unconfirmed" };
|
|
875
|
+
}
|
|
876
|
+
return { kind: "cancelled", reason: "delete-rejected" };
|
|
877
|
+
}
|
|
878
|
+
|
|
584
879
|
export async function executeTelegramWorkspaceRetirement(input: {
|
|
585
880
|
store: Pick<
|
|
586
881
|
TelegramTopicTargetStore,
|
|
@@ -588,6 +883,8 @@ export async function executeTelegramWorkspaceRetirement(input: {
|
|
|
588
883
|
| "listWorkspaceBindings"
|
|
589
884
|
| "listWorkspaceRetirementIntents"
|
|
590
885
|
| "commitWorkspaceRetirement"
|
|
886
|
+
| "removeWorkspaceRetirementIntent"
|
|
887
|
+
| "persist"
|
|
591
888
|
>;
|
|
592
889
|
admission: Pick<
|
|
593
890
|
TelegramWorkspaceAdmissionLedger,
|
|
@@ -597,6 +894,8 @@ export async function executeTelegramWorkspaceRetirement(input: {
|
|
|
597
894
|
| "adoptRetirementFence"
|
|
598
895
|
| "issueDeletionPermit"
|
|
599
896
|
| "confirmRetirementAbsence"
|
|
897
|
+
| "confirmRetirementRejection"
|
|
898
|
+
| "completeRejectedRetirementFence"
|
|
600
899
|
| "releaseUnissuedRetirementFence"
|
|
601
900
|
| "completeRetirementFence"
|
|
602
901
|
>;
|
|
@@ -646,6 +945,9 @@ export async function executeTelegramWorkspaceRetirement(input: {
|
|
|
646
945
|
leaderEpoch: input.intent.leaderEpoch,
|
|
647
946
|
});
|
|
648
947
|
}
|
|
948
|
+
if (fence?.phase === "deletion-rejected") {
|
|
949
|
+
return cancelRejectedTelegramWorkspaceRetirement({ ...input, isCurrent }, fence);
|
|
950
|
+
}
|
|
649
951
|
const intent = input.store.listWorkspaceRetirementIntents().find((candidate) =>
|
|
650
952
|
isDeepStrictEqual(candidate, input.intent),
|
|
651
953
|
);
|
|
@@ -678,7 +980,8 @@ export async function executeTelegramWorkspaceRetirement(input: {
|
|
|
678
980
|
if (!eligible()) return { kind: "retained", reason: "protection-changed" };
|
|
679
981
|
if (!fence) {
|
|
680
982
|
const acquired = input.admission.acquireRetirementFence({
|
|
681
|
-
|
|
983
|
+
// A fresh attempt after proven rejection must not revive an old permit.
|
|
984
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
682
985
|
retirementIntentId: input.intent.id,
|
|
683
986
|
bindingKey: binding.bindingKey,
|
|
684
987
|
slot: binding.slot!,
|
|
@@ -719,6 +1022,12 @@ export async function executeTelegramWorkspaceRetirement(input: {
|
|
|
719
1022
|
);
|
|
720
1023
|
} catch (error) {
|
|
721
1024
|
if (!isTelegramWorkspaceDeletionConfirmedAbsent(error)) {
|
|
1025
|
+
const target = getTelegramApiErrorRequestTarget(error);
|
|
1026
|
+
if (target && sameTarget(target, binding.target) && isTelegramApiRequestRejected(error, "deleteForumTopic")) {
|
|
1027
|
+
fence = input.admission.confirmRetirementRejection(fence);
|
|
1028
|
+
const cancellation = await cancelRejectedTelegramWorkspaceRetirement({ ...input, isCurrent }, fence);
|
|
1029
|
+
return cancellation.kind === "cancelled" ? { kind: "retained", reason: "delete-rejected" } : cancellation;
|
|
1030
|
+
}
|
|
722
1031
|
return { kind: "retained", reason: "delete-unconfirmed" };
|
|
723
1032
|
}
|
|
724
1033
|
}
|
|
@@ -922,6 +1231,31 @@ export async function runTelegramWorkspaceRetirementLifecycle(input: {
|
|
|
922
1231
|
>[0]["confirmTargetAbsent"];
|
|
923
1232
|
}): Promise<TelegramWorkspaceRetirementLifecycleResult> {
|
|
924
1233
|
let intent = input.store.listWorkspaceRetirementIntents()[0];
|
|
1234
|
+
const retainedFence = input.admission.read().fence;
|
|
1235
|
+
if (retainedFence && isTelegramWorkspaceRetirementFence(retainedFence) &&
|
|
1236
|
+
retainedFence.phase === "deletion-rejected") {
|
|
1237
|
+
return input.runExclusive(() => cancelRejectedTelegramWorkspaceRetirement(input, retainedFence));
|
|
1238
|
+
}
|
|
1239
|
+
if (!intent && retainedFence && isTelegramWorkspaceRetirementFence(retainedFence) &&
|
|
1240
|
+
(retainedFence.destructiveKind ?? "pressure-retirement") === "pressure-retirement" &&
|
|
1241
|
+
retainedFence.phase === "commit-ready") {
|
|
1242
|
+
return input.runExclusive(async () => {
|
|
1243
|
+
const epoch = input.getLeaderEpoch();
|
|
1244
|
+
if (epoch === undefined || input.getProfileKey() !== retainedFence.profileKey ||
|
|
1245
|
+
input.isCurrent?.() === false) return { kind: "retained", reason: "authority-changed" };
|
|
1246
|
+
if (input.store.listWorkspaceRetirementIntents().length ||
|
|
1247
|
+
input.store.listWorkspaceBindings().some((binding) =>
|
|
1248
|
+
binding.bindingKey === retainedFence.bindingKey)) {
|
|
1249
|
+
return { kind: "retained", reason: "stale-intent" };
|
|
1250
|
+
}
|
|
1251
|
+
const owner = input.admission.getOwner();
|
|
1252
|
+
const fence = retainedFence.leaderEpoch === epoch &&
|
|
1253
|
+
isDeepStrictEqual(retainedFence.owner, owner) ? retainedFence :
|
|
1254
|
+
input.admission.adoptRetirementFence(retainedFence, { owner, leaderEpoch: epoch });
|
|
1255
|
+
input.admission.completeRetirementFence(fence);
|
|
1256
|
+
return { kind: "retired", bindingKey: fence.bindingKey, slot: fence.slot };
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
925
1259
|
if (intent) {
|
|
926
1260
|
const adoption = await adoptTelegramWorkspaceRetirementIntent({
|
|
927
1261
|
store: input.store,
|
|
@@ -966,3 +1300,127 @@ export async function runTelegramWorkspaceRetirementLifecycle(input: {
|
|
|
966
1300
|
confirmTargetAbsent: input.confirmTargetAbsent,
|
|
967
1301
|
});
|
|
968
1302
|
}
|
|
1303
|
+
|
|
1304
|
+
export type TelegramWorkspaceCapacityRunner = <T>(operation: () => Promise<T>) => Promise<T>;
|
|
1305
|
+
|
|
1306
|
+
export interface TelegramWorkspaceSlotRotationPorts extends TelegramWorkspaceOperationGate {
|
|
1307
|
+
getAdmission: () => TelegramWorkspaceAdmissionLedger | undefined;
|
|
1308
|
+
deleteThread: TelegramWorkspaceThreadDeletionTransport;
|
|
1309
|
+
reclaimDeadOwnerQueuedWork?: (
|
|
1310
|
+
binding: TelegramWorkspaceThreadBinding,
|
|
1311
|
+
isCurrent: () => boolean,
|
|
1312
|
+
) => Promise<TelegramWorkspaceDeadQueueReclamation>;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/** Retry allocation once, only after the failed operation released all ordinary leases. */
|
|
1316
|
+
export function createTelegramWorkspaceSlotRotation(input: TelegramWorkspaceSlotRotationPorts & {
|
|
1317
|
+
store: TelegramTopicTargetStore;
|
|
1318
|
+
getLeaderEpoch: () => number | string | undefined;
|
|
1319
|
+
getExternalProtection: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceExternalProtectionEvidence;
|
|
1320
|
+
recordEvent: (message: string, details: Record<string, unknown>) => void;
|
|
1321
|
+
}): TelegramWorkspaceCapacityRunner {
|
|
1322
|
+
const requests = createTelegramWorkspaceOperationGate();
|
|
1323
|
+
return async (operation) => {
|
|
1324
|
+
const admission = input.getAdmission();
|
|
1325
|
+
const epoch = input.getLeaderEpoch();
|
|
1326
|
+
if (!admission || epoch === undefined) return operation();
|
|
1327
|
+
const profileKey = admission.getProfileKey();
|
|
1328
|
+
const isCurrent = () => input.getLeaderEpoch() === epoch &&
|
|
1329
|
+
input.getAdmission()?.getProfileKey() === profileKey;
|
|
1330
|
+
return requests.runExclusive(async () => {
|
|
1331
|
+
if (!isCurrent()) throw new Error("Telegram Workspace allocation lost leader authority.");
|
|
1332
|
+
const rotate = async () => {
|
|
1333
|
+
const result = await input.runExclusive(async () => {
|
|
1334
|
+
if (!isCurrent()) throw new Error("Telegram Workspace rotation lost leader authority.");
|
|
1335
|
+
await input.store.load();
|
|
1336
|
+
if (!isCurrent()) throw new Error("Telegram Workspace rotation lost leader authority.");
|
|
1337
|
+
return runTelegramWorkspaceRetirementLifecycle({
|
|
1338
|
+
store: input.store, admission, getExternalProtection: input.getExternalProtection,
|
|
1339
|
+
getLeaderEpoch: input.getLeaderEpoch, getProfileKey: () => profileKey, isCurrent,
|
|
1340
|
+
// This entire lifecycle already owns the shared gate, without an admission lease.
|
|
1341
|
+
runExclusive: async (action) => action(),
|
|
1342
|
+
deleteForumTopic(permit, body) {
|
|
1343
|
+
return input.deleteThread(() => {
|
|
1344
|
+
const fence = admission.read().fence;
|
|
1345
|
+
if (!isCurrent() || !fence || !isTelegramWorkspaceRetirementFence(fence) ||
|
|
1346
|
+
(fence.destructiveKind ?? "pressure-retirement") !== "pressure-retirement" ||
|
|
1347
|
+
(permit.destructiveKind ?? "pressure-retirement") !== "pressure-retirement" ||
|
|
1348
|
+
fence.phase !== "deletion-issued" ||
|
|
1349
|
+
!isDeepStrictEqual(fence.owner, admission.getOwner()) ||
|
|
1350
|
+
fence.profileKey !== permit.profileKey || fence.leaderEpoch !== permit.leaderEpoch ||
|
|
1351
|
+
fence.operationId !== permit.operationId || fence.retirementIntentId !== permit.retirementIntentId ||
|
|
1352
|
+
fence.bindingKey !== permit.bindingKey || fence.slot !== permit.slot ||
|
|
1353
|
+
fence.deletionIssuedAtMs !== permit.issuedAtMs ||
|
|
1354
|
+
!sameTarget(fence.target, permit.target) ||
|
|
1355
|
+
body.chat_id !== permit.target.chatId || body.message_thread_id !== permit.target.threadId) {
|
|
1356
|
+
throw new Error("Telegram Workspace deletion permit is stale.");
|
|
1357
|
+
}
|
|
1358
|
+
return { ...permit.target };
|
|
1359
|
+
});
|
|
1360
|
+
},
|
|
1361
|
+
});
|
|
1362
|
+
});
|
|
1363
|
+
if (result.kind !== "retired" && result.kind !== "not-needed" && result.kind !== "cancelled") {
|
|
1364
|
+
throw new Error(`Telegram Workspace slots A-Z are unavailable; rotation blocked (${result.reason}).`);
|
|
1365
|
+
}
|
|
1366
|
+
if (result.kind === "retired") {
|
|
1367
|
+
try { input.recordEvent("Telegram Workspace slot rotated.", { slot: result.slot }); }
|
|
1368
|
+
catch { /* Diagnostics cannot turn completed retirement into another attempt. */ }
|
|
1369
|
+
}
|
|
1370
|
+
if (!isCurrent()) throw new Error("Telegram Workspace rotation lost leader authority.");
|
|
1371
|
+
return result;
|
|
1372
|
+
};
|
|
1373
|
+
const pending = await input.runExclusive(async () => {
|
|
1374
|
+
await input.store.load();
|
|
1375
|
+
if (!isCurrent()) throw new Error("Telegram Workspace allocation lost leader authority.");
|
|
1376
|
+
return input.store.listWorkspaceRetirementIntents().length > 0;
|
|
1377
|
+
});
|
|
1378
|
+
const fence = admission.read().fence;
|
|
1379
|
+
if (pending || (fence && isTelegramWorkspaceRetirementFence(fence) &&
|
|
1380
|
+
(fence.destructiveKind ?? "pressure-retirement") === "pressure-retirement")) {
|
|
1381
|
+
const recovered = await rotate();
|
|
1382
|
+
if (recovered.kind !== "cancelled") return operation();
|
|
1383
|
+
}
|
|
1384
|
+
try { return await operation(); }
|
|
1385
|
+
catch (error) {
|
|
1386
|
+
if (!(error instanceof TelegramWorkspaceSlotUnavailableError)) throw error;
|
|
1387
|
+
if (input.reclaimDeadOwnerQueuedWork) {
|
|
1388
|
+
const candidates = await input.runExclusive(async () => {
|
|
1389
|
+
await input.store.load();
|
|
1390
|
+
if (!isCurrent()) throw new Error("Telegram Workspace reclamation lost leader authority.");
|
|
1391
|
+
const snapshot = input.store.captureWorkspaceSlotOccupancy(input.getExternalProtection);
|
|
1392
|
+
const allocation = planTelegramWorkspaceSlotAllocation({ ...snapshot, nowMs: Date.now() });
|
|
1393
|
+
if (allocation.kind !== "blocked" || allocation.reason !== "protected-capacity") return [];
|
|
1394
|
+
return input.store.listWorkspaceBindings().filter((binding) =>
|
|
1395
|
+
typeof binding.inactiveSinceMs === "number" &&
|
|
1396
|
+
Number.isFinite(binding.inactiveSinceMs) &&
|
|
1397
|
+
binding.inactiveSinceMs >= 0,
|
|
1398
|
+
).sort((left, right) =>
|
|
1399
|
+
left.inactiveSinceMs! - right.inactiveSinceMs! ||
|
|
1400
|
+
(left.slot ?? "").localeCompare(right.slot ?? ""),
|
|
1401
|
+
);
|
|
1402
|
+
});
|
|
1403
|
+
for (const binding of candidates) {
|
|
1404
|
+
const evidence = input.getExternalProtection(binding);
|
|
1405
|
+
if (evidence.liveOwner !== "clear" ||
|
|
1406
|
+
evidence.acceptedWork !== "protected" ||
|
|
1407
|
+
evidence.deliveryAuthority !== "clear") continue;
|
|
1408
|
+
const reclaimed = await input.reclaimDeadOwnerQueuedWork(binding, isCurrent);
|
|
1409
|
+
if (reclaimed.kind === "recovered") {
|
|
1410
|
+
try {
|
|
1411
|
+
input.recordEvent("Telegram Workspace dead-owner queue reclaimed.", {
|
|
1412
|
+
slot: binding.slot,
|
|
1413
|
+
receipts: reclaimed.receipts,
|
|
1414
|
+
updateCount: reclaimed.updateIds.length,
|
|
1415
|
+
});
|
|
1416
|
+
} catch { /* Diagnostics cannot revoke journal-owned recovery. */ }
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
await rotate();
|
|
1422
|
+
return operation();
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
export const TELEGRAM_WORKSPACE_SLOTS = "abcdefghijklmnopqrstuvwxyz";
|
|
10
10
|
|
|
11
|
+
export class TelegramWorkspaceSlotUnavailableError extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("Telegram Workspace slot reservation is unavailable.");
|
|
14
|
+
this.name = "TelegramWorkspaceSlotUnavailableError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
export interface TelegramWorkspaceSlotOccupancy {
|
|
12
19
|
bindingKey: string;
|
|
13
20
|
slot: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"@llblab/pi-clean-room": "0.1.1",
|
|
45
45
|
"@llblab/pi-codex-usage": "0.10.0",
|
|
46
46
|
"@llblab/pi-grow-loop": "0.8.1",
|
|
47
|
-
"@llblab/pi-state-flow": "0.17.
|
|
48
|
-
"@llblab/pi-telegram": "0.
|
|
47
|
+
"@llblab/pi-state-flow": "0.17.4",
|
|
48
|
+
"@llblab/pi-telegram": "0.51.0",
|
|
49
49
|
"@llblab/skills": "1.15.0"
|
|
50
50
|
},
|
|
51
51
|
"bundledDependencies": [
|