@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
package/lib/recovery.ts
CHANGED
|
@@ -1,399 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram
|
|
2
|
+
* Telegram runtime storage housekeeping
|
|
3
3
|
* Zones: filesystem diagnostics, unclean-shutdown recovery
|
|
4
|
-
* Owns
|
|
4
|
+
* Owns best-effort removal of obsolete recovery folders and orphaned session journal families
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
lstatSync,
|
|
10
|
-
readFileSync,
|
|
11
|
-
readdirSync,
|
|
12
|
-
rmdirSync,
|
|
13
|
-
rmSync,
|
|
14
|
-
} from "node:fs";
|
|
15
|
-
import { basename, dirname, join } from "node:path";
|
|
16
|
-
|
|
17
|
-
import { decodeTelegramSessionDirectoryName, getTelegramProfilePathSuffix } from "./paths.ts";
|
|
7
|
+
import { lstatSync, readdirSync, rmdirSync, rmSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
18
9
|
|
|
19
10
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
withTelegramFileTransaction,
|
|
24
|
-
type TelegramFileTransactionOptions,
|
|
25
|
-
} from "./locks.ts";
|
|
26
|
-
|
|
27
|
-
export type TelegramRuntimeArtifactKind = "owners" | "state" | "transaction";
|
|
28
|
-
|
|
29
|
-
export interface TelegramRuntimeCorruptArtifact {
|
|
30
|
-
kind: TelegramRuntimeArtifactKind;
|
|
31
|
-
path: string;
|
|
32
|
-
reason: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type TelegramRuntimeRecoveryClassification =
|
|
36
|
-
| { kind: "clean" }
|
|
37
|
-
| {
|
|
38
|
-
kind: "recoverable-corruption";
|
|
39
|
-
artifacts: TelegramRuntimeCorruptArtifact[];
|
|
40
|
-
}
|
|
41
|
-
| {
|
|
42
|
-
kind: "blocked-live-owner";
|
|
43
|
-
artifacts: TelegramRuntimeCorruptArtifact[];
|
|
44
|
-
livePids: number[];
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export interface TelegramRuntimeRecoveryClassificationOptions {
|
|
48
|
-
ownersPath: string;
|
|
49
|
-
statePaths?: readonly string[];
|
|
50
|
-
transactionPath?: string;
|
|
51
|
-
isProcessAlive?: (pid: number) => boolean;
|
|
52
|
-
nowMs?: number;
|
|
53
|
-
staleHeartbeatMs?: number;
|
|
54
|
-
ignoredTransactionPids?: readonly number[];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export type TelegramRuntimeRecoveryResult =
|
|
58
|
-
| { kind: "not-needed" }
|
|
59
|
-
| {
|
|
60
|
-
kind: "blocked-live-owner";
|
|
61
|
-
livePids: number[];
|
|
62
|
-
deletedPaths?: string[];
|
|
63
|
-
}
|
|
64
|
-
| {
|
|
65
|
-
kind: "recovered";
|
|
66
|
-
artifacts: TelegramRuntimeCorruptArtifact[];
|
|
67
|
-
/** Damaged disposable artifacts deleted per the approved corruption policy; nothing is quarantined. */
|
|
68
|
-
deletedPaths: string[];
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export interface TelegramRuntimeRecoveryOptions
|
|
72
|
-
extends TelegramRuntimeRecoveryClassificationOptions {
|
|
73
|
-
recoveryTransactionPath?: string;
|
|
74
|
-
pid?: number;
|
|
75
|
-
getNowMs?: () => number;
|
|
76
|
-
/** Fault-injection seam; production uses bounded-retry recursive removal. */
|
|
77
|
-
removePath?: (path: string) => void;
|
|
78
|
-
transactionOptions?: TelegramFileTransactionOptions;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export type TelegramPollingStartRecoveryDecision =
|
|
82
|
-
| { kind: "unhandled" }
|
|
83
|
-
| { kind: "retry"; message: string }
|
|
84
|
-
| { kind: "blocked"; message: string };
|
|
85
|
-
|
|
86
|
-
export interface TelegramPollingStartRecoveryHandlerDeps {
|
|
87
|
-
getOwnersPath: () => string;
|
|
88
|
-
getStatePaths: () => readonly string[];
|
|
89
|
-
suspendPolling: () => Promise<unknown>;
|
|
90
|
-
releaseOwnership?: () => unknown | Promise<unknown>;
|
|
91
|
-
recordRuntimeEvent?: (
|
|
92
|
-
category: string,
|
|
93
|
-
error: unknown,
|
|
94
|
-
details?: Record<string, unknown>,
|
|
95
|
-
) => void;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
interface ArtifactInspection {
|
|
99
|
-
source: TelegramRuntimeArtifactKind;
|
|
100
|
-
corrupt?: TelegramRuntimeCorruptArtifact;
|
|
101
|
-
ownerPids: number[];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const OWNER_FILE_PATTERN = /^owner\.([A-Za-z0-9-]+)\.json$/u;
|
|
105
|
-
const RECLAIM_FILE_PATTERN =
|
|
106
|
-
/^owner\.reclaim\.(\d+)\.([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.json$/u;
|
|
107
|
-
|
|
108
|
-
function corruption(
|
|
109
|
-
kind: TelegramRuntimeArtifactKind,
|
|
110
|
-
path: string,
|
|
111
|
-
reason: string,
|
|
112
|
-
): ArtifactInspection {
|
|
113
|
-
return { source: kind, corrupt: { kind, path, reason }, ownerPids: [] };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function inspectOwners(
|
|
117
|
-
path: string,
|
|
118
|
-
nowMs: number,
|
|
119
|
-
staleHeartbeatMs: number,
|
|
120
|
-
): ArtifactInspection {
|
|
121
|
-
if (!existsSync(path)) return { source: "owners", ownerPids: [] };
|
|
122
|
-
let value: unknown;
|
|
123
|
-
try {
|
|
124
|
-
value = JSON.parse(readFileSync(path, "utf8"));
|
|
125
|
-
} catch {
|
|
126
|
-
return corruption("owners", path, "owners.json is not valid JSON");
|
|
127
|
-
}
|
|
128
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
129
|
-
return corruption("owners", path, "owners.json is not an object");
|
|
130
|
-
}
|
|
131
|
-
const ownerPids = Object.values(value).flatMap((candidate) => {
|
|
132
|
-
const entry = parseTelegramLockEntry(candidate);
|
|
133
|
-
if (
|
|
134
|
-
!entry ||
|
|
135
|
-
(typeof entry.heartbeatMs === "number" &&
|
|
136
|
-
nowMs - entry.heartbeatMs > staleHeartbeatMs)
|
|
137
|
-
) {
|
|
138
|
-
return [];
|
|
139
|
-
}
|
|
140
|
-
return [entry.pid];
|
|
141
|
-
});
|
|
142
|
-
return { source: "owners", ownerPids };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function inspectState(path: string): ArtifactInspection {
|
|
146
|
-
if (!existsSync(path)) return { source: "state", ownerPids: [] };
|
|
147
|
-
try {
|
|
148
|
-
const stat = lstatSync(path);
|
|
149
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 8 * 1024 * 1024)
|
|
150
|
-
return corruption("state", path, `${basename(path)} cannot be safely inspected`);
|
|
151
|
-
JSON.parse(readFileSync(path, "utf8"));
|
|
152
|
-
return { source: "state", ownerPids: [] };
|
|
153
|
-
} catch {
|
|
154
|
-
return corruption("state", path, `${basename(path)} is not valid JSON`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function parseTransactionOwner(path: string): number | undefined {
|
|
159
|
-
const value: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
160
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
161
|
-
return undefined;
|
|
162
|
-
}
|
|
163
|
-
const pid = (value as Record<string, unknown>).pid;
|
|
164
|
-
const acquiredAtMs = (value as Record<string, unknown>).acquiredAtMs;
|
|
165
|
-
const generation = (value as Record<string, unknown>).generation;
|
|
166
|
-
return typeof pid === "number" &&
|
|
167
|
-
typeof acquiredAtMs === "number" &&
|
|
168
|
-
typeof generation === "string"
|
|
169
|
-
? pid
|
|
170
|
-
: undefined;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function inspectTransaction(path: string): ArtifactInspection {
|
|
174
|
-
if (!existsSync(path)) return { source: "transaction", ownerPids: [] };
|
|
175
|
-
try {
|
|
176
|
-
const stat = lstatSync(path);
|
|
177
|
-
let ownerPath: string;
|
|
178
|
-
let expectedOwnerPid: number | undefined;
|
|
179
|
-
if (stat.isDirectory()) {
|
|
180
|
-
const entries = readdirSync(path);
|
|
181
|
-
if (entries.length !== 1) {
|
|
182
|
-
return corruption(
|
|
183
|
-
"transaction",
|
|
184
|
-
path,
|
|
185
|
-
"transaction guard has an unverifiable directory shape",
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
const entry = entries[0];
|
|
189
|
-
const ownerMatch = OWNER_FILE_PATTERN.exec(entry);
|
|
190
|
-
const reclaimMatch = RECLAIM_FILE_PATTERN.exec(entry);
|
|
191
|
-
if (!ownerMatch && !reclaimMatch) {
|
|
192
|
-
return corruption(
|
|
193
|
-
"transaction",
|
|
194
|
-
path,
|
|
195
|
-
"transaction guard has an unrecognized owner marker",
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
ownerPath = join(path, entry);
|
|
199
|
-
expectedOwnerPid = reclaimMatch ? Number(reclaimMatch[1]) : undefined;
|
|
200
|
-
const ownerPid = parseTransactionOwner(ownerPath);
|
|
201
|
-
if (ownerPid === undefined) {
|
|
202
|
-
return corruption(
|
|
203
|
-
"transaction",
|
|
204
|
-
path,
|
|
205
|
-
"transaction guard owner metadata is invalid",
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
if (ownerMatch) {
|
|
209
|
-
const value = JSON.parse(readFileSync(ownerPath, "utf8")) as Record<
|
|
210
|
-
string,
|
|
211
|
-
unknown
|
|
212
|
-
>;
|
|
213
|
-
if (value.generation !== ownerMatch[1]) {
|
|
214
|
-
return corruption(
|
|
215
|
-
"transaction",
|
|
216
|
-
path,
|
|
217
|
-
"transaction guard generation does not match its owner marker",
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return {
|
|
222
|
-
source: "transaction",
|
|
223
|
-
ownerPids: [expectedOwnerPid ?? ownerPid],
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
if (!stat.isFile()) {
|
|
227
|
-
return corruption(
|
|
228
|
-
"transaction",
|
|
229
|
-
path,
|
|
230
|
-
"transaction guard has an unsupported filesystem type",
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
ownerPath = path;
|
|
234
|
-
const ownerPid = parseTransactionOwner(ownerPath);
|
|
235
|
-
return ownerPid === undefined
|
|
236
|
-
? corruption(
|
|
237
|
-
"transaction",
|
|
238
|
-
path,
|
|
239
|
-
"legacy transaction guard owner metadata is invalid",
|
|
240
|
-
)
|
|
241
|
-
: { source: "transaction", ownerPids: [ownerPid] };
|
|
242
|
-
} catch {
|
|
243
|
-
return corruption(
|
|
244
|
-
"transaction",
|
|
245
|
-
path,
|
|
246
|
-
"transaction guard cannot be inspected",
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Classify disposable runtime corruption without mutating any artifact.
|
|
253
|
-
*
|
|
254
|
-
* Corruption remains recoverable only when neither owners.json nor a
|
|
255
|
-
* verifiable transaction marker identifies a process that is still alive.
|
|
256
|
-
*/
|
|
257
|
-
export function classifyTelegramRuntimeRecovery(
|
|
258
|
-
options: TelegramRuntimeRecoveryClassificationOptions,
|
|
259
|
-
): TelegramRuntimeRecoveryClassification {
|
|
260
|
-
const transactionPath =
|
|
261
|
-
options.transactionPath ?? `${options.ownersPath}.transaction`;
|
|
262
|
-
const inspections = [
|
|
263
|
-
inspectOwners(
|
|
264
|
-
options.ownersPath,
|
|
265
|
-
options.nowMs ?? Date.now(),
|
|
266
|
-
options.staleHeartbeatMs ?? TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
|
|
267
|
-
),
|
|
268
|
-
...(options.statePaths ?? []).map(inspectState),
|
|
269
|
-
inspectTransaction(transactionPath),
|
|
270
|
-
];
|
|
271
|
-
const artifacts = inspections.flatMap((inspection) =>
|
|
272
|
-
inspection.corrupt ? [inspection.corrupt] : [],
|
|
273
|
-
);
|
|
274
|
-
if (artifacts.length === 0) return { kind: "clean" };
|
|
275
|
-
|
|
276
|
-
const processAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
277
|
-
const ignoredTransactionPids = new Set(
|
|
278
|
-
options.ignoredTransactionPids ?? [],
|
|
279
|
-
);
|
|
280
|
-
const livePids = [
|
|
281
|
-
...new Set(
|
|
282
|
-
inspections.flatMap((inspection) =>
|
|
283
|
-
inspection.ownerPids.filter(
|
|
284
|
-
(pid) =>
|
|
285
|
-
!(
|
|
286
|
-
inspection.source === "transaction" &&
|
|
287
|
-
ignoredTransactionPids.has(pid)
|
|
288
|
-
) && processAlive(pid),
|
|
289
|
-
),
|
|
290
|
-
),
|
|
291
|
-
),
|
|
292
|
-
].sort((left, right) => left - right);
|
|
293
|
-
return livePids.length > 0
|
|
294
|
-
? { kind: "blocked-live-owner", artifacts, livePids }
|
|
295
|
-
: { kind: "recoverable-corruption", artifacts };
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Delete classifier-approved disposable corruption under two guards.
|
|
300
|
-
*
|
|
301
|
-
* A dedicated recovery transaction serializes recoverers. The ownership
|
|
302
|
-
* transaction then prevents a new Telegram owner from appearing between the
|
|
303
|
-
* final classification and mutation. Damaged ownership debris and canonical
|
|
304
|
-
* state are deleted (operator policy: unfinished Restores in unreadable state
|
|
305
|
-
* are acceptable loss). Durable config and diagnostics never enter the set.
|
|
306
|
-
*/
|
|
307
|
-
export function recoverTelegramRuntimeState(
|
|
308
|
-
options: TelegramRuntimeRecoveryOptions,
|
|
309
|
-
): TelegramRuntimeRecoveryResult {
|
|
310
|
-
const transactionPath =
|
|
311
|
-
options.transactionPath ?? `${options.ownersPath}.transaction`;
|
|
312
|
-
const recoveryTransactionPath =
|
|
313
|
-
options.recoveryTransactionPath ??
|
|
314
|
-
join(dirname(options.ownersPath), "runtime-recovery.transaction");
|
|
315
|
-
const removePath = options.removePath ?? ((path: string) =>
|
|
316
|
-
rmSync(path, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 }));
|
|
317
|
-
const classificationOptions = {
|
|
318
|
-
ownersPath: options.ownersPath,
|
|
319
|
-
statePaths: options.statePaths,
|
|
320
|
-
transactionPath,
|
|
321
|
-
isProcessAlive: options.isProcessAlive,
|
|
322
|
-
nowMs: options.nowMs ?? options.getNowMs?.(),
|
|
323
|
-
staleHeartbeatMs: options.staleHeartbeatMs,
|
|
324
|
-
} satisfies TelegramRuntimeRecoveryClassificationOptions;
|
|
325
|
-
|
|
326
|
-
return withTelegramFileTransaction(
|
|
327
|
-
recoveryTransactionPath,
|
|
328
|
-
() => {
|
|
329
|
-
const initial = classifyTelegramRuntimeRecovery(classificationOptions);
|
|
330
|
-
if (initial.kind === "clean") return { kind: "not-needed" };
|
|
331
|
-
if (initial.kind === "blocked-live-owner") {
|
|
332
|
-
return {
|
|
333
|
-
kind: "blocked-live-owner",
|
|
334
|
-
livePids: initial.livePids,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const deletedPaths: string[] = [];
|
|
339
|
-
const recoveredArtifacts: TelegramRuntimeCorruptArtifact[] = [];
|
|
340
|
-
const deleteArtifact = (
|
|
341
|
-
artifact: TelegramRuntimeCorruptArtifact,
|
|
342
|
-
): void => {
|
|
343
|
-
if (!existsSync(artifact.path)) return;
|
|
344
|
-
removePath(artifact.path);
|
|
345
|
-
deletedPaths.push(artifact.path);
|
|
346
|
-
recoveredArtifacts.push(artifact);
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
for (const artifact of initial.artifacts) {
|
|
350
|
-
if (artifact.kind === "transaction") deleteArtifact(artifact);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return withTelegramFileTransaction(
|
|
354
|
-
transactionPath,
|
|
355
|
-
() => {
|
|
356
|
-
const current = classifyTelegramRuntimeRecovery({
|
|
357
|
-
...classificationOptions,
|
|
358
|
-
ignoredTransactionPids: [process.pid],
|
|
359
|
-
});
|
|
360
|
-
if (current.kind === "blocked-live-owner") {
|
|
361
|
-
return {
|
|
362
|
-
kind: "blocked-live-owner",
|
|
363
|
-
livePids: current.livePids,
|
|
364
|
-
...(deletedPaths.length > 0 ? { deletedPaths } : {}),
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
if (current.kind === "recoverable-corruption") {
|
|
368
|
-
for (const artifact of current.artifacts) {
|
|
369
|
-
if (artifact.kind !== "transaction") deleteArtifact(artifact);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
return recoveredArtifacts.length > 0
|
|
373
|
-
? {
|
|
374
|
-
kind: "recovered",
|
|
375
|
-
artifacts: recoveredArtifacts,
|
|
376
|
-
deletedPaths,
|
|
377
|
-
}
|
|
378
|
-
: { kind: "not-needed" };
|
|
379
|
-
},
|
|
380
|
-
options.transactionOptions,
|
|
381
|
-
);
|
|
382
|
-
},
|
|
383
|
-
options.transactionOptions,
|
|
384
|
-
);
|
|
385
|
-
}
|
|
11
|
+
decodeTelegramSessionDirectoryName,
|
|
12
|
+
getTelegramProfilePathSuffix,
|
|
13
|
+
} from "./paths.ts";
|
|
386
14
|
|
|
387
15
|
/**
|
|
388
16
|
* Remove recovery folders written by earlier releases (runtime root and session folders).
|
|
389
17
|
* Current releases delete damaged files instead of quarantining them; nothing reads these copies.
|
|
390
18
|
*/
|
|
391
|
-
export function removeTelegramLegacyRecoveryStorage(
|
|
19
|
+
export function removeTelegramLegacyRecoveryStorage(
|
|
20
|
+
runtimeDir: string,
|
|
21
|
+
): string[] {
|
|
392
22
|
const removed: string[] = [];
|
|
393
23
|
const remove = (path: string): void => {
|
|
394
24
|
try {
|
|
395
25
|
if (!lstatSync(path).isDirectory()) return;
|
|
396
|
-
rmSync(path, {
|
|
26
|
+
rmSync(path, {
|
|
27
|
+
recursive: true,
|
|
28
|
+
force: true,
|
|
29
|
+
maxRetries: 3,
|
|
30
|
+
retryDelay: 50,
|
|
31
|
+
});
|
|
397
32
|
removed.push(path);
|
|
398
33
|
} catch {
|
|
399
34
|
// Best-effort housekeeping; a later startup retries.
|
|
@@ -401,8 +36,13 @@ export function removeTelegramLegacyRecoveryStorage(runtimeDir: string): string[
|
|
|
401
36
|
};
|
|
402
37
|
remove(join(runtimeDir, "recovery"));
|
|
403
38
|
let sessions: string[] = [];
|
|
404
|
-
try {
|
|
405
|
-
|
|
39
|
+
try {
|
|
40
|
+
sessions = readdirSync(join(runtimeDir, "sessions"));
|
|
41
|
+
} catch {
|
|
42
|
+
/* no sessions yet */
|
|
43
|
+
}
|
|
44
|
+
for (const session of sessions)
|
|
45
|
+
remove(join(runtimeDir, "sessions", session, "recovery"));
|
|
406
46
|
return removed;
|
|
407
47
|
}
|
|
408
48
|
|
|
@@ -421,22 +61,34 @@ export interface TelegramSessionFolderSweeperDeps {
|
|
|
421
61
|
* Leader housekeeping (operator policy): a session without a Workspace slot loses its
|
|
422
62
|
* current-profile journal family; the folder disappears once no profile uses it.
|
|
423
63
|
*/
|
|
424
|
-
export function createTelegramSessionFolderSweeper(
|
|
64
|
+
export function createTelegramSessionFolderSweeper(
|
|
65
|
+
deps: TelegramSessionFolderSweeperDeps,
|
|
66
|
+
): { sweep: () => string[] } {
|
|
425
67
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
426
68
|
const intervalMs = deps.intervalMs ?? TELEGRAM_SESSION_SWEEP_INTERVAL_MS;
|
|
427
69
|
let lastSweepAtMs: number | undefined;
|
|
428
70
|
return {
|
|
429
71
|
sweep() {
|
|
430
72
|
const now = getNowMs();
|
|
431
|
-
if (lastSweepAtMs !== undefined && now - lastSweepAtMs < intervalMs)
|
|
73
|
+
if (lastSweepAtMs !== undefined && now - lastSweepAtMs < intervalMs)
|
|
74
|
+
return [];
|
|
432
75
|
lastSweepAtMs = now;
|
|
433
76
|
const sessionsDir = deps.getSessionsDir();
|
|
434
77
|
const kept = new Set(deps.getKeptSessionIds());
|
|
435
|
-
const suffix = getTelegramProfilePathSuffix(
|
|
436
|
-
|
|
78
|
+
const suffix = getTelegramProfilePathSuffix(
|
|
79
|
+
deps.getProfileName(),
|
|
80
|
+
).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
81
|
+
const family = new RegExp(
|
|
82
|
+
`^journal\\.[a-f0-9]{16}${suffix}\\.json(?:\\.segments|\\.retained)?$`,
|
|
83
|
+
"u",
|
|
84
|
+
);
|
|
437
85
|
const removed: string[] = [];
|
|
438
86
|
let names: string[];
|
|
439
|
-
try {
|
|
87
|
+
try {
|
|
88
|
+
names = readdirSync(sessionsDir);
|
|
89
|
+
} catch {
|
|
90
|
+
return removed;
|
|
91
|
+
}
|
|
440
92
|
for (const name of names) {
|
|
441
93
|
const sessionId = decodeTelegramSessionDirectoryName(name);
|
|
442
94
|
if (sessionId === undefined || kept.has(sessionId)) continue;
|
|
@@ -446,10 +98,18 @@ export function createTelegramSessionFolderSweeper(deps: TelegramSessionFolderSw
|
|
|
446
98
|
for (const entry of readdirSync(folder)) {
|
|
447
99
|
if (!family.test(entry)) continue;
|
|
448
100
|
const path = join(folder, entry);
|
|
449
|
-
rmSync(path, {
|
|
101
|
+
rmSync(path, {
|
|
102
|
+
recursive: true,
|
|
103
|
+
force: true,
|
|
104
|
+
maxRetries: 3,
|
|
105
|
+
retryDelay: 50,
|
|
106
|
+
});
|
|
450
107
|
removed.push(path);
|
|
451
108
|
}
|
|
452
|
-
if (readdirSync(folder).length === 0) {
|
|
109
|
+
if (readdirSync(folder).length === 0) {
|
|
110
|
+
rmdirSync(folder);
|
|
111
|
+
removed.push(folder);
|
|
112
|
+
}
|
|
453
113
|
} catch {
|
|
454
114
|
// Best-effort housekeeping; the next sweep retries.
|
|
455
115
|
}
|
|
@@ -458,67 +118,3 @@ export function createTelegramSessionFolderSweeper(deps: TelegramSessionFolderSw
|
|
|
458
118
|
},
|
|
459
119
|
};
|
|
460
120
|
}
|
|
461
|
-
|
|
462
|
-
/** Build the `/telegram-connect` recovery boundary around runtime artifacts. */
|
|
463
|
-
export function createTelegramPollingStartRecoveryHandler(
|
|
464
|
-
deps: TelegramPollingStartRecoveryHandlerDeps,
|
|
465
|
-
): () => Promise<TelegramPollingStartRecoveryDecision> {
|
|
466
|
-
return async () => {
|
|
467
|
-
const ownersPath = deps.getOwnersPath();
|
|
468
|
-
const statePaths = deps.getStatePaths();
|
|
469
|
-
const classification = classifyTelegramRuntimeRecovery({
|
|
470
|
-
ownersPath,
|
|
471
|
-
statePaths,
|
|
472
|
-
});
|
|
473
|
-
if (classification.kind === "clean") return { kind: "unhandled" };
|
|
474
|
-
if (classification.kind === "blocked-live-owner") {
|
|
475
|
-
return {
|
|
476
|
-
kind: "blocked",
|
|
477
|
-
message: `Telegram temporary state is damaged, but owner process ${classification.livePids.join(", ")} is still live. Restart that Pi instance, then run /telegram-connect again.`,
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
try {
|
|
482
|
-
await deps.suspendPolling();
|
|
483
|
-
} catch (error) {
|
|
484
|
-
deps.recordRuntimeEvent?.("recovery", error, {
|
|
485
|
-
phase: "suspend-before-reset",
|
|
486
|
-
});
|
|
487
|
-
return {
|
|
488
|
-
kind: "blocked",
|
|
489
|
-
message:
|
|
490
|
-
"Telegram polling could not stop safely, so temporary state was not reset. Restart this Pi instance and run /telegram-connect again.",
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
try {
|
|
494
|
-
await deps.releaseOwnership?.();
|
|
495
|
-
} catch (error) {
|
|
496
|
-
deps.recordRuntimeEvent?.("recovery", error, {
|
|
497
|
-
phase: "release-before-reset",
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
try {
|
|
501
|
-
const recovery = recoverTelegramRuntimeState({ ownersPath, statePaths });
|
|
502
|
-
if (recovery.kind === "blocked-live-owner") {
|
|
503
|
-
return {
|
|
504
|
-
kind: "blocked",
|
|
505
|
-
message: `Telegram temporary state changed during recovery and is now protected by owner process ${recovery.livePids.join(", ")}. Restart that Pi instance, then run /telegram-connect again.`,
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
return {
|
|
509
|
-
kind: "retry",
|
|
510
|
-
message:
|
|
511
|
-
recovery.kind === "recovered"
|
|
512
|
-
? "Telegram temporary state was damaged after an unclean shutdown and has been reset."
|
|
513
|
-
: "Telegram temporary state was recovered by another Pi instance.",
|
|
514
|
-
};
|
|
515
|
-
} catch (error) {
|
|
516
|
-
deps.recordRuntimeEvent?.("recovery", error, { phase: "runtime-reset" });
|
|
517
|
-
return {
|
|
518
|
-
kind: "blocked",
|
|
519
|
-
message:
|
|
520
|
-
"Telegram temporary-state recovery failed. Restart this Pi instance and run /telegram-connect again.",
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
};
|
|
524
|
-
}
|
package/lib/rendering.ts
CHANGED
|
@@ -13,8 +13,6 @@ const TELEGRAM_TABLE_GRAPHEME_SEGMENTER =
|
|
|
13
13
|
const TELEGRAM_TABLE_EMOJI_GRAPHEME_PATTERN =
|
|
14
14
|
/\p{Extended_Pictographic}|\p{Emoji_Presentation}|\p{Regional_Indicator}/u;
|
|
15
15
|
|
|
16
|
-
// --- HTML Helpers ---
|
|
17
|
-
|
|
18
16
|
interface OpenHtmlTag {
|
|
19
17
|
name: string;
|
|
20
18
|
openTag: string;
|
|
@@ -333,7 +331,7 @@ function replaceMarkdownLink(
|
|
|
333
331
|
},
|
|
334
332
|
): string {
|
|
335
333
|
let result = "";
|
|
336
|
-
for (let index = 0; index < text.length;
|
|
334
|
+
for (let index = 0; index < text.length;) {
|
|
337
335
|
const inlineLink = parseMarkdownInlineLinkAt(text, index);
|
|
338
336
|
if (inlineLink) {
|
|
339
337
|
result += options.renderInlineLink(
|