@llblab/pi-telegram 0.45.11 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BACKLOG.md +5 -1
- package/CHANGELOG.md +12 -0
- package/README.md +6 -6
- package/dist/api/activity.d.ts +6 -0
- package/dist/api/activity.js +6 -0
- package/dist/api/commands.d.ts +6 -0
- package/dist/api/commands.js +6 -0
- package/dist/api/delivery.d.ts +6 -0
- package/dist/api/delivery.js +6 -0
- package/dist/api/inbound.d.ts +6 -0
- package/dist/api/inbound.js +6 -0
- package/dist/api/keyboard.d.ts +6 -0
- package/dist/api/keyboard.js +6 -0
- package/dist/api/outbound.d.ts +6 -0
- package/dist/api/outbound.js +6 -0
- package/dist/api/sections.d.ts +7 -0
- package/dist/api/sections.js +6 -0
- package/dist/api/status.d.ts +6 -0
- package/dist/api/status.js +6 -0
- package/dist/api/updates.d.ts +6 -0
- package/dist/api/updates.js +6 -0
- package/dist/api/voice.d.ts +6 -0
- package/dist/api/voice.js +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/activity-verbosity.d.ts +52 -0
- package/dist/lib/activity-verbosity.js +596 -0
- package/dist/lib/activity.d.ts +220 -0
- package/dist/lib/activity.js +574 -0
- package/dist/lib/agent-messages.d.ts +28 -0
- package/dist/lib/agent-messages.js +86 -0
- package/dist/lib/bindings.d.ts +250 -0
- package/dist/lib/bindings.js +936 -0
- package/dist/lib/bus-api.d.ts +18 -0
- package/dist/lib/bus-api.js +254 -0
- package/dist/lib/bus-follower.d.ts +464 -0
- package/dist/lib/bus-follower.js +1686 -0
- package/dist/lib/bus-leader.d.ts +292 -0
- package/dist/lib/bus-leader.js +2242 -0
- package/dist/lib/bus-transport.d.ts +64 -0
- package/dist/lib/bus-transport.js +140 -0
- package/dist/lib/bus.d.ts +518 -0
- package/dist/lib/bus.js +2055 -0
- package/dist/lib/channel-posts.d.ts +170 -0
- package/dist/lib/channel-posts.js +611 -0
- package/dist/lib/command-templates.d.ts +70 -0
- package/dist/lib/command-templates.js +744 -0
- package/dist/lib/commands.d.ts +541 -0
- package/dist/lib/commands.js +1155 -0
- package/dist/lib/config.d.ts +252 -0
- package/dist/lib/config.js +889 -0
- package/dist/lib/delivery.d.ts +163 -0
- package/dist/lib/delivery.js +542 -0
- package/dist/lib/extension.d.ts +7 -0
- package/dist/lib/extension.js +1608 -0
- package/dist/lib/generative-app-worker.mjs +104 -0
- package/dist/lib/generative-apps.d.ts +212 -0
- package/dist/lib/generative-apps.js +1006 -0
- package/dist/lib/inbound.d.ts +91 -0
- package/dist/lib/inbound.js +502 -0
- package/dist/lib/journal.d.ts +665 -0
- package/dist/lib/journal.js +3701 -0
- package/dist/lib/keyboard.d.ts +23 -0
- package/dist/lib/keyboard.js +37 -0
- package/dist/lib/lifecycle.d.ts +157 -0
- package/dist/lib/lifecycle.js +395 -0
- package/dist/lib/locks.d.ts +164 -0
- package/dist/lib/locks.js +1208 -0
- package/dist/lib/logging.d.ts +50 -0
- package/dist/lib/logging.js +274 -0
- package/dist/lib/media.d.ts +181 -0
- package/dist/lib/media.js +602 -0
- package/dist/lib/menu-model.d.ts +217 -0
- package/dist/lib/menu-model.js +663 -0
- package/dist/lib/menu-queue.d.ts +37 -0
- package/dist/lib/menu-queue.js +408 -0
- package/dist/lib/menu-settings.d.ts +116 -0
- package/dist/lib/menu-settings.js +595 -0
- package/dist/lib/menu-status.d.ts +32 -0
- package/dist/lib/menu-status.js +112 -0
- package/dist/lib/menu-thinking.d.ts +29 -0
- package/dist/lib/menu-thinking.js +82 -0
- package/dist/lib/menu.d.ts +171 -0
- package/dist/lib/menu.js +323 -0
- package/dist/lib/model.d.ts +127 -0
- package/dist/lib/model.js +409 -0
- package/dist/lib/outbound-attachments.d.ts +241 -0
- package/dist/lib/outbound-attachments.js +639 -0
- package/dist/lib/outbound-buttons.d.ts +69 -0
- package/dist/lib/outbound-buttons.js +248 -0
- package/dist/lib/outbound-markup.d.ts +42 -0
- package/dist/lib/outbound-markup.js +678 -0
- package/dist/lib/outbound-voice.d.ts +55 -0
- package/dist/lib/outbound-voice.js +152 -0
- package/dist/lib/outbound.d.ts +185 -0
- package/dist/lib/outbound.js +516 -0
- package/dist/lib/ownership.d.ts +77 -0
- package/dist/lib/ownership.js +174 -0
- package/dist/lib/paths.d.ts +40 -0
- package/dist/lib/paths.js +94 -0
- package/dist/lib/pi.d.ts +72 -0
- package/dist/lib/pi.js +121 -0
- package/dist/lib/polling.d.ts +351 -0
- package/dist/lib/polling.js +1196 -0
- package/dist/lib/preview.d.ts +192 -0
- package/dist/lib/preview.js +614 -0
- package/dist/lib/prompt-templates.d.ts +24 -0
- package/dist/lib/prompt-templates.js +118 -0
- package/dist/lib/prompts.d.ts +57 -0
- package/dist/lib/prompts.js +167 -0
- package/dist/lib/queue.d.ts +766 -0
- package/dist/lib/queue.js +1965 -0
- package/dist/lib/recovery.d.ts +86 -0
- package/dist/lib/recovery.js +285 -0
- package/dist/lib/rendering.d.ts +20 -0
- package/dist/lib/rendering.js +983 -0
- package/dist/lib/replies.d.ts +214 -0
- package/dist/lib/replies.js +737 -0
- package/dist/lib/routing.d.ts +207 -0
- package/dist/lib/routing.js +2282 -0
- package/dist/lib/runtime.d.ts +172 -0
- package/dist/lib/runtime.js +402 -0
- package/dist/lib/sections.d.ts +165 -0
- package/dist/lib/sections.js +327 -0
- package/dist/lib/setup.d.ts +82 -0
- package/dist/lib/setup.js +150 -0
- package/dist/lib/skills.d.ts +8 -0
- package/dist/lib/skills.js +12 -0
- package/dist/lib/status.d.ts +442 -0
- package/dist/lib/status.js +1006 -0
- package/dist/lib/sync.d.ts +179 -0
- package/dist/lib/sync.js +782 -0
- package/dist/lib/target.d.ts +20 -0
- package/dist/lib/target.js +27 -0
- package/dist/lib/telegram-api.d.ts +541 -0
- package/dist/lib/telegram-api.js +1159 -0
- package/dist/lib/text-groups.d.ts +89 -0
- package/dist/lib/text-groups.js +317 -0
- package/dist/lib/thread-cleanup-manager.d.ts +288 -0
- package/dist/lib/thread-cleanup-manager.js +560 -0
- package/dist/lib/thread-display.d.ts +46 -0
- package/dist/lib/thread-display.js +257 -0
- package/dist/lib/thread-naming.d.ts +46 -0
- package/dist/lib/thread-naming.js +78 -0
- package/dist/lib/thread-reconciler.d.ts +239 -0
- package/dist/lib/thread-reconciler.js +644 -0
- package/dist/lib/threads.d.ts +621 -0
- package/dist/lib/threads.js +3679 -0
- package/dist/lib/time-injection.d.ts +15 -0
- package/dist/lib/time-injection.js +56 -0
- package/dist/lib/turns.d.ts +109 -0
- package/dist/lib/turns.js +500 -0
- package/dist/lib/updates.d.ts +1290 -0
- package/dist/lib/updates.js +3634 -0
- package/dist/lib/voice.d.ts +117 -0
- package/dist/lib/voice.js +174 -0
- package/dist/lib/workspace-admission.d.ts +264 -0
- package/dist/lib/workspace-admission.js +1136 -0
- package/dist/lib/workspace-retirement.d.ts +219 -0
- package/dist/lib/workspace-retirement.js +587 -0
- package/dist/lib/workspace-slots.d.ts +30 -0
- package/dist/lib/workspace-slots.js +54 -0
- package/dist/package.json +126 -0
- package/dist/pi-telegram/index.js +1 -0
- package/dist/skills/generated-control-surface/SKILL.md +107 -0
- package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
- package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
- package/dist/skills/generative-apps/SKILL.md +115 -0
- package/dist/skills/show-me/SKILL.md +166 -0
- package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
- package/dist/skills/telegram-bridge/SKILL.md +129 -0
- package/dist/skills/telegram-bridge/references/configuration.md +15 -0
- package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
- package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
- package/docs/architecture.md +1 -1
- package/docs/compact-matrix-literal.md +10 -3
- package/docs/generative-apps.md +15 -13
- package/docs/multi-instance-bus.md +17 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +2 -2
- package/lib/bindings.ts +89 -6
- package/lib/bus-follower.ts +11 -3
- package/lib/bus-leader.ts +57 -20
- package/lib/bus.ts +12 -2
- package/lib/commands.ts +2 -2
- package/lib/config.ts +14 -7
- package/lib/delivery.ts +38 -6
- package/lib/extension.ts +26 -1
- package/lib/generative-apps.ts +379 -13
- package/lib/menu-settings.ts +21 -5
- package/lib/outbound-markup.ts +16 -7
- package/lib/pi.ts +4 -0
- package/lib/sync.ts +3 -0
- package/lib/telegram-api.ts +12 -1
- package/lib/thread-cleanup-manager.ts +36 -6
- package/lib/thread-display.ts +96 -18
- package/lib/threads.ts +150 -30
- package/package.json +56 -13
- package/scripts/build-dist.mjs +44 -0
- package/scripts/measure-bus.mjs +8 -1
- package/scripts/measure-workspace.mjs +8 -1
- package/skills/generative-apps/SKILL.md +1 -1
- package/skills/show-me/SKILL.md +1 -1
- package/skills/telegram-bridge/SKILL.md +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram disposable runtime recovery classification
|
|
3
|
+
* Zones: filesystem diagnostics, unclean-shutdown recovery
|
|
4
|
+
* Owns fail-safe classification of temporary ownership and routing artifacts
|
|
5
|
+
*/
|
|
6
|
+
import { renameSync } from "node:fs";
|
|
7
|
+
import { type TelegramFileTransactionOptions } from "./locks.ts";
|
|
8
|
+
export type TelegramRuntimeArtifactKind = "owners" | "state" | "transaction";
|
|
9
|
+
export interface TelegramRuntimeCorruptArtifact {
|
|
10
|
+
kind: TelegramRuntimeArtifactKind;
|
|
11
|
+
path: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
}
|
|
14
|
+
export type TelegramRuntimeRecoveryClassification = {
|
|
15
|
+
kind: "clean";
|
|
16
|
+
} | {
|
|
17
|
+
kind: "recoverable-corruption";
|
|
18
|
+
artifacts: TelegramRuntimeCorruptArtifact[];
|
|
19
|
+
} | {
|
|
20
|
+
kind: "blocked-live-owner";
|
|
21
|
+
artifacts: TelegramRuntimeCorruptArtifact[];
|
|
22
|
+
livePids: number[];
|
|
23
|
+
};
|
|
24
|
+
export interface TelegramRuntimeRecoveryClassificationOptions {
|
|
25
|
+
ownersPath: string;
|
|
26
|
+
statePaths?: readonly string[];
|
|
27
|
+
transactionPath?: string;
|
|
28
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
29
|
+
nowMs?: number;
|
|
30
|
+
staleHeartbeatMs?: number;
|
|
31
|
+
ignoredTransactionPids?: readonly number[];
|
|
32
|
+
}
|
|
33
|
+
export type TelegramRuntimeRecoveryResult = {
|
|
34
|
+
kind: "not-needed";
|
|
35
|
+
} | {
|
|
36
|
+
kind: "blocked-live-owner";
|
|
37
|
+
livePids: number[];
|
|
38
|
+
quarantineDir?: string;
|
|
39
|
+
} | {
|
|
40
|
+
kind: "recovered";
|
|
41
|
+
artifacts: TelegramRuntimeCorruptArtifact[];
|
|
42
|
+
quarantineDir: string;
|
|
43
|
+
};
|
|
44
|
+
export interface TelegramRuntimeRecoveryOptions extends TelegramRuntimeRecoveryClassificationOptions {
|
|
45
|
+
recoveryTransactionPath?: string;
|
|
46
|
+
quarantineRoot?: string;
|
|
47
|
+
pid?: number;
|
|
48
|
+
getNowMs?: () => number;
|
|
49
|
+
quarantineRename?: typeof renameSync;
|
|
50
|
+
quarantineRenameRetryDelayMs?: number;
|
|
51
|
+
transactionOptions?: TelegramFileTransactionOptions;
|
|
52
|
+
}
|
|
53
|
+
export type TelegramPollingStartRecoveryDecision = {
|
|
54
|
+
kind: "unhandled";
|
|
55
|
+
} | {
|
|
56
|
+
kind: "retry";
|
|
57
|
+
message: string;
|
|
58
|
+
} | {
|
|
59
|
+
kind: "blocked";
|
|
60
|
+
message: string;
|
|
61
|
+
};
|
|
62
|
+
export interface TelegramPollingStartRecoveryHandlerDeps {
|
|
63
|
+
getOwnersPath: () => string;
|
|
64
|
+
getStatePaths: () => readonly string[];
|
|
65
|
+
suspendPolling: () => Promise<unknown>;
|
|
66
|
+
releaseOwnership?: () => unknown | Promise<unknown>;
|
|
67
|
+
recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Classify disposable runtime corruption without mutating any artifact.
|
|
71
|
+
*
|
|
72
|
+
* Corruption remains recoverable only when neither owners.json nor a
|
|
73
|
+
* verifiable transaction marker identifies a process that is still alive.
|
|
74
|
+
*/
|
|
75
|
+
export declare function classifyTelegramRuntimeRecovery(options: TelegramRuntimeRecoveryClassificationOptions): TelegramRuntimeRecoveryClassification;
|
|
76
|
+
/**
|
|
77
|
+
* Quarantine classifier-approved disposable corruption under two guards.
|
|
78
|
+
*
|
|
79
|
+
* A dedicated recovery transaction serializes recoverers. The ownership
|
|
80
|
+
* transaction then prevents a new Telegram owner from appearing between the
|
|
81
|
+
* final classification and mutation. Every artifact is renamed within its
|
|
82
|
+
* filesystem; durable config and diagnostics never enter the candidate set.
|
|
83
|
+
*/
|
|
84
|
+
export declare function recoverTelegramRuntimeState(options: TelegramRuntimeRecoveryOptions): TelegramRuntimeRecoveryResult;
|
|
85
|
+
/** Build the `/telegram-connect` recovery boundary around runtime artifacts. */
|
|
86
|
+
export declare function createTelegramPollingStartRecoveryHandler(deps: TelegramPollingStartRecoveryHandlerDeps): () => Promise<TelegramPollingStartRecoveryDecision>;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram disposable runtime recovery classification
|
|
3
|
+
* Zones: filesystem diagnostics, unclean-shutdown recovery
|
|
4
|
+
* Owns fail-safe classification of temporary ownership and routing artifacts
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, } from "node:fs";
|
|
8
|
+
import { basename, dirname, join } from "node:path";
|
|
9
|
+
import { isProcessAlive as defaultIsProcessAlive, parseTelegramLockEntry, renameTelegramPathWithRetry, TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, withTelegramFileTransaction, } from "./locks.js";
|
|
10
|
+
const OWNER_FILE_PATTERN = /^owner\.([A-Za-z0-9-]+)\.json$/u;
|
|
11
|
+
const RECLAIM_FILE_PATTERN = /^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;
|
|
12
|
+
function corruption(kind, path, reason) {
|
|
13
|
+
return { source: kind, corrupt: { kind, path, reason }, ownerPids: [] };
|
|
14
|
+
}
|
|
15
|
+
function inspectOwners(path, nowMs, staleHeartbeatMs) {
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
return { source: "owners", ownerPids: [] };
|
|
18
|
+
let value;
|
|
19
|
+
try {
|
|
20
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return corruption("owners", path, "owners.json is not valid JSON");
|
|
24
|
+
}
|
|
25
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
26
|
+
return corruption("owners", path, "owners.json is not an object");
|
|
27
|
+
}
|
|
28
|
+
const ownerPids = Object.values(value).flatMap((candidate) => {
|
|
29
|
+
const entry = parseTelegramLockEntry(candidate);
|
|
30
|
+
if (!entry ||
|
|
31
|
+
(typeof entry.heartbeatMs === "number" &&
|
|
32
|
+
nowMs - entry.heartbeatMs > staleHeartbeatMs)) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return [entry.pid];
|
|
36
|
+
});
|
|
37
|
+
return { source: "owners", ownerPids };
|
|
38
|
+
}
|
|
39
|
+
function inspectState(path) {
|
|
40
|
+
if (!existsSync(path))
|
|
41
|
+
return { source: "state", ownerPids: [] };
|
|
42
|
+
try {
|
|
43
|
+
JSON.parse(readFileSync(path, "utf8"));
|
|
44
|
+
return { source: "state", ownerPids: [] };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return corruption("state", path, `${basename(path)} is not valid JSON`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function parseTransactionOwner(path) {
|
|
51
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
52
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const pid = value.pid;
|
|
56
|
+
const acquiredAtMs = value.acquiredAtMs;
|
|
57
|
+
const generation = value.generation;
|
|
58
|
+
return typeof pid === "number" &&
|
|
59
|
+
typeof acquiredAtMs === "number" &&
|
|
60
|
+
typeof generation === "string"
|
|
61
|
+
? pid
|
|
62
|
+
: undefined;
|
|
63
|
+
}
|
|
64
|
+
function inspectTransaction(path) {
|
|
65
|
+
if (!existsSync(path))
|
|
66
|
+
return { source: "transaction", ownerPids: [] };
|
|
67
|
+
try {
|
|
68
|
+
const stat = lstatSync(path);
|
|
69
|
+
let ownerPath;
|
|
70
|
+
let expectedOwnerPid;
|
|
71
|
+
if (stat.isDirectory()) {
|
|
72
|
+
const entries = readdirSync(path);
|
|
73
|
+
if (entries.length !== 1) {
|
|
74
|
+
return corruption("transaction", path, "transaction guard has an unverifiable directory shape");
|
|
75
|
+
}
|
|
76
|
+
const entry = entries[0];
|
|
77
|
+
const ownerMatch = OWNER_FILE_PATTERN.exec(entry);
|
|
78
|
+
const reclaimMatch = RECLAIM_FILE_PATTERN.exec(entry);
|
|
79
|
+
if (!ownerMatch && !reclaimMatch) {
|
|
80
|
+
return corruption("transaction", path, "transaction guard has an unrecognized owner marker");
|
|
81
|
+
}
|
|
82
|
+
ownerPath = join(path, entry);
|
|
83
|
+
expectedOwnerPid = reclaimMatch ? Number(reclaimMatch[1]) : undefined;
|
|
84
|
+
const ownerPid = parseTransactionOwner(ownerPath);
|
|
85
|
+
if (ownerPid === undefined) {
|
|
86
|
+
return corruption("transaction", path, "transaction guard owner metadata is invalid");
|
|
87
|
+
}
|
|
88
|
+
if (ownerMatch) {
|
|
89
|
+
const value = JSON.parse(readFileSync(ownerPath, "utf8"));
|
|
90
|
+
if (value.generation !== ownerMatch[1]) {
|
|
91
|
+
return corruption("transaction", path, "transaction guard generation does not match its owner marker");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
source: "transaction",
|
|
96
|
+
ownerPids: [expectedOwnerPid ?? ownerPid],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (!stat.isFile()) {
|
|
100
|
+
return corruption("transaction", path, "transaction guard has an unsupported filesystem type");
|
|
101
|
+
}
|
|
102
|
+
ownerPath = path;
|
|
103
|
+
const ownerPid = parseTransactionOwner(ownerPath);
|
|
104
|
+
return ownerPid === undefined
|
|
105
|
+
? corruption("transaction", path, "legacy transaction guard owner metadata is invalid")
|
|
106
|
+
: { source: "transaction", ownerPids: [ownerPid] };
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return corruption("transaction", path, "transaction guard cannot be inspected");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Classify disposable runtime corruption without mutating any artifact.
|
|
114
|
+
*
|
|
115
|
+
* Corruption remains recoverable only when neither owners.json nor a
|
|
116
|
+
* verifiable transaction marker identifies a process that is still alive.
|
|
117
|
+
*/
|
|
118
|
+
export function classifyTelegramRuntimeRecovery(options) {
|
|
119
|
+
const transactionPath = options.transactionPath ?? `${options.ownersPath}.transaction`;
|
|
120
|
+
const inspections = [
|
|
121
|
+
inspectOwners(options.ownersPath, options.nowMs ?? Date.now(), options.staleHeartbeatMs ?? TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS),
|
|
122
|
+
...(options.statePaths ?? []).map(inspectState),
|
|
123
|
+
inspectTransaction(transactionPath),
|
|
124
|
+
];
|
|
125
|
+
const artifacts = inspections.flatMap((inspection) => inspection.corrupt ? [inspection.corrupt] : []);
|
|
126
|
+
if (artifacts.length === 0)
|
|
127
|
+
return { kind: "clean" };
|
|
128
|
+
const processAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
129
|
+
const ignoredTransactionPids = new Set(options.ignoredTransactionPids ?? []);
|
|
130
|
+
const livePids = [
|
|
131
|
+
...new Set(inspections.flatMap((inspection) => inspection.ownerPids.filter((pid) => !(inspection.source === "transaction" &&
|
|
132
|
+
ignoredTransactionPids.has(pid)) && processAlive(pid)))),
|
|
133
|
+
].sort((left, right) => left - right);
|
|
134
|
+
return livePids.length > 0
|
|
135
|
+
? { kind: "blocked-live-owner", artifacts, livePids }
|
|
136
|
+
: { kind: "recoverable-corruption", artifacts };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Quarantine classifier-approved disposable corruption under two guards.
|
|
140
|
+
*
|
|
141
|
+
* A dedicated recovery transaction serializes recoverers. The ownership
|
|
142
|
+
* transaction then prevents a new Telegram owner from appearing between the
|
|
143
|
+
* final classification and mutation. Every artifact is renamed within its
|
|
144
|
+
* filesystem; durable config and diagnostics never enter the candidate set.
|
|
145
|
+
*/
|
|
146
|
+
export function recoverTelegramRuntimeState(options) {
|
|
147
|
+
const pid = options.pid ?? process.pid;
|
|
148
|
+
const transactionPath = options.transactionPath ?? `${options.ownersPath}.transaction`;
|
|
149
|
+
const recoveryTransactionPath = options.recoveryTransactionPath ??
|
|
150
|
+
join(dirname(options.ownersPath), "runtime-recovery.transaction");
|
|
151
|
+
const quarantineRoot = options.quarantineRoot ?? join(dirname(options.ownersPath), "recovery");
|
|
152
|
+
const classificationOptions = {
|
|
153
|
+
ownersPath: options.ownersPath,
|
|
154
|
+
statePaths: options.statePaths,
|
|
155
|
+
transactionPath,
|
|
156
|
+
isProcessAlive: options.isProcessAlive,
|
|
157
|
+
nowMs: options.nowMs ?? options.getNowMs?.(),
|
|
158
|
+
staleHeartbeatMs: options.staleHeartbeatMs,
|
|
159
|
+
};
|
|
160
|
+
return withTelegramFileTransaction(recoveryTransactionPath, () => {
|
|
161
|
+
const initial = classifyTelegramRuntimeRecovery(classificationOptions);
|
|
162
|
+
if (initial.kind === "clean")
|
|
163
|
+
return { kind: "not-needed" };
|
|
164
|
+
if (initial.kind === "blocked-live-owner") {
|
|
165
|
+
return {
|
|
166
|
+
kind: "blocked-live-owner",
|
|
167
|
+
livePids: initial.livePids,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
let quarantineDir;
|
|
171
|
+
const recoveredArtifacts = [];
|
|
172
|
+
const ensureQuarantineDir = () => {
|
|
173
|
+
if (quarantineDir)
|
|
174
|
+
return quarantineDir;
|
|
175
|
+
quarantineDir = join(quarantineRoot, `${options.getNowMs?.() ?? Date.now()}-${pid}-${randomUUID()}`);
|
|
176
|
+
mkdirSync(quarantineDir, { recursive: true, mode: 0o700 });
|
|
177
|
+
return quarantineDir;
|
|
178
|
+
};
|
|
179
|
+
const quarantineArtifact = (artifact) => {
|
|
180
|
+
if (!existsSync(artifact.path))
|
|
181
|
+
return;
|
|
182
|
+
const destination = join(ensureQuarantineDir(), basename(artifact.path));
|
|
183
|
+
if (renameTelegramPathWithRetry(artifact.path, destination, {
|
|
184
|
+
rename: options.quarantineRename,
|
|
185
|
+
retryDelayMs: options.quarantineRenameRetryDelayMs,
|
|
186
|
+
})) {
|
|
187
|
+
recoveredArtifacts.push(artifact);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
for (const artifact of initial.artifacts) {
|
|
191
|
+
if (artifact.kind === "transaction")
|
|
192
|
+
quarantineArtifact(artifact);
|
|
193
|
+
}
|
|
194
|
+
return withTelegramFileTransaction(transactionPath, () => {
|
|
195
|
+
const current = classifyTelegramRuntimeRecovery({
|
|
196
|
+
...classificationOptions,
|
|
197
|
+
ignoredTransactionPids: [process.pid],
|
|
198
|
+
});
|
|
199
|
+
if (current.kind === "blocked-live-owner") {
|
|
200
|
+
return {
|
|
201
|
+
kind: "blocked-live-owner",
|
|
202
|
+
livePids: current.livePids,
|
|
203
|
+
quarantineDir,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (current.kind === "recoverable-corruption") {
|
|
207
|
+
for (const artifact of current.artifacts) {
|
|
208
|
+
if (artifact.kind !== "transaction")
|
|
209
|
+
quarantineArtifact(artifact);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return recoveredArtifacts.length > 0 && quarantineDir
|
|
213
|
+
? {
|
|
214
|
+
kind: "recovered",
|
|
215
|
+
artifacts: recoveredArtifacts,
|
|
216
|
+
quarantineDir,
|
|
217
|
+
}
|
|
218
|
+
: { kind: "not-needed" };
|
|
219
|
+
}, options.transactionOptions);
|
|
220
|
+
}, options.transactionOptions);
|
|
221
|
+
}
|
|
222
|
+
/** Build the `/telegram-connect` recovery boundary around runtime artifacts. */
|
|
223
|
+
export function createTelegramPollingStartRecoveryHandler(deps) {
|
|
224
|
+
return async () => {
|
|
225
|
+
const ownersPath = deps.getOwnersPath();
|
|
226
|
+
const statePaths = deps.getStatePaths();
|
|
227
|
+
const classification = classifyTelegramRuntimeRecovery({
|
|
228
|
+
ownersPath,
|
|
229
|
+
statePaths,
|
|
230
|
+
});
|
|
231
|
+
if (classification.kind === "clean")
|
|
232
|
+
return { kind: "unhandled" };
|
|
233
|
+
if (classification.kind === "blocked-live-owner") {
|
|
234
|
+
return {
|
|
235
|
+
kind: "blocked",
|
|
236
|
+
message: `Telegram temporary state is damaged, but owner process ${classification.livePids.join(", ")} is still live. Restart that Pi instance, then run /telegram-connect again.`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
await deps.suspendPolling();
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
deps.recordRuntimeEvent?.("recovery", error, {
|
|
244
|
+
phase: "suspend-before-reset",
|
|
245
|
+
});
|
|
246
|
+
return {
|
|
247
|
+
kind: "blocked",
|
|
248
|
+
message: "Telegram polling could not stop safely, so temporary state was not reset. Restart this Pi instance and run /telegram-connect again.",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
await deps.releaseOwnership?.();
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
deps.recordRuntimeEvent?.("recovery", error, {
|
|
256
|
+
phase: "release-before-reset",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const recovery = recoverTelegramRuntimeState({
|
|
261
|
+
ownersPath,
|
|
262
|
+
statePaths,
|
|
263
|
+
});
|
|
264
|
+
if (recovery.kind === "blocked-live-owner") {
|
|
265
|
+
return {
|
|
266
|
+
kind: "blocked",
|
|
267
|
+
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.`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
kind: "retry",
|
|
272
|
+
message: recovery.kind === "recovered"
|
|
273
|
+
? "Telegram temporary state was damaged after an unclean shutdown and has been reset."
|
|
274
|
+
: "Telegram temporary state was recovered by another Pi instance.",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
deps.recordRuntimeEvent?.("recovery", error, { phase: "runtime-reset" });
|
|
279
|
+
return {
|
|
280
|
+
kind: "blocked",
|
|
281
|
+
message: "Telegram temporary-state recovery failed. Restart this Pi instance and run /telegram-connect again.",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram UI/compat rendering helpers
|
|
3
|
+
* Zones: telegram rendering, shared text utils
|
|
4
|
+
* Converts bridge-owned UI/status/menu/interactive text into Telegram-safe plain text and HTML chunks with chunk-boundary handling
|
|
5
|
+
*/
|
|
6
|
+
export declare const MAX_MESSAGE_LENGTH = 4096;
|
|
7
|
+
export declare function escapeHtml(text: string): string;
|
|
8
|
+
export declare function escapeHtmlAttribute(text: string): string;
|
|
9
|
+
export declare function chunkHtmlPreservingTags(html: string, maxLength: number): string[];
|
|
10
|
+
export declare function renderTelegramInlineMarkdownHtml(text: string, options?: {
|
|
11
|
+
allowLinks?: boolean;
|
|
12
|
+
}): string;
|
|
13
|
+
export type TelegramRenderMode = "plain" | "markdown" | "html";
|
|
14
|
+
export interface TelegramRenderedChunk {
|
|
15
|
+
text: string;
|
|
16
|
+
parseMode?: "HTML";
|
|
17
|
+
}
|
|
18
|
+
export declare function renderTelegramMessage(text: string, options?: {
|
|
19
|
+
mode?: TelegramRenderMode;
|
|
20
|
+
}): TelegramRenderedChunk[];
|