@llblab/pi-kit 0.27.5 → 0.28.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/BACKLOG.md +3 -1
- package/CHANGELOG.md +9 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +6 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +4 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +26 -13
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +3 -1
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -1
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +25 -12
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
- package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
- package/node_modules/@llblab/pi-telegram/README.md +15 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
- package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
- package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
- package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
- package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
- package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
- package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
- package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
- package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
- package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
- package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
- package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
- package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
- package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
- package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
- package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
- package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
- package/node_modules/@llblab/pi-telegram/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
- package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/package.json +3 -3
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
import { isDeepStrictEqual } from "node:util";
|
|
9
9
|
|
|
10
10
|
import type {
|
|
11
|
+
TelegramJournalNamespaceInspection,
|
|
11
12
|
TelegramUpdateJournalDeadQueueOwnerRecoveryInput,
|
|
12
13
|
TelegramUpdateJournalDeadQueueOwnerRecoveryResult,
|
|
13
14
|
TelegramUpdateJournalEntry,
|
|
14
15
|
TelegramUpdateJournalQueueOwnerIdentity,
|
|
15
16
|
} from "./journal.ts";
|
|
16
|
-
import type
|
|
17
|
+
import { areTelegramTargetsEqual as sameTarget, type TelegramTarget } from "./target.ts";
|
|
17
18
|
import {
|
|
18
19
|
getTelegramApiErrorRequestTarget,
|
|
19
20
|
isTelegramApiRequestRejected,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
} from "./telegram-api.ts";
|
|
22
23
|
import type {
|
|
23
24
|
TelegramTopicTargetStore,
|
|
25
|
+
TelegramWorkspaceJournalSource,
|
|
24
26
|
TelegramWorkspaceExternalProtectionEvidence,
|
|
25
27
|
TelegramWorkspaceRetirementIntent,
|
|
26
28
|
TelegramWorkspaceProtectionState,
|
|
@@ -104,8 +106,8 @@ export interface TelegramWorkspaceJournalProtectionCapture {
|
|
|
104
106
|
|
|
105
107
|
interface TelegramWorkspaceJournalReader {
|
|
106
108
|
recoveryKey?: string;
|
|
107
|
-
journal: { read: () => { entries: readonly { update: unknown }[] } };
|
|
108
|
-
readForProtection?: () => { entries: readonly { update: unknown }[] };
|
|
109
|
+
journal: { read: () => { entries: readonly { update: unknown }[]; exists?: boolean } };
|
|
110
|
+
readForProtection?: () => { entries: readonly { update: unknown }[]; exists?: boolean };
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
export function captureTelegramWorkspaceJournalProtectionSources(input: {
|
|
@@ -114,6 +116,10 @@ export function captureTelegramWorkspaceJournalProtectionSources(input: {
|
|
|
114
116
|
createFollowerResolver: (
|
|
115
117
|
journalBindingKey: string,
|
|
116
118
|
) => () => TelegramWorkspaceJournalReader | undefined;
|
|
119
|
+
createSessionResolver?: (
|
|
120
|
+
recipientBindingKey: string,
|
|
121
|
+
sessionId: string,
|
|
122
|
+
) => () => TelegramWorkspaceJournalReader | undefined;
|
|
117
123
|
withJournalReference?: <T>(binding: TelegramWorkspaceJournalReader,
|
|
118
124
|
operation: () => T) => T;
|
|
119
125
|
discovery?: {
|
|
@@ -140,7 +146,8 @@ export function captureTelegramWorkspaceJournalProtectionSources(input: {
|
|
|
140
146
|
const snapshot = input.withJournalReference
|
|
141
147
|
? input.withJournalReference(binding, read)
|
|
142
148
|
: read();
|
|
143
|
-
sources.push({ kind: "available", scope, entries: snapshot.entries
|
|
149
|
+
sources.push({ kind: "available", scope, entries: snapshot.entries,
|
|
150
|
+
...(snapshot.exists === undefined ? {} : { exists: snapshot.exists }) });
|
|
144
151
|
} catch {
|
|
145
152
|
complete = false;
|
|
146
153
|
sources.push({ kind: "unknown", scope });
|
|
@@ -157,6 +164,11 @@ export function captureTelegramWorkspaceJournalProtectionSources(input: {
|
|
|
157
164
|
input.createFollowerResolver(journalBindingKey),
|
|
158
165
|
);
|
|
159
166
|
}
|
|
167
|
+
for (const source of input.binding.journalSources ?? []) {
|
|
168
|
+
capture({ kind: "binding", bindingKey: input.binding.bindingKey,
|
|
169
|
+
journalBindingKey: source.recipientBindingKey, sessionId: source.sessionId },
|
|
170
|
+
input.createSessionResolver?.(source.recipientBindingKey, source.sessionId) ?? (() => undefined));
|
|
171
|
+
}
|
|
160
172
|
for (const path of input.discovery?.paths ?? []) {
|
|
161
173
|
capture({ kind: "discovered", path }, input.discovery!.createResolver(path));
|
|
162
174
|
}
|
|
@@ -168,15 +180,17 @@ export type TelegramWorkspaceJournalProtectionSource =
|
|
|
168
180
|
kind: "available";
|
|
169
181
|
scope:
|
|
170
182
|
| { kind: "shared" }
|
|
171
|
-
| { kind: "binding"; bindingKey: string; journalBindingKey: string }
|
|
183
|
+
| { kind: "binding"; bindingKey: string; journalBindingKey: string; sessionId?: string }
|
|
172
184
|
| { kind: "discovered"; path: string };
|
|
173
185
|
entries: readonly { update: unknown }[];
|
|
186
|
+
/** Positive filesystem presence is distinct from a complete namespace with a missing historical family. */
|
|
187
|
+
exists?: boolean;
|
|
174
188
|
}
|
|
175
189
|
| {
|
|
176
190
|
kind: "unknown";
|
|
177
191
|
scope:
|
|
178
192
|
| { kind: "shared" }
|
|
179
|
-
| { kind: "binding"; bindingKey: string; journalBindingKey: string }
|
|
193
|
+
| { kind: "binding"; bindingKey: string; journalBindingKey: string; sessionId?: string }
|
|
180
194
|
| { kind: "discovered"; path: string };
|
|
181
195
|
};
|
|
182
196
|
|
|
@@ -206,18 +220,13 @@ function getJournalUpdateTarget(update: unknown):
|
|
|
206
220
|
};
|
|
207
221
|
}
|
|
208
222
|
|
|
209
|
-
function sameTarget(
|
|
210
|
-
left: { chatId: number; threadId?: number },
|
|
211
|
-
right: { chatId: number; threadId?: number },
|
|
212
|
-
): boolean {
|
|
213
|
-
return left.chatId === right.chatId && left.threadId === right.threadId;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
223
|
export function resolveTelegramWorkspaceAcceptedWorkProtection(input: {
|
|
217
224
|
binding: TelegramWorkspaceThreadBinding;
|
|
218
225
|
localAcceptedTargets: readonly { chatId: number; threadId?: number }[];
|
|
219
226
|
journalSources: readonly TelegramWorkspaceJournalProtectionSource[];
|
|
220
227
|
sourcesComplete: boolean;
|
|
228
|
+
/** Relocation cannot infer execution ownership from the original Telegram message target. */
|
|
229
|
+
requireBindingProvenance?: boolean;
|
|
221
230
|
}): TelegramWorkspaceProtectionState {
|
|
222
231
|
if (input.localAcceptedTargets.some((target) => sameTarget(target, input.binding.target))) {
|
|
223
232
|
return "protected";
|
|
@@ -231,9 +240,17 @@ export function resolveTelegramWorkspaceAcceptedWorkProtection(input: {
|
|
|
231
240
|
unknown = true;
|
|
232
241
|
continue;
|
|
233
242
|
}
|
|
243
|
+
if (input.requireBindingProvenance && source.scope.kind === "binding" && source.exists === false) {
|
|
244
|
+
unknown = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
234
247
|
if (source.scope.kind === "binding" && source.entries.length > 0) {
|
|
235
248
|
return "protected";
|
|
236
249
|
}
|
|
250
|
+
if (input.requireBindingProvenance && source.entries.length > 0) {
|
|
251
|
+
unknown = true;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
237
254
|
for (const entry of source.entries) {
|
|
238
255
|
const target = getJournalUpdateTarget(entry.update);
|
|
239
256
|
if (!target) {
|
|
@@ -251,19 +268,22 @@ export type TelegramWorkspaceJournalPruneResult =
|
|
|
251
268
|
kind: "committed";
|
|
252
269
|
binding: TelegramWorkspaceThreadBinding;
|
|
253
270
|
removedKeys: string[];
|
|
271
|
+
removedSources?: TelegramWorkspaceJournalSource[];
|
|
254
272
|
}
|
|
255
273
|
| {
|
|
256
274
|
kind: "blocked";
|
|
257
|
-
reason: "incomplete-evidence" | "writer-not-quiescent" | "state-changed";
|
|
275
|
+
reason: "incomplete-evidence" | "writer-not-quiescent" | "state-changed" | "publication-refused";
|
|
258
276
|
};
|
|
259
277
|
|
|
260
278
|
export async function pruneTelegramWorkspaceJournalEvidence(input: {
|
|
261
279
|
store: Pick<
|
|
262
280
|
TelegramTopicTargetStore,
|
|
263
|
-
"commitWorkspaceJournalEvidence" | "
|
|
281
|
+
"commitWorkspaceJournalEvidence" | "persistWorkspaceJournalEvidence"
|
|
264
282
|
>;
|
|
265
283
|
binding: TelegramWorkspaceThreadBinding;
|
|
266
|
-
|
|
284
|
+
/** Capture fresh source evidence after admission, not a snapshot taken before the lease. */
|
|
285
|
+
capture: () => TelegramWorkspaceJournalProtectionCapture;
|
|
286
|
+
runExclusive: TelegramWorkspaceOperationGate["runExclusive"];
|
|
267
287
|
getJournalWriterProtection: (
|
|
268
288
|
journalBindingKey: string,
|
|
269
289
|
) => TelegramWorkspaceProtectionState;
|
|
@@ -289,72 +309,68 @@ export async function pruneTelegramWorkspaceJournalEvidence(input: {
|
|
|
289
309
|
"Telegram Workspace journal pruning requires current leader authority.",
|
|
290
310
|
);
|
|
291
311
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
shared[0]?.kind !== "available" ||
|
|
311
|
-
bindingSources.length !==
|
|
312
|
-
(input.binding.journalBindingKeys ?? []).length ||
|
|
313
|
-
sourceByKey.size !== (input.binding.journalBindingKeys ?? []).length ||
|
|
314
|
-
Array.from(sourceByKey.values()).some(
|
|
315
|
-
(source) => source.kind !== "available",
|
|
316
|
-
)
|
|
317
|
-
) {
|
|
312
|
+
let capture: TelegramWorkspaceJournalProtectionCapture;
|
|
313
|
+
try { capture = input.capture(); }
|
|
314
|
+
catch { return { kind: "blocked", reason: "incomplete-evidence" }; }
|
|
315
|
+
const address = (key: string, sessionId?: string) => JSON.stringify([sessionId ?? null, key]);
|
|
316
|
+
const keys = input.binding.journalBindingKeys ?? [];
|
|
317
|
+
const sessions = input.binding.journalSources ?? [];
|
|
318
|
+
const expected = [
|
|
319
|
+
...keys.map(key => address(key)),
|
|
320
|
+
...sessions.map(source => address(source.recipientBindingKey, source.sessionId)),
|
|
321
|
+
];
|
|
322
|
+
const shared = capture.sources.filter(source => source.scope.kind === "shared");
|
|
323
|
+
const bindingSources = capture.sources.filter(source => source.scope.kind === "binding" &&
|
|
324
|
+
source.scope.bindingKey === input.binding.bindingKey);
|
|
325
|
+
const sourceByAddress = new Map(bindingSources.flatMap(source => source.scope.kind === "binding"
|
|
326
|
+
? [[address(source.scope.journalBindingKey, source.scope.sessionId), source] as const] : []));
|
|
327
|
+
if (!capture.complete || shared.length !== 1 || shared[0]?.kind !== "available" ||
|
|
328
|
+
bindingSources.length !== expected.length || sourceByAddress.size !== expected.length ||
|
|
329
|
+
expected.some(key => sourceByAddress.get(key)?.kind !== "available")) {
|
|
318
330
|
return { kind: "blocked", reason: "incomplete-evidence" };
|
|
319
331
|
}
|
|
320
|
-
const
|
|
321
|
-
const source =
|
|
332
|
+
const empty = new Set(expected.filter(key => {
|
|
333
|
+
const source = sourceByAddress.get(key);
|
|
322
334
|
return source?.kind === "available" && source.entries.length === 0;
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
)
|
|
335
|
+
}));
|
|
336
|
+
const emptyWriterKeys = new Set([
|
|
337
|
+
...keys.filter(key => empty.has(address(key))),
|
|
338
|
+
...sessions.filter(source => empty.has(address(source.recipientBindingKey, source.sessionId)))
|
|
339
|
+
.map(source => source.recipientBindingKey),
|
|
340
|
+
]);
|
|
341
|
+
const quiescent = new Set([...emptyWriterKeys].filter(key => input.getJournalWriterProtection(key) === "clear"));
|
|
342
|
+
const removedKeys = keys.filter(key => empty.has(address(key)) && quiescent.has(key));
|
|
343
|
+
const removedSources = sessions.filter(source => empty.has(address(source.recipientBindingKey, source.sessionId)) &&
|
|
344
|
+
quiescent.has(source.recipientBindingKey));
|
|
345
|
+
if (empty.size > 0 && removedKeys.length === 0 && removedSources.length === 0) {
|
|
329
346
|
return { kind: "blocked", reason: "writer-not-quiescent" };
|
|
330
347
|
}
|
|
331
|
-
const retainedKeys = (
|
|
332
|
-
|
|
333
|
-
);
|
|
348
|
+
const retainedKeys = keys.filter(key => !removedKeys.includes(key));
|
|
349
|
+
const retainedSources = sessions.filter(source => !removedSources.includes(source));
|
|
334
350
|
if (!isCurrent()) {
|
|
335
351
|
throw new Error("Telegram Workspace journal pruning lost leader authority.");
|
|
336
352
|
}
|
|
337
353
|
const binding = input.store.commitWorkspaceJournalEvidence(
|
|
338
|
-
input.binding,
|
|
339
|
-
retainedKeys,
|
|
340
|
-
input.binding.journalBindingsComplete === true,
|
|
354
|
+
input.binding, retainedKeys, input.binding.journalBindingsComplete === true, retainedSources,
|
|
341
355
|
);
|
|
342
356
|
if (!binding) return { kind: "blocked", reason: "state-changed" };
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
357
|
+
if (removedKeys.length > 0 || removedSources.length > 0) {
|
|
358
|
+
const published = await input.store.persistWorkspaceJournalEvidence(binding, isCurrent);
|
|
359
|
+
if (!isCurrent()) throw new Error("Telegram Workspace journal pruning lost leader authority.");
|
|
360
|
+
if (!published) return { kind: "blocked", reason: "publication-refused" };
|
|
361
|
+
}
|
|
347
362
|
if (!isCurrent()) {
|
|
348
363
|
throw new Error("Telegram Workspace journal pruning lost leader authority.");
|
|
349
364
|
}
|
|
350
|
-
return { kind: "committed", binding, removedKeys
|
|
365
|
+
return { kind: "committed", binding, removedKeys,
|
|
366
|
+
...(removedSources.length ? { removedSources: removedSources.map(source => ({ ...source })) } : {}) };
|
|
351
367
|
};
|
|
352
368
|
return runWithTelegramWorkspaceAdmissionsAsync({
|
|
353
369
|
ledger: input.admission,
|
|
354
370
|
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
355
371
|
operationKind: "workspace.prune-journal-evidence",
|
|
356
372
|
scopes: [{ kind: "target", target: input.binding.target }],
|
|
357
|
-
operation,
|
|
373
|
+
operation: () => input.runExclusive(operation),
|
|
358
374
|
onReleaseError(error) {
|
|
359
375
|
input.onAdmissionReleaseError?.(error);
|
|
360
376
|
},
|
|
@@ -363,6 +379,7 @@ export async function pruneTelegramWorkspaceJournalEvidence(input: {
|
|
|
363
379
|
|
|
364
380
|
export function captureTelegramWorkspaceExternalProtection(input: {
|
|
365
381
|
binding: TelegramWorkspaceThreadBinding;
|
|
382
|
+
requireBindingProvenance?: boolean;
|
|
366
383
|
getLiveOwnerProtection: (
|
|
367
384
|
binding: TelegramWorkspaceThreadBinding,
|
|
368
385
|
) => TelegramWorkspaceProtectionState;
|
|
@@ -392,6 +409,7 @@ export function captureTelegramWorkspaceExternalProtection(input: {
|
|
|
392
409
|
localAcceptedTargets: local.targets,
|
|
393
410
|
journalSources: journals.sources,
|
|
394
411
|
sourcesComplete: journals.complete && local.complete,
|
|
412
|
+
requireBindingProvenance: input.requireBindingProvenance,
|
|
395
413
|
});
|
|
396
414
|
} catch {
|
|
397
415
|
// Unavailable queue or journal evidence must not clear accepted work.
|
|
@@ -404,7 +422,7 @@ export function captureTelegramWorkspaceExternalProtection(input: {
|
|
|
404
422
|
return { liveOwner, acceptedWork, deliveryAuthority };
|
|
405
423
|
}
|
|
406
424
|
|
|
407
|
-
|
|
425
|
+
interface TelegramWorkspaceProtectionObserverDeps {
|
|
408
426
|
listFollowers: () => readonly { target?: TelegramTarget }[];
|
|
409
427
|
getActiveTurnTarget: () => TelegramTarget | undefined;
|
|
410
428
|
getQueuedItems: () => readonly { chatId: number; target?: TelegramTarget }[];
|
|
@@ -412,7 +430,13 @@ export function createTelegramWorkspaceExternalProtectionCapture(deps: {
|
|
|
412
430
|
createFollowerJournalResolver: (
|
|
413
431
|
journalBindingKey: string,
|
|
414
432
|
) => () => TelegramWorkspaceJournalReader | undefined;
|
|
433
|
+
createSessionJournalResolver?: (
|
|
434
|
+
recipientBindingKey: string,
|
|
435
|
+
sessionId: string,
|
|
436
|
+
) => () => TelegramWorkspaceJournalReader | undefined;
|
|
415
437
|
discoverFollowerJournals?: () => { paths: readonly string[]; complete: boolean };
|
|
438
|
+
/** Strict read-only namespace evidence; it never substitutes for writer closure. */
|
|
439
|
+
inspectJournalNamespace?: () => TelegramJournalNamespaceInspection;
|
|
416
440
|
createJournalPathResolver?: (
|
|
417
441
|
path: string,
|
|
418
442
|
) => () => TelegramWorkspaceJournalReader | undefined;
|
|
@@ -424,19 +448,56 @@ export function createTelegramWorkspaceExternalProtectionCapture(deps: {
|
|
|
424
448
|
getDeliveryAuthorityProtection?: (
|
|
425
449
|
binding: TelegramWorkspaceThreadBinding,
|
|
426
450
|
) => TelegramWorkspaceProtectionState;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export interface TelegramWorkspaceProtectionObserver {
|
|
454
|
+
capture: (binding: TelegramWorkspaceThreadBinding,
|
|
455
|
+
options?: { requireBindingProvenance?: boolean }) => TelegramWorkspaceExternalProtectionEvidence;
|
|
456
|
+
captureJournalSources: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceJournalProtectionCapture;
|
|
457
|
+
getJournalWriterProtection: (journalBindingKey: string) => TelegramWorkspaceProtectionState;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Shared read-only observer: protection and metadata pruning use the same scoped evidence path. */
|
|
461
|
+
export function createTelegramWorkspaceProtectionObserver(deps: TelegramWorkspaceProtectionObserverDeps): TelegramWorkspaceProtectionObserver {
|
|
462
|
+
const captureJournalSources: TelegramWorkspaceProtectionObserver["captureJournalSources"] = (candidate) => {
|
|
463
|
+
const namespace = deps.inspectJournalNamespace?.();
|
|
464
|
+
if (deps.inspectJournalNamespace && (!namespace || namespace.sources.filter(source => source.role === "polling").length !== 1))
|
|
465
|
+
throw new Error("Telegram strict namespace evidence is unavailable or incomplete.");
|
|
466
|
+
if (namespace?.retainedInputs?.some(original => original.state !== "committed")) throw new Error(
|
|
467
|
+
"Telegram namespace contains uncommitted private retention evidence.",
|
|
468
|
+
);
|
|
469
|
+
const discovery = namespace
|
|
470
|
+
? { paths: namespace.sources.filter(source => source.role !== "polling" && source.evidence.kind === "present")
|
|
471
|
+
.map(source => source.path), complete: true }
|
|
472
|
+
: candidate.journalBindingsComplete === true ? undefined : deps.discoverFollowerJournals?.();
|
|
473
|
+
if (namespace && !deps.createJournalPathResolver) throw new Error(
|
|
474
|
+
"Telegram strict namespace protection requires discovered source resolution.",
|
|
475
|
+
);
|
|
476
|
+
return captureTelegramWorkspaceJournalProtectionSources({
|
|
477
|
+
binding: candidate,
|
|
478
|
+
resolveLeader: deps.resolveLeaderJournal,
|
|
479
|
+
createFollowerResolver: deps.createFollowerJournalResolver,
|
|
480
|
+
...(deps.createSessionJournalResolver ? { createSessionResolver: deps.createSessionJournalResolver } : {}),
|
|
481
|
+
...(deps.withJournalReference ? { withJournalReference: deps.withJournalReference } : {}),
|
|
482
|
+
...(discovery && deps.createJournalPathResolver
|
|
483
|
+
? { discovery: { ...discovery, createResolver: deps.createJournalPathResolver } } : {}),
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
const capture: TelegramWorkspaceProtectionObserver["capture"] = function (binding, options) {
|
|
431
487
|
return captureTelegramWorkspaceExternalProtection({
|
|
432
488
|
binding,
|
|
489
|
+
requireBindingProvenance: options?.requireBindingProvenance,
|
|
433
490
|
getLiveOwnerProtection(candidate) {
|
|
434
491
|
if (deps.listFollowers().some((follower) =>
|
|
435
492
|
!!follower.target && sameTarget(follower.target, candidate.target),
|
|
436
493
|
)) return "protected";
|
|
437
494
|
if (!deps.getJournalWriterProtection) return "unknown";
|
|
438
495
|
let unknown = candidate.journalBindingsComplete !== true;
|
|
439
|
-
|
|
496
|
+
const writerKeys = new Set([
|
|
497
|
+
...(candidate.journalBindingKeys ?? []),
|
|
498
|
+
...(candidate.journalSources ?? []).map(source => source.recipientBindingKey),
|
|
499
|
+
]);
|
|
500
|
+
for (const journalBindingKey of writerKeys) {
|
|
440
501
|
const protection = deps.getJournalWriterProtection(journalBindingKey);
|
|
441
502
|
if (protection === "protected") return "protected";
|
|
442
503
|
if (protection === "unknown") unknown = true;
|
|
@@ -455,29 +516,40 @@ export function createTelegramWorkspaceExternalProtectionCapture(deps: {
|
|
|
455
516
|
}
|
|
456
517
|
return { targets, complete };
|
|
457
518
|
},
|
|
458
|
-
captureJournalSources
|
|
459
|
-
const discovery = candidate.journalBindingsComplete === true
|
|
460
|
-
? undefined
|
|
461
|
-
: deps.discoverFollowerJournals?.();
|
|
462
|
-
return captureTelegramWorkspaceJournalProtectionSources({
|
|
463
|
-
binding: candidate,
|
|
464
|
-
resolveLeader: deps.resolveLeaderJournal,
|
|
465
|
-
createFollowerResolver: deps.createFollowerJournalResolver,
|
|
466
|
-
...(deps.withJournalReference
|
|
467
|
-
? { withJournalReference: deps.withJournalReference } : {}),
|
|
468
|
-
...(discovery && deps.createJournalPathResolver
|
|
469
|
-
? { discovery: {
|
|
470
|
-
...discovery,
|
|
471
|
-
createResolver: deps.createJournalPathResolver,
|
|
472
|
-
} }
|
|
473
|
-
: {}),
|
|
474
|
-
});
|
|
475
|
-
},
|
|
519
|
+
captureJournalSources,
|
|
476
520
|
...(deps.getDeliveryAuthorityProtection
|
|
477
521
|
? { getDeliveryAuthorityProtection: deps.getDeliveryAuthorityProtection }
|
|
478
522
|
: {}),
|
|
479
523
|
});
|
|
480
524
|
};
|
|
525
|
+
return { capture, captureJournalSources,
|
|
526
|
+
getJournalWriterProtection: deps.getJournalWriterProtection ?? (() => "unknown") };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Callable protection view retained for callers that need no metadata observation ports. */
|
|
530
|
+
export function createTelegramWorkspaceExternalProtectionCapture(deps: TelegramWorkspaceProtectionObserverDeps): TelegramWorkspaceProtectionObserver["capture"] {
|
|
531
|
+
return createTelegramWorkspaceProtectionObserver(deps).capture;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function createTelegramWorkspaceJournalEvidencePruner(deps: TelegramWorkspaceOperationGate & {
|
|
535
|
+
store: TelegramTopicTargetStore;
|
|
536
|
+
getAdmission: () => TelegramWorkspaceAdmissionLedger | undefined;
|
|
537
|
+
getLeaderEpoch: () => number | string | undefined;
|
|
538
|
+
protection: Pick<TelegramWorkspaceProtectionObserver, "captureJournalSources" | "getJournalWriterProtection">;
|
|
539
|
+
}): (binding: TelegramWorkspaceThreadBinding, isCurrent: () => boolean) => Promise<TelegramWorkspaceJournalPruneResult> {
|
|
540
|
+
return async (binding, isCurrent) => {
|
|
541
|
+
const admission = deps.getAdmission();
|
|
542
|
+
if (!admission) return { kind: "blocked", reason: "state-changed" };
|
|
543
|
+
const profileKey = admission.getProfileKey();
|
|
544
|
+
return pruneTelegramWorkspaceJournalEvidence({
|
|
545
|
+
store: deps.store, binding, admission, runExclusive: deps.runExclusive,
|
|
546
|
+
capture: () => deps.protection.captureJournalSources(binding),
|
|
547
|
+
getJournalWriterProtection: deps.protection.getJournalWriterProtection,
|
|
548
|
+
getLeaderEpoch: deps.getLeaderEpoch,
|
|
549
|
+
getProfileKey: () => deps.getAdmission()?.getProfileKey() ?? "",
|
|
550
|
+
isCurrent: () => isCurrent() && deps.getAdmission()?.getProfileKey() === profileKey,
|
|
551
|
+
});
|
|
552
|
+
};
|
|
481
553
|
}
|
|
482
554
|
|
|
483
555
|
export function isCurrentTelegramWorkspaceBinding(
|
|
@@ -530,6 +602,10 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
|
|
|
530
602
|
createFollowerJournalResolver: (
|
|
531
603
|
journalBindingKey: string,
|
|
532
604
|
) => () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
|
|
605
|
+
createSessionJournalResolver?: (
|
|
606
|
+
recipientBindingKey: string,
|
|
607
|
+
sessionId: string,
|
|
608
|
+
) => () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
|
|
533
609
|
discoverFollowerJournals?: () => { paths: readonly string[]; complete: boolean };
|
|
534
610
|
createJournalPathResolver?: (
|
|
535
611
|
path: string,
|
|
@@ -564,6 +640,9 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
|
|
|
564
640
|
if (!isCurrent() || !deps.isBindingCurrent(binding)) {
|
|
565
641
|
return { kind: "blocked", reason: "authority-changed" };
|
|
566
642
|
}
|
|
643
|
+
// Session addresses require exact resolution; never substitute a flat same-hash journal.
|
|
644
|
+
if (binding.journalSources?.length && !deps.createSessionJournalResolver)
|
|
645
|
+
return { kind: "blocked", reason: "incomplete-source" };
|
|
567
646
|
const initial = deps.getExternalProtection(binding);
|
|
568
647
|
if (initial.liveOwner !== "clear" || initial.deliveryAuthority !== "clear") {
|
|
569
648
|
return { kind: "blocked", reason: "live-owner" };
|
|
@@ -607,6 +686,9 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
|
|
|
607
686
|
for (const key of binding.journalBindingKeys ?? []) {
|
|
608
687
|
capture("binding", deps.createFollowerJournalResolver(key));
|
|
609
688
|
}
|
|
689
|
+
for (const source of binding.journalSources ?? []) {
|
|
690
|
+
capture("binding", deps.createSessionJournalResolver!(source.recipientBindingKey, source.sessionId));
|
|
691
|
+
}
|
|
610
692
|
for (const path of discovery?.paths ?? []) {
|
|
611
693
|
if (!deps.createJournalPathResolver) {
|
|
612
694
|
complete = false;
|
|
@@ -1339,6 +1421,8 @@ export type TelegramWorkspaceCapacityRunner = <T>(operation: () => Promise<T>) =
|
|
|
1339
1421
|
export interface TelegramWorkspaceSlotRotationPorts extends TelegramWorkspaceOperationGate {
|
|
1340
1422
|
getAdmission: () => TelegramWorkspaceAdmissionLedger | undefined;
|
|
1341
1423
|
deleteThread: TelegramWorkspaceThreadDeletionTransport;
|
|
1424
|
+
pruneJournalEvidence?: (binding: TelegramWorkspaceThreadBinding,
|
|
1425
|
+
isCurrent: () => boolean) => Promise<TelegramWorkspaceJournalPruneResult>;
|
|
1342
1426
|
reclaimDeadOwnerQueuedWork?: (
|
|
1343
1427
|
binding: TelegramWorkspaceThreadBinding,
|
|
1344
1428
|
isCurrent: () => boolean,
|
|
@@ -1417,14 +1501,16 @@ export function createTelegramWorkspaceSlotRotation(input: TelegramWorkspaceSlot
|
|
|
1417
1501
|
try { return await operation(); }
|
|
1418
1502
|
catch (error) {
|
|
1419
1503
|
if (!(error instanceof TelegramWorkspaceSlotUnavailableError)) throw error;
|
|
1420
|
-
if (input.reclaimDeadOwnerQueuedWork) {
|
|
1504
|
+
if (input.pruneJournalEvidence || input.reclaimDeadOwnerQueuedWork) {
|
|
1421
1505
|
const candidates = await input.runExclusive(async () => {
|
|
1422
1506
|
await input.store.load();
|
|
1423
1507
|
if (!isCurrent()) throw new Error("Telegram Workspace reclamation lost leader authority.");
|
|
1424
1508
|
const snapshot = input.store.captureWorkspaceSlotOccupancy(input.getExternalProtection);
|
|
1425
1509
|
const allocation = planTelegramWorkspaceSlotAllocation({ ...snapshot, nowMs: Date.now() });
|
|
1426
|
-
if (allocation.kind
|
|
1510
|
+
if (allocation.kind === "free" || (allocation.kind === "blocked" && allocation.reason !== "protected-capacity")) return [];
|
|
1511
|
+
if (allocation.kind === "reclaim" && !input.pruneJournalEvidence) return [];
|
|
1427
1512
|
return input.store.listWorkspaceBindings().filter((binding) =>
|
|
1513
|
+
(allocation.kind !== "reclaim" || binding.bindingKey === allocation.candidate.bindingKey) &&
|
|
1428
1514
|
typeof binding.inactiveSinceMs === "number" &&
|
|
1429
1515
|
Number.isFinite(binding.inactiveSinceMs) &&
|
|
1430
1516
|
binding.inactiveSinceMs >= 0,
|
|
@@ -1433,7 +1519,20 @@ export function createTelegramWorkspaceSlotRotation(input: TelegramWorkspaceSlot
|
|
|
1433
1519
|
(left.slot ?? "").localeCompare(right.slot ?? ""),
|
|
1434
1520
|
);
|
|
1435
1521
|
});
|
|
1436
|
-
for (const
|
|
1522
|
+
for (const original of candidates) {
|
|
1523
|
+
let binding = original;
|
|
1524
|
+
if (input.pruneJournalEvidence && ((binding.journalBindingKeys?.length ?? 0) + (binding.journalSources?.length ?? 0) > 0)) {
|
|
1525
|
+
const pruned = await input.pruneJournalEvidence(binding, isCurrent);
|
|
1526
|
+
if (!isCurrent()) throw new Error("Telegram Workspace pruning lost leader authority.");
|
|
1527
|
+
if (pruned.kind !== "committed") continue;
|
|
1528
|
+
binding = pruned.binding;
|
|
1529
|
+
const removed = pruned.removedKeys.length + (pruned.removedSources?.length ?? 0);
|
|
1530
|
+
if (removed > 0) {
|
|
1531
|
+
try { input.recordEvent("Telegram Workspace journal evidence pruned.", { slot: binding.slot, removed }); }
|
|
1532
|
+
catch { /* Diagnostics cannot revoke a published metadata subset. */ }
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (!input.reclaimDeadOwnerQueuedWork) continue;
|
|
1437
1536
|
const evidence = input.getExternalProtection(binding);
|
|
1438
1537
|
if (evidence.liveOwner !== "clear" ||
|
|
1439
1538
|
evidence.acceptedWork !== "protected" ||
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"node": ">=22.19.0"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
|
-
"test": "node --experimental-strip-types --test --test-concurrency=4 --test-reporter=dot tests/*.test.ts",
|
|
31
|
-
"test:verbose": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
|
|
30
|
+
"test": "node --experimental-strip-types --import ./tests/fixtures/canonical-tmpdir.ts --test --test-concurrency=4 --test-reporter=dot tests/*.test.ts",
|
|
31
|
+
"test:verbose": "node --experimental-strip-types --import ./tests/fixtures/canonical-tmpdir.ts --test --test-reporter=spec tests/*.test.ts",
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
33
|
"build": "node scripts/build-dist.mjs",
|
|
34
34
|
"build:check": "node scripts/build-dist.mjs --check",
|
|
@@ -109,9 +109,9 @@
|
|
|
109
109
|
"image": "https://raw.githubusercontent.com/llblab/pi-telegram/main/screenshot.png"
|
|
110
110
|
},
|
|
111
111
|
"peerDependencies": {
|
|
112
|
-
"@earendil-works/pi-agent-core": ">=0.
|
|
113
|
-
"@earendil-works/pi-ai": ">=0.
|
|
114
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
112
|
+
"@earendil-works/pi-agent-core": ">=1.0.0",
|
|
113
|
+
"@earendil-works/pi-ai": ">=1.0.0",
|
|
114
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0",
|
|
115
115
|
"@sinclair/typebox": "*"
|
|
116
116
|
},
|
|
117
117
|
"devDependencies": {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only export-reference inventory for package-private lib modules.
|
|
3
|
+
* Zones: developer tooling
|
|
4
|
+
* Usage: node scripts/audit-exports.mjs [project-root]
|
|
5
|
+
* Candidates are not deletion proof: review docs, generated code, dynamic
|
|
6
|
+
* namespace access and public declaration reachability before changing them.
|
|
7
|
+
*/
|
|
8
|
+
import { resolve, relative } from "node:path";
|
|
9
|
+
import ts from "typescript";
|
|
10
|
+
|
|
11
|
+
const root = resolve(process.argv[2] ?? ".");
|
|
12
|
+
const configPath = resolve(root, "tsconfig.json");
|
|
13
|
+
if (!ts.sys.fileExists(configPath)) throw new Error("Export audit requires tsconfig.json.");
|
|
14
|
+
const config = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
15
|
+
if (config.error) throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, "\n"));
|
|
16
|
+
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, root);
|
|
17
|
+
if (parsed.errors.length) throw new Error(parsed.errors.map(error =>
|
|
18
|
+
ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"));
|
|
19
|
+
const files = ts.sys.readDirectory(root, [".ts", ".mjs", ".js"], ["node_modules", "dist", ".git"]);
|
|
20
|
+
const program = ts.createProgram(files, { ...parsed.options, allowJs: true, noEmit: true });
|
|
21
|
+
const checker = program.getTypeChecker();
|
|
22
|
+
const sources = program.getSourceFiles().filter(source => files.includes(source.fileName));
|
|
23
|
+
const candidates = new Map();
|
|
24
|
+
const modules = new Map();
|
|
25
|
+
|
|
26
|
+
function original(symbol) {
|
|
27
|
+
return symbol && symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const source of sources) {
|
|
31
|
+
if (!relative(root, source.fileName).replaceAll("\\", "/").startsWith("lib/")) continue;
|
|
32
|
+
const module = checker.getSymbolAtLocation(source);
|
|
33
|
+
if (!module) continue;
|
|
34
|
+
const scope = { file: relative(root, source.fileName).replaceAll("\\", "/"), rows: [], escapes: new Set() };
|
|
35
|
+
modules.set(module, scope);
|
|
36
|
+
for (const exported of checker.getExportsOfModule(module)) {
|
|
37
|
+
const symbol = original(exported);
|
|
38
|
+
const declarations = symbol?.getDeclarations() ?? [];
|
|
39
|
+
const declaration = declarations.find(node => node.getSourceFile() === source);
|
|
40
|
+
if (!declaration || exported.name === "default") continue;
|
|
41
|
+
const row = {
|
|
42
|
+
file: scope.file,
|
|
43
|
+
name: exported.name,
|
|
44
|
+
line: source.getLineAndCharacterOfPosition(declaration.getStart()).line + 1,
|
|
45
|
+
typeOnly: declarations.every(node => ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)),
|
|
46
|
+
localReferences: 0,
|
|
47
|
+
externalReferences: new Set(),
|
|
48
|
+
namespaceEscapes: scope.escapes,
|
|
49
|
+
};
|
|
50
|
+
candidates.set(symbol, row);
|
|
51
|
+
scope.rows.push(row);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const source of sources) {
|
|
56
|
+
const file = relative(root, source.fileName).replaceAll("\\", "/");
|
|
57
|
+
// Star reexports need no identifier token naming the individual symbol.
|
|
58
|
+
if (file.startsWith("api/")) {
|
|
59
|
+
const module = checker.getSymbolAtLocation(source);
|
|
60
|
+
for (const exported of module ? checker.getExportsOfModule(module) : []) {
|
|
61
|
+
const symbol = original(exported);
|
|
62
|
+
candidates.get(symbol)?.externalReferences.add(file);
|
|
63
|
+
// A public namespace exposes every member without naming each one.
|
|
64
|
+
for (const row of modules.get(symbol)?.rows ?? []) row.externalReferences.add(file);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const visit = node => {
|
|
68
|
+
if (ts.isIdentifier(node) || ts.isStringLiteral(node)) {
|
|
69
|
+
const symbol = original(checker.getSymbolAtLocation(node));
|
|
70
|
+
const scope = modules.get(symbol);
|
|
71
|
+
const parent = node.parent;
|
|
72
|
+
if (scope && scope.file !== file && ts.isIdentifier(node) &&
|
|
73
|
+
!ts.isNamespaceImport(parent) &&
|
|
74
|
+
!(ts.isPropertyAccessExpression(parent) && parent.expression === node) &&
|
|
75
|
+
!(ts.isQualifiedName(parent) && parent.left === node) &&
|
|
76
|
+
!(ts.isElementAccessExpression(parent) && parent.expression === node &&
|
|
77
|
+
parent.argumentExpression && ts.isStringLiteral(parent.argumentExpression))) {
|
|
78
|
+
scope.escapes.add(file);
|
|
79
|
+
}
|
|
80
|
+
const row = candidates.get(symbol);
|
|
81
|
+
if (row && !symbol.getDeclarations()?.some(declaration => declaration.name === node)) {
|
|
82
|
+
if (file === row.file) row.localReferences++;
|
|
83
|
+
else row.externalReferences.add(file);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
ts.forEachChild(node, visit);
|
|
87
|
+
};
|
|
88
|
+
visit(source);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rows = [...candidates.values()].map(row => ({
|
|
92
|
+
...row, externalReferences: [...row.externalReferences].sort(),
|
|
93
|
+
namespaceEscapes: [...row.namespaceEscapes].sort(),
|
|
94
|
+
})).sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
95
|
+
const unused = rows.filter(row => row.externalReferences.length === 0);
|
|
96
|
+
console.log(JSON.stringify({
|
|
97
|
+
scannedSources: sources.length,
|
|
98
|
+
libExports: rows.length,
|
|
99
|
+
candidates: unused,
|
|
100
|
+
}, null, 2));
|