@llblab/pi-kit 0.27.6 → 0.28.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 +2 -5
- package/CHANGELOG.md +5 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
- package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
- package/node_modules/@llblab/pi-telegram/README.md +15 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
- package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
- package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
- package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
- package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
- package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
- package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
- package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
- package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
- package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
- package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
- package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
- package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
- package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
- package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
- package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
- package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
- package/node_modules/@llblab/pi-telegram/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
- package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/package.json +2 -2
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram singleton lock helpers
|
|
3
3
|
* Zones: telegram ownership, filesystem, transport authority
|
|
4
|
-
* Owns
|
|
4
|
+
* Owns filesystem authority, atomic runtime-section publication and Telegram bridge ownership semantics
|
|
5
5
|
*/
|
|
6
|
-
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
6
|
+
import { chmodSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { isDeepStrictEqual } from "node:util";
|
|
9
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
10
|
+
import { isWireRecord as runtimeStateRecord } from "./wire.js";
|
|
11
|
+
import { isTelegramSessionPollingJournalPath, resolveTelegramOwnersPath, resolveTelegramSessionPollingJournalPath, resolveTelegramUpdateJournalPathForProfile, } from "./paths.js";
|
|
10
12
|
export const TELEGRAM_LOCK_KEY = "default";
|
|
11
13
|
export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS = 8_000;
|
|
12
14
|
export const TELEGRAM_OWNERSHIP_CHECK_MS = 1_000;
|
|
@@ -42,6 +44,53 @@ export function createTelegramLockKeyResolver(activeProfile) {
|
|
|
42
44
|
return resolveTelegramLockKey(activeProfile.getActiveProfileName());
|
|
43
45
|
};
|
|
44
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Captures exact context, session generation and owned leader epoch before awaits.
|
|
49
|
+
* Session replacement, release or re-election revokes the grant; a successor never renews it.
|
|
50
|
+
*/
|
|
51
|
+
export function createTelegramOwnedStateAuthorityCapture(lock, session) {
|
|
52
|
+
return () => {
|
|
53
|
+
const ctx = session.get(), generation = session.getGeneration();
|
|
54
|
+
if (ctx === undefined || !session.isCurrent(ctx, generation))
|
|
55
|
+
return undefined;
|
|
56
|
+
const epoch = lock.getOwnedLeaderEpoch();
|
|
57
|
+
if (epoch === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
const current = () => session.isCurrent(ctx, generation) && lock.owns(ctx) && lock.getOwnedLeaderEpoch() === epoch;
|
|
60
|
+
return current() ? current : undefined;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Leader polling journal: the path owners.json names, else the session that would host it on acquisition.
|
|
65
|
+
* Without a session identity, the flat root `inbox` remains the compatibility fallback.
|
|
66
|
+
*/
|
|
67
|
+
export function createTelegramLeaderJournalPathResolver(deps) {
|
|
68
|
+
// An existing flat root journal keeps its custody; new installations never create one.
|
|
69
|
+
const createOwn = (profileName = deps.getProfileName()) => {
|
|
70
|
+
const root = resolveTelegramUpdateJournalPathForProfile(profileName);
|
|
71
|
+
if (existsSync(root))
|
|
72
|
+
return root;
|
|
73
|
+
const sessionId = deps.getSessionId();
|
|
74
|
+
return sessionId === undefined ? undefined : resolveTelegramSessionPollingJournalPath(sessionId, undefined, profileName);
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
createJournalPath: () => createOwn(),
|
|
78
|
+
resolve(profileName) {
|
|
79
|
+
const named = deps.getNamedJournalPath();
|
|
80
|
+
if (named && (isTelegramSessionPollingJournalPath(named) ||
|
|
81
|
+
named === resolveTelegramUpdateJournalPathForProfile(profileName)))
|
|
82
|
+
return named;
|
|
83
|
+
return createOwn(profileName) ?? resolveTelegramUpdateJournalPathForProfile(profileName);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** A released key keeps only `{ journalPath }`: no owner, but the successor's polling custody. */
|
|
88
|
+
export function readTelegramLockJournalPath(value) {
|
|
89
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
90
|
+
return undefined;
|
|
91
|
+
const path = value.journalPath;
|
|
92
|
+
return typeof path === "string" && path ? path : undefined;
|
|
93
|
+
}
|
|
45
94
|
export function readLocks(path = getOwnersPath()) {
|
|
46
95
|
if (!existsSync(path))
|
|
47
96
|
return {};
|
|
@@ -479,6 +528,201 @@ export function withTelegramFileTransaction(transactionPath, operation, options
|
|
|
479
528
|
releaseLockTransactionGuard(transactionPath, owner);
|
|
480
529
|
}
|
|
481
530
|
}
|
|
531
|
+
export class TelegramRuntimeStateError extends Error {
|
|
532
|
+
code;
|
|
533
|
+
constructor(code, message, options) {
|
|
534
|
+
super(message, options);
|
|
535
|
+
this.name = "TelegramRuntimeStateError";
|
|
536
|
+
this.code = code;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const TELEGRAM_RUNTIME_STATE_SECTIONS = ["transport", "workspace", "admission", "runtime"];
|
|
540
|
+
const TELEGRAM_ACTIVE_STATE_TRANSACTIONS = Symbol.for("@llblab/pi-telegram/active-state-transactions");
|
|
541
|
+
function requireRuntimeStatePath(path) {
|
|
542
|
+
if (typeof path !== "string" || !isAbsolute(path) || resolve(path) !== path)
|
|
543
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state path must be canonical and absolute.");
|
|
544
|
+
}
|
|
545
|
+
function requireRuntimeStateProfile(profile) {
|
|
546
|
+
if (typeof profile !== "string" || !profile.length || profile.length > 512 || /[\x00-\x1f]/u.test(profile))
|
|
547
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state profile is invalid.");
|
|
548
|
+
}
|
|
549
|
+
/** Private single-link regular file read with identity continuity; initial absence only is positive. */
|
|
550
|
+
function readTelegramRuntimeSource(path) {
|
|
551
|
+
requireRuntimeStatePath(path);
|
|
552
|
+
let source;
|
|
553
|
+
let observed = false;
|
|
554
|
+
try {
|
|
555
|
+
const stat = lstatSync(path);
|
|
556
|
+
observed = true;
|
|
557
|
+
if (!stat.isFile() || stat.nlink !== 1 ||
|
|
558
|
+
(process.platform !== "win32" && (stat.mode & 0o077) !== 0))
|
|
559
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state must be a private regular file.");
|
|
560
|
+
const fd = openSync(path, "r");
|
|
561
|
+
try {
|
|
562
|
+
const opened = fstatSync(fd);
|
|
563
|
+
if (opened.dev !== stat.dev || opened.ino !== stat.ino || opened.nlink !== 1 || !opened.isFile() ||
|
|
564
|
+
(process.platform !== "win32" && (opened.mode & 0o077) !== 0))
|
|
565
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state identity changed before reading.");
|
|
566
|
+
source = readFileSync(fd).toString("utf8");
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
closeSync(fd);
|
|
570
|
+
}
|
|
571
|
+
const after = lstatSync(path);
|
|
572
|
+
if (after.dev !== stat.dev || after.ino !== stat.ino || after.size !== stat.size || after.mtimeMs !== stat.mtimeMs || after.ctimeMs !== stat.ctimeMs)
|
|
573
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state changed during observation.");
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
if (!observed && error.code === "ENOENT")
|
|
577
|
+
return undefined;
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
return source;
|
|
581
|
+
}
|
|
582
|
+
/** Strict, observational envelope read; section owners validate their own payloads. Legacy state is never adopted here. */
|
|
583
|
+
export function readTelegramRuntimeState(path) {
|
|
584
|
+
const observed = readTelegramRuntimeSource(path);
|
|
585
|
+
if (observed === undefined)
|
|
586
|
+
return { version: 2, profiles: {} };
|
|
587
|
+
let value;
|
|
588
|
+
try {
|
|
589
|
+
value = JSON.parse(observed);
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state is unreadable.", { cause: error });
|
|
593
|
+
}
|
|
594
|
+
if (!runtimeStateRecord(value) || value.version !== 2 || !runtimeStateRecord(value.profiles) ||
|
|
595
|
+
Object.keys(value).some(key => key !== "version" && key !== "profiles"))
|
|
596
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state envelope is unsupported or malformed.");
|
|
597
|
+
for (const [profile, sections] of Object.entries(value.profiles)) {
|
|
598
|
+
requireRuntimeStateProfile(profile);
|
|
599
|
+
if (!runtimeStateRecord(sections) || Object.keys(sections).some(key => !TELEGRAM_RUNTIME_STATE_SECTIONS.includes(key)))
|
|
600
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state sections are malformed.");
|
|
601
|
+
}
|
|
602
|
+
return value;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Operator-approved optimistic recovery before leader acquisition: when the shared envelope, any transport section or a
|
|
606
|
+
* caller-validated section is damaged, publish a fresh empty envelope instead of refusing. All profiles lose runtime
|
|
607
|
+
* continuity. Filesystem access errors are not damage and still throw. Returns whether a reset was published.
|
|
608
|
+
*/
|
|
609
|
+
export function resetDamagedTelegramRuntimeState(path, validateProfile) {
|
|
610
|
+
requireRuntimeStatePath(path);
|
|
611
|
+
const runtimeDir = join(dirname(path), "runtime");
|
|
612
|
+
mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
|
|
613
|
+
return withTelegramFileTransaction(join(runtimeDir, `${basename(path)}.transaction`), () => {
|
|
614
|
+
let damaged = false;
|
|
615
|
+
try {
|
|
616
|
+
for (const [profile, sections] of Object.entries(readTelegramRuntimeState(path).profiles)) {
|
|
617
|
+
assertTelegramStateTransport(sections.transport);
|
|
618
|
+
try {
|
|
619
|
+
validateProfile?.(profile, sections);
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
damaged = true;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
if (!(error instanceof TelegramRuntimeStateError))
|
|
628
|
+
throw error;
|
|
629
|
+
damaged = true;
|
|
630
|
+
}
|
|
631
|
+
if (!damaged)
|
|
632
|
+
return false;
|
|
633
|
+
const tempPath = join(runtimeDir, `${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
634
|
+
try {
|
|
635
|
+
writeFileSync(tempPath, `${JSON.stringify({ version: 2, profiles: {} }, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
636
|
+
if (!renameTelegramPathWithRetry(tempPath, path))
|
|
637
|
+
throw new Error("Telegram runtime state reset publication disappeared.");
|
|
638
|
+
}
|
|
639
|
+
finally {
|
|
640
|
+
try {
|
|
641
|
+
unlinkSync(tempPath);
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
if (error.code !== "ENOENT")
|
|
645
|
+
throw error;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return true;
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* One physical read/check/write transaction for one named section. The caller supplies domain policy; copies expose
|
|
653
|
+
* current sibling facts without granting writes to them. No await, nested transaction, repair or legacy import.
|
|
654
|
+
*/
|
|
655
|
+
export function mutateTelegramRuntimeStateSection(path, profile, section, mutate, options) {
|
|
656
|
+
requireRuntimeStatePath(path);
|
|
657
|
+
requireRuntimeStateProfile(profile);
|
|
658
|
+
if (!TELEGRAM_RUNTIME_STATE_SECTIONS.includes(section))
|
|
659
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state section is invalid.");
|
|
660
|
+
const active = (globalThis[TELEGRAM_ACTIVE_STATE_TRANSACTIONS] ??= new Set());
|
|
661
|
+
if (active.has(path))
|
|
662
|
+
throw new TelegramRuntimeStateError("invalid", "Nested Telegram runtime state transaction is not allowed.");
|
|
663
|
+
const assertCurrent = () => {
|
|
664
|
+
if (!options.isCurrent())
|
|
665
|
+
throw new TelegramRuntimeStateError("authority-changed", "Telegram runtime state publication authority changed.");
|
|
666
|
+
};
|
|
667
|
+
assertCurrent();
|
|
668
|
+
active.add(path);
|
|
669
|
+
try {
|
|
670
|
+
const runtimeDir = join(dirname(path), "runtime");
|
|
671
|
+
mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
|
|
672
|
+
return withTelegramFileTransaction(join(runtimeDir, `${basename(path)}.transaction`), () => {
|
|
673
|
+
assertCurrent();
|
|
674
|
+
const file = readTelegramRuntimeState(path);
|
|
675
|
+
const previous = Object.hasOwn(file.profiles, profile) ? file.profiles[profile] : {};
|
|
676
|
+
const outcome = mutate(structuredClone(previous[section]), structuredClone(previous));
|
|
677
|
+
if (!runtimeStateRecord(outcome) || !Object.hasOwn(outcome, "value") || !Object.hasOwn(outcome, "result") || "then" in outcome)
|
|
678
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state mutations must return a synchronous value and result.");
|
|
679
|
+
// Normalize intentional optional properties to their wire representation before comparison/publication.
|
|
680
|
+
const next = outcome.value === undefined ? undefined : JSON.parse(JSON.stringify(outcome.value));
|
|
681
|
+
assertCurrent();
|
|
682
|
+
if (isDeepStrictEqual(previous[section], next))
|
|
683
|
+
return outcome.result;
|
|
684
|
+
const updated = { ...previous };
|
|
685
|
+
if (next === undefined)
|
|
686
|
+
delete updated[section];
|
|
687
|
+
else
|
|
688
|
+
updated[section] = next;
|
|
689
|
+
if (Object.keys(updated).length)
|
|
690
|
+
Object.defineProperty(file.profiles, profile, { value: updated, enumerable: true, configurable: true, writable: true });
|
|
691
|
+
else
|
|
692
|
+
delete file.profiles[profile];
|
|
693
|
+
const tempPath = join(runtimeDir, `${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
694
|
+
try {
|
|
695
|
+
options.onPublicationBoundary?.("before-write");
|
|
696
|
+
assertCurrent();
|
|
697
|
+
writeFileSync(tempPath, `${JSON.stringify(file, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
698
|
+
options.onPublicationBoundary?.("after-write-before-rename");
|
|
699
|
+
assertCurrent();
|
|
700
|
+
try {
|
|
701
|
+
if (!renameTelegramPathWithRetry(tempPath, path, { rename: options.publishRename }))
|
|
702
|
+
throw new Error("Telegram runtime state temporary publication disappeared.");
|
|
703
|
+
options.onPublicationBoundary?.("after-rename");
|
|
704
|
+
assertCurrent();
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
throw new TelegramRuntimeStateError("publication-unknown", "Telegram runtime state publication outcome is unknown.", { cause: error });
|
|
708
|
+
}
|
|
709
|
+
return outcome.result;
|
|
710
|
+
}
|
|
711
|
+
finally {
|
|
712
|
+
try {
|
|
713
|
+
unlinkSync(tempPath);
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
if (error.code !== "ENOENT")
|
|
717
|
+
throw error;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
finally {
|
|
723
|
+
active.delete(path);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
482
726
|
function withLockTransaction(locksPath, mutate) {
|
|
483
727
|
return withTelegramFileTransaction(`${locksPath}.transaction`, () => {
|
|
484
728
|
const locks = readLocksForTransaction(locksPath);
|
|
@@ -541,6 +785,7 @@ export function parseTelegramLockEntry(value) {
|
|
|
541
785
|
? record.busSocketPath
|
|
542
786
|
: undefined,
|
|
543
787
|
busSecret: typeof record.busSecret === "string" ? record.busSecret : undefined,
|
|
788
|
+
...(readTelegramLockJournalPath(record) ? { journalPath: readTelegramLockJournalPath(record) } : {}),
|
|
544
789
|
};
|
|
545
790
|
}
|
|
546
791
|
export function isProcessAlive(pid) {
|
|
@@ -555,7 +800,7 @@ export function isProcessAlive(pid) {
|
|
|
555
800
|
return error.code !== "ESRCH";
|
|
556
801
|
}
|
|
557
802
|
}
|
|
558
|
-
|
|
803
|
+
function formatTelegramLockEntry(lock) {
|
|
559
804
|
return lock.cwd ? `pid ${lock.pid}, cwd ${lock.cwd}` : `pid ${lock.pid}`;
|
|
560
805
|
}
|
|
561
806
|
function formatTelegramFollowerRegistrationFailure(message) {
|
|
@@ -606,6 +851,8 @@ function canSupersedeSameProcessOwner(current, pid, ctx, instanceId, runtimeGene
|
|
|
606
851
|
}
|
|
607
852
|
function createLockEntry(pid, ctx, options) {
|
|
608
853
|
const lock = { pid, cwd: ctx.cwd };
|
|
854
|
+
if (options.journalPath)
|
|
855
|
+
lock.journalPath = options.journalPath;
|
|
609
856
|
if (options.instanceId) {
|
|
610
857
|
const nowMs = options.getNowMs?.();
|
|
611
858
|
lock.instanceId = options.instanceId;
|
|
@@ -631,8 +878,26 @@ function formatLockState(state) {
|
|
|
631
878
|
return `stale (${formatTelegramLockEntry(state.lock)})`;
|
|
632
879
|
}
|
|
633
880
|
}
|
|
881
|
+
function assertTelegramStateTransport(value) {
|
|
882
|
+
if (value === undefined)
|
|
883
|
+
return;
|
|
884
|
+
if (!runtimeStateRecord(value))
|
|
885
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram state transport is malformed.");
|
|
886
|
+
const keys = ["pid", "cwd", "instanceId", "heartbeatMs", "leaderEpoch", "runtimeGeneration", "busSocketPath", "busSecret", "journalPath"];
|
|
887
|
+
if (Object.keys(value).some(key => !keys.includes(key)) ||
|
|
888
|
+
(value.pid === undefined ? Object.keys(value).length !== 1 || !readTelegramLockJournalPath(value) :
|
|
889
|
+
!Number.isSafeInteger(value.pid) || value.pid <= 0) ||
|
|
890
|
+
["cwd", "instanceId", "busSocketPath", "busSecret", "journalPath"].some(key => value[key] !== undefined && typeof value[key] !== "string") ||
|
|
891
|
+
["heartbeatMs", "runtimeGeneration"].some(key => value[key] !== undefined && (!Number.isSafeInteger(value[key]) || value[key] < 0)) ||
|
|
892
|
+
(value.journalPath !== undefined && !readTelegramLockJournalPath(value)) ||
|
|
893
|
+
(value.leaderEpoch !== undefined && (typeof value.leaderEpoch === "string" ? value.leaderEpoch.length === 0 : !Number.isSafeInteger(value.leaderEpoch))))
|
|
894
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram state transport is malformed.");
|
|
895
|
+
}
|
|
634
896
|
export function createTelegramLockRuntime(options = {}) {
|
|
635
897
|
const key = options.key ?? TELEGRAM_LOCK_KEY;
|
|
898
|
+
if (options.statePath && options.locksPath)
|
|
899
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram transport must select one storage identity.");
|
|
900
|
+
const statePath = options.statePath;
|
|
636
901
|
const locksPath = options.locksPath ?? getOwnersPath();
|
|
637
902
|
const pid = options.pid ?? process.pid;
|
|
638
903
|
const isAlive = options.isProcessAlive ?? isProcessAlive;
|
|
@@ -650,9 +915,59 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
650
915
|
return key() || TELEGRAM_LOCK_KEY;
|
|
651
916
|
return key;
|
|
652
917
|
};
|
|
918
|
+
const readOwners = () => {
|
|
919
|
+
if (!statePath)
|
|
920
|
+
return readLocks(locksPath);
|
|
921
|
+
// Read-only ownership queries fail closed (no owner); acquisition/publication still refuse malformed state.
|
|
922
|
+
try {
|
|
923
|
+
const transports = Object.fromEntries(Object.entries(readTelegramRuntimeState(statePath).profiles)
|
|
924
|
+
.filter(([, value]) => Object.hasOwn(value, "transport")).map(([profile, value]) => [profile, value.transport]));
|
|
925
|
+
assertTelegramStateTransport(transports[resolveEffectiveKey()]);
|
|
926
|
+
return transports;
|
|
927
|
+
}
|
|
928
|
+
catch {
|
|
929
|
+
return {};
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
const transactOwners = (mutate) => {
|
|
933
|
+
if (!statePath)
|
|
934
|
+
return withLockTransaction(locksPath, mutate);
|
|
935
|
+
const profile = resolveEffectiveKey();
|
|
936
|
+
return mutateTelegramRuntimeStateSection(statePath, profile, "transport", current => {
|
|
937
|
+
assertTelegramStateTransport(current);
|
|
938
|
+
const locks = { [profile]: current };
|
|
939
|
+
const outcome = mutate(locks);
|
|
940
|
+
const value = outcome.changed ? locks[profile] : current;
|
|
941
|
+
assertTelegramStateTransport(value);
|
|
942
|
+
return { value, result: outcome.result };
|
|
943
|
+
}, { ...options.statePublication, isCurrent: () => resolveEffectiveKey() === profile });
|
|
944
|
+
};
|
|
653
945
|
const readLock = () => {
|
|
654
946
|
const effectiveKey = resolveEffectiveKey();
|
|
655
|
-
return parseTelegramLockEntry(
|
|
947
|
+
return parseTelegramLockEntry(readOwners()[effectiveKey]);
|
|
948
|
+
};
|
|
949
|
+
// A live older-release leader polls the same bot from its own directory. Only identity is exposed: its bus
|
|
950
|
+
// endpoint, secret and epoch belong to another protocol and must never become a follower target.
|
|
951
|
+
const readLegacyOwner = (effectiveKey) => {
|
|
952
|
+
if (!options.legacyLocksPath || options.legacyLocksPath === (statePath ?? locksPath))
|
|
953
|
+
return undefined;
|
|
954
|
+
try {
|
|
955
|
+
const records = statePath ? readLocksForTransaction(options.legacyLocksPath) : readLocks(options.legacyLocksPath);
|
|
956
|
+
if (statePath)
|
|
957
|
+
for (const value of Object.values(records))
|
|
958
|
+
assertTelegramStateTransport(value);
|
|
959
|
+
const raw = records[effectiveKey];
|
|
960
|
+
const legacy = parseTelegramLockEntry(raw);
|
|
961
|
+
if (!legacy || getLockState(legacy, -1, isAlive, stateOptions()).kind !== "active-elsewhere")
|
|
962
|
+
return undefined;
|
|
963
|
+
return { pid: legacy.pid, ...(legacy.cwd ? { cwd: legacy.cwd } : {}), ...(legacy.instanceId ? { instanceId: legacy.instanceId } : {}),
|
|
964
|
+
...(legacy.heartbeatMs !== undefined ? { heartbeatMs: legacy.heartbeatMs } : {}) };
|
|
965
|
+
}
|
|
966
|
+
catch (error) {
|
|
967
|
+
if (statePath)
|
|
968
|
+
throw error;
|
|
969
|
+
return undefined;
|
|
970
|
+
}
|
|
656
971
|
};
|
|
657
972
|
const adoptCompatibleOwnedLock = (effectiveKey, lock, ctx) => {
|
|
658
973
|
if (ownedLock) {
|
|
@@ -671,8 +986,11 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
671
986
|
return ownedLock;
|
|
672
987
|
};
|
|
673
988
|
return {
|
|
674
|
-
acquire: (ctx, acquireOptions = {}) =>
|
|
989
|
+
acquire: (ctx, acquireOptions = {}) => transactOwners((locks) => {
|
|
675
990
|
const effectiveKey = resolveEffectiveKey();
|
|
991
|
+
const legacy = readLegacyOwner(effectiveKey);
|
|
992
|
+
if (legacy)
|
|
993
|
+
return { result: { ok: false, lock: legacy }, changed: false };
|
|
676
994
|
const current = parseTelegramLockEntry(locks[effectiveKey]);
|
|
677
995
|
const state = getLockState(current, pid, isAlive, stateOptions());
|
|
678
996
|
const expectedOwned = adoptCompatibleOwnedLock(effectiveKey, current, ctx);
|
|
@@ -714,7 +1032,9 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
714
1032
|
changed: false,
|
|
715
1033
|
};
|
|
716
1034
|
}
|
|
1035
|
+
const journalPath = readTelegramLockJournalPath(locks[effectiveKey]) ?? options.createJournalPath?.(ctx);
|
|
717
1036
|
const lock = createLockEntry(pid, ctx, {
|
|
1037
|
+
journalPath,
|
|
718
1038
|
instanceId: options.instanceId,
|
|
719
1039
|
busSocketPath: options.busSocketPath,
|
|
720
1040
|
busSecret: options.busSecret,
|
|
@@ -738,13 +1058,17 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
738
1058
|
release: () => {
|
|
739
1059
|
// Withdraw local send authority even if the durable release fails.
|
|
740
1060
|
deliveryRevoked = true;
|
|
741
|
-
return
|
|
1061
|
+
return transactOwners((locks) => {
|
|
742
1062
|
const effectiveKey = resolveEffectiveKey();
|
|
743
1063
|
const state = getLockState(parseTelegramLockEntry(locks[effectiveKey]), pid, isAlive, stateOptions());
|
|
744
1064
|
const changed = ownedLockKey === effectiveKey &&
|
|
745
1065
|
hasSameLockOwner(parseTelegramLockEntry(locks[effectiveKey]), ownedLock);
|
|
746
1066
|
if (changed) {
|
|
747
|
-
|
|
1067
|
+
const journalPath = readTelegramLockJournalPath(locks[effectiveKey]);
|
|
1068
|
+
if (journalPath)
|
|
1069
|
+
locks[effectiveKey] = { journalPath };
|
|
1070
|
+
else
|
|
1071
|
+
delete locks[effectiveKey];
|
|
748
1072
|
ownedLockKey = undefined;
|
|
749
1073
|
ownedLock = undefined;
|
|
750
1074
|
}
|
|
@@ -752,12 +1076,13 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
752
1076
|
});
|
|
753
1077
|
},
|
|
754
1078
|
getState: () => getLockState(readLock(), pid, isAlive, stateOptions()),
|
|
1079
|
+
getJournalPath: () => readTelegramLockJournalPath(readOwners()[resolveEffectiveKey()]),
|
|
755
1080
|
getStatusLabel: () => formatLockState(getLockState(readLock(), pid, isAlive, stateOptions())),
|
|
756
1081
|
getOwnedLeaderEpoch: () => {
|
|
757
1082
|
if (deliveryRevoked)
|
|
758
1083
|
return undefined;
|
|
759
1084
|
const effectiveKey = resolveEffectiveKey();
|
|
760
|
-
const lock = parseTelegramLockEntry(
|
|
1085
|
+
const lock = parseTelegramLockEntry(readOwners()[effectiveKey]);
|
|
761
1086
|
const exactOwner = adoptCompatibleOwnedLock(effectiveKey, lock);
|
|
762
1087
|
return hasSameLockOwner(lock, exactOwner) ? lock?.leaderEpoch : undefined;
|
|
763
1088
|
},
|
|
@@ -765,10 +1090,10 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
765
1090
|
if (deliveryRevoked)
|
|
766
1091
|
return false;
|
|
767
1092
|
const effectiveKey = resolveEffectiveKey();
|
|
768
|
-
const lock = parseTelegramLockEntry(
|
|
1093
|
+
const lock = parseTelegramLockEntry(readOwners()[effectiveKey]);
|
|
769
1094
|
return hasSameLockOwner(lock, adoptCompatibleOwnedLock(effectiveKey, lock, ctx));
|
|
770
1095
|
},
|
|
771
|
-
commitIfOwned: (commit) => !deliveryRevoked &&
|
|
1096
|
+
commitIfOwned: (commit) => !deliveryRevoked && transactOwners((locks) => {
|
|
772
1097
|
const effectiveKey = resolveEffectiveKey();
|
|
773
1098
|
const lock = parseTelegramLockEntry(locks[effectiveKey]);
|
|
774
1099
|
const exactOwner = ownedLockKey === effectiveKey && hasSameLockOwner(lock, ownedLock);
|
|
@@ -780,9 +1105,34 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
780
1105
|
return { result: false, changed: false };
|
|
781
1106
|
}
|
|
782
1107
|
commit();
|
|
783
|
-
|
|
1108
|
+
// External-file commits may retain an independent effect; never acknowledge under a changed unified owner.
|
|
1109
|
+
const current = !statePath || (!deliveryRevoked && hasSameLockOwner(parseTelegramLockEntry(readOwners()[effectiveKey]), ownedLock));
|
|
1110
|
+
return { result: current, changed: false };
|
|
784
1111
|
}),
|
|
785
|
-
|
|
1112
|
+
publishStateSectionIfOwned(section, mutate, publication) {
|
|
1113
|
+
if (section !== "workspace" && section !== "runtime")
|
|
1114
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram owner section publication is restricted to Workspace and runtime observations.");
|
|
1115
|
+
if (!statePath)
|
|
1116
|
+
throw new TelegramRuntimeStateError("invalid", "Consolidated transport storage is not selected.");
|
|
1117
|
+
const profile = resolveEffectiveKey(), expected = ownedLock ? { ...ownedLock } : undefined;
|
|
1118
|
+
if (publication.expectedScope && (publication.expectedScope.path !== statePath || publication.expectedScope.profile !== profile))
|
|
1119
|
+
return { committed: false };
|
|
1120
|
+
if (deliveryRevoked || ownedLockKey !== profile || !expected || !publication.isCurrent())
|
|
1121
|
+
return { committed: false };
|
|
1122
|
+
const isCurrent = () => !deliveryRevoked && resolveEffectiveKey() === profile && ownedLockKey === profile &&
|
|
1123
|
+
hasSameLockOwner(ownedLock, expected) && publication.isCurrent() &&
|
|
1124
|
+
hasSameLockOwner(parseTelegramLockEntry(readOwners()[profile]), expected);
|
|
1125
|
+
if (!isCurrent())
|
|
1126
|
+
return { committed: false };
|
|
1127
|
+
return mutateTelegramRuntimeStateSection(statePath, profile, section, (current, observed) => {
|
|
1128
|
+
assertTelegramStateTransport(observed.transport);
|
|
1129
|
+
if (!hasSameLockOwner(parseTelegramLockEntry(observed.transport), expected))
|
|
1130
|
+
return { value: current, result: { committed: false } };
|
|
1131
|
+
const outcome = mutate(current, observed);
|
|
1132
|
+
return { value: outcome.value, result: { committed: true, result: outcome.result } };
|
|
1133
|
+
}, { ...publication, isCurrent });
|
|
1134
|
+
},
|
|
1135
|
+
refresh: (ctx) => !deliveryRevoked && transactOwners((locks) => {
|
|
786
1136
|
const effectiveKey = resolveEffectiveKey();
|
|
787
1137
|
const lock = parseTelegramLockEntry(locks[effectiveKey]);
|
|
788
1138
|
const expectedOwner = adoptCompatibleOwnedLock(effectiveKey, lock, ctx);
|
|
@@ -806,6 +1156,7 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
806
1156
|
? { busSocketPath: options.busSocketPath }
|
|
807
1157
|
: {}),
|
|
808
1158
|
busSecret: options.busSecret ?? lock.busSecret,
|
|
1159
|
+
...(lock.journalPath ? { journalPath: lock.journalPath } : {}),
|
|
809
1160
|
};
|
|
810
1161
|
locks[effectiveKey] = refreshedLock;
|
|
811
1162
|
ownedLockKey = effectiveKey;
|
|
@@ -999,6 +1350,15 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
999
1350
|
await ownershipStop;
|
|
1000
1351
|
if (!isCurrent())
|
|
1001
1352
|
return cancelled;
|
|
1353
|
+
if (!options.election && deps.resetDamagedState) {
|
|
1354
|
+
try {
|
|
1355
|
+
if (deps.resetDamagedState())
|
|
1356
|
+
deps.recordRuntimeEvent?.("lock", new Error("Damaged Telegram runtime state was reset; previous runtime continuity was discarded."), { phase: "state-reset" });
|
|
1357
|
+
}
|
|
1358
|
+
catch (error) {
|
|
1359
|
+
deps.recordRuntimeEvent?.("lock", error, { phase: "state-reset" });
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1002
1362
|
let acquired = deps.lock.acquire(ctx, {
|
|
1003
1363
|
force: options.force,
|
|
1004
1364
|
expectedOwner: options.election?.expectedOwner ??
|
|
@@ -1093,6 +1453,12 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
1093
1453
|
return "Telegram bridge disconnected.";
|
|
1094
1454
|
},
|
|
1095
1455
|
suspend: suspendPolling,
|
|
1456
|
+
captureTransportAuthority(ctx) {
|
|
1457
|
+
const generation = pollingGeneration;
|
|
1458
|
+
const current = () => generation === pollingGeneration && activeContext === ctx && !ownershipStop &&
|
|
1459
|
+
(deps.isContextCurrent?.(ctx) ?? true) && deps.lock.owns(snapshotLockContext(ctx));
|
|
1460
|
+
return current() ? current : undefined;
|
|
1461
|
+
},
|
|
1096
1462
|
isSuspended: () => suspendedGeneration === pollingGeneration &&
|
|
1097
1463
|
suspensionsInFlight === 0 && startupsInFlight === 0 && !sessionAutoStartRun && !ownershipStop,
|
|
1098
1464
|
onPersistentConflict: async (ctx, count) => {
|
|
@@ -4,22 +4,27 @@
|
|
|
4
4
|
* Owns bounded JSONL runtime evidence files, previous-log preservation, and profile-aware log paths without becoming routing state
|
|
5
5
|
*/
|
|
6
6
|
import * as Status from "./status.ts";
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
type TelegramLogPathInput = string | (() => string);
|
|
8
|
+
interface TelegramRuntimeJsonlEvent {
|
|
9
9
|
at: number;
|
|
10
10
|
category: string;
|
|
11
11
|
message: string;
|
|
12
12
|
details?: Record<string, unknown>;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
interface TelegramRuntimeJsonlLogOptions {
|
|
15
15
|
path?: TelegramLogPathInput;
|
|
16
16
|
previousPath?: TelegramLogPathInput;
|
|
17
17
|
maxBytes?: number;
|
|
18
18
|
getNowMs?: () => number;
|
|
19
19
|
canReset?: () => boolean;
|
|
20
20
|
commitReset?: (commit: () => void) => boolean;
|
|
21
|
+
/** Explicit shared-file protocol; profile-labelled scope reset is an append, never truncation. */
|
|
22
|
+
sharedProfiles?: {
|
|
23
|
+
getProfileName: () => string | undefined;
|
|
24
|
+
captureAuthority: () => (() => boolean) | undefined;
|
|
25
|
+
};
|
|
21
26
|
}
|
|
22
|
-
|
|
27
|
+
interface TelegramRuntimeJsonlLog {
|
|
23
28
|
getPath: () => string;
|
|
24
29
|
reset: (reason: string, scope?: Record<string, unknown>) => void;
|
|
25
30
|
resetIfScopeChanged: (scopeKey: string, reason: string, scope?: Record<string, unknown>) => void;
|
|
@@ -28,7 +33,18 @@ export interface TelegramRuntimeJsonlLog {
|
|
|
28
33
|
export declare function getTelegramRuntimeLogPath(agentDir?: string, profileName?: string): string;
|
|
29
34
|
export declare function getTelegramPreviousRuntimeLogPath(agentDir?: string, profileName?: string): string;
|
|
30
35
|
export declare function createTelegramRuntimeJsonlLog(options?: TelegramRuntimeJsonlLogOptions): TelegramRuntimeJsonlLog;
|
|
31
|
-
|
|
36
|
+
interface TelegramRuntimeDiagnosticsStatusPorts<TContext> {
|
|
37
|
+
instanceId: string;
|
|
38
|
+
updateStatus(ctx: TContext, error?: string): void;
|
|
39
|
+
getStatusState(): Status.TelegramBridgeStatusLineState;
|
|
40
|
+
persistSnapshot(snapshot: ReturnType<typeof Status.createTelegramStatusSnapshot>): Promise<void>;
|
|
41
|
+
session?: {
|
|
42
|
+
get(): TContext | undefined;
|
|
43
|
+
getGeneration(): number;
|
|
44
|
+
isCurrent(ctx: TContext, generation?: number): boolean;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
interface TelegramRuntimeDiagnosticsRuntime<TContext> {
|
|
32
48
|
events: Status.TelegramRuntimeEventRecorder;
|
|
33
49
|
recordRuntimeEvent(category: string, error: unknown, details?: Record<string, unknown>): void;
|
|
34
50
|
bindStorage(ports: {
|
|
@@ -36,15 +52,17 @@ export interface TelegramRuntimeDiagnosticsRuntime<TContext> {
|
|
|
36
52
|
getProfileName(): string | undefined;
|
|
37
53
|
canReset(): boolean;
|
|
38
54
|
commitReset(commit: () => void): boolean;
|
|
55
|
+
captureAuthority?: () => (() => boolean) | undefined;
|
|
39
56
|
}): void;
|
|
40
|
-
bindStatus(ports:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
getStatusState(): Status.TelegramBridgeStatusLineState;
|
|
44
|
-
persistSnapshot(snapshot: ReturnType<typeof Status.createTelegramStatusSnapshot>): Promise<void>;
|
|
45
|
-
}): void;
|
|
57
|
+
bindStatus(ports: TelegramRuntimeDiagnosticsStatusPorts<TContext>): void;
|
|
58
|
+
onSessionStart(): void;
|
|
59
|
+
onSessionShutdown(): Promise<void>;
|
|
46
60
|
updateStatus(ctx: TContext, error?: string): void;
|
|
47
61
|
getStatusLines(options?: Status.TelegramBridgeStatusLineOptions): string[];
|
|
48
62
|
scheduleSnapshotPersist(): void;
|
|
49
63
|
}
|
|
50
|
-
export declare function createTelegramRuntimeDiagnosticsRuntime<TContext>(
|
|
64
|
+
export declare function createTelegramRuntimeDiagnosticsRuntime<TContext>(options?: {
|
|
65
|
+
sharedFile?: boolean;
|
|
66
|
+
snapshotTimer?: Pick<Parameters<typeof Status.createTelegramRuntimeDiagnosticsSnapshotScheduler>[0], "setTimer" | "clearTimer">;
|
|
67
|
+
}): TelegramRuntimeDiagnosticsRuntime<TContext>;
|
|
68
|
+
export {};
|