@llblab/pi-telegram 0.51.5 → 0.52.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/AGENTS.md +10 -8
- package/BACKLOG.md +1 -8
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +16 -6
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +5 -7
- package/dist/lib/bindings.d.ts +9 -2
- package/dist/lib/bindings.js +22 -13
- package/dist/lib/bus-api.js +4 -2
- package/dist/lib/bus-follower.d.ts +55 -57
- package/dist/lib/bus-follower.js +213 -140
- package/dist/lib/bus-leader.d.ts +21 -2
- package/dist/lib/bus-leader.js +181 -25
- package/dist/lib/bus-transport.d.ts +4 -1
- package/dist/lib/bus-transport.js +28 -4
- package/dist/lib/bus.d.ts +52 -56
- package/dist/lib/bus.js +78 -246
- package/dist/lib/channel-posts.d.ts +1 -3
- package/dist/lib/channel-posts.js +23 -26
- package/dist/lib/command-templates.d.ts +0 -5
- package/dist/lib/command-templates.js +5 -5
- package/dist/lib/commands.d.ts +12 -26
- package/dist/lib/commands.js +152 -98
- package/dist/lib/config.d.ts +0 -17
- package/dist/lib/config.js +12 -14
- package/dist/lib/delivery.d.ts +0 -4
- package/dist/lib/delivery.js +2 -5
- package/dist/lib/extension.js +281 -61
- package/dist/lib/generative-apps.d.ts +0 -19
- package/dist/lib/inbound.d.ts +0 -2
- package/dist/lib/inbound.js +2 -2
- package/dist/lib/journal.d.ts +177 -16
- package/dist/lib/journal.js +1115 -249
- package/dist/lib/keyboard.d.ts +0 -2
- package/dist/lib/keyboard.js +2 -2
- package/dist/lib/lifecycle.d.ts +57 -2
- package/dist/lib/lifecycle.js +110 -5
- package/dist/lib/locks.d.ts +100 -12
- package/dist/lib/locks.js +380 -14
- package/dist/lib/logging.d.ts +30 -12
- package/dist/lib/logging.js +129 -72
- package/dist/lib/media.d.ts +28 -5
- package/dist/lib/media.js +26 -8
- package/dist/lib/menu-model.d.ts +0 -11
- package/dist/lib/menu-model.js +8 -8
- package/dist/lib/menu-settings.d.ts +0 -18
- package/dist/lib/menu-settings.js +18 -18
- package/dist/lib/menu-status.d.ts +4 -4
- package/dist/lib/menu-status.js +12 -7
- package/dist/lib/menu-thinking.d.ts +1 -1
- package/dist/lib/menu-thinking.js +1 -1
- package/dist/lib/menu.d.ts +1 -0
- package/dist/lib/menu.js +4 -3
- package/dist/lib/model.js +2 -4
- package/dist/lib/outbound-attachments.d.ts +0 -1
- package/dist/lib/outbound-attachments.js +1 -1
- package/dist/lib/outbound-buttons.js +1 -8
- package/dist/lib/outbound-markup.d.ts +1 -6
- package/dist/lib/outbound-markup.js +4 -21
- package/dist/lib/outbound.d.ts +0 -8
- package/dist/lib/outbound.js +5 -5
- package/dist/lib/paths.d.ts +50 -7
- package/dist/lib/paths.js +155 -17
- package/dist/lib/pi.d.ts +1 -0
- package/dist/lib/pi.js +2 -1
- package/dist/lib/polling.d.ts +0 -5
- package/dist/lib/polling.js +5 -5
- package/dist/lib/preview.d.ts +0 -3
- package/dist/lib/preview.js +3 -3
- package/dist/lib/prompt-templates.d.ts +0 -1
- package/dist/lib/prompt-templates.js +1 -1
- package/dist/lib/queue.d.ts +3 -15
- package/dist/lib/queue.js +69 -8
- package/dist/lib/recovery.d.ts +29 -9
- package/dist/lib/recovery.js +104 -33
- package/dist/lib/replies.d.ts +0 -4
- package/dist/lib/replies.js +1 -1
- package/dist/lib/routing.d.ts +73 -8
- package/dist/lib/routing.js +2206 -307
- package/dist/lib/sections.d.ts +0 -2
- package/dist/lib/sections.js +1 -1
- package/dist/lib/status.d.ts +46 -9
- package/dist/lib/status.js +179 -27
- package/dist/lib/sync.d.ts +0 -5
- package/dist/lib/sync.js +9 -8
- package/dist/lib/telegram-api.d.ts +11 -3
- package/dist/lib/telegram-api.js +34 -23
- package/dist/lib/thread-cleanup-manager.d.ts +1 -0
- package/dist/lib/thread-cleanup-manager.js +18 -21
- package/dist/lib/thread-naming.d.ts +32 -5
- package/dist/lib/thread-naming.js +191 -7
- package/dist/lib/thread-reconciler.d.ts +2 -0
- package/dist/lib/thread-reconciler.js +1 -1
- package/dist/lib/threads.d.ts +292 -59
- package/dist/lib/threads.js +1664 -373
- package/dist/lib/turns.d.ts +0 -1
- package/dist/lib/turns.js +1 -1
- package/dist/lib/updates.d.ts +184 -28
- package/dist/lib/updates.js +1032 -114
- package/dist/lib/wire.d.ts +12 -0
- package/dist/lib/wire.js +21 -0
- package/dist/lib/workspace-admission.d.ts +25 -17
- package/dist/lib/workspace-admission.js +95 -20
- package/dist/lib/workspace-identity.d.ts +20 -0
- package/dist/lib/workspace-identity.js +103 -0
- package/dist/lib/workspace-retirement.d.ts +45 -10
- package/dist/lib/workspace-retirement.js +196 -64
- package/dist/package.json +6 -6
- package/dist/skills/generated-control-surface/SKILL.md +3 -3
- package/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/docs/README.md +1 -1
- package/docs/activity.md +1 -1
- package/docs/architecture.md +158 -39
- package/docs/callback-namespaces.md +1 -1
- package/docs/delivery.md +1 -1
- package/docs/generative-apps.md +2 -2
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +127 -19
- package/docs/public-api.md +5 -3
- package/docs/ui-style.md +26 -8
- package/docs/updates.md +1 -1
- package/lib/activity-verbosity.ts +5 -9
- package/lib/bindings.ts +23 -10
- package/lib/bus-api.ts +5 -2
- package/lib/bus-follower.ts +251 -226
- package/lib/bus-leader.ts +178 -31
- package/lib/bus-transport.ts +30 -8
- package/lib/bus.ts +110 -334
- package/lib/channel-posts.ts +29 -29
- package/lib/command-templates.ts +5 -5
- package/lib/commands.ts +150 -112
- package/lib/config.ts +12 -17
- package/lib/delivery.ts +2 -8
- package/lib/extension.ts +282 -72
- package/lib/generative-apps.ts +0 -22
- package/lib/inbound.ts +2 -2
- package/lib/journal.ts +1182 -326
- package/lib/keyboard.ts +2 -2
- package/lib/lifecycle.ts +141 -5
- package/lib/locks.ts +402 -23
- package/lib/logging.ts +151 -101
- package/lib/media.ts +51 -9
- package/lib/menu-model.ts +8 -8
- package/lib/menu-settings.ts +18 -18
- package/lib/menu-status.ts +11 -0
- package/lib/menu-thinking.ts +2 -2
- package/lib/menu.ts +5 -1
- package/lib/model.ts +2 -4
- package/lib/outbound-attachments.ts +1 -1
- package/lib/outbound-buttons.ts +1 -10
- package/lib/outbound-markup.ts +5 -24
- package/lib/outbound.ts +5 -5
- package/lib/paths.ts +177 -17
- package/lib/pi.ts +5 -2
- package/lib/polling.ts +5 -5
- package/lib/preview.ts +3 -3
- package/lib/prompt-templates.ts +1 -1
- package/lib/queue.ts +67 -9
- package/lib/recovery.ts +104 -44
- package/lib/replies.ts +1 -1
- package/lib/routing.ts +1955 -375
- package/lib/sections.ts +1 -1
- package/lib/status.ts +185 -36
- package/lib/sync.ts +8 -8
- package/lib/telegram-api.ts +48 -33
- package/lib/thread-cleanup-manager.ts +24 -21
- package/lib/thread-naming.ts +267 -9
- package/lib/thread-reconciler.ts +3 -1
- package/lib/threads.ts +1718 -491
- package/lib/turns.ts +1 -1
- package/lib/updates.ts +1072 -151
- package/lib/wire.ts +28 -0
- package/lib/workspace-admission.ts +97 -47
- package/lib/workspace-identity.ts +147 -0
- package/lib/workspace-retirement.ts +234 -102
- package/package.json +6 -6
- package/scripts/audit-exports.mjs +100 -0
- package/scripts/check-downgrade.mjs +80 -43
- package/skills/generated-control-surface/SKILL.md +3 -3
- package/skills/telegram-bridge/references/diagnosis.md +3 -3
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* Owns fail-closed protection, demand-driven pressure retirement, exact-intent admission,
|
|
5
5
|
* successor recovery, and fenced one-shot deletion before durable slot reuse.
|
|
6
6
|
*/
|
|
7
|
-
import type { TelegramUpdateJournalDeadQueueOwnerRecoveryInput, TelegramUpdateJournalDeadQueueOwnerRecoveryResult, TelegramUpdateJournalEntry, TelegramUpdateJournalQueueOwnerIdentity } from "./journal.ts";
|
|
8
|
-
import type
|
|
7
|
+
import type { TelegramJournalNamespaceInspection, TelegramUpdateJournalDeadQueueOwnerRecoveryInput, TelegramUpdateJournalDeadQueueOwnerRecoveryResult, TelegramUpdateJournalEntry, TelegramUpdateJournalQueueOwnerIdentity } from "./journal.ts";
|
|
8
|
+
import { type TelegramTarget } from "./target.ts";
|
|
9
9
|
import { type TelegramWorkspaceThreadDeletionTransport } from "./telegram-api.ts";
|
|
10
|
-
import type { TelegramTopicTargetStore, TelegramWorkspaceExternalProtectionEvidence, TelegramWorkspaceRetirementIntent, TelegramWorkspaceProtectionState, TelegramWorkspaceThreadBinding } from "./threads.ts";
|
|
10
|
+
import type { TelegramTopicTargetStore, TelegramWorkspaceJournalSource, TelegramWorkspaceExternalProtectionEvidence, TelegramWorkspaceRetirementIntent, TelegramWorkspaceProtectionState, TelegramWorkspaceThreadBinding } from "./threads.ts";
|
|
11
11
|
import { type TelegramWorkspaceAdmissionLedger, type TelegramWorkspaceAdmissionScope, type TelegramWorkspaceDeletionPermit } from "./workspace-admission.ts";
|
|
12
12
|
export interface TelegramWorkspaceOperationGate {
|
|
13
13
|
runExclusive: <T>(operation: () => Promise<T>) => Promise<T>;
|
|
@@ -36,18 +36,21 @@ interface TelegramWorkspaceJournalReader {
|
|
|
36
36
|
entries: readonly {
|
|
37
37
|
update: unknown;
|
|
38
38
|
}[];
|
|
39
|
+
exists?: boolean;
|
|
39
40
|
};
|
|
40
41
|
};
|
|
41
42
|
readForProtection?: () => {
|
|
42
43
|
entries: readonly {
|
|
43
44
|
update: unknown;
|
|
44
45
|
}[];
|
|
46
|
+
exists?: boolean;
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
export declare function captureTelegramWorkspaceJournalProtectionSources(input: {
|
|
48
50
|
binding: TelegramWorkspaceThreadBinding;
|
|
49
51
|
resolveLeader: () => TelegramWorkspaceJournalReader | undefined;
|
|
50
52
|
createFollowerResolver: (journalBindingKey: string) => () => TelegramWorkspaceJournalReader | undefined;
|
|
53
|
+
createSessionResolver?: (recipientBindingKey: string, sessionId: string) => () => TelegramWorkspaceJournalReader | undefined;
|
|
51
54
|
withJournalReference?: <T>(binding: TelegramWorkspaceJournalReader, operation: () => T) => T;
|
|
52
55
|
discovery?: {
|
|
53
56
|
paths: readonly string[];
|
|
@@ -63,6 +66,7 @@ export type TelegramWorkspaceJournalProtectionSource = {
|
|
|
63
66
|
kind: "binding";
|
|
64
67
|
bindingKey: string;
|
|
65
68
|
journalBindingKey: string;
|
|
69
|
+
sessionId?: string;
|
|
66
70
|
} | {
|
|
67
71
|
kind: "discovered";
|
|
68
72
|
path: string;
|
|
@@ -70,6 +74,8 @@ export type TelegramWorkspaceJournalProtectionSource = {
|
|
|
70
74
|
entries: readonly {
|
|
71
75
|
update: unknown;
|
|
72
76
|
}[];
|
|
77
|
+
/** Positive filesystem presence is distinct from a complete namespace with a missing historical family. */
|
|
78
|
+
exists?: boolean;
|
|
73
79
|
} | {
|
|
74
80
|
kind: "unknown";
|
|
75
81
|
scope: {
|
|
@@ -78,6 +84,7 @@ export type TelegramWorkspaceJournalProtectionSource = {
|
|
|
78
84
|
kind: "binding";
|
|
79
85
|
bindingKey: string;
|
|
80
86
|
journalBindingKey: string;
|
|
87
|
+
sessionId?: string;
|
|
81
88
|
} | {
|
|
82
89
|
kind: "discovered";
|
|
83
90
|
path: string;
|
|
@@ -91,19 +98,24 @@ export declare function resolveTelegramWorkspaceAcceptedWorkProtection(input: {
|
|
|
91
98
|
}[];
|
|
92
99
|
journalSources: readonly TelegramWorkspaceJournalProtectionSource[];
|
|
93
100
|
sourcesComplete: boolean;
|
|
101
|
+
/** Relocation cannot infer execution ownership from the original Telegram message target. */
|
|
102
|
+
requireBindingProvenance?: boolean;
|
|
94
103
|
}): TelegramWorkspaceProtectionState;
|
|
95
104
|
export type TelegramWorkspaceJournalPruneResult = {
|
|
96
105
|
kind: "committed";
|
|
97
106
|
binding: TelegramWorkspaceThreadBinding;
|
|
98
107
|
removedKeys: string[];
|
|
108
|
+
removedSources?: TelegramWorkspaceJournalSource[];
|
|
99
109
|
} | {
|
|
100
110
|
kind: "blocked";
|
|
101
|
-
reason: "incomplete-evidence" | "writer-not-quiescent" | "state-changed";
|
|
111
|
+
reason: "incomplete-evidence" | "writer-not-quiescent" | "state-changed" | "publication-refused";
|
|
102
112
|
};
|
|
103
113
|
export declare function pruneTelegramWorkspaceJournalEvidence(input: {
|
|
104
|
-
store: Pick<TelegramTopicTargetStore, "commitWorkspaceJournalEvidence" | "
|
|
114
|
+
store: Pick<TelegramTopicTargetStore, "commitWorkspaceJournalEvidence" | "persistWorkspaceJournalEvidence">;
|
|
105
115
|
binding: TelegramWorkspaceThreadBinding;
|
|
106
|
-
|
|
116
|
+
/** Capture fresh source evidence after admission, not a snapshot taken before the lease. */
|
|
117
|
+
capture: () => TelegramWorkspaceJournalProtectionCapture;
|
|
118
|
+
runExclusive: TelegramWorkspaceOperationGate["runExclusive"];
|
|
107
119
|
getJournalWriterProtection: (journalBindingKey: string) => TelegramWorkspaceProtectionState;
|
|
108
120
|
getLeaderEpoch: () => number | string | undefined;
|
|
109
121
|
getProfileKey: () => string;
|
|
@@ -113,6 +125,7 @@ export declare function pruneTelegramWorkspaceJournalEvidence(input: {
|
|
|
113
125
|
}): Promise<TelegramWorkspaceJournalPruneResult>;
|
|
114
126
|
export declare function captureTelegramWorkspaceExternalProtection(input: {
|
|
115
127
|
binding: TelegramWorkspaceThreadBinding;
|
|
128
|
+
requireBindingProvenance?: boolean;
|
|
116
129
|
getLiveOwnerProtection: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceProtectionState;
|
|
117
130
|
getLocalAcceptedTargets: (binding: TelegramWorkspaceThreadBinding) => {
|
|
118
131
|
targets: readonly TelegramTarget[];
|
|
@@ -121,7 +134,7 @@ export declare function captureTelegramWorkspaceExternalProtection(input: {
|
|
|
121
134
|
captureJournalSources: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceJournalProtectionCapture;
|
|
122
135
|
getDeliveryAuthorityProtection?: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceProtectionState;
|
|
123
136
|
}): TelegramWorkspaceExternalProtectionEvidence;
|
|
124
|
-
|
|
137
|
+
interface TelegramWorkspaceProtectionObserverDeps {
|
|
125
138
|
listFollowers: () => readonly {
|
|
126
139
|
target?: TelegramTarget;
|
|
127
140
|
}[];
|
|
@@ -132,15 +145,35 @@ export declare function createTelegramWorkspaceExternalProtectionCapture(deps: {
|
|
|
132
145
|
}[];
|
|
133
146
|
resolveLeaderJournal: () => TelegramWorkspaceJournalReader | undefined;
|
|
134
147
|
createFollowerJournalResolver: (journalBindingKey: string) => () => TelegramWorkspaceJournalReader | undefined;
|
|
148
|
+
createSessionJournalResolver?: (recipientBindingKey: string, sessionId: string) => () => TelegramWorkspaceJournalReader | undefined;
|
|
135
149
|
discoverFollowerJournals?: () => {
|
|
136
150
|
paths: readonly string[];
|
|
137
151
|
complete: boolean;
|
|
138
152
|
};
|
|
153
|
+
/** Strict read-only namespace evidence; it never substitutes for writer closure. */
|
|
154
|
+
inspectJournalNamespace?: () => TelegramJournalNamespaceInspection;
|
|
139
155
|
createJournalPathResolver?: (path: string) => () => TelegramWorkspaceJournalReader | undefined;
|
|
140
156
|
withJournalReference?: <T>(binding: TelegramWorkspaceJournalReader, operation: () => T) => T;
|
|
141
157
|
getJournalWriterProtection?: (journalBindingKey: string) => TelegramWorkspaceProtectionState;
|
|
142
158
|
getDeliveryAuthorityProtection?: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceProtectionState;
|
|
143
|
-
}
|
|
159
|
+
}
|
|
160
|
+
export interface TelegramWorkspaceProtectionObserver {
|
|
161
|
+
capture: (binding: TelegramWorkspaceThreadBinding, options?: {
|
|
162
|
+
requireBindingProvenance?: boolean;
|
|
163
|
+
}) => TelegramWorkspaceExternalProtectionEvidence;
|
|
164
|
+
captureJournalSources: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceJournalProtectionCapture;
|
|
165
|
+
getJournalWriterProtection: (journalBindingKey: string) => TelegramWorkspaceProtectionState;
|
|
166
|
+
}
|
|
167
|
+
/** Shared read-only observer: protection and metadata pruning use the same scoped evidence path. */
|
|
168
|
+
export declare function createTelegramWorkspaceProtectionObserver(deps: TelegramWorkspaceProtectionObserverDeps): TelegramWorkspaceProtectionObserver;
|
|
169
|
+
/** Callable protection view retained for callers that need no metadata observation ports. */
|
|
170
|
+
export declare function createTelegramWorkspaceExternalProtectionCapture(deps: TelegramWorkspaceProtectionObserverDeps): TelegramWorkspaceProtectionObserver["capture"];
|
|
171
|
+
export declare function createTelegramWorkspaceJournalEvidencePruner(deps: TelegramWorkspaceOperationGate & {
|
|
172
|
+
store: TelegramTopicTargetStore;
|
|
173
|
+
getAdmission: () => TelegramWorkspaceAdmissionLedger | undefined;
|
|
174
|
+
getLeaderEpoch: () => number | string | undefined;
|
|
175
|
+
protection: Pick<TelegramWorkspaceProtectionObserver, "captureJournalSources" | "getJournalWriterProtection">;
|
|
176
|
+
}): (binding: TelegramWorkspaceThreadBinding, isCurrent: () => boolean) => Promise<TelegramWorkspaceJournalPruneResult>;
|
|
144
177
|
export declare function isCurrentTelegramWorkspaceBinding(store: Pick<TelegramTopicTargetStore, "listWorkspaceBindings">, expected: TelegramWorkspaceThreadBinding): boolean;
|
|
145
178
|
export interface TelegramWorkspaceDeadQueueJournalBinding {
|
|
146
179
|
recoveryKey?: string;
|
|
@@ -175,6 +208,7 @@ export declare function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
|
|
|
175
208
|
}[];
|
|
176
209
|
resolveLeaderJournal: () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
|
|
177
210
|
createFollowerJournalResolver: (journalBindingKey: string) => () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
|
|
211
|
+
createSessionJournalResolver?: (recipientBindingKey: string, sessionId: string) => () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
|
|
178
212
|
discoverFollowerJournals?: () => {
|
|
179
213
|
paths: readonly string[];
|
|
180
214
|
complete: boolean;
|
|
@@ -218,7 +252,7 @@ export type TelegramWorkspaceRetirementExecution = {
|
|
|
218
252
|
};
|
|
219
253
|
export type TelegramWorkspaceRetirementAbsence = "absent" | "present" | "unknown";
|
|
220
254
|
export declare function executeTelegramWorkspaceRetirement(input: {
|
|
221
|
-
store: Pick<TelegramTopicTargetStore, "captureWorkspaceSlotOccupancy" | "listWorkspaceBindings" | "listWorkspaceRetirementIntents" | "commitWorkspaceRetirement" | "removeWorkspaceRetirementIntent" | "persist">;
|
|
255
|
+
store: Pick<TelegramTopicTargetStore, "captureWorkspaceSlotOccupancy" | "listWorkspaceBindings" | "listWorkspaceRetirementIntents" | "commitWorkspaceRetirement" | "removeWorkspaceRetirementIntent" | "markStaleByTarget" | "persist">;
|
|
222
256
|
admission: Pick<TelegramWorkspaceAdmissionLedger, "getOwner" | "read" | "acquireRetirementFence" | "adoptRetirementFence" | "issueDeletionPermit" | "confirmRetirementAbsence" | "confirmRetirementRejection" | "completeRejectedRetirementFence" | "releaseUnissuedRetirementFence" | "completeRetirementFence">;
|
|
223
257
|
intent: TelegramWorkspaceRetirementIntent;
|
|
224
258
|
getExternalProtection: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceExternalProtectionEvidence;
|
|
@@ -263,7 +297,7 @@ export type TelegramWorkspaceRetirementLifecycleResult = TelegramWorkspaceRetire
|
|
|
263
297
|
reason: string;
|
|
264
298
|
};
|
|
265
299
|
export declare function runTelegramWorkspaceRetirementLifecycle(input: {
|
|
266
|
-
store: TelegramWorkspaceRetirementPreparationDeps["store"] & Pick<TelegramTopicTargetStore, "replaceWorkspaceRetirementIntent" | "commitWorkspaceRetirement">;
|
|
300
|
+
store: TelegramWorkspaceRetirementPreparationDeps["store"] & Pick<TelegramTopicTargetStore, "replaceWorkspaceRetirementIntent" | "commitWorkspaceRetirement" | "markStaleByTarget">;
|
|
267
301
|
getExternalProtection: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceExternalProtectionEvidence;
|
|
268
302
|
getLeaderEpoch: () => number | string | undefined;
|
|
269
303
|
getProfileKey: () => string;
|
|
@@ -278,6 +312,7 @@ export type TelegramWorkspaceCapacityRunner = <T>(operation: () => Promise<T>) =
|
|
|
278
312
|
export interface TelegramWorkspaceSlotRotationPorts extends TelegramWorkspaceOperationGate {
|
|
279
313
|
getAdmission: () => TelegramWorkspaceAdmissionLedger | undefined;
|
|
280
314
|
deleteThread: TelegramWorkspaceThreadDeletionTransport;
|
|
315
|
+
pruneJournalEvidence?: (binding: TelegramWorkspaceThreadBinding, isCurrent: () => boolean) => Promise<TelegramWorkspaceJournalPruneResult>;
|
|
281
316
|
reclaimDeadOwnerQueuedWork?: (binding: TelegramWorkspaceThreadBinding, isCurrent: () => boolean) => Promise<TelegramWorkspaceDeadQueueReclamation>;
|
|
282
317
|
}
|
|
283
318
|
/** Retry allocation once, only after the failed operation released all ordinary leases. */
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* successor recovery, and fenced one-shot deletion before durable slot reuse.
|
|
6
6
|
*/
|
|
7
7
|
import { isDeepStrictEqual } from "node:util";
|
|
8
|
+
import { areTelegramTargetsEqual as sameTarget } from "./target.js";
|
|
8
9
|
import { getTelegramApiErrorRequestTarget, isTelegramApiRequestRejected, } from "./telegram-api.js";
|
|
9
10
|
import { planTelegramWorkspaceSlotAllocation, TelegramWorkspaceSlotUnavailableError, } from "./workspace-slots.js";
|
|
10
11
|
import { createTelegramWorkspaceAdmissionOperationId, isTelegramWorkspaceRetirementFence, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
|
|
@@ -55,7 +56,8 @@ export function captureTelegramWorkspaceJournalProtectionSources(input) {
|
|
|
55
56
|
const snapshot = input.withJournalReference
|
|
56
57
|
? input.withJournalReference(binding, read)
|
|
57
58
|
: read();
|
|
58
|
-
sources.push({ kind: "available", scope, entries: snapshot.entries
|
|
59
|
+
sources.push({ kind: "available", scope, entries: snapshot.entries,
|
|
60
|
+
...(snapshot.exists === undefined ? {} : { exists: snapshot.exists }) });
|
|
59
61
|
}
|
|
60
62
|
catch {
|
|
61
63
|
complete = false;
|
|
@@ -70,6 +72,10 @@ export function captureTelegramWorkspaceJournalProtectionSources(input) {
|
|
|
70
72
|
journalBindingKey,
|
|
71
73
|
}, input.createFollowerResolver(journalBindingKey));
|
|
72
74
|
}
|
|
75
|
+
for (const source of input.binding.journalSources ?? []) {
|
|
76
|
+
capture({ kind: "binding", bindingKey: input.binding.bindingKey,
|
|
77
|
+
journalBindingKey: source.recipientBindingKey, sessionId: source.sessionId }, input.createSessionResolver?.(source.recipientBindingKey, source.sessionId) ?? (() => undefined));
|
|
78
|
+
}
|
|
73
79
|
for (const path of input.discovery?.paths ?? []) {
|
|
74
80
|
capture({ kind: "discovered", path }, input.discovery.createResolver(path));
|
|
75
81
|
}
|
|
@@ -101,9 +107,6 @@ function getJournalUpdateTarget(update) {
|
|
|
101
107
|
...(typeof threadId === "number" ? { threadId } : {}),
|
|
102
108
|
};
|
|
103
109
|
}
|
|
104
|
-
function sameTarget(left, right) {
|
|
105
|
-
return left.chatId === right.chatId && left.threadId === right.threadId;
|
|
106
|
-
}
|
|
107
110
|
export function resolveTelegramWorkspaceAcceptedWorkProtection(input) {
|
|
108
111
|
if (input.localAcceptedTargets.some((target) => sameTarget(target, input.binding.target))) {
|
|
109
112
|
return "protected";
|
|
@@ -118,9 +121,17 @@ export function resolveTelegramWorkspaceAcceptedWorkProtection(input) {
|
|
|
118
121
|
unknown = true;
|
|
119
122
|
continue;
|
|
120
123
|
}
|
|
124
|
+
if (input.requireBindingProvenance && source.scope.kind === "binding" && source.exists === false) {
|
|
125
|
+
unknown = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
121
128
|
if (source.scope.kind === "binding" && source.entries.length > 0) {
|
|
122
129
|
return "protected";
|
|
123
130
|
}
|
|
131
|
+
if (input.requireBindingProvenance && source.entries.length > 0) {
|
|
132
|
+
unknown = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
124
135
|
for (const entry of source.entries) {
|
|
125
136
|
const target = getJournalUpdateTarget(entry.update);
|
|
126
137
|
if (!target) {
|
|
@@ -144,49 +155,73 @@ export async function pruneTelegramWorkspaceJournalEvidence(input) {
|
|
|
144
155
|
if (!isCurrent()) {
|
|
145
156
|
throw new Error("Telegram Workspace journal pruning requires current leader authority.");
|
|
146
157
|
}
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
let capture;
|
|
159
|
+
try {
|
|
160
|
+
capture = input.capture();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return { kind: "blocked", reason: "incomplete-evidence" };
|
|
164
|
+
}
|
|
165
|
+
const address = (key, sessionId) => JSON.stringify([sessionId ?? null, key]);
|
|
166
|
+
const keys = input.binding.journalBindingKeys ?? [];
|
|
167
|
+
const sessions = input.binding.journalSources ?? [];
|
|
168
|
+
const expected = [
|
|
169
|
+
...keys.map(key => address(key)),
|
|
170
|
+
...sessions.map(source => address(source.recipientBindingKey, source.sessionId)),
|
|
171
|
+
];
|
|
172
|
+
const shared = capture.sources.filter(source => source.scope.kind === "shared");
|
|
173
|
+
const bindingSources = capture.sources.filter(source => source.scope.kind === "binding" &&
|
|
149
174
|
source.scope.bindingKey === input.binding.bindingKey);
|
|
150
|
-
const
|
|
151
|
-
? [[source.scope.journalBindingKey, source]]
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
shared[0]?.kind !== "available" ||
|
|
156
|
-
bindingSources.length !==
|
|
157
|
-
(input.binding.journalBindingKeys ?? []).length ||
|
|
158
|
-
sourceByKey.size !== (input.binding.journalBindingKeys ?? []).length ||
|
|
159
|
-
Array.from(sourceByKey.values()).some((source) => source.kind !== "available")) {
|
|
175
|
+
const sourceByAddress = new Map(bindingSources.flatMap(source => source.scope.kind === "binding"
|
|
176
|
+
? [[address(source.scope.journalBindingKey, source.scope.sessionId), source]] : []));
|
|
177
|
+
if (!capture.complete || shared.length !== 1 || shared[0]?.kind !== "available" ||
|
|
178
|
+
bindingSources.length !== expected.length || sourceByAddress.size !== expected.length ||
|
|
179
|
+
expected.some(key => sourceByAddress.get(key)?.kind !== "available")) {
|
|
160
180
|
return { kind: "blocked", reason: "incomplete-evidence" };
|
|
161
181
|
}
|
|
162
|
-
const
|
|
163
|
-
const source =
|
|
182
|
+
const empty = new Set(expected.filter(key => {
|
|
183
|
+
const source = sourceByAddress.get(key);
|
|
164
184
|
return source?.kind === "available" && source.entries.length === 0;
|
|
165
|
-
});
|
|
166
|
-
|
|
185
|
+
}));
|
|
186
|
+
const emptyWriterKeys = new Set([
|
|
187
|
+
...keys.filter(key => empty.has(address(key))),
|
|
188
|
+
...sessions.filter(source => empty.has(address(source.recipientBindingKey, source.sessionId)))
|
|
189
|
+
.map(source => source.recipientBindingKey),
|
|
190
|
+
]);
|
|
191
|
+
const quiescent = new Set([...emptyWriterKeys].filter(key => input.getJournalWriterProtection(key) === "clear"));
|
|
192
|
+
const removedKeys = keys.filter(key => empty.has(address(key)) && quiescent.has(key));
|
|
193
|
+
const removedSources = sessions.filter(source => empty.has(address(source.recipientBindingKey, source.sessionId)) &&
|
|
194
|
+
quiescent.has(source.recipientBindingKey));
|
|
195
|
+
if (empty.size > 0 && removedKeys.length === 0 && removedSources.length === 0) {
|
|
167
196
|
return { kind: "blocked", reason: "writer-not-quiescent" };
|
|
168
197
|
}
|
|
169
|
-
const retainedKeys =
|
|
198
|
+
const retainedKeys = keys.filter(key => !removedKeys.includes(key));
|
|
199
|
+
const retainedSources = sessions.filter(source => !removedSources.includes(source));
|
|
170
200
|
if (!isCurrent()) {
|
|
171
201
|
throw new Error("Telegram Workspace journal pruning lost leader authority.");
|
|
172
202
|
}
|
|
173
|
-
const binding = input.store.commitWorkspaceJournalEvidence(input.binding, retainedKeys, input.binding.journalBindingsComplete === true);
|
|
203
|
+
const binding = input.store.commitWorkspaceJournalEvidence(input.binding, retainedKeys, input.binding.journalBindingsComplete === true, retainedSources);
|
|
174
204
|
if (!binding)
|
|
175
205
|
return { kind: "blocked", reason: "state-changed" };
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
206
|
+
if (removedKeys.length > 0 || removedSources.length > 0) {
|
|
207
|
+
const published = await input.store.persistWorkspaceJournalEvidence(binding, isCurrent);
|
|
208
|
+
if (!isCurrent())
|
|
209
|
+
throw new Error("Telegram Workspace journal pruning lost leader authority.");
|
|
210
|
+
if (!published)
|
|
211
|
+
return { kind: "blocked", reason: "publication-refused" };
|
|
212
|
+
}
|
|
179
213
|
if (!isCurrent()) {
|
|
180
214
|
throw new Error("Telegram Workspace journal pruning lost leader authority.");
|
|
181
215
|
}
|
|
182
|
-
return { kind: "committed", binding, removedKeys
|
|
216
|
+
return { kind: "committed", binding, removedKeys,
|
|
217
|
+
...(removedSources.length ? { removedSources: removedSources.map(source => ({ ...source })) } : {}) };
|
|
183
218
|
};
|
|
184
219
|
return runWithTelegramWorkspaceAdmissionsAsync({
|
|
185
220
|
ledger: input.admission,
|
|
186
221
|
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
187
222
|
operationKind: "workspace.prune-journal-evidence",
|
|
188
223
|
scopes: [{ kind: "target", target: input.binding.target }],
|
|
189
|
-
operation,
|
|
224
|
+
operation: () => input.runExclusive(operation),
|
|
190
225
|
onReleaseError(error) {
|
|
191
226
|
input.onAdmissionReleaseError?.(error);
|
|
192
227
|
},
|
|
@@ -210,6 +245,7 @@ export function captureTelegramWorkspaceExternalProtection(input) {
|
|
|
210
245
|
localAcceptedTargets: local.targets,
|
|
211
246
|
journalSources: journals.sources,
|
|
212
247
|
sourcesComplete: journals.complete && local.complete,
|
|
248
|
+
requireBindingProvenance: input.requireBindingProvenance,
|
|
213
249
|
});
|
|
214
250
|
}
|
|
215
251
|
catch {
|
|
@@ -223,17 +259,45 @@ export function captureTelegramWorkspaceExternalProtection(input) {
|
|
|
223
259
|
}
|
|
224
260
|
return { liveOwner, acceptedWork, deliveryAuthority };
|
|
225
261
|
}
|
|
226
|
-
|
|
227
|
-
|
|
262
|
+
/** Shared read-only observer: protection and metadata pruning use the same scoped evidence path. */
|
|
263
|
+
export function createTelegramWorkspaceProtectionObserver(deps) {
|
|
264
|
+
const captureJournalSources = (candidate) => {
|
|
265
|
+
const namespace = deps.inspectJournalNamespace?.();
|
|
266
|
+
if (deps.inspectJournalNamespace && (!namespace || namespace.sources.filter(source => source.role === "polling").length !== 1))
|
|
267
|
+
throw new Error("Telegram strict namespace evidence is unavailable or incomplete.");
|
|
268
|
+
if (namespace?.retainedInputs?.some(original => original.state !== "committed"))
|
|
269
|
+
throw new Error("Telegram namespace contains uncommitted private retention evidence.");
|
|
270
|
+
const discovery = namespace
|
|
271
|
+
? { paths: namespace.sources.filter(source => source.role !== "polling" && source.evidence.kind === "present")
|
|
272
|
+
.map(source => source.path), complete: true }
|
|
273
|
+
: candidate.journalBindingsComplete === true ? undefined : deps.discoverFollowerJournals?.();
|
|
274
|
+
if (namespace && !deps.createJournalPathResolver)
|
|
275
|
+
throw new Error("Telegram strict namespace protection requires discovered source resolution.");
|
|
276
|
+
return captureTelegramWorkspaceJournalProtectionSources({
|
|
277
|
+
binding: candidate,
|
|
278
|
+
resolveLeader: deps.resolveLeaderJournal,
|
|
279
|
+
createFollowerResolver: deps.createFollowerJournalResolver,
|
|
280
|
+
...(deps.createSessionJournalResolver ? { createSessionResolver: deps.createSessionJournalResolver } : {}),
|
|
281
|
+
...(deps.withJournalReference ? { withJournalReference: deps.withJournalReference } : {}),
|
|
282
|
+
...(discovery && deps.createJournalPathResolver
|
|
283
|
+
? { discovery: { ...discovery, createResolver: deps.createJournalPathResolver } } : {}),
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
const capture = function (binding, options) {
|
|
228
287
|
return captureTelegramWorkspaceExternalProtection({
|
|
229
288
|
binding,
|
|
289
|
+
requireBindingProvenance: options?.requireBindingProvenance,
|
|
230
290
|
getLiveOwnerProtection(candidate) {
|
|
231
291
|
if (deps.listFollowers().some((follower) => !!follower.target && sameTarget(follower.target, candidate.target)))
|
|
232
292
|
return "protected";
|
|
233
293
|
if (!deps.getJournalWriterProtection)
|
|
234
294
|
return "unknown";
|
|
235
295
|
let unknown = candidate.journalBindingsComplete !== true;
|
|
236
|
-
|
|
296
|
+
const writerKeys = new Set([
|
|
297
|
+
...(candidate.journalBindingKeys ?? []),
|
|
298
|
+
...(candidate.journalSources ?? []).map(source => source.recipientBindingKey),
|
|
299
|
+
]);
|
|
300
|
+
for (const journalBindingKey of writerKeys) {
|
|
237
301
|
const protection = deps.getJournalWriterProtection(journalBindingKey);
|
|
238
302
|
if (protection === "protected")
|
|
239
303
|
return "protected";
|
|
@@ -257,29 +321,34 @@ export function createTelegramWorkspaceExternalProtectionCapture(deps) {
|
|
|
257
321
|
}
|
|
258
322
|
return { targets, complete };
|
|
259
323
|
},
|
|
260
|
-
captureJournalSources
|
|
261
|
-
const discovery = candidate.journalBindingsComplete === true
|
|
262
|
-
? undefined
|
|
263
|
-
: deps.discoverFollowerJournals?.();
|
|
264
|
-
return captureTelegramWorkspaceJournalProtectionSources({
|
|
265
|
-
binding: candidate,
|
|
266
|
-
resolveLeader: deps.resolveLeaderJournal,
|
|
267
|
-
createFollowerResolver: deps.createFollowerJournalResolver,
|
|
268
|
-
...(deps.withJournalReference
|
|
269
|
-
? { withJournalReference: deps.withJournalReference } : {}),
|
|
270
|
-
...(discovery && deps.createJournalPathResolver
|
|
271
|
-
? { discovery: {
|
|
272
|
-
...discovery,
|
|
273
|
-
createResolver: deps.createJournalPathResolver,
|
|
274
|
-
} }
|
|
275
|
-
: {}),
|
|
276
|
-
});
|
|
277
|
-
},
|
|
324
|
+
captureJournalSources,
|
|
278
325
|
...(deps.getDeliveryAuthorityProtection
|
|
279
326
|
? { getDeliveryAuthorityProtection: deps.getDeliveryAuthorityProtection }
|
|
280
327
|
: {}),
|
|
281
328
|
});
|
|
282
329
|
};
|
|
330
|
+
return { capture, captureJournalSources,
|
|
331
|
+
getJournalWriterProtection: deps.getJournalWriterProtection ?? (() => "unknown") };
|
|
332
|
+
}
|
|
333
|
+
/** Callable protection view retained for callers that need no metadata observation ports. */
|
|
334
|
+
export function createTelegramWorkspaceExternalProtectionCapture(deps) {
|
|
335
|
+
return createTelegramWorkspaceProtectionObserver(deps).capture;
|
|
336
|
+
}
|
|
337
|
+
export function createTelegramWorkspaceJournalEvidencePruner(deps) {
|
|
338
|
+
return async (binding, isCurrent) => {
|
|
339
|
+
const admission = deps.getAdmission();
|
|
340
|
+
if (!admission)
|
|
341
|
+
return { kind: "blocked", reason: "state-changed" };
|
|
342
|
+
const profileKey = admission.getProfileKey();
|
|
343
|
+
return pruneTelegramWorkspaceJournalEvidence({
|
|
344
|
+
store: deps.store, binding, admission, runExclusive: deps.runExclusive,
|
|
345
|
+
capture: () => deps.protection.captureJournalSources(binding),
|
|
346
|
+
getJournalWriterProtection: deps.protection.getJournalWriterProtection,
|
|
347
|
+
getLeaderEpoch: deps.getLeaderEpoch,
|
|
348
|
+
getProfileKey: () => deps.getAdmission()?.getProfileKey() ?? "",
|
|
349
|
+
isCurrent: () => isCurrent() && deps.getAdmission()?.getProfileKey() === profileKey,
|
|
350
|
+
});
|
|
351
|
+
};
|
|
283
352
|
}
|
|
284
353
|
export function isCurrentTelegramWorkspaceBinding(store, expected) {
|
|
285
354
|
return store.listWorkspaceBindings().some((binding) => isDeepStrictEqual(binding, expected));
|
|
@@ -307,6 +376,9 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps) {
|
|
|
307
376
|
if (!isCurrent() || !deps.isBindingCurrent(binding)) {
|
|
308
377
|
return { kind: "blocked", reason: "authority-changed" };
|
|
309
378
|
}
|
|
379
|
+
// Session addresses require exact resolution; never substitute a flat same-hash journal.
|
|
380
|
+
if (binding.journalSources?.length && !deps.createSessionJournalResolver)
|
|
381
|
+
return { kind: "blocked", reason: "incomplete-source" };
|
|
310
382
|
const initial = deps.getExternalProtection(binding);
|
|
311
383
|
if (initial.liveOwner !== "clear" || initial.deliveryAuthority !== "clear") {
|
|
312
384
|
return { kind: "blocked", reason: "live-owner" };
|
|
@@ -345,6 +417,9 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps) {
|
|
|
345
417
|
for (const key of binding.journalBindingKeys ?? []) {
|
|
346
418
|
capture("binding", deps.createFollowerJournalResolver(key));
|
|
347
419
|
}
|
|
420
|
+
for (const source of binding.journalSources ?? []) {
|
|
421
|
+
capture("binding", deps.createSessionJournalResolver(source.recipientBindingKey, source.sessionId));
|
|
422
|
+
}
|
|
348
423
|
for (const path of discovery?.paths ?? []) {
|
|
349
424
|
if (!deps.createJournalPathResolver) {
|
|
350
425
|
complete = false;
|
|
@@ -605,6 +680,12 @@ export async function executeTelegramWorkspaceRetirement(input) {
|
|
|
605
680
|
}
|
|
606
681
|
return { kind: "retained", reason: "stale-intent" };
|
|
607
682
|
}
|
|
683
|
+
if (fence?.phase === "commit-ready") {
|
|
684
|
+
// Exact confirmed absence retires any same-target routing record that
|
|
685
|
+
// survived the deletion attempt. The fence prevents replacement target
|
|
686
|
+
// publication while this stale local projection is removed.
|
|
687
|
+
input.store.markStaleByTarget(binding.target, "deleted");
|
|
688
|
+
}
|
|
608
689
|
const eligible = () => input.store.captureWorkspaceSlotOccupancy(input.getExternalProtection, { expectedRetirement: input.intent }).bindings.find((candidate) => candidate.bindingKey === input.intent.binding.bindingKey)?.protection === "eligible";
|
|
609
690
|
if (!eligible())
|
|
610
691
|
return { kind: "retained", reason: "protection-changed" };
|
|
@@ -669,6 +750,9 @@ export async function executeTelegramWorkspaceRetirement(input) {
|
|
|
669
750
|
}
|
|
670
751
|
if (!isCurrent())
|
|
671
752
|
return { kind: "retained", reason: "authority-changed" };
|
|
753
|
+
if (fence.phase === "commit-ready") {
|
|
754
|
+
input.store.markStaleByTarget(binding.target, "deleted");
|
|
755
|
+
}
|
|
672
756
|
if (!eligible())
|
|
673
757
|
return { kind: "retained", reason: "protection-changed" };
|
|
674
758
|
if (!await input.store.commitWorkspaceRetirement(input.intent, isCurrent)) {
|
|
@@ -799,19 +883,46 @@ export async function runTelegramWorkspaceRetirementLifecycle(input) {
|
|
|
799
883
|
});
|
|
800
884
|
}
|
|
801
885
|
if (intent) {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
intent
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
886
|
+
if (retainedFence && isTelegramWorkspaceRetirementFence(retainedFence) &&
|
|
887
|
+
retainedFence.phase === "commit-ready" &&
|
|
888
|
+
matchesTelegramWorkspaceRetirementFence(retainedFence, intent)) {
|
|
889
|
+
const resumed = await input.runExclusive(async () => {
|
|
890
|
+
const epoch = input.getLeaderEpoch();
|
|
891
|
+
const profileKey = input.getProfileKey();
|
|
892
|
+
const isCurrent = () => epoch !== undefined && input.getLeaderEpoch() === epoch &&
|
|
893
|
+
input.getProfileKey() === profileKey && input.isCurrent?.() !== false;
|
|
894
|
+
if (!isCurrent() || intent.profileKey !== profileKey)
|
|
895
|
+
return undefined;
|
|
896
|
+
const intents = input.store.listWorkspaceRetirementIntents();
|
|
897
|
+
const binding = input.store.listWorkspaceBindings().find(candidate => candidate.bindingKey === intent.binding.bindingKey);
|
|
898
|
+
if (intents.length !== 1 || !isDeepStrictEqual(intents[0], intent) ||
|
|
899
|
+
!binding || !isDeepStrictEqual(binding, intent.binding))
|
|
900
|
+
return undefined;
|
|
901
|
+
if (intent.leaderEpoch === epoch)
|
|
902
|
+
return intent;
|
|
903
|
+
const replacement = { ...intent, leaderEpoch: epoch };
|
|
904
|
+
return await input.store.replaceWorkspaceRetirementIntent(intent, replacement, isCurrent)
|
|
905
|
+
? replacement : undefined;
|
|
906
|
+
});
|
|
907
|
+
if (!resumed)
|
|
908
|
+
return { kind: "blocked", stage: "adoption", reason: "commit-rejected" };
|
|
909
|
+
intent = resumed;
|
|
910
|
+
}
|
|
911
|
+
else {
|
|
912
|
+
const adoption = await adoptTelegramWorkspaceRetirementIntent({
|
|
913
|
+
store: input.store,
|
|
914
|
+
intent,
|
|
915
|
+
getExternalProtection: input.getExternalProtection,
|
|
916
|
+
getLeaderEpoch: input.getLeaderEpoch,
|
|
917
|
+
getProfileKey: input.getProfileKey,
|
|
918
|
+
isCurrent: input.isCurrent,
|
|
919
|
+
runExclusive: input.runExclusive,
|
|
920
|
+
});
|
|
921
|
+
if (adoption.kind !== "adopted") {
|
|
922
|
+
return { kind: "blocked", stage: "adoption", reason: adoption.reason };
|
|
923
|
+
}
|
|
924
|
+
intent = adoption.intent;
|
|
813
925
|
}
|
|
814
|
-
intent = adoption.intent;
|
|
815
926
|
}
|
|
816
927
|
else {
|
|
817
928
|
const preparation = await prepareTelegramWorkspaceRetirement({
|
|
@@ -921,21 +1032,42 @@ export function createTelegramWorkspaceSlotRotation(input) {
|
|
|
921
1032
|
catch (error) {
|
|
922
1033
|
if (!(error instanceof TelegramWorkspaceSlotUnavailableError))
|
|
923
1034
|
throw error;
|
|
924
|
-
if (input.reclaimDeadOwnerQueuedWork) {
|
|
1035
|
+
if (input.pruneJournalEvidence || input.reclaimDeadOwnerQueuedWork) {
|
|
925
1036
|
const candidates = await input.runExclusive(async () => {
|
|
926
1037
|
await input.store.load();
|
|
927
1038
|
if (!isCurrent())
|
|
928
1039
|
throw new Error("Telegram Workspace reclamation lost leader authority.");
|
|
929
1040
|
const snapshot = input.store.captureWorkspaceSlotOccupancy(input.getExternalProtection);
|
|
930
1041
|
const allocation = planTelegramWorkspaceSlotAllocation({ ...snapshot, nowMs: Date.now() });
|
|
931
|
-
if (allocation.kind
|
|
1042
|
+
if (allocation.kind === "free" || (allocation.kind === "blocked" && allocation.reason !== "protected-capacity"))
|
|
1043
|
+
return [];
|
|
1044
|
+
if (allocation.kind === "reclaim" && !input.pruneJournalEvidence)
|
|
932
1045
|
return [];
|
|
933
|
-
return input.store.listWorkspaceBindings().filter((binding) =>
|
|
1046
|
+
return input.store.listWorkspaceBindings().filter((binding) => (allocation.kind !== "reclaim" || binding.bindingKey === allocation.candidate.bindingKey) &&
|
|
1047
|
+
typeof binding.inactiveSinceMs === "number" &&
|
|
934
1048
|
Number.isFinite(binding.inactiveSinceMs) &&
|
|
935
1049
|
binding.inactiveSinceMs >= 0).sort((left, right) => left.inactiveSinceMs - right.inactiveSinceMs ||
|
|
936
1050
|
(left.slot ?? "").localeCompare(right.slot ?? ""));
|
|
937
1051
|
});
|
|
938
|
-
for (const
|
|
1052
|
+
for (const original of candidates) {
|
|
1053
|
+
let binding = original;
|
|
1054
|
+
if (input.pruneJournalEvidence && ((binding.journalBindingKeys?.length ?? 0) + (binding.journalSources?.length ?? 0) > 0)) {
|
|
1055
|
+
const pruned = await input.pruneJournalEvidence(binding, isCurrent);
|
|
1056
|
+
if (!isCurrent())
|
|
1057
|
+
throw new Error("Telegram Workspace pruning lost leader authority.");
|
|
1058
|
+
if (pruned.kind !== "committed")
|
|
1059
|
+
continue;
|
|
1060
|
+
binding = pruned.binding;
|
|
1061
|
+
const removed = pruned.removedKeys.length + (pruned.removedSources?.length ?? 0);
|
|
1062
|
+
if (removed > 0) {
|
|
1063
|
+
try {
|
|
1064
|
+
input.recordEvent("Telegram Workspace journal evidence pruned.", { slot: binding.slot, removed });
|
|
1065
|
+
}
|
|
1066
|
+
catch { /* Diagnostics cannot revoke a published metadata subset. */ }
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (!input.reclaimDeadOwnerQueuedWork)
|
|
1070
|
+
continue;
|
|
939
1071
|
const evidence = input.getExternalProtection(binding);
|
|
940
1072
|
if (evidence.liveOwner !== "clear" ||
|
|
941
1073
|
evidence.acceptedWork !== "protected" ||
|
package/dist/package.json
CHANGED
|
@@ -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": {
|
|
@@ -46,7 +46,7 @@ A disabled control is not an action: it needs no prompt or selected style and mu
|
|
|
46
46
|
|
|
47
47
|
Prompts must name any target, operation, constraint, or freshness identity whose omission could change the action. Reuse visible context only when it remains unambiguous under delayed or reordered clicks. Never encode volatile output that should be freshly inspected.
|
|
48
48
|
|
|
49
|
-
Every generated human-readable action label must use `emoji + space + text`; emoji-free text labels are invalid. Choose the emoji by action semantics, keep its meaning consistent, and never rely on emoji or color alone. Coordinates, established symbolic controls, and intentionally spatial glyphs already satisfy the marker role through their domain grammar. If prompt buttons are unavailable, preserve the same choices as a numbered list.
|
|
49
|
+
Every generated human-readable action label must use `emoji + space + text`; emoji-free text labels are invalid. Choose the emoji by action semantics, keep its meaning consistent, and never rely on emoji or color alone. State/value controls follow their UI-kit grammar; unmarked inactive list items are not action labels. Coordinates, established symbolic controls, and intentionally spatial glyphs already satisfy the marker role through their domain grammar. If prompt buttons are unavailable, preserve the same choices as a numbered list.
|
|
50
50
|
|
|
51
51
|
## Layout Kernel
|
|
52
52
|
|
|
@@ -97,10 +97,10 @@ Show concise failure evidence and emit valid recovery controls such as diagnose,
|
|
|
97
97
|
Before sending:
|
|
98
98
|
|
|
99
99
|
- At least one useful button is present whenever the transport supports prompt buttons.
|
|
100
|
-
- Every human-readable
|
|
100
|
+
- Every human-readable action label begins with a semantic emoji and one ASCII space; state/value controls follow their owning documented grammar.
|
|
101
101
|
- State, actions, owner, and target agree.
|
|
102
102
|
- Live claims are fresh; adaptation and incompleteness are explicit.
|
|
103
|
-
- Every prompt is sufficient
|
|
103
|
+
- Every prompt is sufficient; labels follow their control grammar.
|
|
104
104
|
- Row grouping reflects real hierarchy or peer relationships.
|
|
105
105
|
- High-impact actions require confirmation.
|
|
106
106
|
- No secret or hidden reasoning appears.
|