@llblab/pi-telegram 0.52.2 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +15 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +65 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +252 -127
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +128 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +516 -301
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
|
@@ -3,30 +3,31 @@
|
|
|
3
3
|
* Zones: telegram threads, workspace lifecycle
|
|
4
4
|
* Owns fail-closed cleanup eligibility projection without persistence or Bot API effects
|
|
5
5
|
*/
|
|
6
|
-
import { chmodSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { dirname } from "node:path";
|
|
8
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
9
|
-
import {
|
|
7
|
+
import { publishTelegramPrivateFile, readTelegramPrivateFile, TelegramPrivateFileError, withTelegramFileTransaction, } from "./locks.js";
|
|
10
8
|
import { getTelegramJournalPublicationPaths } from "./paths.js";
|
|
11
9
|
import { isWireRecord as isObject, hasOnlyWireKeys as onlyKeys, isNonNegativeWireInteger as safeTime, } from "./wire.js";
|
|
12
10
|
function targetKey(target) {
|
|
13
11
|
return `${target.chatId}:${target.threadId}`;
|
|
14
12
|
}
|
|
15
13
|
function validTarget(target) {
|
|
16
|
-
return Number.isSafeInteger(target.chatId) &&
|
|
17
|
-
target.threadId
|
|
14
|
+
return (Number.isSafeInteger(target.chatId) &&
|
|
15
|
+
Number.isSafeInteger(target.threadId) &&
|
|
16
|
+
target.threadId > 0);
|
|
18
17
|
}
|
|
19
18
|
function validSessionIdentity(value) {
|
|
20
19
|
if (value.sessionId === undefined && value.sessionKey === undefined)
|
|
21
20
|
return true;
|
|
22
|
-
if (typeof value.sessionId !== "string" ||
|
|
21
|
+
if (typeof value.sessionId !== "string" ||
|
|
22
|
+
!value.sessionId ||
|
|
23
23
|
value.sessionId !== value.sessionId.trim() ||
|
|
24
24
|
Buffer.byteLength(value.sessionId, "utf8") > 256 ||
|
|
25
|
-
typeof value.sessionKey !== "string" ||
|
|
25
|
+
typeof value.sessionKey !== "string" ||
|
|
26
|
+
!/^[a-f0-9]{64}$/u.test(value.sessionKey)) {
|
|
26
27
|
return false;
|
|
27
28
|
}
|
|
28
|
-
return createHash("sha256").update(value.sessionId).digest("hex") ===
|
|
29
|
-
value.sessionKey;
|
|
29
|
+
return (createHash("sha256").update(value.sessionId).digest("hex") ===
|
|
30
|
+
value.sessionKey);
|
|
30
31
|
}
|
|
31
32
|
/** Returns no candidates when any identity/evidence ambiguity exists. */
|
|
32
33
|
export function planTelegramInactiveThreadCleanup(input) {
|
|
@@ -37,16 +38,25 @@ export function planTelegramInactiveThreadCleanup(input) {
|
|
|
37
38
|
for (const binding of input.bindings) {
|
|
38
39
|
const key = targetKey(binding.target);
|
|
39
40
|
const slot = binding.slot;
|
|
40
|
-
if (!binding.bindingKey ||
|
|
41
|
-
|
|
42
|
-
!
|
|
41
|
+
if (!binding.bindingKey ||
|
|
42
|
+
!binding.cwd ||
|
|
43
|
+
!binding.workspaceKey ||
|
|
44
|
+
!binding.instanceSlot ||
|
|
45
|
+
typeof slot !== "string" ||
|
|
46
|
+
!slot ||
|
|
47
|
+
!validSessionIdentity(binding) ||
|
|
48
|
+
!validTarget(binding.target) ||
|
|
49
|
+
bindingKeys.has(binding.bindingKey) ||
|
|
50
|
+
bindingTargets.has(key))
|
|
43
51
|
return [];
|
|
44
52
|
bindingKeys.add(binding.bindingKey);
|
|
45
53
|
bindingTargets.add(key);
|
|
46
54
|
}
|
|
47
55
|
const evidenceByBinding = new Map();
|
|
48
56
|
for (const evidence of input.protection) {
|
|
49
|
-
if (!evidence.bindingKey ||
|
|
57
|
+
if (!evidence.bindingKey ||
|
|
58
|
+
evidenceByBinding.has(evidence.bindingKey) ||
|
|
59
|
+
!validTarget(evidence.target))
|
|
50
60
|
return [];
|
|
51
61
|
evidenceByBinding.set(evidence.bindingKey, evidence);
|
|
52
62
|
}
|
|
@@ -55,80 +65,120 @@ export function planTelegramInactiveThreadCleanup(input) {
|
|
|
55
65
|
...(input.provisioningTargets ?? []),
|
|
56
66
|
...(input.cleanupTargets ?? []),
|
|
57
67
|
];
|
|
58
|
-
if (competingTargets.some(target => !validTarget(target)))
|
|
68
|
+
if (competingTargets.some((target) => !validTarget(target)))
|
|
59
69
|
return [];
|
|
60
70
|
const competing = new Set(competingTargets.map(targetKey));
|
|
61
71
|
const candidates = [];
|
|
62
72
|
for (const binding of input.bindings) {
|
|
63
73
|
const inactiveSinceMs = binding.inactiveSinceMs;
|
|
64
74
|
const slot = binding.slot;
|
|
65
|
-
if (typeof slot !== "string" ||
|
|
66
|
-
|
|
67
|
-
!Number.isSafeInteger(
|
|
75
|
+
if (typeof slot !== "string" ||
|
|
76
|
+
!slot ||
|
|
77
|
+
!Number.isSafeInteger(inactiveSinceMs) ||
|
|
78
|
+
inactiveSinceMs === undefined ||
|
|
79
|
+
inactiveSinceMs < 0 ||
|
|
80
|
+
!Number.isSafeInteger(binding.updatedAtMs) ||
|
|
81
|
+
binding.updatedAtMs < inactiveSinceMs)
|
|
68
82
|
continue;
|
|
69
83
|
const evidence = evidenceByBinding.get(binding.bindingKey);
|
|
70
|
-
if (!evidence ||
|
|
71
|
-
evidence.
|
|
72
|
-
evidence.
|
|
84
|
+
if (!evidence ||
|
|
85
|
+
targetKey(evidence.target) !== targetKey(binding.target) ||
|
|
86
|
+
evidence.liveOwner !== "clear" ||
|
|
87
|
+
evidence.acceptedWork !== "clear" ||
|
|
88
|
+
evidence.deliveryAuthority !== "clear" ||
|
|
89
|
+
competing.has(targetKey(binding.target)))
|
|
73
90
|
continue;
|
|
74
|
-
candidates.push({
|
|
75
|
-
|
|
91
|
+
candidates.push({
|
|
92
|
+
profileName: input.profileName,
|
|
93
|
+
bindingKey: binding.bindingKey,
|
|
94
|
+
cwd: binding.cwd,
|
|
95
|
+
workspaceKey: binding.workspaceKey,
|
|
76
96
|
...(binding.sessionId && binding.sessionKey
|
|
77
97
|
? { sessionId: binding.sessionId, sessionKey: binding.sessionKey }
|
|
78
98
|
: {}),
|
|
79
|
-
instanceSlot: binding.instanceSlot,
|
|
80
|
-
|
|
99
|
+
instanceSlot: binding.instanceSlot,
|
|
100
|
+
slot,
|
|
101
|
+
target: { ...binding.target },
|
|
102
|
+
inactiveSinceMs,
|
|
103
|
+
bindingUpdatedAtMs: binding.updatedAtMs,
|
|
104
|
+
});
|
|
81
105
|
}
|
|
82
106
|
return candidates.sort((left, right) => left.bindingKey.localeCompare(right.bindingKey));
|
|
83
107
|
}
|
|
84
108
|
export function captureTelegramInactiveThreadCleanupEvidence(input) {
|
|
85
109
|
const sourceBindings = input.listBindings();
|
|
86
|
-
const bindings = sourceBindings.map(binding => ({
|
|
87
|
-
cwd: binding.cwd,
|
|
110
|
+
const bindings = sourceBindings.map((binding) => ({
|
|
111
|
+
cwd: binding.cwd,
|
|
112
|
+
workspaceKey: binding.workspaceKey,
|
|
88
113
|
...(binding.sessionId && binding.sessionKey
|
|
89
114
|
? { sessionId: binding.sessionId, sessionKey: binding.sessionKey }
|
|
90
115
|
: {}),
|
|
91
116
|
instanceSlot: binding.instanceSlot,
|
|
92
|
-
...(binding.slot === undefined ? {} : { slot: binding.slot }),
|
|
93
|
-
|
|
117
|
+
...(binding.slot === undefined ? {} : { slot: binding.slot }),
|
|
118
|
+
bindingKey: binding.bindingKey,
|
|
119
|
+
target: { ...binding.target },
|
|
120
|
+
...(binding.inactiveSinceMs === undefined
|
|
121
|
+
? {}
|
|
122
|
+
: { inactiveSinceMs: binding.inactiveSinceMs }),
|
|
94
123
|
updatedAtMs: binding.updatedAtMs,
|
|
95
124
|
}));
|
|
96
125
|
const protection = sourceBindings.map((source, index) => {
|
|
97
126
|
const binding = bindings[index];
|
|
98
127
|
try {
|
|
99
|
-
return {
|
|
100
|
-
|
|
128
|
+
return {
|
|
129
|
+
bindingKey: binding.bindingKey,
|
|
130
|
+
target: { ...binding.target },
|
|
131
|
+
...input.getProtection(source),
|
|
132
|
+
};
|
|
101
133
|
}
|
|
102
134
|
catch {
|
|
103
|
-
return {
|
|
104
|
-
|
|
105
|
-
|
|
135
|
+
return {
|
|
136
|
+
bindingKey: binding.bindingKey,
|
|
137
|
+
target: { ...binding.target },
|
|
138
|
+
liveOwner: "unknown",
|
|
139
|
+
acceptedWork: "unknown",
|
|
140
|
+
deliveryAuthority: "unknown",
|
|
141
|
+
};
|
|
106
142
|
}
|
|
107
143
|
});
|
|
108
144
|
return {
|
|
109
145
|
profileName: input.profileName,
|
|
110
146
|
bindings,
|
|
111
147
|
protection,
|
|
112
|
-
reservedTargets: input
|
|
113
|
-
|
|
114
|
-
|
|
148
|
+
reservedTargets: input
|
|
149
|
+
.listReservations()
|
|
150
|
+
.map((entry) => ({ ...entry.target })),
|
|
151
|
+
provisioningTargets: input
|
|
152
|
+
.listPendingProvisions()
|
|
153
|
+
.flatMap((entry) => (entry.target ? [{ ...entry.target }] : [])),
|
|
154
|
+
cleanupTargets: input
|
|
155
|
+
.listPendingCleanups()
|
|
156
|
+
.map((entry) => ({ ...entry.target })),
|
|
115
157
|
};
|
|
116
158
|
}
|
|
117
159
|
export function createTelegramInactiveThreadCleanupReviewRuntime(deps) {
|
|
118
160
|
return {
|
|
119
161
|
review() {
|
|
120
|
-
return deps.runWorkspaceOperation({
|
|
121
|
-
|
|
162
|
+
return deps.runWorkspaceOperation({
|
|
163
|
+
operationId: `thread-cleanup-review:${randomUUID()}`,
|
|
164
|
+
operationKind: "thread-cleanup-review",
|
|
165
|
+
scopes: [{ kind: "profile" }],
|
|
166
|
+
}, async () => {
|
|
122
167
|
const evidence = captureTelegramInactiveThreadCleanupEvidence({
|
|
123
|
-
profileName: deps.getProfileName(),
|
|
124
|
-
|
|
168
|
+
profileName: deps.getProfileName(),
|
|
169
|
+
listBindings: deps.listBindings,
|
|
170
|
+
getProtection: deps.getProtection,
|
|
171
|
+
listReservations: deps.listReservations,
|
|
125
172
|
listPendingProvisions: deps.listPendingProvisions,
|
|
126
173
|
listPendingCleanups: deps.listPendingCleanups,
|
|
127
174
|
});
|
|
128
175
|
const candidates = planTelegramInactiveThreadCleanup(evidence);
|
|
129
176
|
if (candidates.length === 0)
|
|
130
177
|
return { count: 0 };
|
|
131
|
-
const digest = createHash("sha256")
|
|
178
|
+
const digest = createHash("sha256")
|
|
179
|
+
.update(JSON.stringify(candidates))
|
|
180
|
+
.digest("hex")
|
|
181
|
+
.slice(0, 32);
|
|
132
182
|
const operationId = `thread-cleanup:${digest}`;
|
|
133
183
|
deps.getWorkStore().prepare(operationId, candidates);
|
|
134
184
|
return { count: candidates.length, operationId };
|
|
@@ -137,54 +187,126 @@ export function createTelegramInactiveThreadCleanupReviewRuntime(deps) {
|
|
|
137
187
|
};
|
|
138
188
|
}
|
|
139
189
|
function validateWorkSet(value, profileName) {
|
|
140
|
-
if (!isObject(value) ||
|
|
141
|
-
|
|
142
|
-
|
|
190
|
+
if (!isObject(value) ||
|
|
191
|
+
!onlyKeys(value, ["operationId", "createdAtMs", "entries"]) ||
|
|
192
|
+
typeof value.operationId !== "string" ||
|
|
193
|
+
!value.operationId ||
|
|
194
|
+
!safeTime(value.createdAtMs) ||
|
|
195
|
+
!Array.isArray(value.entries) ||
|
|
196
|
+
value.entries.length === 0)
|
|
143
197
|
throw new Error("Telegram Thread cleanup work-set schema is invalid.");
|
|
144
198
|
const createdAtMs = value.createdAtMs;
|
|
145
|
-
const entries = value.entries.map(raw => {
|
|
146
|
-
if (!isObject(raw) ||
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
199
|
+
const entries = value.entries.map((raw) => {
|
|
200
|
+
if (!isObject(raw) ||
|
|
201
|
+
!onlyKeys(raw, [
|
|
202
|
+
"profileName",
|
|
203
|
+
"bindingKey",
|
|
204
|
+
"cwd",
|
|
205
|
+
"workspaceKey",
|
|
206
|
+
"sessionId",
|
|
207
|
+
"sessionKey",
|
|
208
|
+
"instanceSlot",
|
|
209
|
+
"slot",
|
|
210
|
+
"target",
|
|
211
|
+
"inactiveSinceMs",
|
|
212
|
+
"bindingUpdatedAtMs",
|
|
213
|
+
"state",
|
|
214
|
+
"updatedAtMs",
|
|
215
|
+
"issuedAtMs",
|
|
216
|
+
"permitOperationId",
|
|
217
|
+
"permitIntentId",
|
|
218
|
+
"permitLeaderEpoch",
|
|
219
|
+
"deletedAtMs",
|
|
220
|
+
]) ||
|
|
221
|
+
raw.profileName !== profileName ||
|
|
222
|
+
typeof raw.bindingKey !== "string" ||
|
|
223
|
+
!raw.bindingKey ||
|
|
224
|
+
typeof raw.cwd !== "string" ||
|
|
225
|
+
!raw.cwd ||
|
|
226
|
+
typeof raw.workspaceKey !== "string" ||
|
|
227
|
+
!raw.workspaceKey ||
|
|
228
|
+
!validSessionIdentity(raw) ||
|
|
229
|
+
typeof raw.instanceSlot !== "string" ||
|
|
230
|
+
!raw.instanceSlot ||
|
|
231
|
+
typeof raw.slot !== "string" ||
|
|
232
|
+
!raw.slot ||
|
|
233
|
+
!isObject(raw.target) ||
|
|
234
|
+
!onlyKeys(raw.target, ["chatId", "threadId"]) ||
|
|
235
|
+
!validTarget(raw.target) ||
|
|
236
|
+
!safeTime(raw.inactiveSinceMs) ||
|
|
237
|
+
!safeTime(raw.bindingUpdatedAtMs) ||
|
|
238
|
+
raw.bindingUpdatedAtMs < raw.inactiveSinceMs ||
|
|
239
|
+
!safeTime(raw.updatedAtMs))
|
|
156
240
|
throw new Error("Telegram Thread cleanup entry schema is invalid.");
|
|
157
|
-
const base = {
|
|
158
|
-
|
|
241
|
+
const base = {
|
|
242
|
+
profileName,
|
|
243
|
+
bindingKey: raw.bindingKey,
|
|
244
|
+
cwd: raw.cwd,
|
|
245
|
+
workspaceKey: raw.workspaceKey,
|
|
246
|
+
...(typeof raw.sessionId === "string" &&
|
|
247
|
+
typeof raw.sessionKey === "string"
|
|
159
248
|
? { sessionId: raw.sessionId, sessionKey: raw.sessionKey }
|
|
160
249
|
: {}),
|
|
161
|
-
instanceSlot: raw.instanceSlot,
|
|
162
|
-
|
|
163
|
-
|
|
250
|
+
instanceSlot: raw.instanceSlot,
|
|
251
|
+
slot: raw.slot,
|
|
252
|
+
target: {
|
|
253
|
+
chatId: raw.target.chatId,
|
|
254
|
+
threadId: raw.target.threadId,
|
|
255
|
+
},
|
|
256
|
+
inactiveSinceMs: raw.inactiveSinceMs,
|
|
257
|
+
bindingUpdatedAtMs: raw.bindingUpdatedAtMs,
|
|
258
|
+
updatedAtMs: raw.updatedAtMs,
|
|
259
|
+
};
|
|
164
260
|
if (base.updatedAtMs < base.bindingUpdatedAtMs)
|
|
165
261
|
throw new Error("Telegram Thread cleanup entry clock is invalid.");
|
|
166
|
-
const noPermit = raw.permitOperationId === undefined &&
|
|
262
|
+
const noPermit = raw.permitOperationId === undefined &&
|
|
263
|
+
raw.permitIntentId === undefined &&
|
|
167
264
|
raw.permitLeaderEpoch === undefined;
|
|
168
|
-
if (raw.state === "prepared" &&
|
|
265
|
+
if (raw.state === "prepared" &&
|
|
266
|
+
raw.issuedAtMs === undefined &&
|
|
267
|
+
raw.deletedAtMs === undefined &&
|
|
268
|
+
noPermit)
|
|
169
269
|
return { ...base, state: "prepared" };
|
|
170
|
-
const validPermit = typeof raw.permitOperationId === "string" &&
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
raw.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
raw.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
270
|
+
const validPermit = typeof raw.permitOperationId === "string" &&
|
|
271
|
+
!!raw.permitOperationId &&
|
|
272
|
+
typeof raw.permitIntentId === "string" &&
|
|
273
|
+
!!raw.permitIntentId &&
|
|
274
|
+
(typeof raw.permitLeaderEpoch === "string" ||
|
|
275
|
+
Number.isSafeInteger(raw.permitLeaderEpoch));
|
|
276
|
+
if (raw.state === "outcome-unknown" &&
|
|
277
|
+
safeTime(raw.issuedAtMs) &&
|
|
278
|
+
validPermit &&
|
|
279
|
+
raw.issuedAtMs === base.updatedAtMs &&
|
|
280
|
+
raw.deletedAtMs === undefined)
|
|
281
|
+
return {
|
|
282
|
+
...base,
|
|
283
|
+
state: "outcome-unknown",
|
|
284
|
+
issuedAtMs: raw.issuedAtMs,
|
|
285
|
+
permitOperationId: raw.permitOperationId,
|
|
286
|
+
permitIntentId: raw.permitIntentId,
|
|
287
|
+
permitLeaderEpoch: raw.permitLeaderEpoch,
|
|
288
|
+
};
|
|
289
|
+
if (raw.state === "deleted" &&
|
|
290
|
+
safeTime(raw.issuedAtMs) &&
|
|
291
|
+
validPermit &&
|
|
292
|
+
safeTime(raw.deletedAtMs) &&
|
|
293
|
+
raw.deletedAtMs === base.updatedAtMs &&
|
|
294
|
+
raw.deletedAtMs >= raw.issuedAtMs)
|
|
295
|
+
return {
|
|
296
|
+
...base,
|
|
297
|
+
state: "deleted",
|
|
298
|
+
issuedAtMs: raw.issuedAtMs,
|
|
299
|
+
permitOperationId: raw.permitOperationId,
|
|
300
|
+
permitIntentId: raw.permitIntentId,
|
|
301
|
+
permitLeaderEpoch: raw.permitLeaderEpoch,
|
|
302
|
+
deletedAtMs: raw.deletedAtMs,
|
|
303
|
+
};
|
|
183
304
|
throw new Error("Telegram Thread cleanup entry state is invalid.");
|
|
184
305
|
});
|
|
185
|
-
if (entries.some(entry => entry.updatedAtMs < createdAtMs) ||
|
|
186
|
-
new Set(entries.map(entry => entry.bindingKey)).size !== entries.length ||
|
|
187
|
-
new Set(entries.map(entry => targetKey(entry.target))).size !==
|
|
306
|
+
if (entries.some((entry) => entry.updatedAtMs < createdAtMs) ||
|
|
307
|
+
new Set(entries.map((entry) => entry.bindingKey)).size !== entries.length ||
|
|
308
|
+
new Set(entries.map((entry) => targetKey(entry.target))).size !==
|
|
309
|
+
entries.length)
|
|
188
310
|
throw new Error("Telegram Thread cleanup work-set identity is ambiguous.");
|
|
189
311
|
return { operationId: value.operationId, createdAtMs, entries };
|
|
190
312
|
}
|
|
@@ -192,11 +314,16 @@ function sameCandidates(left, right) {
|
|
|
192
314
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
193
315
|
}
|
|
194
316
|
function parseTelegramThreadCleanupWorkFile(value, profileName, tokenSha256) {
|
|
195
|
-
if (!isObject(value) ||
|
|
196
|
-
value
|
|
317
|
+
if (!isObject(value) ||
|
|
318
|
+
!onlyKeys(value, ["version", "profileName", "tokenSha256", "workSets"]) ||
|
|
319
|
+
value.version !== 1 ||
|
|
320
|
+
value.profileName !== profileName ||
|
|
321
|
+
value.tokenSha256 !== tokenSha256 ||
|
|
322
|
+
!Array.isArray(value.workSets))
|
|
197
323
|
throw new Error("Telegram Thread cleanup store identity or schema does not match.");
|
|
198
|
-
const workSets = value.workSets.map(workSet => validateWorkSet(workSet, profileName));
|
|
199
|
-
if (new Set(workSets.map(workSet => workSet.operationId)).size !==
|
|
324
|
+
const workSets = value.workSets.map((workSet) => validateWorkSet(workSet, profileName));
|
|
325
|
+
if (new Set(workSets.map((workSet) => workSet.operationId)).size !==
|
|
326
|
+
workSets.length)
|
|
200
327
|
throw new Error("Telegram Thread cleanup operation identity is ambiguous.");
|
|
201
328
|
return { version: 1, profileName, tokenSha256, workSets };
|
|
202
329
|
}
|
|
@@ -206,40 +333,35 @@ export function createTelegramThreadCleanupWorkStore(options) {
|
|
|
206
333
|
const maxWorkSets = options.maxWorkSets ?? 32;
|
|
207
334
|
const maxBytes = options.maxBytes ?? 1024 * 1024;
|
|
208
335
|
const now = options.getNowMs ?? Date.now;
|
|
209
|
-
if (!options.path ||
|
|
210
|
-
!
|
|
211
|
-
|
|
336
|
+
if (!options.path ||
|
|
337
|
+
!options.profileName ||
|
|
338
|
+
!/^[a-f0-9]{64}$/u.test(options.tokenSha256) ||
|
|
339
|
+
!Number.isSafeInteger(maxWorkSets) ||
|
|
340
|
+
maxWorkSets <= 0 ||
|
|
341
|
+
!Number.isSafeInteger(maxBytes) ||
|
|
342
|
+
maxBytes <= 0)
|
|
212
343
|
throw new Error("Telegram Thread cleanup store options are invalid.");
|
|
213
|
-
const empty = () => ({
|
|
214
|
-
|
|
344
|
+
const empty = () => ({
|
|
345
|
+
version: 1,
|
|
346
|
+
profileName: options.profileName,
|
|
347
|
+
tokenSha256: options.tokenSha256,
|
|
348
|
+
workSets: [],
|
|
349
|
+
});
|
|
215
350
|
const read = () => {
|
|
216
|
-
let
|
|
351
|
+
let text;
|
|
217
352
|
try {
|
|
218
|
-
|
|
353
|
+
text = readTelegramPrivateFile(options.path, maxBytes);
|
|
219
354
|
}
|
|
220
355
|
catch (error) {
|
|
221
|
-
if (error
|
|
222
|
-
|
|
223
|
-
throw error
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (!constants.O_NOFOLLOW || !constants.O_NONBLOCK || uid === undefined || !before.isFile() ||
|
|
227
|
-
before.isSymbolicLink() || before.uid !== BigInt(uid) || before.nlink !== 1n ||
|
|
228
|
-
(before.mode & 63n) !== 0n || before.size > BigInt(maxBytes))
|
|
229
|
-
throw new Error("Telegram Thread cleanup store is not a bounded private regular file.");
|
|
230
|
-
const fd = openSync(options.path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
231
|
-
let value;
|
|
232
|
-
try {
|
|
233
|
-
const opened = fstatSync(fd, { bigint: true });
|
|
234
|
-
if (opened.dev !== before.dev || opened.ino !== before.ino || opened.size !== before.size ||
|
|
235
|
-
opened.mtimeNs !== before.mtimeNs)
|
|
236
|
-
throw new Error("Telegram Thread cleanup store changed during inspection.");
|
|
237
|
-
value = JSON.parse(readFileSync(fd, "utf8"));
|
|
356
|
+
if (!(error instanceof TelegramPrivateFileError))
|
|
357
|
+
throw error;
|
|
358
|
+
throw new Error(error.failure === "changed"
|
|
359
|
+
? "Telegram Thread cleanup store changed during inspection."
|
|
360
|
+
: "Telegram Thread cleanup store is not a bounded private regular file.", { cause: error });
|
|
238
361
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return parseTelegramThreadCleanupWorkFile(value, options.profileName, options.tokenSha256);
|
|
362
|
+
return text === undefined
|
|
363
|
+
? empty()
|
|
364
|
+
: parseTelegramThreadCleanupWorkFile(JSON.parse(text), options.profileName, options.tokenSha256);
|
|
243
365
|
};
|
|
244
366
|
const publish = (file) => {
|
|
245
367
|
if (file.workSets.length > maxWorkSets)
|
|
@@ -247,32 +369,17 @@ export function createTelegramThreadCleanupWorkStore(options) {
|
|
|
247
369
|
const serialized = `${JSON.stringify(file, null, 2)}\n`;
|
|
248
370
|
if (Buffer.byteLength(serialized) > maxBytes)
|
|
249
371
|
throw new Error("Telegram Thread cleanup byte capacity reached.");
|
|
250
|
-
|
|
251
|
-
mkdirSync(dirname(options.path), { recursive: true, mode: 0o700 });
|
|
252
|
-
mkdirSync(dirname(temporaryPath), { recursive: true, mode: 0o700 });
|
|
253
|
-
try {
|
|
254
|
-
writeFileSync(temporaryPath, serialized, { mode: 0o600 });
|
|
255
|
-
chmodSync(temporaryPath, 0o600);
|
|
256
|
-
options.onPublicationBoundary?.("after-write-before-rename");
|
|
257
|
-
if (!renameTelegramPathWithRetry(temporaryPath, options.path))
|
|
258
|
-
throw new Error("Telegram Thread cleanup staging file disappeared.");
|
|
259
|
-
chmodSync(options.path, 0o600);
|
|
260
|
-
options.onPublicationBoundary?.("after-rename");
|
|
261
|
-
}
|
|
262
|
-
finally {
|
|
263
|
-
try {
|
|
264
|
-
unlinkSync(temporaryPath);
|
|
265
|
-
}
|
|
266
|
-
catch { /* rename consumed it */ }
|
|
267
|
-
}
|
|
372
|
+
publishTelegramPrivateFile(options.path, publicationPaths.temporaryBasePath, serialized, "Telegram Thread cleanup", options.onPublicationBoundary);
|
|
268
373
|
};
|
|
269
374
|
const mutate = (operation) => withTelegramFileTransaction(publicationPaths.transactionPath, () => operation(read()));
|
|
270
375
|
return {
|
|
271
376
|
prepare(operationId, candidates) {
|
|
272
|
-
if (!operationId ||
|
|
377
|
+
if (!operationId ||
|
|
378
|
+
candidates.length === 0 ||
|
|
379
|
+
candidates.some((candidate) => candidate.profileName !== options.profileName))
|
|
273
380
|
throw new Error("Telegram Thread cleanup work-set is invalid.");
|
|
274
|
-
return mutate(file => {
|
|
275
|
-
const existing = file.workSets.find(workSet => workSet.operationId === operationId);
|
|
381
|
+
return mutate((file) => {
|
|
382
|
+
const existing = file.workSets.find((workSet) => workSet.operationId === operationId);
|
|
276
383
|
if (existing) {
|
|
277
384
|
const projected = existing.entries.map(({ state: _state, updatedAtMs: _updated, issuedAtMs: _issued, permitOperationId: _permitOperation, permitIntentId: _permitIntent, permitLeaderEpoch: _permitEpoch, deletedAtMs: _deleted, ...candidate }) => candidate);
|
|
278
385
|
if (!sameCandidates(projected, candidates))
|
|
@@ -282,21 +389,35 @@ export function createTelegramThreadCleanupWorkStore(options) {
|
|
|
282
389
|
const atMs = now();
|
|
283
390
|
if (!safeTime(atMs))
|
|
284
391
|
throw new Error("Telegram Thread cleanup clock is invalid.");
|
|
285
|
-
const workSet = validateWorkSet({
|
|
286
|
-
|
|
392
|
+
const workSet = validateWorkSet({
|
|
393
|
+
operationId,
|
|
394
|
+
createdAtMs: atMs,
|
|
395
|
+
entries: candidates.map((candidate) => ({
|
|
396
|
+
...structuredClone(candidate),
|
|
397
|
+
state: "prepared",
|
|
398
|
+
updatedAtMs: atMs,
|
|
399
|
+
})),
|
|
400
|
+
}, options.profileName);
|
|
287
401
|
publish({ ...file, workSets: [...file.workSets, workSet] });
|
|
288
402
|
return { prepared: true, workSet: structuredClone(workSet) };
|
|
289
403
|
});
|
|
290
404
|
},
|
|
291
405
|
recordDeletionIssued(input) {
|
|
292
|
-
return mutate(file => {
|
|
293
|
-
const workSet = file.workSets.find(candidate => candidate.operationId === input.operationId);
|
|
294
|
-
const entry = workSet?.entries.find(candidate => candidate.bindingKey === input.bindingKey);
|
|
406
|
+
return mutate((file) => {
|
|
407
|
+
const workSet = file.workSets.find((candidate) => candidate.operationId === input.operationId);
|
|
408
|
+
const entry = workSet?.entries.find((candidate) => candidate.bindingKey === input.bindingKey);
|
|
295
409
|
const permit = input.permit;
|
|
296
|
-
if (!workSet ||
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
410
|
+
if (!workSet ||
|
|
411
|
+
!entry ||
|
|
412
|
+
entry.bindingUpdatedAtMs !== input.bindingUpdatedAtMs ||
|
|
413
|
+
permit.destructiveKind !== "manual-thread-cleanup" ||
|
|
414
|
+
permit.profileKey !== entry.profileName ||
|
|
415
|
+
permit.bindingKey !== entry.bindingKey ||
|
|
416
|
+
permit.slot !== entry.slot ||
|
|
417
|
+
targetKey(permit.target) !== targetKey(entry.target) ||
|
|
418
|
+
!permit.operationId ||
|
|
419
|
+
!permit.retirementIntentId ||
|
|
420
|
+
!safeTime(permit.issuedAtMs))
|
|
300
421
|
throw new Error("Telegram Thread cleanup entry or deletion permit is stale or mismatched.");
|
|
301
422
|
if (entry.state !== "prepared") {
|
|
302
423
|
const exact = entry.permitOperationId === permit.operationId &&
|
|
@@ -307,7 +428,9 @@ export function createTelegramThreadCleanupWorkStore(options) {
|
|
|
307
428
|
return { recorded: false, entry: structuredClone(entry) };
|
|
308
429
|
}
|
|
309
430
|
const atMs = now();
|
|
310
|
-
if (!safeTime(atMs) ||
|
|
431
|
+
if (!safeTime(atMs) ||
|
|
432
|
+
atMs < entry.updatedAtMs ||
|
|
433
|
+
atMs < permit.issuedAtMs)
|
|
311
434
|
throw new Error("Telegram Thread cleanup clock is invalid.");
|
|
312
435
|
entry.state = "outcome-unknown";
|
|
313
436
|
entry.issuedAtMs = atMs;
|
|
@@ -320,9 +443,10 @@ export function createTelegramThreadCleanupWorkStore(options) {
|
|
|
320
443
|
});
|
|
321
444
|
},
|
|
322
445
|
confirmDeleted(input) {
|
|
323
|
-
return mutate(file => {
|
|
324
|
-
const entry = file.workSets
|
|
325
|
-
|
|
446
|
+
return mutate((file) => {
|
|
447
|
+
const entry = file.workSets
|
|
448
|
+
.find((candidate) => candidate.operationId === input.operationId)
|
|
449
|
+
?.entries.find((candidate) => candidate.bindingKey === input.bindingKey);
|
|
326
450
|
if (!entry)
|
|
327
451
|
throw new Error("Telegram Thread cleanup entry is missing.");
|
|
328
452
|
if (entry.state === "deleted")
|
|
@@ -345,15 +469,20 @@ export function createTelegramThreadCleanupWorkStore(options) {
|
|
|
345
469
|
};
|
|
346
470
|
}
|
|
347
471
|
export async function commitTelegramInactiveThreadCleanup(input) {
|
|
348
|
-
if (!await input.commitBinding())
|
|
472
|
+
if (!(await input.commitBinding()))
|
|
349
473
|
return false;
|
|
350
|
-
input.store.confirmDeleted({
|
|
474
|
+
input.store.confirmDeleted({
|
|
475
|
+
operationId: input.operationId,
|
|
476
|
+
bindingKey: input.bindingKey,
|
|
477
|
+
});
|
|
351
478
|
return true;
|
|
352
479
|
}
|
|
353
480
|
export async function executeTelegramInactiveThreadCleanup(input) {
|
|
354
481
|
return input.withWorkspaceDeletionBoundary(async () => {
|
|
355
|
-
const workSet = input.store
|
|
356
|
-
|
|
482
|
+
const workSet = input.store
|
|
483
|
+
.list()
|
|
484
|
+
.find((candidate) => candidate.operationId === input.operationId);
|
|
485
|
+
const retained = workSet?.entries.find((candidate) => candidate.bindingKey === input.bindingKey);
|
|
357
486
|
if (!retained)
|
|
358
487
|
return { status: "blocked" };
|
|
359
488
|
if (retained.state === "deleted")
|
|
@@ -361,33 +490,45 @@ export async function executeTelegramInactiveThreadCleanup(input) {
|
|
|
361
490
|
if (retained.state === "outcome-unknown")
|
|
362
491
|
return { status: "outcome-unknown", entry: retained };
|
|
363
492
|
const planned = planTelegramInactiveThreadCleanup(await input.loadFreshEvidence());
|
|
364
|
-
const fresh = planned.find(candidate => candidate.bindingKey === retained.bindingKey);
|
|
493
|
+
const fresh = planned.find((candidate) => candidate.bindingKey === retained.bindingKey);
|
|
365
494
|
const retainedCandidate = (({ state: _state, updatedAtMs: _updated, issuedAtMs: _issued, permitOperationId: _permitOperation, permitIntentId: _permitIntent, permitLeaderEpoch: _permitEpoch, deletedAtMs: _deleted, ...candidate }) => candidate)(retained);
|
|
366
495
|
if (!fresh || !sameCandidates([fresh], [retainedCandidate]))
|
|
367
496
|
return { status: "blocked" };
|
|
368
497
|
const permitResult = await input.acquireDeletionPermit(fresh);
|
|
369
498
|
if (permitResult.kind !== "issued")
|
|
370
499
|
return { status: "blocked" };
|
|
371
|
-
const recorded = input.store.recordDeletionIssued({
|
|
372
|
-
|
|
373
|
-
|
|
500
|
+
const recorded = input.store.recordDeletionIssued({
|
|
501
|
+
operationId: input.operationId,
|
|
502
|
+
bindingKey: input.bindingKey,
|
|
503
|
+
bindingUpdatedAtMs: retained.bindingUpdatedAtMs,
|
|
504
|
+
permit: permitResult.permit,
|
|
505
|
+
});
|
|
374
506
|
if (!recorded.recorded)
|
|
375
507
|
return { status: "outcome-unknown", entry: recorded.entry };
|
|
376
508
|
await input.deleteWithPermit(permitResult.permit, fresh);
|
|
377
|
-
const confirmed = input.store.confirmDeleted({
|
|
378
|
-
|
|
509
|
+
const confirmed = input.store.confirmDeleted({
|
|
510
|
+
operationId: input.operationId,
|
|
511
|
+
bindingKey: input.bindingKey,
|
|
512
|
+
});
|
|
379
513
|
return { status: "deleted", entry: confirmed.entry };
|
|
380
514
|
});
|
|
381
515
|
}
|
|
382
516
|
export function createTelegramThreadCleanupPermitRuntime(deps) {
|
|
383
517
|
const now = deps.getNowMs ?? Date.now;
|
|
384
518
|
const findExactFence = (candidate, workSetId) => {
|
|
385
|
-
const operationId = `thread-cleanup-fence:${createHash("sha256")
|
|
519
|
+
const operationId = `thread-cleanup-fence:${createHash("sha256")
|
|
520
|
+
.update(`${workSetId}\u0000${candidate.bindingKey}`, "utf8")
|
|
521
|
+
.digest("hex")}`;
|
|
386
522
|
const fence = deps.ledger.read().fence;
|
|
387
|
-
return fence?.operationId === operationId &&
|
|
388
|
-
fence.
|
|
389
|
-
fence.
|
|
390
|
-
|
|
523
|
+
return fence?.operationId === operationId &&
|
|
524
|
+
fence.destructiveKind === "manual-thread-cleanup" &&
|
|
525
|
+
fence.profileKey === candidate.profileName &&
|
|
526
|
+
fence.retirementIntentId === workSetId &&
|
|
527
|
+
fence.bindingKey === candidate.bindingKey &&
|
|
528
|
+
fence.slot === candidate.slot &&
|
|
529
|
+
targetKey(fence.target) === targetKey(candidate.target)
|
|
530
|
+
? fence
|
|
531
|
+
: undefined;
|
|
391
532
|
};
|
|
392
533
|
const adoptExactFence = (candidate, workSetId) => {
|
|
393
534
|
let fence = findExactFence(candidate, workSetId);
|
|
@@ -395,12 +536,16 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
|
|
|
395
536
|
if (!fence || leaderEpoch === undefined)
|
|
396
537
|
return undefined;
|
|
397
538
|
const owner = deps.getOwner();
|
|
398
|
-
if (fence.leaderEpoch !== leaderEpoch ||
|
|
539
|
+
if (fence.leaderEpoch !== leaderEpoch ||
|
|
540
|
+
fence.owner.processId !== owner.processId ||
|
|
399
541
|
fence.owner.processBirthId !== owner.processBirthId) {
|
|
400
542
|
if (!deps.canAdoptFence(fence))
|
|
401
543
|
return undefined;
|
|
402
544
|
try {
|
|
403
|
-
const adopted = deps.ledger.adoptThreadCleanupFence(fence, {
|
|
545
|
+
const adopted = deps.ledger.adoptThreadCleanupFence(fence, {
|
|
546
|
+
owner,
|
|
547
|
+
leaderEpoch,
|
|
548
|
+
});
|
|
404
549
|
if (adopted.destructiveKind !== "manual-thread-cleanup")
|
|
405
550
|
return undefined;
|
|
406
551
|
fence = adopted;
|
|
@@ -422,8 +567,11 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
|
|
|
422
567
|
const owner = deps.getOwner();
|
|
423
568
|
const leaderEpoch = deps.getLeaderEpoch();
|
|
424
569
|
const owns = exact.owner.processId === owner.processId &&
|
|
425
|
-
exact.owner.processBirthId === owner.processBirthId &&
|
|
426
|
-
|
|
570
|
+
exact.owner.processBirthId === owner.processBirthId &&
|
|
571
|
+
exact.leaderEpoch === leaderEpoch;
|
|
572
|
+
return owns || deps.canAdoptFence(exact)
|
|
573
|
+
? exact.phase
|
|
574
|
+
: "authority-blocked";
|
|
427
575
|
},
|
|
428
576
|
findCommitReady(candidate, workSetId) {
|
|
429
577
|
const fence = adoptExactFence(candidate, workSetId);
|
|
@@ -431,31 +579,46 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
|
|
|
431
579
|
},
|
|
432
580
|
async acquire(candidate, workSetId) {
|
|
433
581
|
const leaderEpoch = deps.getLeaderEpoch();
|
|
434
|
-
if (leaderEpoch === undefined ||
|
|
582
|
+
if (leaderEpoch === undefined ||
|
|
583
|
+
candidate.profileName.length === 0 ||
|
|
435
584
|
candidate.profileName !== deps.getProfileName())
|
|
436
585
|
return { kind: "blocked" };
|
|
437
586
|
const operationId = `thread-cleanup-fence:${createHash("sha256")
|
|
438
|
-
.update(workSetId)
|
|
587
|
+
.update(workSetId)
|
|
588
|
+
.update("\0")
|
|
589
|
+
.update(candidate.bindingKey)
|
|
590
|
+
.digest("hex")}`;
|
|
439
591
|
const existing = adoptExactFence(candidate, workSetId) ?? deps.ledger.read().fence;
|
|
440
|
-
if (existing &&
|
|
592
|
+
if (existing &&
|
|
593
|
+
existing.operationId === operationId &&
|
|
441
594
|
existing.destructiveKind === "manual-thread-cleanup" &&
|
|
442
|
-
existing.retirementIntentId === workSetId &&
|
|
443
|
-
existing.
|
|
444
|
-
existing.
|
|
595
|
+
existing.retirementIntentId === workSetId &&
|
|
596
|
+
existing.bindingKey === candidate.bindingKey &&
|
|
597
|
+
existing.slot === candidate.slot &&
|
|
598
|
+
targetKey(existing.target) === targetKey(candidate.target) &&
|
|
599
|
+
existing.leaderEpoch === leaderEpoch &&
|
|
600
|
+
existing.phase !== "fenced") {
|
|
445
601
|
return { kind: "already-issued" };
|
|
446
602
|
}
|
|
447
603
|
let acquired;
|
|
448
604
|
try {
|
|
449
|
-
acquired = deps.ledger.acquireThreadCleanupFence({
|
|
450
|
-
|
|
451
|
-
|
|
605
|
+
acquired = deps.ledger.acquireThreadCleanupFence({
|
|
606
|
+
operationId,
|
|
607
|
+
cleanupWorkSetId: workSetId,
|
|
608
|
+
bindingKey: candidate.bindingKey,
|
|
609
|
+
slot: candidate.slot,
|
|
610
|
+
target: candidate.target,
|
|
611
|
+
leaderEpoch,
|
|
612
|
+
cleanupRequestedAtMs: now(),
|
|
613
|
+
});
|
|
452
614
|
}
|
|
453
615
|
catch {
|
|
454
616
|
return { kind: "blocked" };
|
|
455
617
|
}
|
|
456
618
|
if (acquired.kind === "blocked")
|
|
457
619
|
return { kind: "blocked" };
|
|
458
|
-
if (deps.getLeaderEpoch() !== leaderEpoch ||
|
|
620
|
+
if (deps.getLeaderEpoch() !== leaderEpoch ||
|
|
621
|
+
!(await deps.revalidateUnderFence(candidate))) {
|
|
459
622
|
if (acquired.fence.phase === "fenced")
|
|
460
623
|
deps.ledger.releaseUnissuedThreadCleanupFence(acquired.fence);
|
|
461
624
|
return { kind: "blocked" };
|
|
@@ -467,7 +630,7 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
|
|
|
467
630
|
},
|
|
468
631
|
async settleDeleted(fence, commit) {
|
|
469
632
|
const ready = deps.ledger.confirmThreadCleanupAbsence(fence);
|
|
470
|
-
if (!await commit())
|
|
633
|
+
if (!(await commit()))
|
|
471
634
|
return "commit-pending";
|
|
472
635
|
deps.ledger.completeThreadCleanupFence(ready);
|
|
473
636
|
return "completed";
|
|
@@ -475,12 +638,14 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
|
|
|
475
638
|
};
|
|
476
639
|
}
|
|
477
640
|
export function createTelegramInactiveThreadCleanupSettingsPort(deps) {
|
|
478
|
-
return operationId => cleanReviewedInactiveThreads({ ...deps, operationId });
|
|
641
|
+
return (operationId) => cleanReviewedInactiveThreads({ ...deps, operationId });
|
|
479
642
|
}
|
|
480
643
|
export async function cleanReviewedInactiveThreads(input) {
|
|
481
644
|
if (!/^thread-cleanup:[a-f0-9]{32}$/u.test(input.operationId))
|
|
482
645
|
throw new Error("Telegram Thread cleanup confirmation identity is invalid.");
|
|
483
|
-
const workSet = input.store
|
|
646
|
+
const workSet = input.store
|
|
647
|
+
.list()
|
|
648
|
+
.find((candidate) => candidate.operationId === input.operationId);
|
|
484
649
|
if (!workSet)
|
|
485
650
|
throw new Error("Telegram Thread cleanup review is missing.");
|
|
486
651
|
let deleted = 0;
|
|
@@ -500,8 +665,12 @@ export async function cleanReviewedInactiveThreads(input) {
|
|
|
500
665
|
outcomeUnknown += 1;
|
|
501
666
|
continue;
|
|
502
667
|
}
|
|
503
|
-
const settlement = await input.permitRuntime.settleDeleted(ready, () => commitTelegramInactiveThreadCleanup({
|
|
504
|
-
|
|
668
|
+
const settlement = await input.permitRuntime.settleDeleted(ready, () => commitTelegramInactiveThreadCleanup({
|
|
669
|
+
store: input.store,
|
|
670
|
+
operationId: input.operationId,
|
|
671
|
+
bindingKey: entry.bindingKey,
|
|
672
|
+
commitBinding: () => input.commitBinding(entry),
|
|
673
|
+
}));
|
|
505
674
|
if (settlement === "completed")
|
|
506
675
|
deleted += 1;
|
|
507
676
|
else
|
|
@@ -523,8 +692,10 @@ export async function cleanReviewedInactiveThreads(input) {
|
|
|
523
692
|
continue;
|
|
524
693
|
}
|
|
525
694
|
try {
|
|
526
|
-
const recorded = input.store.recordDeletionIssued({
|
|
527
|
-
|
|
695
|
+
const recorded = input.store.recordDeletionIssued({
|
|
696
|
+
operationId: input.operationId,
|
|
697
|
+
bindingKey: entry.bindingKey,
|
|
698
|
+
bindingUpdatedAtMs: entry.bindingUpdatedAtMs,
|
|
528
699
|
permit: authority.permit,
|
|
529
700
|
});
|
|
530
701
|
if (!recorded.recorded) {
|
|
@@ -540,8 +711,12 @@ export async function cleanReviewedInactiveThreads(input) {
|
|
|
540
711
|
continue;
|
|
541
712
|
}
|
|
542
713
|
await input.deleteWithPermit(authority.permit, entry);
|
|
543
|
-
const settlement = await input.permitRuntime.settleDeleted(authority.fence, () => commitTelegramInactiveThreadCleanup({
|
|
544
|
-
|
|
714
|
+
const settlement = await input.permitRuntime.settleDeleted(authority.fence, () => commitTelegramInactiveThreadCleanup({
|
|
715
|
+
store: input.store,
|
|
716
|
+
operationId: input.operationId,
|
|
717
|
+
bindingKey: entry.bindingKey,
|
|
718
|
+
commitBinding: () => input.commitBinding(entry, fullBinding),
|
|
719
|
+
}));
|
|
545
720
|
if (settlement === "completed")
|
|
546
721
|
deleted += 1;
|
|
547
722
|
else
|
|
@@ -551,11 +726,19 @@ export async function cleanReviewedInactiveThreads(input) {
|
|
|
551
726
|
outcomeUnknown += 1;
|
|
552
727
|
}
|
|
553
728
|
}
|
|
554
|
-
const retainedEntries = input.store
|
|
729
|
+
const retainedEntries = input.store
|
|
730
|
+
.list()
|
|
731
|
+
.find((candidate) => candidate.operationId === input.operationId)
|
|
732
|
+
?.entries ?? [];
|
|
555
733
|
if (outcomeUnknown > 0) {
|
|
556
|
-
const diagnosis = retainedEntries
|
|
557
|
-
|
|
558
|
-
|
|
734
|
+
const diagnosis = retainedEntries
|
|
735
|
+
.map((entry) => input.permitRuntime.diagnoseRecovery(entry, input.operationId))
|
|
736
|
+
.find((value) => value !== "none") ?? "none";
|
|
737
|
+
const recovery = diagnosis === "commit-ready"
|
|
738
|
+
? "commit-ready"
|
|
739
|
+
: diagnosis === "deletion-issued"
|
|
740
|
+
? "deletion-outcome-unknown"
|
|
741
|
+
: "authority-blocked";
|
|
559
742
|
return { deleted, outcomeUnknown, blocked, recovery };
|
|
560
743
|
}
|
|
561
744
|
return { deleted, outcomeUnknown, blocked };
|