@llblab/pi-telegram 0.27.12 → 0.29.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 +152 -258
- package/BACKLOG.md +1 -169
- package/CHANGELOG.md +398 -443
- package/README.md +10 -7
- package/api/updates.ts +5 -0
- package/docs/architecture.md +73 -28
- package/docs/multi-instance-bus.md +23 -5
- package/docs/outbound.md +2 -2
- package/docs/public-api.md +8 -7
- package/docs/ui-style.md +6 -6
- package/docs/updates.md +29 -11
- package/index.ts +358 -246
- package/lib/activity-verbosity.ts +26 -0
- package/lib/bindings.ts +240 -5
- package/lib/bus-follower.ts +436 -238
- package/lib/bus-leader.ts +395 -42
- package/lib/bus.ts +994 -153
- package/lib/commands.ts +184 -30
- package/lib/config.ts +23 -2
- package/lib/journal.ts +3140 -0
- package/lib/lifecycle.ts +4 -0
- package/lib/locks.ts +4 -2
- package/lib/media.ts +71 -32
- package/lib/menu-queue.ts +31 -17
- package/lib/menu.ts +5 -3
- package/lib/model.ts +51 -24
- package/lib/ownership.ts +42 -7
- package/lib/paths.ts +35 -0
- package/lib/polling.ts +591 -106
- package/lib/prompts.ts +20 -89
- package/lib/queue.ts +732 -143
- package/lib/routing.ts +291 -64
- package/lib/runtime.ts +26 -10
- package/lib/skills.ts +21 -0
- package/lib/status.ts +257 -18
- package/lib/sync.ts +131 -5
- package/lib/telegram-api.ts +41 -11
- package/lib/text-groups.ts +75 -35
- package/lib/threads.ts +112 -4
- package/lib/turns.ts +79 -14
- package/lib/updates.ts +3771 -223
- package/package.json +7 -3
- package/scripts/check-downgrade.mjs +435 -0
- package/skills/button-console/SKILL.md +139 -0
- package/skills/telegram-bridge/SKILL.md +138 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"node": ">=22.19.0"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
|
-
"test": "node --experimental-strip-types --test --test-reporter=dot tests/*.test.ts",
|
|
30
|
+
"test": "node --experimental-strip-types --test --test-concurrency=4 --test-reporter=dot tests/*.test.ts",
|
|
31
31
|
"test:verbose": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
33
|
"audit": "npm audit --omit=peer",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"CHANGELOG.md",
|
|
46
46
|
"docs/",
|
|
47
47
|
"scripts/",
|
|
48
|
+
"skills/",
|
|
48
49
|
"screenshot.png"
|
|
49
50
|
],
|
|
50
51
|
"exports": {
|
|
@@ -64,7 +65,10 @@
|
|
|
64
65
|
"extensions": [
|
|
65
66
|
"./index.ts"
|
|
66
67
|
],
|
|
67
|
-
"
|
|
68
|
+
"skills": [
|
|
69
|
+
"./skills"
|
|
70
|
+
],
|
|
71
|
+
"image": "https://raw.githubusercontent.com/llblab/pi-telegram/main/screenshot.png"
|
|
68
72
|
},
|
|
69
73
|
"peerDependencies": {
|
|
70
74
|
"@earendil-works/pi-agent-core": ">=0.80.6",
|
|
@@ -0,0 +1,435 @@
|
|
|
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
|
+
existsSync,
|
|
26
|
+
lstatSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
readdirSync,
|
|
29
|
+
} from "node:fs";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
import { basename, join, resolve } from "node:path";
|
|
32
|
+
|
|
33
|
+
const SNAPSHOT_PATTERN = /^(?:inbox|follower-inbox-[a-f0-9]{16})(?:\.[a-zA-Z0-9._-]+)?\.json$/u;
|
|
34
|
+
const SEGMENT_NAME_PATTERN = /^\d{16}\.json$/u;
|
|
35
|
+
const AUTHORITY_STATES = new Set(["pending", "retry-wait", "queued", "failed"]);
|
|
36
|
+
const SNAPSHOT_KEYS = new Set([
|
|
37
|
+
"version",
|
|
38
|
+
"revision",
|
|
39
|
+
"profile",
|
|
40
|
+
"botIdentity",
|
|
41
|
+
"entries",
|
|
42
|
+
"operatorDispositions",
|
|
43
|
+
]);
|
|
44
|
+
const SEGMENT_KEYS = new Set([
|
|
45
|
+
"version",
|
|
46
|
+
"revision",
|
|
47
|
+
"previousRevision",
|
|
48
|
+
"profile",
|
|
49
|
+
"botIdentity",
|
|
50
|
+
"upsertedEntries",
|
|
51
|
+
"removedUpdateIds",
|
|
52
|
+
"operatorDispositions",
|
|
53
|
+
]);
|
|
54
|
+
const ENTRY_KEYS = new Set([
|
|
55
|
+
"updateId",
|
|
56
|
+
"update",
|
|
57
|
+
"admittedAtMs",
|
|
58
|
+
"state",
|
|
59
|
+
"queueKind",
|
|
60
|
+
"queueReceiptId",
|
|
61
|
+
"queueOwner",
|
|
62
|
+
"queueHandoff",
|
|
63
|
+
"failure",
|
|
64
|
+
"nextRetryAtMs",
|
|
65
|
+
"terminalAtMs",
|
|
66
|
+
"terminalReason",
|
|
67
|
+
"terminalFailureId",
|
|
68
|
+
]);
|
|
69
|
+
const DISPOSITION_KEYS = new Set([
|
|
70
|
+
"failureId",
|
|
71
|
+
"updateId",
|
|
72
|
+
"action",
|
|
73
|
+
"committedAtMs",
|
|
74
|
+
"attemptCount",
|
|
75
|
+
"failureClass",
|
|
76
|
+
"terminalAtMs",
|
|
77
|
+
"terminalReason",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
function resolveAgentDir() {
|
|
81
|
+
if (process.argv[2]) return resolve(process.argv[2]);
|
|
82
|
+
if (process.env.PI_CODING_AGENT_DIR) {
|
|
83
|
+
return resolve(process.env.PI_CODING_AGENT_DIR);
|
|
84
|
+
}
|
|
85
|
+
const executableName = basename(process.execPath).toLowerCase();
|
|
86
|
+
const invokedName = basename(process.argv[1] ?? "").toLowerCase();
|
|
87
|
+
return join(
|
|
88
|
+
homedir(),
|
|
89
|
+
executableName.startsWith("omp") || invokedName.startsWith("omp")
|
|
90
|
+
? ".omp"
|
|
91
|
+
: ".pi",
|
|
92
|
+
"agent",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function block(message) {
|
|
97
|
+
console.error(`BLOCKED: ${message}`);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function hasOnlyKeys(value, allowed) {
|
|
102
|
+
return Object.keys(value).every((key) => allowed.has(key));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isRecord(value) {
|
|
106
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isPositiveInteger(value) {
|
|
110
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isNonNegativeInteger(value) {
|
|
114
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readJson(path) {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
block(
|
|
122
|
+
`cannot verify ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
123
|
+
);
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validateBotIdentity(value) {
|
|
129
|
+
return (
|
|
130
|
+
isRecord(value) &&
|
|
131
|
+
hasOnlyKeys(value, new Set(["botId", "tokenSha256"])) &&
|
|
132
|
+
(value.botId === undefined || isPositiveInteger(value.botId)) &&
|
|
133
|
+
typeof value.tokenSha256 === "string" &&
|
|
134
|
+
/^[a-f0-9]{64}$/u.test(value.tokenSha256)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function identitiesMatch(left, right) {
|
|
139
|
+
if (
|
|
140
|
+
left.botId !== undefined &&
|
|
141
|
+
right.botId !== undefined &&
|
|
142
|
+
left.botId !== right.botId
|
|
143
|
+
) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return (
|
|
147
|
+
(left.botId !== undefined && left.botId === right.botId) ||
|
|
148
|
+
left.tokenSha256 === right.tokenSha256
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function mergeBotIdentity(stored, current) {
|
|
153
|
+
return {
|
|
154
|
+
...(current.botId !== undefined
|
|
155
|
+
? { botId: current.botId }
|
|
156
|
+
: stored.botId !== undefined
|
|
157
|
+
? { botId: stored.botId }
|
|
158
|
+
: {}),
|
|
159
|
+
tokenSha256: current.tokenSha256,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function validateEntries(value) {
|
|
164
|
+
if (!Array.isArray(value)) return undefined;
|
|
165
|
+
const entries = new Map();
|
|
166
|
+
let previousId = -1;
|
|
167
|
+
for (const entry of value) {
|
|
168
|
+
if (
|
|
169
|
+
!isRecord(entry) ||
|
|
170
|
+
!hasOnlyKeys(entry, ENTRY_KEYS) ||
|
|
171
|
+
!isNonNegativeInteger(entry.updateId) ||
|
|
172
|
+
entry.updateId <= previousId ||
|
|
173
|
+
!AUTHORITY_STATES.has(entry.state) ||
|
|
174
|
+
!isRecord(entry.update) ||
|
|
175
|
+
entry.update.update_id !== entry.updateId ||
|
|
176
|
+
!isNonNegativeInteger(entry.admittedAtMs)
|
|
177
|
+
) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
const hasQueueMetadata =
|
|
181
|
+
entry.queueKind !== undefined ||
|
|
182
|
+
entry.queueReceiptId !== undefined ||
|
|
183
|
+
entry.queueOwner !== undefined ||
|
|
184
|
+
entry.queueHandoff !== undefined;
|
|
185
|
+
const hasFailureMetadata =
|
|
186
|
+
entry.failure !== undefined ||
|
|
187
|
+
entry.nextRetryAtMs !== undefined ||
|
|
188
|
+
entry.terminalAtMs !== undefined ||
|
|
189
|
+
entry.terminalReason !== undefined ||
|
|
190
|
+
entry.terminalFailureId !== undefined;
|
|
191
|
+
if (
|
|
192
|
+
(entry.state === "pending" && (hasQueueMetadata || hasFailureMetadata)) ||
|
|
193
|
+
(entry.state === "queued" &&
|
|
194
|
+
((entry.queueKind !== "prompt" && entry.queueKind !== "control") ||
|
|
195
|
+
typeof entry.queueReceiptId !== "string" ||
|
|
196
|
+
entry.queueReceiptId.length === 0 ||
|
|
197
|
+
entry.queueOwner === undefined ||
|
|
198
|
+
hasFailureMetadata)) ||
|
|
199
|
+
((entry.state === "retry-wait" || entry.state === "failed") &&
|
|
200
|
+
(hasQueueMetadata || !isRecord(entry.failure)))
|
|
201
|
+
) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
previousId = entry.updateId;
|
|
205
|
+
entries.set(entry.updateId, entry);
|
|
206
|
+
}
|
|
207
|
+
return entries;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function validateOperatorDispositions(value) {
|
|
211
|
+
if (value === undefined) return true;
|
|
212
|
+
if (!Array.isArray(value)) return false;
|
|
213
|
+
const failureIds = new Set();
|
|
214
|
+
for (const disposition of value) {
|
|
215
|
+
if (
|
|
216
|
+
!isRecord(disposition) ||
|
|
217
|
+
!hasOnlyKeys(disposition, DISPOSITION_KEYS) ||
|
|
218
|
+
typeof disposition.failureId !== "string" ||
|
|
219
|
+
disposition.failureId.length === 0 ||
|
|
220
|
+
failureIds.has(disposition.failureId) ||
|
|
221
|
+
!isNonNegativeInteger(disposition.updateId) ||
|
|
222
|
+
(disposition.action !== "retry" && disposition.action !== "discard") ||
|
|
223
|
+
!isNonNegativeInteger(disposition.committedAtMs) ||
|
|
224
|
+
!isPositiveInteger(disposition.attemptCount) ||
|
|
225
|
+
typeof disposition.failureClass !== "string" ||
|
|
226
|
+
disposition.failureClass.length === 0 ||
|
|
227
|
+
!isNonNegativeInteger(disposition.terminalAtMs) ||
|
|
228
|
+
disposition.committedAtMs < disposition.terminalAtMs ||
|
|
229
|
+
typeof disposition.terminalReason !== "string" ||
|
|
230
|
+
disposition.terminalReason.length === 0
|
|
231
|
+
) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
failureIds.add(disposition.failureId);
|
|
235
|
+
}
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function validateSnapshot(value) {
|
|
240
|
+
if (
|
|
241
|
+
!isRecord(value) ||
|
|
242
|
+
!hasOnlyKeys(value, SNAPSHOT_KEYS) ||
|
|
243
|
+
value.version !== 1 ||
|
|
244
|
+
(value.revision !== undefined && !isPositiveInteger(value.revision)) ||
|
|
245
|
+
typeof value.profile !== "string" ||
|
|
246
|
+
value.profile.length === 0 ||
|
|
247
|
+
!validateBotIdentity(value.botIdentity) ||
|
|
248
|
+
!validateOperatorDispositions(value.operatorDispositions)
|
|
249
|
+
) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
const entries = validateEntries(value.entries);
|
|
253
|
+
if (!entries) return undefined;
|
|
254
|
+
return {
|
|
255
|
+
revision: value.revision ?? 0,
|
|
256
|
+
profile: value.profile,
|
|
257
|
+
botIdentity: value.botIdentity,
|
|
258
|
+
entries,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function validateSegment(value) {
|
|
263
|
+
if (
|
|
264
|
+
!isRecord(value) ||
|
|
265
|
+
!hasOnlyKeys(value, SEGMENT_KEYS) ||
|
|
266
|
+
value.version !== 1 ||
|
|
267
|
+
!isPositiveInteger(value.revision) ||
|
|
268
|
+
!isNonNegativeInteger(value.previousRevision) ||
|
|
269
|
+
value.previousRevision !== value.revision - 1 ||
|
|
270
|
+
typeof value.profile !== "string" ||
|
|
271
|
+
value.profile.length === 0 ||
|
|
272
|
+
!validateBotIdentity(value.botIdentity) ||
|
|
273
|
+
!Array.isArray(value.upsertedEntries) ||
|
|
274
|
+
!Array.isArray(value.removedUpdateIds) ||
|
|
275
|
+
!validateOperatorDispositions(value.operatorDispositions)
|
|
276
|
+
) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
const upsertedEntries = validateEntries(
|
|
280
|
+
[...value.upsertedEntries].sort((left, right) =>
|
|
281
|
+
Number(left?.updateId) - Number(right?.updateId),
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
if (!upsertedEntries || upsertedEntries.size !== value.upsertedEntries.length) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
const removedUpdateIds = new Set();
|
|
288
|
+
for (const updateId of value.removedUpdateIds) {
|
|
289
|
+
if (
|
|
290
|
+
!isNonNegativeInteger(updateId) ||
|
|
291
|
+
removedUpdateIds.has(updateId) ||
|
|
292
|
+
upsertedEntries.has(updateId)
|
|
293
|
+
) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
removedUpdateIds.add(updateId);
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
revision: value.revision,
|
|
300
|
+
previousRevision: value.previousRevision,
|
|
301
|
+
profile: value.profile,
|
|
302
|
+
botIdentity: value.botIdentity,
|
|
303
|
+
upsertedEntries,
|
|
304
|
+
removedUpdateIds,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const agentDir = resolveAgentDir();
|
|
309
|
+
const runtimeDir = join(agentDir, "tmp", "telegram");
|
|
310
|
+
if (!existsSync(runtimeDir)) {
|
|
311
|
+
console.log("SAFE: no Telegram runtime directory exists.");
|
|
312
|
+
process.exit(0);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let runtimeNames;
|
|
316
|
+
try {
|
|
317
|
+
runtimeNames = readdirSync(runtimeDir);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
block(
|
|
320
|
+
`cannot verify Telegram runtime directory ${runtimeDir}: ${error instanceof Error ? error.message : String(error)}`,
|
|
321
|
+
);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const snapshotNames = runtimeNames.filter((name) => SNAPSHOT_PATTERN.test(name));
|
|
326
|
+
const snapshotSet = new Set(snapshotNames);
|
|
327
|
+
for (const name of runtimeNames) {
|
|
328
|
+
const snapshotName = name.endsWith(".segments")
|
|
329
|
+
? name.slice(0, -".segments".length)
|
|
330
|
+
: undefined;
|
|
331
|
+
if (snapshotName && SNAPSHOT_PATTERN.test(snapshotName)) {
|
|
332
|
+
if (!snapshotSet.has(snapshotName)) {
|
|
333
|
+
block(
|
|
334
|
+
`cannot verify orphan journal segments without ${join(runtimeDir, snapshotName)}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (
|
|
340
|
+
(name.startsWith("inbox") || name.startsWith("follower-inbox")) &&
|
|
341
|
+
!SNAPSHOT_PATTERN.test(name)
|
|
342
|
+
) {
|
|
343
|
+
block(`cannot verify unrecognized journal-like artifact ${join(runtimeDir, name)}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const name of snapshotNames) {
|
|
348
|
+
const path = join(runtimeDir, name);
|
|
349
|
+
let pathStat;
|
|
350
|
+
try {
|
|
351
|
+
pathStat = lstatSync(path);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
block(`cannot verify journal snapshot ${path}: ${String(error)}`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (!pathStat.isFile()) {
|
|
357
|
+
block(`cannot verify non-file journal snapshot ${path}`);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const snapshot = validateSnapshot(readJson(path));
|
|
361
|
+
if (!snapshot) {
|
|
362
|
+
block(`cannot verify malformed journal snapshot ${path}`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let revision = snapshot.revision;
|
|
367
|
+
let botIdentity = snapshot.botIdentity;
|
|
368
|
+
const entries = snapshot.entries;
|
|
369
|
+
const segmentDir = `${path}.segments`;
|
|
370
|
+
if (existsSync(segmentDir)) {
|
|
371
|
+
let segmentNames;
|
|
372
|
+
try {
|
|
373
|
+
if (!lstatSync(segmentDir).isDirectory()) {
|
|
374
|
+
block(`cannot verify non-directory journal segments ${segmentDir}`);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
segmentNames = readdirSync(segmentDir);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
block(`cannot verify journal segments ${segmentDir}: ${String(error)}`);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const unrecognized = segmentNames.filter(
|
|
383
|
+
(segmentName) => !SEGMENT_NAME_PATTERN.test(segmentName),
|
|
384
|
+
);
|
|
385
|
+
for (const segmentName of unrecognized) {
|
|
386
|
+
block(`cannot verify unrecognized journal segment ${join(segmentDir, segmentName)}`);
|
|
387
|
+
}
|
|
388
|
+
for (const segmentName of segmentNames.filter((candidate) =>
|
|
389
|
+
SEGMENT_NAME_PATTERN.test(candidate),
|
|
390
|
+
).sort()) {
|
|
391
|
+
const nameRevision = Number(segmentName.slice(0, 16));
|
|
392
|
+
if (nameRevision <= revision) continue;
|
|
393
|
+
const segmentPath = join(segmentDir, segmentName);
|
|
394
|
+
let segmentStat;
|
|
395
|
+
try {
|
|
396
|
+
segmentStat = lstatSync(segmentPath);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
block(`cannot verify journal segment ${segmentPath}: ${String(error)}`);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
const segment = segmentStat.isFile()
|
|
402
|
+
? validateSegment(readJson(segmentPath))
|
|
403
|
+
: undefined;
|
|
404
|
+
if (
|
|
405
|
+
!segment ||
|
|
406
|
+
segment.revision !== nameRevision ||
|
|
407
|
+
segment.previousRevision !== revision
|
|
408
|
+
) {
|
|
409
|
+
block(`cannot verify malformed or gapped journal segment ${segmentPath}`);
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
if (
|
|
413
|
+
segment.profile !== snapshot.profile ||
|
|
414
|
+
!identitiesMatch(segment.botIdentity, botIdentity)
|
|
415
|
+
) {
|
|
416
|
+
block(`cannot verify foreign journal segment identity ${segmentPath}`);
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
for (const updateId of segment.removedUpdateIds) entries.delete(updateId);
|
|
420
|
+
for (const [updateId, entry] of segment.upsertedEntries) {
|
|
421
|
+
entries.set(updateId, entry);
|
|
422
|
+
}
|
|
423
|
+
revision = segment.revision;
|
|
424
|
+
botIdentity = mergeBotIdentity(botIdentity, segment.botIdentity);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (entries.size > 0) {
|
|
428
|
+
block(
|
|
429
|
+
`${path} retains ${entries.size} unresolved update(s); drain with 0.28.x before downgrade.`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (process.exitCode) process.exit(1);
|
|
435
|
+
console.log(`SAFE: ${snapshotNames.length} Telegram journal(s) contain no unresolved updates.`);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: button-console
|
|
3
|
+
description: Turns terminal programs, filesystem navigation, system inspection, and operator workflows into contextual agent-generated button interfaces while preserving full or faithfully adapted console output. Use when a user asks for controls, menus, navigation, actions, or an operating-system/CLI interface through Telegram or another prompt-button transport.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Button Console
|
|
7
|
+
|
|
8
|
+
Build a temporary, truthful button interface over terminal and operating-system capabilities. The agent remains the interpreter and safety boundary; buttons are contextual prompts, not a second shell, static application, or hidden automation daemon.
|
|
9
|
+
|
|
10
|
+
## Concept
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
User intent → narrow inspection/action → console evidence → readable output → contextual buttons → next user intent
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Each response is one generated screen. Reinspect current reality and regenerate controls after every action rather than maintaining a parallel navigation model.
|
|
17
|
+
|
|
18
|
+
## Core Contract
|
|
19
|
+
|
|
20
|
+
- Inspect reality before rendering entries or controls that depend on current state.
|
|
21
|
+
- Use normal console programs as capability owners.
|
|
22
|
+
- Show complete output when reasonably sized; otherwise adapt it without changing material facts and offer pagination, filtering, raw output, or drill-down.
|
|
23
|
+
- Make every button prompt self-contained: name the exact target, operation, output expectation, and safety restriction.
|
|
24
|
+
- Treat button clicks as ordinary user requests subject to the same authority and validation rules as typed requests.
|
|
25
|
+
- Never infer permission for destructive, privileged, credential-bearing, external, or irreversible work merely because a button exists.
|
|
26
|
+
- Do not read secrets to populate navigation. Names and safe metadata may be listed; contents require justified, explicit authorization.
|
|
27
|
+
- Never place credentials, private keys, tokens, cookies, wallet material, or sensitive file contents in labels or prompts.
|
|
28
|
+
|
|
29
|
+
## Screen Model
|
|
30
|
+
|
|
31
|
+
A screen normally contains:
|
|
32
|
+
|
|
33
|
+
1. A short title naming the current target.
|
|
34
|
+
2. Console output or a faithful adaptation.
|
|
35
|
+
3. Optional provenance such as path, command class, timestamp, exit status, or truncation note.
|
|
36
|
+
4. Buttons for likely next actions.
|
|
37
|
+
5. `Back` or `Up` for hierarchy navigation.
|
|
38
|
+
6. `Refresh` when state may change.
|
|
39
|
+
|
|
40
|
+
Prefer 4–12 useful buttons. Split larger sets into categories or pages rather than creating a dense button wall.
|
|
41
|
+
|
|
42
|
+
## Console Fidelity
|
|
43
|
+
|
|
44
|
+
Complete output preserves ordering, names, identifiers, numeric values, units, warnings, errors, and relevant exit status. Use a code block only when formatting is semantically meaningful; use compact records for simple listings.
|
|
45
|
+
|
|
46
|
+
Adaptation may:
|
|
47
|
+
|
|
48
|
+
- Replace columns with labeled records.
|
|
49
|
+
- Normalize human-readable sizes.
|
|
50
|
+
- Group entries by type.
|
|
51
|
+
- Collapse repeated successful lines.
|
|
52
|
+
- Show a bounded head, tail, page, or ranked subset.
|
|
53
|
+
- Translate labels into the user's language.
|
|
54
|
+
|
|
55
|
+
Adaptation must not:
|
|
56
|
+
|
|
57
|
+
- Convert failure into success.
|
|
58
|
+
- Omit material warnings.
|
|
59
|
+
- Change identities, values, or ordering claims.
|
|
60
|
+
- Present a filtered subset as complete.
|
|
61
|
+
- Hide truncation, filtering, or an unavailable measurement.
|
|
62
|
+
|
|
63
|
+
State adaptation explicitly, for example: `Показаны 20 из 184 записей, по размеру`.
|
|
64
|
+
|
|
65
|
+
## Filesystem Navigation
|
|
66
|
+
|
|
67
|
+
- Resolve the requested path before listing it.
|
|
68
|
+
- List directories without reading file contents.
|
|
69
|
+
- Include every ordinary entry unless the user requested a filter.
|
|
70
|
+
- Do not silently omit a sensitive-looking entry; show its name when listing is safe, then handle its contents conservatively.
|
|
71
|
+
- Hidden directories default to names and metadata only.
|
|
72
|
+
- Use absolute or otherwise unambiguous paths in button prompts.
|
|
73
|
+
- Keep `Up`, `Home`, and `Refresh` where useful.
|
|
74
|
+
- Offer safe file operations first: metadata, non-sensitive preview, attach/send, or open with an appropriate application.
|
|
75
|
+
|
|
76
|
+
Never expose credential-file contents through a preview button. This includes `*.keys`, private SSH keys, credential stores, browser profiles, cookies, tokens, and wallets.
|
|
77
|
+
|
|
78
|
+
## System And Process Controls
|
|
79
|
+
|
|
80
|
+
Read-only controls may directly request system status, uptime, load, memory, temperatures, disk use, process ranking, service state, network state, application discovery, and bounded redacted logs.
|
|
81
|
+
|
|
82
|
+
Use a two-stage flow for high-impact actions:
|
|
83
|
+
|
|
84
|
+
1. An action button opens a confirmation screen naming the exact target and consequences.
|
|
85
|
+
2. A distinct confirmation button requests the exact operation.
|
|
86
|
+
|
|
87
|
+
This applies to shutdown, reboot, process termination, package removal, file deletion, permission changes, service mutation, disk operations, and similar work. Use danger styling when available. Re-check the target immediately before execution and report resulting console evidence.
|
|
88
|
+
|
|
89
|
+
## Button Generation
|
|
90
|
+
|
|
91
|
+
When the transport supports prompt buttons, emit its canonical button action syntax. For pi-telegram this is a top-level hidden `telegram_button` comment:
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
<!-- telegram_button: {"label":"📂 Downloads","prompt":"Show the current contents of /home/user/Downloads without reading file contents, then provide contextual navigation buttons."} -->
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Button prompts must:
|
|
98
|
+
|
|
99
|
+
- Use an exact target where possible.
|
|
100
|
+
- Describe one coherent intent.
|
|
101
|
+
- Preserve the user's language.
|
|
102
|
+
- State important exclusions such as not reading secrets.
|
|
103
|
+
- Request fresh state after mutations.
|
|
104
|
+
- Avoid embedding volatile output that should be reinspected.
|
|
105
|
+
|
|
106
|
+
Labels stay short, distinct, and scannable. Emoji are optional semantic markers; do not rely on color alone. If buttons are unavailable, render the same interface as a numbered choice list.
|
|
107
|
+
|
|
108
|
+
## Action Procedure
|
|
109
|
+
|
|
110
|
+
1. Identify the current target and capability.
|
|
111
|
+
2. Classify the action as read-only, ordinary mutation, privileged, destructive, secret-bearing, or external.
|
|
112
|
+
3. Run the narrowest console inspection needed for a truthful screen.
|
|
113
|
+
4. Check exit status and stderr; never build a success menu from failed evidence.
|
|
114
|
+
5. Render complete or explicitly adapted output.
|
|
115
|
+
6. Generate only context-relevant next-action buttons.
|
|
116
|
+
7. On the next turn, reinspect when freshness matters and execute only the newly authorized action.
|
|
117
|
+
8. Report outcome evidence and regenerate the screen from retained reality.
|
|
118
|
+
|
|
119
|
+
## Failure And Empty States
|
|
120
|
+
|
|
121
|
+
- On command failure, show the concise error and offer diagnosis, retry, Back, or a narrower action.
|
|
122
|
+
- For an empty directory, say so and retain Up, Home, and Refresh.
|
|
123
|
+
- If a target disappeared, return to its nearest valid parent rather than reusing stale evidence.
|
|
124
|
+
- On access denial, do not escalate privileges automatically.
|
|
125
|
+
- If output may contain secrets, stop before display and offer metadata-only or redacted alternatives.
|
|
126
|
+
- Mark unsupported, sentinel, or obviously invalid sensor values as unreliable instead of reporting them as facts.
|
|
127
|
+
|
|
128
|
+
## Quality Check
|
|
129
|
+
|
|
130
|
+
Before sending a screen, verify:
|
|
131
|
+
|
|
132
|
+
- Displayed state comes from current console evidence.
|
|
133
|
+
- Complete versus filtered output is labeled honestly.
|
|
134
|
+
- No ordinary entry was accidentally omitted.
|
|
135
|
+
- No secret appears in text or button payloads.
|
|
136
|
+
- Every button has a valid self-contained next intent.
|
|
137
|
+
- Destructive actions lead to confirmation rather than immediate execution.
|
|
138
|
+
- Back/Up and Refresh exist when materially useful.
|
|
139
|
+
- The response remains readable on a mobile screen.
|