@llblab/pi-telegram 0.52.2 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +11 -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 +61 -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 +249 -126
- 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 +125 -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 +511 -300
- 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/workspace-slots.ts
CHANGED
|
@@ -40,8 +40,13 @@ function isValidSnapshot(
|
|
|
40
40
|
const slots = new Set<string>();
|
|
41
41
|
const keys = new Set<string>();
|
|
42
42
|
for (const binding of bindings) {
|
|
43
|
-
if (
|
|
44
|
-
|
|
43
|
+
if (
|
|
44
|
+
!isSlot(binding.slot) ||
|
|
45
|
+
!binding.bindingKey ||
|
|
46
|
+
slots.has(binding.slot) ||
|
|
47
|
+
keys.has(binding.bindingKey)
|
|
48
|
+
)
|
|
49
|
+
return false;
|
|
45
50
|
slots.add(binding.slot);
|
|
46
51
|
keys.add(binding.bindingKey);
|
|
47
52
|
}
|
|
@@ -54,17 +59,21 @@ function eligibleByInactivity(
|
|
|
54
59
|
nowMs: number,
|
|
55
60
|
): TelegramWorkspaceSlotOccupancy[] {
|
|
56
61
|
const reserved = new Set(reservedSlots);
|
|
57
|
-
return bindings
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
return bindings
|
|
63
|
+
.filter(
|
|
64
|
+
(binding) =>
|
|
65
|
+
binding.protection === "eligible" &&
|
|
66
|
+
!reserved.has(binding.slot) &&
|
|
67
|
+
typeof binding.inactiveSinceMs === "number" &&
|
|
68
|
+
Number.isFinite(binding.inactiveSinceMs) &&
|
|
69
|
+
binding.inactiveSinceMs >= 0 &&
|
|
70
|
+
binding.inactiveSinceMs <= nowMs,
|
|
71
|
+
)
|
|
72
|
+
.sort(
|
|
73
|
+
(left, right) =>
|
|
74
|
+
left.inactiveSinceMs! - right.inactiveSinceMs! ||
|
|
75
|
+
left.slot.charCodeAt(0) - right.slot.charCodeAt(0),
|
|
76
|
+
);
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
/** Caller must recheck exact ownership and protected work before retirement. */
|
package/package.json
CHANGED
|
@@ -1,472 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Conservative pre-downgrade journal authority check.
|
|
5
|
-
* Usage: node scripts/check-downgrade.mjs [agent-dir]
|
|
6
|
-
*
|
|
7
|
-
* This is an intentionally simpler schema validator than `lib/journal.ts`.
|
|
8
|
-
* It only needs to prove whether unresolved journal authority remains, so it
|
|
9
|
-
* errs toward BLOCKED and does not cross-check the rules the runtime owns:
|
|
10
|
-
*
|
|
11
|
-
* - Operator dispositions are not reconciled against surviving entries
|
|
12
|
-
* (`lib/journal.ts` rejects a "discard" disposition whose entry is still
|
|
13
|
-
* present).
|
|
14
|
-
* - `queueOwner` and `failure` are presence-checked, not deep-validated
|
|
15
|
-
* (acquisition shape, retry/terminal derivation, and disposition id
|
|
16
|
-
* generation stay in `lib/journal.ts`).
|
|
17
|
-
*
|
|
18
|
-
* The shared entry rules (keys, state enum, update_id agreement, and
|
|
19
|
-
* queue/failure metadata presence) MUST stay in sync with
|
|
20
|
-
* `lib/journal.ts` `validateJournalEntry`; `tests/journal-downgrade.test.ts`
|
|
21
|
-
* reconciles the two surfaces on those rules.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
lstatSync,
|
|
26
|
-
readFileSync,
|
|
27
|
-
opendirSync,
|
|
28
|
-
} from "node:fs";
|
|
29
|
-
import { homedir } from "node:os";
|
|
30
|
-
import { basename, join, resolve } from "node:path";
|
|
31
|
-
import { decodeTelegramSessionDirectoryName, resolveTelegramTempDir } from "../dist/lib/paths.js";
|
|
32
|
-
|
|
33
|
-
const MAX_DIRECTORY_ENTRIES = 10_000;
|
|
34
|
-
const MAX_FILES = 4_096;
|
|
35
|
-
const MAX_BYTES = 64 * 1024 * 1024;
|
|
36
|
-
let remainingEntries = MAX_DIRECTORY_ENTRIES;
|
|
37
|
-
let remainingFiles = MAX_FILES;
|
|
38
|
-
let remainingBytes = MAX_BYTES;
|
|
39
|
-
const SESSION_SNAPSHOT_PATTERN = /^(?:journal\.[a-f0-9]{16}|inbox)(?:\.[a-zA-Z0-9._-]+)?\.json$/u;
|
|
40
|
-
const SNAPSHOT_PATTERN = /^(?:inbox|follower-inbox-[a-f0-9]{16})(?:\.[a-zA-Z0-9._-]+)?\.json$/u;
|
|
41
|
-
const SEGMENT_NAME_PATTERN = /^\d{16}\.json$/u;
|
|
42
|
-
const AUTHORITY_STATES = new Set(["pending", "retry-wait", "queued", "failed"]);
|
|
43
|
-
const SNAPSHOT_KEYS = new Set([
|
|
44
|
-
"version",
|
|
45
|
-
"revision",
|
|
46
|
-
"profile",
|
|
47
|
-
"botIdentity",
|
|
48
|
-
"entries",
|
|
49
|
-
"operatorDispositions",
|
|
50
|
-
]);
|
|
51
|
-
const SEGMENT_KEYS = new Set([
|
|
52
|
-
"version",
|
|
53
|
-
"revision",
|
|
54
|
-
"previousRevision",
|
|
55
|
-
"profile",
|
|
56
|
-
"botIdentity",
|
|
57
|
-
"upsertedEntries",
|
|
58
|
-
"removedUpdateIds",
|
|
59
|
-
"operatorDispositions",
|
|
60
|
-
]);
|
|
61
|
-
const ENTRY_KEYS = new Set([
|
|
62
|
-
"updateId",
|
|
63
|
-
"update",
|
|
64
|
-
"admittedAtMs",
|
|
65
|
-
"state",
|
|
66
|
-
"queueKind",
|
|
67
|
-
"queueReceiptId",
|
|
68
|
-
"queueOwner",
|
|
69
|
-
"queueHandoff",
|
|
70
|
-
"failure",
|
|
71
|
-
"nextRetryAtMs",
|
|
72
|
-
"terminalAtMs",
|
|
73
|
-
"terminalReason",
|
|
74
|
-
"terminalFailureId",
|
|
75
|
-
]);
|
|
76
|
-
const DISPOSITION_KEYS = new Set([
|
|
77
|
-
"failureId",
|
|
78
|
-
"updateId",
|
|
79
|
-
"action",
|
|
80
|
-
"committedAtMs",
|
|
81
|
-
"attemptCount",
|
|
82
|
-
"failureClass",
|
|
83
|
-
"terminalAtMs",
|
|
84
|
-
"terminalReason",
|
|
85
|
-
]);
|
|
86
|
-
|
|
87
|
-
function resolveAgentDir() {
|
|
88
|
-
if (process.argv[2]) return resolve(process.argv[2]);
|
|
89
|
-
if (process.env.PI_CODING_AGENT_DIR) {
|
|
90
|
-
return resolve(process.env.PI_CODING_AGENT_DIR);
|
|
91
|
-
}
|
|
92
|
-
const executableName = basename(process.execPath).toLowerCase();
|
|
93
|
-
const invokedName = basename(process.argv[1] ?? "").toLowerCase();
|
|
94
|
-
return join(
|
|
95
|
-
homedir(),
|
|
96
|
-
executableName.startsWith("omp") || invokedName.startsWith("omp")
|
|
97
|
-
? ".omp"
|
|
98
|
-
: ".pi",
|
|
99
|
-
"agent",
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function block(message) {
|
|
104
|
-
console.error(`BLOCKED: ${message}`);
|
|
105
|
-
process.exitCode = 1;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function hasOnlyKeys(value, allowed) {
|
|
109
|
-
return Object.keys(value).every((key) => allowed.has(key));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function isRecord(value) {
|
|
113
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function isPositiveInteger(value) {
|
|
117
|
-
return Number.isSafeInteger(value) && value > 0;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function isNonNegativeInteger(value) {
|
|
121
|
-
return Number.isSafeInteger(value) && value >= 0;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function listDirectory(path) {
|
|
125
|
-
const directory = opendirSync(path);
|
|
126
|
-
const names = [];
|
|
127
|
-
try {
|
|
128
|
-
for (;;) {
|
|
129
|
-
const entry = directory.readSync();
|
|
130
|
-
if (!entry) return names.sort();
|
|
131
|
-
if (--remainingEntries < 0) throw new Error("journal directory listing limit exhausted");
|
|
132
|
-
names.push(entry.name);
|
|
133
|
-
}
|
|
134
|
-
} finally { directory.closeSync(); }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function readJson(path, stat) {
|
|
138
|
-
try {
|
|
139
|
-
if (--remainingFiles < 0 || stat.size > remainingBytes)
|
|
140
|
-
throw new Error("journal file/byte inspection limit exhausted");
|
|
141
|
-
remainingBytes -= stat.size;
|
|
142
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
143
|
-
} catch (error) {
|
|
144
|
-
block(
|
|
145
|
-
`cannot verify ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
146
|
-
);
|
|
147
|
-
return undefined;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function validateBotIdentity(value) {
|
|
152
|
-
return (
|
|
153
|
-
isRecord(value) &&
|
|
154
|
-
hasOnlyKeys(value, new Set(["botId", "tokenSha256"])) &&
|
|
155
|
-
(value.botId === undefined || isPositiveInteger(value.botId)) &&
|
|
156
|
-
typeof value.tokenSha256 === "string" &&
|
|
157
|
-
/^[a-f0-9]{64}$/u.test(value.tokenSha256)
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function identitiesMatch(left, right) {
|
|
162
|
-
if (
|
|
163
|
-
left.botId !== undefined &&
|
|
164
|
-
right.botId !== undefined &&
|
|
165
|
-
left.botId !== right.botId
|
|
166
|
-
) {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
return (
|
|
170
|
-
(left.botId !== undefined && left.botId === right.botId) ||
|
|
171
|
-
left.tokenSha256 === right.tokenSha256
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function mergeBotIdentity(stored, current) {
|
|
176
|
-
return {
|
|
177
|
-
...(current.botId !== undefined
|
|
178
|
-
? { botId: current.botId }
|
|
179
|
-
: stored.botId !== undefined
|
|
180
|
-
? { botId: stored.botId }
|
|
181
|
-
: {}),
|
|
182
|
-
tokenSha256: current.tokenSha256,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function validateEntries(value) {
|
|
187
|
-
if (!Array.isArray(value)) return undefined;
|
|
188
|
-
const entries = new Map();
|
|
189
|
-
let previousId = -1;
|
|
190
|
-
for (const entry of value) {
|
|
191
|
-
if (
|
|
192
|
-
!isRecord(entry) ||
|
|
193
|
-
!hasOnlyKeys(entry, ENTRY_KEYS) ||
|
|
194
|
-
!isNonNegativeInteger(entry.updateId) ||
|
|
195
|
-
entry.updateId <= previousId ||
|
|
196
|
-
!AUTHORITY_STATES.has(entry.state) ||
|
|
197
|
-
!isRecord(entry.update) ||
|
|
198
|
-
entry.update.update_id !== entry.updateId ||
|
|
199
|
-
!isNonNegativeInteger(entry.admittedAtMs)
|
|
200
|
-
) {
|
|
201
|
-
return undefined;
|
|
202
|
-
}
|
|
203
|
-
const hasQueueMetadata =
|
|
204
|
-
entry.queueKind !== undefined ||
|
|
205
|
-
entry.queueReceiptId !== undefined ||
|
|
206
|
-
entry.queueOwner !== undefined ||
|
|
207
|
-
entry.queueHandoff !== undefined;
|
|
208
|
-
const hasFailureMetadata =
|
|
209
|
-
entry.failure !== undefined ||
|
|
210
|
-
entry.nextRetryAtMs !== undefined ||
|
|
211
|
-
entry.terminalAtMs !== undefined ||
|
|
212
|
-
entry.terminalReason !== undefined ||
|
|
213
|
-
entry.terminalFailureId !== undefined;
|
|
214
|
-
if (
|
|
215
|
-
(entry.state === "pending" && (hasQueueMetadata || hasFailureMetadata)) ||
|
|
216
|
-
(entry.state === "queued" &&
|
|
217
|
-
((entry.queueKind !== "prompt" && entry.queueKind !== "control") ||
|
|
218
|
-
typeof entry.queueReceiptId !== "string" ||
|
|
219
|
-
entry.queueReceiptId.length === 0 ||
|
|
220
|
-
entry.queueOwner === undefined ||
|
|
221
|
-
hasFailureMetadata)) ||
|
|
222
|
-
((entry.state === "retry-wait" || entry.state === "failed") &&
|
|
223
|
-
(hasQueueMetadata || !isRecord(entry.failure)))
|
|
224
|
-
) {
|
|
225
|
-
return undefined;
|
|
226
|
-
}
|
|
227
|
-
previousId = entry.updateId;
|
|
228
|
-
entries.set(entry.updateId, entry);
|
|
229
|
-
}
|
|
230
|
-
return entries;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function validateOperatorDispositions(value) {
|
|
234
|
-
if (value === undefined) return true;
|
|
235
|
-
if (!Array.isArray(value)) return false;
|
|
236
|
-
const failureIds = new Set();
|
|
237
|
-
for (const disposition of value) {
|
|
238
|
-
if (
|
|
239
|
-
!isRecord(disposition) ||
|
|
240
|
-
!hasOnlyKeys(disposition, DISPOSITION_KEYS) ||
|
|
241
|
-
typeof disposition.failureId !== "string" ||
|
|
242
|
-
disposition.failureId.length === 0 ||
|
|
243
|
-
failureIds.has(disposition.failureId) ||
|
|
244
|
-
!isNonNegativeInteger(disposition.updateId) ||
|
|
245
|
-
(disposition.action !== "retry" && disposition.action !== "discard") ||
|
|
246
|
-
!isNonNegativeInteger(disposition.committedAtMs) ||
|
|
247
|
-
!isPositiveInteger(disposition.attemptCount) ||
|
|
248
|
-
typeof disposition.failureClass !== "string" ||
|
|
249
|
-
disposition.failureClass.length === 0 ||
|
|
250
|
-
!isNonNegativeInteger(disposition.terminalAtMs) ||
|
|
251
|
-
disposition.committedAtMs < disposition.terminalAtMs ||
|
|
252
|
-
typeof disposition.terminalReason !== "string" ||
|
|
253
|
-
disposition.terminalReason.length === 0
|
|
254
|
-
) {
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
failureIds.add(disposition.failureId);
|
|
258
|
-
}
|
|
259
|
-
return true;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function validateSnapshot(value) {
|
|
263
|
-
if (
|
|
264
|
-
!isRecord(value) ||
|
|
265
|
-
!hasOnlyKeys(value, SNAPSHOT_KEYS) ||
|
|
266
|
-
value.version !== 1 ||
|
|
267
|
-
(value.revision !== undefined && !isPositiveInteger(value.revision)) ||
|
|
268
|
-
typeof value.profile !== "string" ||
|
|
269
|
-
value.profile.length === 0 ||
|
|
270
|
-
!validateBotIdentity(value.botIdentity) ||
|
|
271
|
-
!validateOperatorDispositions(value.operatorDispositions)
|
|
272
|
-
) {
|
|
273
|
-
return undefined;
|
|
274
|
-
}
|
|
275
|
-
const entries = validateEntries(value.entries);
|
|
276
|
-
if (!entries) return undefined;
|
|
277
|
-
return {
|
|
278
|
-
revision: value.revision ?? 0,
|
|
279
|
-
profile: value.profile,
|
|
280
|
-
botIdentity: value.botIdentity,
|
|
281
|
-
entries,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function validateSegment(value) {
|
|
286
|
-
if (
|
|
287
|
-
!isRecord(value) ||
|
|
288
|
-
!hasOnlyKeys(value, SEGMENT_KEYS) ||
|
|
289
|
-
value.version !== 1 ||
|
|
290
|
-
!isPositiveInteger(value.revision) ||
|
|
291
|
-
!isNonNegativeInteger(value.previousRevision) ||
|
|
292
|
-
value.previousRevision !== value.revision - 1 ||
|
|
293
|
-
typeof value.profile !== "string" ||
|
|
294
|
-
value.profile.length === 0 ||
|
|
295
|
-
!validateBotIdentity(value.botIdentity) ||
|
|
296
|
-
!Array.isArray(value.upsertedEntries) ||
|
|
297
|
-
!Array.isArray(value.removedUpdateIds) ||
|
|
298
|
-
!validateOperatorDispositions(value.operatorDispositions)
|
|
299
|
-
) {
|
|
300
|
-
return undefined;
|
|
301
|
-
}
|
|
302
|
-
const upsertedEntries = validateEntries(
|
|
303
|
-
[...value.upsertedEntries].sort((left, right) =>
|
|
304
|
-
Number(left?.updateId) - Number(right?.updateId),
|
|
305
|
-
),
|
|
306
|
-
);
|
|
307
|
-
if (!upsertedEntries || upsertedEntries.size !== value.upsertedEntries.length) {
|
|
308
|
-
return undefined;
|
|
309
|
-
}
|
|
310
|
-
const removedUpdateIds = new Set();
|
|
311
|
-
for (const updateId of value.removedUpdateIds) {
|
|
312
|
-
if (
|
|
313
|
-
!isNonNegativeInteger(updateId) ||
|
|
314
|
-
removedUpdateIds.has(updateId) ||
|
|
315
|
-
upsertedEntries.has(updateId)
|
|
316
|
-
) {
|
|
317
|
-
return undefined;
|
|
318
|
-
}
|
|
319
|
-
removedUpdateIds.add(updateId);
|
|
320
|
-
}
|
|
321
|
-
return {
|
|
322
|
-
revision: value.revision,
|
|
323
|
-
previousRevision: value.previousRevision,
|
|
324
|
-
profile: value.profile,
|
|
325
|
-
botIdentity: value.botIdentity,
|
|
326
|
-
upsertedEntries,
|
|
327
|
-
removedUpdateIds,
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const agentDir = resolveAgentDir();
|
|
332
|
-
const runtimeDir = resolveTelegramTempDir(agentDir);
|
|
333
|
-
let runtimeNames;
|
|
334
|
-
let observedRuntime = false;
|
|
335
|
-
try {
|
|
336
|
-
if (!lstatSync(runtimeDir).isDirectory()) throw new Error("non-directory Telegram runtime or link");
|
|
337
|
-
observedRuntime = true;
|
|
338
|
-
runtimeNames = listDirectory(runtimeDir);
|
|
339
|
-
} catch (error) {
|
|
340
|
-
if (error?.code === "ENOENT" && !observedRuntime) {
|
|
341
|
-
console.log("SAFE: no Telegram runtime directory exists.");
|
|
342
|
-
process.exit(0);
|
|
343
|
-
}
|
|
344
|
-
block(`cannot verify Telegram runtime directory ${runtimeDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
345
|
-
process.exit(1);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function collectSnapshots(directory, names, pattern, strict = false) {
|
|
349
|
-
const snapshots = names.filter(name => pattern.test(name));
|
|
350
|
-
const snapshotSet = new Set(snapshots);
|
|
351
|
-
for (const name of names) {
|
|
352
|
-
// Legacy recovery folders hold no journal authority; current releases delete them.
|
|
353
|
-
if (name.toLowerCase() === "recovery") continue;
|
|
354
|
-
const snapshotName = name.endsWith(".segments") ? name.slice(0, -".segments".length) : undefined;
|
|
355
|
-
if (snapshotName && pattern.test(snapshotName)) {
|
|
356
|
-
if (!snapshotSet.has(snapshotName)) block(`cannot verify orphan journal segments without ${join(directory, snapshotName)}`);
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
if (!pattern.test(name) && (strict || /^(?:inbox|follower-inbox|journal\.)/iu.test(name)))
|
|
360
|
-
block(`cannot verify unrecognized journal-like artifact ${join(directory, name)}`);
|
|
361
|
-
}
|
|
362
|
-
return snapshots.map(name => join(directory, name));
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const snapshotPaths = collectSnapshots(runtimeDir, runtimeNames, SNAPSHOT_PATTERN);
|
|
366
|
-
for (const name of runtimeNames.filter(name => name.toLowerCase() === "sessions")) {
|
|
367
|
-
const root = join(runtimeDir, name);
|
|
368
|
-
try {
|
|
369
|
-
if (name !== "sessions" || !lstatSync(root).isDirectory()) throw new Error("noncanonical session root or link/type");
|
|
370
|
-
for (const sessionName of listDirectory(root)) {
|
|
371
|
-
const directory = join(root, sessionName);
|
|
372
|
-
if (decodeTelegramSessionDirectoryName(sessionName) === undefined || !lstatSync(directory).isDirectory()) {
|
|
373
|
-
block(`cannot verify session folder ${directory}`);
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
snapshotPaths.push(...collectSnapshots(directory, listDirectory(directory), SESSION_SNAPSHOT_PATTERN, true));
|
|
377
|
-
}
|
|
378
|
-
} catch (error) { block(`cannot verify session journals ${root}: ${String(error)}`); }
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
for (const path of snapshotPaths) {
|
|
382
|
-
let pathStat;
|
|
383
|
-
try {
|
|
384
|
-
pathStat = lstatSync(path);
|
|
385
|
-
} catch (error) {
|
|
386
|
-
block(`cannot verify journal snapshot ${path}: ${String(error)}`);
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
if (!pathStat.isFile()) {
|
|
390
|
-
block(`cannot verify non-file journal snapshot ${path}`);
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
393
|
-
const snapshot = validateSnapshot(readJson(path, pathStat));
|
|
394
|
-
if (!snapshot) {
|
|
395
|
-
block(`cannot verify malformed journal snapshot ${path}`);
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
let revision = snapshot.revision;
|
|
400
|
-
let botIdentity = snapshot.botIdentity;
|
|
401
|
-
const entries = snapshot.entries;
|
|
402
|
-
const segmentDir = `${path}.segments`;
|
|
403
|
-
let segmentDirectoryStat;
|
|
404
|
-
try { segmentDirectoryStat = lstatSync(segmentDir); }
|
|
405
|
-
catch (error) {
|
|
406
|
-
if (error?.code !== "ENOENT") block(`cannot verify journal segments ${segmentDir}: ${String(error)}`);
|
|
407
|
-
}
|
|
408
|
-
if (segmentDirectoryStat) {
|
|
409
|
-
let segmentNames;
|
|
410
|
-
try {
|
|
411
|
-
if (!segmentDirectoryStat.isDirectory()) {
|
|
412
|
-
block(`cannot verify non-directory journal segments ${segmentDir}`);
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
segmentNames = listDirectory(segmentDir);
|
|
416
|
-
} catch (error) {
|
|
417
|
-
block(`cannot verify journal segments ${segmentDir}: ${String(error)}`);
|
|
418
|
-
continue;
|
|
419
|
-
}
|
|
420
|
-
const unrecognized = segmentNames.filter(
|
|
421
|
-
(segmentName) => !SEGMENT_NAME_PATTERN.test(segmentName),
|
|
422
|
-
);
|
|
423
|
-
for (const segmentName of unrecognized) {
|
|
424
|
-
block(`cannot verify unrecognized journal segment ${join(segmentDir, segmentName)}`);
|
|
425
|
-
}
|
|
426
|
-
for (const segmentName of segmentNames.filter((candidate) =>
|
|
427
|
-
SEGMENT_NAME_PATTERN.test(candidate),
|
|
428
|
-
).sort()) {
|
|
429
|
-
const nameRevision = Number(segmentName.slice(0, 16));
|
|
430
|
-
const segmentPath = join(segmentDir, segmentName);
|
|
431
|
-
let segmentStat;
|
|
432
|
-
try {
|
|
433
|
-
segmentStat = lstatSync(segmentPath);
|
|
434
|
-
} catch (error) {
|
|
435
|
-
block(`cannot verify journal segment ${segmentPath}: ${String(error)}`);
|
|
436
|
-
break;
|
|
437
|
-
}
|
|
438
|
-
if (!segmentStat.isFile()) { block(`cannot verify non-file journal segment ${segmentPath}`); break; }
|
|
439
|
-
if (nameRevision <= revision) continue;
|
|
440
|
-
const segment = validateSegment(readJson(segmentPath, segmentStat));
|
|
441
|
-
if (
|
|
442
|
-
!segment ||
|
|
443
|
-
segment.revision !== nameRevision ||
|
|
444
|
-
segment.previousRevision !== revision
|
|
445
|
-
) {
|
|
446
|
-
block(`cannot verify malformed or gapped journal segment ${segmentPath}`);
|
|
447
|
-
break;
|
|
448
|
-
}
|
|
449
|
-
if (
|
|
450
|
-
segment.profile !== snapshot.profile ||
|
|
451
|
-
!identitiesMatch(segment.botIdentity, botIdentity)
|
|
452
|
-
) {
|
|
453
|
-
block(`cannot verify foreign journal segment identity ${segmentPath}`);
|
|
454
|
-
break;
|
|
455
|
-
}
|
|
456
|
-
for (const updateId of segment.removedUpdateIds) entries.delete(updateId);
|
|
457
|
-
for (const [updateId, entry] of segment.upsertedEntries) {
|
|
458
|
-
entries.set(updateId, entry);
|
|
459
|
-
}
|
|
460
|
-
revision = segment.revision;
|
|
461
|
-
botIdentity = mergeBotIdentity(botIdentity, segment.botIdentity);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
if (entries.size > 0) {
|
|
465
|
-
block(
|
|
466
|
-
`${path} retains ${entries.size} unresolved update(s); drain with a compatible runtime before downgrade.`,
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (process.exitCode) process.exit(1);
|
|
472
|
-
console.log(`SAFE: ${snapshotPaths.length} Telegram journal(s) contain no unresolved updates.`);
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Measures isolated Thread-store filesystem work at representative sizes.
|
|
3
|
-
* Usage: node --experimental-strip-types scripts/measure-workspace.mjs
|
|
4
|
-
*
|
|
5
|
-
* This assertion-backed fixture uses and removes its own temporary directory.
|
|
6
|
-
* It never reads configured profiles, calls Telegram, or measures latency.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import assert from "node:assert/strict";
|
|
10
|
-
import fs from "node:fs/promises";
|
|
11
|
-
import { syncBuiltinESMExports } from "node:module";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import { createTelegramTopicTargetStore } from "../lib/threads.ts";
|
|
15
|
-
|
|
16
|
-
const repetitions = 10;
|
|
17
|
-
const original = { readFile: fs.readFile, writeFile: fs.writeFile, mkdir: fs.mkdir, rename: fs.rename,
|
|
18
|
-
parse: JSON.parse, stringify: JSON.stringify };
|
|
19
|
-
let counts;
|
|
20
|
-
const rows = [];
|
|
21
|
-
const root = await fs.mkdtemp(join(tmpdir(), "pi-telegram-measure-workspace-"));
|
|
22
|
-
|
|
23
|
-
async function measure(size, operation, execute, samples = repetitions) {
|
|
24
|
-
counts = { reads: 0, readBytes: 0, writes: 0, writeBytes: 0, mkdirs: 0, renames: 0,
|
|
25
|
-
parses: 0, stringifies: 0 };
|
|
26
|
-
try {
|
|
27
|
-
for (let index = 0; index < samples; index++) await execute(index);
|
|
28
|
-
const result = { size, operation, repetitions: samples, ...counts };
|
|
29
|
-
rows.push(result);
|
|
30
|
-
return result;
|
|
31
|
-
} finally {
|
|
32
|
-
counts = undefined;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
fs.readFile = async (...args) => {
|
|
38
|
-
if (counts) counts.reads++;
|
|
39
|
-
const result = await original.readFile(...args);
|
|
40
|
-
if (counts) counts.readBytes += Buffer.byteLength(result);
|
|
41
|
-
return result;
|
|
42
|
-
};
|
|
43
|
-
fs.writeFile = async (...args) => {
|
|
44
|
-
if (counts) { counts.writes++; counts.writeBytes += Buffer.byteLength(args[1]); }
|
|
45
|
-
return original.writeFile(...args);
|
|
46
|
-
};
|
|
47
|
-
fs.mkdir = async (...args) => {
|
|
48
|
-
if (counts) counts.mkdirs++;
|
|
49
|
-
return original.mkdir(...args);
|
|
50
|
-
};
|
|
51
|
-
fs.rename = async (...args) => {
|
|
52
|
-
if (counts) counts.renames++;
|
|
53
|
-
return original.rename(...args);
|
|
54
|
-
};
|
|
55
|
-
JSON.parse = (...args) => { if (counts) counts.parses++; return original.parse(...args); };
|
|
56
|
-
JSON.stringify = (...args) => { if (counts) counts.stringifies++; return original.stringify(...args); };
|
|
57
|
-
syncBuiltinESMExports();
|
|
58
|
-
|
|
59
|
-
for (const size of [1, 13, 26]) {
|
|
60
|
-
const path = join(root, `${size}.json`);
|
|
61
|
-
const store = createTelegramTopicTargetStore({ path, getNowMs: () => 1000 });
|
|
62
|
-
for (let index = 0; index < size; index++) {
|
|
63
|
-
const instanceId = `fixture-${index}`;
|
|
64
|
-
const identity = store.claimWorkspaceIdentity(`/fixture/${index}`, instanceId);
|
|
65
|
-
assert.ok(identity);
|
|
66
|
-
const target = { chatId: 7, threadId: index + 1 };
|
|
67
|
-
assert.ok(store.upsertWorkspaceBinding({ ...identity, target, updatedAtMs: 1000 }, instanceId));
|
|
68
|
-
store.upsert({ profileKey: `manual:${instanceId}`, instanceId, slot: identity.slot,
|
|
69
|
-
target, status: "active", createdAtMs: 1000, updatedAtMs: 1000 });
|
|
70
|
-
}
|
|
71
|
-
await store.persist();
|
|
72
|
-
assert.equal(new Set(store.listWorkspaceBindings().map((entry) => entry.slot)).size, size);
|
|
73
|
-
await measure(size, "cold-load", async () => {
|
|
74
|
-
const reader = createTelegramTopicTargetStore({ path });
|
|
75
|
-
await reader.load();
|
|
76
|
-
assert.equal(reader.list().length, size);
|
|
77
|
-
assert.equal(reader.listWorkspaceBindings().length, size);
|
|
78
|
-
});
|
|
79
|
-
const lookup = await measure(size, "lookup-last", () => {
|
|
80
|
-
assert.equal(store.getByProfileKey(`manual:fixture-${size - 1}`)?.target.threadId, size);
|
|
81
|
-
});
|
|
82
|
-
assert.equal(lookup.reads + lookup.writes, 0);
|
|
83
|
-
const beforeReloadSave = JSON.parse(await fs.readFile(path, "utf8"));
|
|
84
|
-
const reloaded = await measure(size, "first-persist-after-reload", () => store.persist(), 1);
|
|
85
|
-
assert.equal(reloaded.writes + reloaded.renames + reloaded.mkdirs, 0);
|
|
86
|
-
assert.deepEqual(JSON.parse(await fs.readFile(path, "utf8")), beforeReloadSave,
|
|
87
|
-
"The first reloaded save must preserve the seeded semantic state");
|
|
88
|
-
const unchanged = await measure(size, "unchanged-persist", () => store.persist());
|
|
89
|
-
assert.equal(unchanged.writes + unchanged.renames + unchanged.mkdirs, 0);
|
|
90
|
-
assert.ok(unchanged.reads >= repetitions, "No-op saves still consult disk authority");
|
|
91
|
-
const changed = await measure(size, "diagnostic-persist", async (index) => {
|
|
92
|
-
store.setStatusSnapshot({ diagnostics: { measurementStep: index } });
|
|
93
|
-
await store.persist();
|
|
94
|
-
});
|
|
95
|
-
assert.equal(changed.writes, repetitions);
|
|
96
|
-
assert.equal(changed.renames, repetitions);
|
|
97
|
-
}
|
|
98
|
-
} finally {
|
|
99
|
-
Object.assign(fs, { readFile: original.readFile, writeFile: original.writeFile,
|
|
100
|
-
mkdir: original.mkdir, rename: original.rename });
|
|
101
|
-
JSON.parse = original.parse;
|
|
102
|
-
JSON.stringify = original.stringify;
|
|
103
|
-
syncBuiltinESMExports();
|
|
104
|
-
await fs.rm(root, { recursive: true, force: true });
|
|
105
|
-
}
|
|
106
|
-
console.log(JSON.stringify({ scope: "Isolated Thread store, not IPC/admission/Telegram or throughput",
|
|
107
|
-
counters: "Aggregate API calls and bytes for each row's repetitions; object-spread clones are not instrumented",
|
|
108
|
-
rows }, null, 2));
|