@llblab/pi-telegram 0.46.0 → 0.48.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 +4 -2
- package/BACKLOG.md +0 -6
- package/CHANGELOG.md +14 -0
- package/README.md +5 -2
- package/dist/api/activity.d.ts +6 -0
- package/dist/api/activity.js +6 -0
- package/dist/api/commands.d.ts +6 -0
- package/dist/api/commands.js +6 -0
- package/dist/api/delivery.d.ts +6 -0
- package/dist/api/delivery.js +6 -0
- package/dist/api/inbound.d.ts +6 -0
- package/dist/api/inbound.js +6 -0
- package/dist/api/keyboard.d.ts +6 -0
- package/dist/api/keyboard.js +6 -0
- package/dist/api/outbound.d.ts +6 -0
- package/dist/api/outbound.js +6 -0
- package/dist/api/sections.d.ts +7 -0
- package/dist/api/sections.js +6 -0
- package/dist/api/status.d.ts +6 -0
- package/dist/api/status.js +6 -0
- package/dist/api/updates.d.ts +6 -0
- package/dist/api/updates.js +6 -0
- package/dist/api/voice.d.ts +6 -0
- package/dist/api/voice.js +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/activity-verbosity.d.ts +52 -0
- package/dist/lib/activity-verbosity.js +596 -0
- package/dist/lib/activity.d.ts +220 -0
- package/dist/lib/activity.js +574 -0
- package/dist/lib/agent-messages.d.ts +28 -0
- package/dist/lib/agent-messages.js +86 -0
- package/dist/lib/bindings.d.ts +250 -0
- package/dist/lib/bindings.js +936 -0
- package/dist/lib/bus-api.d.ts +18 -0
- package/dist/lib/bus-api.js +254 -0
- package/dist/lib/bus-follower.d.ts +464 -0
- package/dist/lib/bus-follower.js +1686 -0
- package/dist/lib/bus-leader.d.ts +292 -0
- package/dist/lib/bus-leader.js +2242 -0
- package/dist/lib/bus-transport.d.ts +64 -0
- package/dist/lib/bus-transport.js +140 -0
- package/dist/lib/bus.d.ts +518 -0
- package/dist/lib/bus.js +2055 -0
- package/dist/lib/channel-posts.d.ts +170 -0
- package/dist/lib/channel-posts.js +611 -0
- package/dist/lib/command-templates.d.ts +70 -0
- package/dist/lib/command-templates.js +744 -0
- package/dist/lib/commands.d.ts +541 -0
- package/dist/lib/commands.js +1155 -0
- package/dist/lib/config.d.ts +252 -0
- package/dist/lib/config.js +889 -0
- package/dist/lib/delivery.d.ts +163 -0
- package/dist/lib/delivery.js +542 -0
- package/dist/lib/extension.d.ts +7 -0
- package/dist/lib/extension.js +1604 -0
- package/dist/lib/generative-app-worker.mjs +104 -0
- package/dist/lib/generative-apps.d.ts +212 -0
- package/dist/lib/generative-apps.js +1006 -0
- package/dist/lib/inbound.d.ts +91 -0
- package/dist/lib/inbound.js +502 -0
- package/dist/lib/journal.d.ts +665 -0
- package/dist/lib/journal.js +3701 -0
- package/dist/lib/keyboard.d.ts +23 -0
- package/dist/lib/keyboard.js +37 -0
- package/dist/lib/lifecycle.d.ts +157 -0
- package/dist/lib/lifecycle.js +395 -0
- package/dist/lib/locks.d.ts +164 -0
- package/dist/lib/locks.js +1208 -0
- package/dist/lib/logging.d.ts +50 -0
- package/dist/lib/logging.js +274 -0
- package/dist/lib/media.d.ts +181 -0
- package/dist/lib/media.js +602 -0
- package/dist/lib/menu-model.d.ts +217 -0
- package/dist/lib/menu-model.js +663 -0
- package/dist/lib/menu-queue.d.ts +37 -0
- package/dist/lib/menu-queue.js +408 -0
- package/dist/lib/menu-settings.d.ts +115 -0
- package/dist/lib/menu-settings.js +595 -0
- package/dist/lib/menu-status.d.ts +32 -0
- package/dist/lib/menu-status.js +112 -0
- package/dist/lib/menu-thinking.d.ts +29 -0
- package/dist/lib/menu-thinking.js +82 -0
- package/dist/lib/menu.d.ts +171 -0
- package/dist/lib/menu.js +323 -0
- package/dist/lib/model.d.ts +127 -0
- package/dist/lib/model.js +409 -0
- package/dist/lib/outbound-attachments.d.ts +241 -0
- package/dist/lib/outbound-attachments.js +639 -0
- package/dist/lib/outbound-buttons.d.ts +69 -0
- package/dist/lib/outbound-buttons.js +248 -0
- package/dist/lib/outbound-markup.d.ts +42 -0
- package/dist/lib/outbound-markup.js +678 -0
- package/dist/lib/outbound-voice.d.ts +55 -0
- package/dist/lib/outbound-voice.js +152 -0
- package/dist/lib/outbound.d.ts +185 -0
- package/dist/lib/outbound.js +516 -0
- package/dist/lib/ownership.d.ts +77 -0
- package/dist/lib/ownership.js +174 -0
- package/dist/lib/paths.d.ts +40 -0
- package/dist/lib/paths.js +94 -0
- package/dist/lib/pi.d.ts +72 -0
- package/dist/lib/pi.js +121 -0
- package/dist/lib/polling.d.ts +351 -0
- package/dist/lib/polling.js +1196 -0
- package/dist/lib/preview.d.ts +192 -0
- package/dist/lib/preview.js +614 -0
- package/dist/lib/prompt-templates.d.ts +24 -0
- package/dist/lib/prompt-templates.js +118 -0
- package/dist/lib/prompts.d.ts +57 -0
- package/dist/lib/prompts.js +167 -0
- package/dist/lib/queue.d.ts +766 -0
- package/dist/lib/queue.js +1965 -0
- package/dist/lib/recovery.d.ts +86 -0
- package/dist/lib/recovery.js +285 -0
- package/dist/lib/rendering.d.ts +20 -0
- package/dist/lib/rendering.js +983 -0
- package/dist/lib/replies.d.ts +214 -0
- package/dist/lib/replies.js +737 -0
- package/dist/lib/routing.d.ts +207 -0
- package/dist/lib/routing.js +2282 -0
- package/dist/lib/runtime.d.ts +172 -0
- package/dist/lib/runtime.js +402 -0
- package/dist/lib/sections.d.ts +165 -0
- package/dist/lib/sections.js +327 -0
- package/dist/lib/setup.d.ts +82 -0
- package/dist/lib/setup.js +150 -0
- package/dist/lib/skills.d.ts +8 -0
- package/dist/lib/skills.js +12 -0
- package/dist/lib/status.d.ts +442 -0
- package/dist/lib/status.js +1008 -0
- package/dist/lib/sync.d.ts +179 -0
- package/dist/lib/sync.js +782 -0
- package/dist/lib/target.d.ts +20 -0
- package/dist/lib/target.js +27 -0
- package/dist/lib/telegram-api.d.ts +541 -0
- package/dist/lib/telegram-api.js +1159 -0
- package/dist/lib/text-groups.d.ts +89 -0
- package/dist/lib/text-groups.js +317 -0
- package/dist/lib/thread-cleanup-manager.d.ts +288 -0
- package/dist/lib/thread-cleanup-manager.js +560 -0
- package/dist/lib/thread-display.d.ts +46 -0
- package/dist/lib/thread-display.js +257 -0
- package/dist/lib/thread-naming.d.ts +46 -0
- package/dist/lib/thread-naming.js +78 -0
- package/dist/lib/thread-reconciler.d.ts +239 -0
- package/dist/lib/thread-reconciler.js +644 -0
- package/dist/lib/threads.d.ts +621 -0
- package/dist/lib/threads.js +3679 -0
- package/dist/lib/time-injection.d.ts +15 -0
- package/dist/lib/time-injection.js +56 -0
- package/dist/lib/turns.d.ts +109 -0
- package/dist/lib/turns.js +500 -0
- package/dist/lib/updates.d.ts +1290 -0
- package/dist/lib/updates.js +3634 -0
- package/dist/lib/voice.d.ts +117 -0
- package/dist/lib/voice.js +174 -0
- package/dist/lib/workspace-admission.d.ts +264 -0
- package/dist/lib/workspace-admission.js +1136 -0
- package/dist/lib/workspace-retirement.d.ts +219 -0
- package/dist/lib/workspace-retirement.js +587 -0
- package/dist/lib/workspace-slots.d.ts +30 -0
- package/dist/lib/workspace-slots.js +54 -0
- package/dist/package.json +126 -0
- package/dist/pi-telegram/index.js +1 -0
- package/dist/skills/generated-control-surface/SKILL.md +107 -0
- package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
- package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
- package/dist/skills/generative-apps/SKILL.md +115 -0
- package/dist/skills/show-me/SKILL.md +166 -0
- package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
- package/dist/skills/telegram-bridge/SKILL.md +129 -0
- package/dist/skills/telegram-bridge/references/configuration.md +15 -0
- package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
- package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
- package/docs/README.md +2 -0
- package/docs/architecture.md +1 -1
- package/docs/compact-matrix-literal.md +10 -3
- package/docs/generative-apps.md +15 -13
- package/docs/multi-instance-bus.md +6 -4
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +3 -3
- package/lib/bindings.ts +89 -6
- package/lib/bus-follower.ts +7 -3
- package/lib/bus-leader.ts +44 -19
- package/lib/bus.ts +4 -1
- package/lib/commands.ts +2 -2
- package/lib/config.ts +14 -7
- package/lib/delivery.ts +38 -6
- package/lib/extension.ts +7 -0
- package/lib/generative-apps.ts +379 -13
- package/lib/menu-settings.ts +20 -6
- package/lib/outbound-markup.ts +16 -7
- package/lib/status.ts +15 -12
- package/lib/telegram-api.ts +12 -1
- package/lib/thread-display.ts +96 -18
- package/package.json +56 -13
- package/scripts/build-dist.mjs +44 -0
- package/scripts/measure-bus.mjs +8 -1
- package/scripts/measure-workspace.mjs +8 -1
- package/skills/generative-apps/SKILL.md +1 -1
- package/skills/show-me/SKILL.md +1 -1
- package/skills/show-me/references/telegram-surfaces.md +1 -1
- package/skills/telegram-bridge/SKILL.md +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Workspace display projection
|
|
3
|
+
* Zones: telegram, tui, thread identity
|
|
4
|
+
* Owns mode-specific labels and serialized, authority-fenced title application.
|
|
5
|
+
* Excludes routing, name allocation, profile mutation, and live-owner discovery.
|
|
6
|
+
*/
|
|
7
|
+
import { isDeepStrictEqual } from "node:util";
|
|
8
|
+
function labelText(value) {
|
|
9
|
+
return value.replace(/\s+/gu, " ").trim();
|
|
10
|
+
}
|
|
11
|
+
function boundedLabel(base, suffix = "") {
|
|
12
|
+
let prefix = "";
|
|
13
|
+
for (const character of base) {
|
|
14
|
+
if (prefix.length + character.length + suffix.length > 128)
|
|
15
|
+
break;
|
|
16
|
+
prefix += character;
|
|
17
|
+
}
|
|
18
|
+
return `${prefix}${suffix}`;
|
|
19
|
+
}
|
|
20
|
+
function directorySegments(cwd) {
|
|
21
|
+
return cwd.split("/").filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
function distinguishingDirectorySegments(cwd, directories) {
|
|
24
|
+
const parts = directorySegments(cwd);
|
|
25
|
+
if (!parts.length)
|
|
26
|
+
return [];
|
|
27
|
+
for (let depth = 1; depth <= parts.length; depth++) {
|
|
28
|
+
const candidate = labelText(parts.slice(-depth).join("/"));
|
|
29
|
+
const collides = directories.some((other) => other !== cwd &&
|
|
30
|
+
labelText(directorySegments(other).slice(-depth).join("/")).toLowerCase() ===
|
|
31
|
+
candidate.toLowerCase());
|
|
32
|
+
if (!collides)
|
|
33
|
+
return parts.slice(-depth);
|
|
34
|
+
}
|
|
35
|
+
return parts;
|
|
36
|
+
}
|
|
37
|
+
function directoryLabel(cwd, directories) {
|
|
38
|
+
const parts = distinguishingDirectorySegments(cwd, directories);
|
|
39
|
+
return parts.length ? labelText(parts.join("/")) : "/";
|
|
40
|
+
}
|
|
41
|
+
/** Pure directory tokenization shared by previews, initial titles, and reconciliation. */
|
|
42
|
+
export function tokenizeTelegramDirectorySegment(segment) {
|
|
43
|
+
return segment
|
|
44
|
+
.replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, "$1\u0000$2")
|
|
45
|
+
.replace(/(\p{Lu})(\p{Lu}\p{Ll})/gu, "$1\u0000$2")
|
|
46
|
+
.split(/[^\p{L}\p{N}]+|\u0000/gu)
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
function formatDirectoryLabel(cwd, directories, mode) {
|
|
50
|
+
const segments = distinguishingDirectorySegments(cwd, directories);
|
|
51
|
+
if (!segments.length)
|
|
52
|
+
return "/";
|
|
53
|
+
const formatted = segments.map((segment) => {
|
|
54
|
+
const tokens = tokenizeTelegramDirectorySegment(segment);
|
|
55
|
+
if (!tokens.length)
|
|
56
|
+
return "";
|
|
57
|
+
if (mode === "directory-snake")
|
|
58
|
+
return tokens.map((token) => token.toLowerCase()).join("_");
|
|
59
|
+
return tokens.map((token) => /\p{L}/u.test(token) && token === token.toUpperCase()
|
|
60
|
+
? token
|
|
61
|
+
: `${token.slice(0, 1).toUpperCase()}${token.slice(1).toLowerCase()}`).join(" ");
|
|
62
|
+
}).filter(Boolean);
|
|
63
|
+
if (!formatted.length)
|
|
64
|
+
return directoryLabel(cwd, directories);
|
|
65
|
+
return formatted.join(mode === "directory-snake" ? "_" : " / ");
|
|
66
|
+
}
|
|
67
|
+
/** Maps one leader-captured authenticated owner roster onto retained binding identities. */
|
|
68
|
+
export function resolveTelegramLiveWorkspaceBindingKeys(bindings, leaderTarget, followers) {
|
|
69
|
+
const targets = new Set();
|
|
70
|
+
const add = (target) => {
|
|
71
|
+
if (target && typeof target.threadId === "number") {
|
|
72
|
+
targets.add(`${target.chatId}:${target.threadId}`);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
add(leaderTarget);
|
|
76
|
+
for (const follower of followers)
|
|
77
|
+
add(follower.target);
|
|
78
|
+
return new Set(bindings.filter((binding) => targets.has(`${binding.target.chatId}:${binding.target.threadId}`)).map((binding) => binding.bindingKey));
|
|
79
|
+
}
|
|
80
|
+
/** Missing or ambiguous metadata yields no label rather than inventing identity. */
|
|
81
|
+
export function resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBindingKeys = new Set()) {
|
|
82
|
+
const labels = new Map();
|
|
83
|
+
const directories = Array.from(new Set(bindings.map((binding) => binding.cwd)));
|
|
84
|
+
const liveDirectoryCounts = new Map();
|
|
85
|
+
for (const binding of bindings) {
|
|
86
|
+
if (!liveBindingKeys.has(binding.bindingKey))
|
|
87
|
+
continue;
|
|
88
|
+
liveDirectoryCounts.set(binding.cwd, (liveDirectoryCounts.get(binding.cwd) ?? 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
const bases = new Map();
|
|
91
|
+
for (const binding of bindings) {
|
|
92
|
+
const slot = binding.slot && /^[A-Z]$/u.test(binding.slot) ? binding.slot : undefined;
|
|
93
|
+
const manualName = binding.manualThreadName
|
|
94
|
+
? labelText(binding.manualThreadName)
|
|
95
|
+
: undefined;
|
|
96
|
+
if (manualName) {
|
|
97
|
+
labels.set(binding.bindingKey, boundedLabel(manualName));
|
|
98
|
+
}
|
|
99
|
+
else if (mode === "letters") {
|
|
100
|
+
if (slot)
|
|
101
|
+
labels.set(binding.bindingKey, slot);
|
|
102
|
+
}
|
|
103
|
+
else if (mode === "names") {
|
|
104
|
+
const name = binding.threadName ? labelText(binding.threadName) : slot;
|
|
105
|
+
if (name)
|
|
106
|
+
labels.set(binding.bindingKey, boundedLabel(name));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const base = mode === "directories"
|
|
110
|
+
? directoryLabel(binding.cwd, directories)
|
|
111
|
+
: formatDirectoryLabel(binding.cwd, directories, mode);
|
|
112
|
+
bases.set(binding.bindingKey, base);
|
|
113
|
+
const showSuffix = mode === "directories"
|
|
114
|
+
? binding.showSlotSuffix || bindings.filter((candidate) => candidate.cwd === binding.cwd).length > 1
|
|
115
|
+
: (liveDirectoryCounts.get(binding.cwd) ?? 0) > 1;
|
|
116
|
+
if (showSuffix && !slot)
|
|
117
|
+
continue;
|
|
118
|
+
const suffix = !showSuffix ? "" : mode === "directory-title"
|
|
119
|
+
? ` ${slot}`
|
|
120
|
+
: `_${slot.toLowerCase()}`;
|
|
121
|
+
labels.set(binding.bindingKey, boundedLabel(base, suffix));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Long or whitespace-normalized paths can collide even after qualification.
|
|
125
|
+
if (mode === "directories" || mode === "directory-snake" || mode === "directory-title") {
|
|
126
|
+
const counts = new Map();
|
|
127
|
+
for (const binding of bindings) {
|
|
128
|
+
const label = labels.get(binding.bindingKey);
|
|
129
|
+
if (!label || ((mode === "directory-snake" || mode === "directory-title") &&
|
|
130
|
+
!liveBindingKeys.has(binding.bindingKey)))
|
|
131
|
+
continue;
|
|
132
|
+
const key = label.toLowerCase();
|
|
133
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
134
|
+
}
|
|
135
|
+
for (const binding of bindings) {
|
|
136
|
+
const label = labels.get(binding.bindingKey);
|
|
137
|
+
if (binding.manualThreadName || !label ||
|
|
138
|
+
(counts.get(label.toLowerCase()) ?? 0) < 2)
|
|
139
|
+
continue;
|
|
140
|
+
if ((mode === "directory-snake" || mode === "directory-title") &&
|
|
141
|
+
(liveDirectoryCounts.get(binding.cwd) ?? 0) < 2) {
|
|
142
|
+
labels.delete(binding.bindingKey);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!binding.slot || !/^[A-Z]$/u.test(binding.slot)) {
|
|
146
|
+
labels.delete(binding.bindingKey);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
labels.set(binding.bindingKey, boundedLabel(bases.get(binding.bindingKey), mode === "directory-title" ? ` ${binding.slot}` : `_${binding.slot.toLowerCase()}`));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const counts = new Map();
|
|
153
|
+
for (const binding of bindings) {
|
|
154
|
+
const label = labels.get(binding.bindingKey);
|
|
155
|
+
if (!label || ((mode === "directory-snake" || mode === "directory-title") &&
|
|
156
|
+
!liveBindingKeys.has(binding.bindingKey)))
|
|
157
|
+
continue;
|
|
158
|
+
const key = label.toLowerCase();
|
|
159
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
160
|
+
}
|
|
161
|
+
for (const [key, label] of labels) {
|
|
162
|
+
if ((counts.get(label.toLowerCase()) ?? 0) > 1)
|
|
163
|
+
labels.delete(key);
|
|
164
|
+
}
|
|
165
|
+
return labels;
|
|
166
|
+
}
|
|
167
|
+
export function resolveTelegramInitialWorkspaceDisplayName(input) {
|
|
168
|
+
const retained = input.bindings.find((binding) => binding.bindingKey === input.binding.bindingKey);
|
|
169
|
+
const binding = retained
|
|
170
|
+
? {
|
|
171
|
+
...input.binding,
|
|
172
|
+
...(retained.showSlotSuffix ? { showSlotSuffix: true } : {}),
|
|
173
|
+
...(input.preserveRetainedManualName !== false && retained.manualThreadName
|
|
174
|
+
? { manualThreadName: retained.manualThreadName }
|
|
175
|
+
: {}),
|
|
176
|
+
}
|
|
177
|
+
: input.binding;
|
|
178
|
+
return resolveTelegramWorkspaceDisplayNames([
|
|
179
|
+
...input.bindings.filter((candidate) => candidate.bindingKey !== binding.bindingKey),
|
|
180
|
+
binding,
|
|
181
|
+
], input.mode, new Set([...(input.liveBindingKeys ?? []), binding.bindingKey])).get(binding.bindingKey);
|
|
182
|
+
}
|
|
183
|
+
export async function applyTelegramThreadDisplaySetting(mode, deps) {
|
|
184
|
+
const profile = deps.getProfileKey();
|
|
185
|
+
const setter = deps.ownsLeader() ? deps.getLeaderSetter() : deps.getFollowerSetter();
|
|
186
|
+
if (!setter)
|
|
187
|
+
throw new Error("Thread display settings require a connected compatible instance.");
|
|
188
|
+
await setter(mode);
|
|
189
|
+
if (deps.getProfileKey() !== profile)
|
|
190
|
+
throw new Error("Telegram Thread display setting changed profile.");
|
|
191
|
+
await deps.reloadConfig();
|
|
192
|
+
if (deps.getProfileKey() !== profile)
|
|
193
|
+
throw new Error("Telegram Thread display setting changed profile.");
|
|
194
|
+
}
|
|
195
|
+
/** Caller owns triggering and reporting; no timer or background retry is created. */
|
|
196
|
+
export function createTelegramThreadDisplayReconciler(deps) {
|
|
197
|
+
let tail = Promise.resolve();
|
|
198
|
+
const apply = async () => {
|
|
199
|
+
const epoch = deps.getLeaderEpoch();
|
|
200
|
+
const profile = deps.getProfileKey();
|
|
201
|
+
const mode = deps.getMode();
|
|
202
|
+
const assertAuthority = () => {
|
|
203
|
+
if (epoch === undefined || deps.getLeaderEpoch() !== epoch ||
|
|
204
|
+
deps.getProfileKey() !== profile || deps.getMode() !== mode) {
|
|
205
|
+
throw new Error("Telegram Thread display update lost profile, mode, or leader authority.");
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
assertAuthority();
|
|
209
|
+
const bindings = deps.store.listWorkspaceBindings();
|
|
210
|
+
const liveBindingKeys = deps.captureLiveBindingKeys(bindings);
|
|
211
|
+
const titles = resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBindingKeys);
|
|
212
|
+
let changed = 0;
|
|
213
|
+
for (const binding of bindings) {
|
|
214
|
+
const isBindingCurrent = deps.captureBindingAuthority(binding);
|
|
215
|
+
if (!isBindingCurrent)
|
|
216
|
+
continue;
|
|
217
|
+
const title = titles.get(binding.bindingKey);
|
|
218
|
+
if (!title)
|
|
219
|
+
throw new Error("Telegram Thread display identity is missing or ambiguous.");
|
|
220
|
+
const assertBinding = (expected) => {
|
|
221
|
+
assertAuthority();
|
|
222
|
+
const current = deps.store.listWorkspaceBindings().find((candidate) => candidate.bindingKey === expected.bindingKey);
|
|
223
|
+
if (!isDeepStrictEqual(current, expected) || !isBindingCurrent()) {
|
|
224
|
+
throw new Error("Telegram Thread display binding changed.");
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
assertBinding(binding);
|
|
228
|
+
if ((binding.displayTitle ?? binding.threadName) === title)
|
|
229
|
+
continue;
|
|
230
|
+
await deps.callApi("editForumTopic", {
|
|
231
|
+
chat_id: binding.target.chatId,
|
|
232
|
+
message_thread_id: binding.target.threadId,
|
|
233
|
+
name: title,
|
|
234
|
+
}, { maxAttempts: 1 });
|
|
235
|
+
assertBinding(binding);
|
|
236
|
+
if (!deps.store.setWorkspaceDisplayTitle(binding, title)) {
|
|
237
|
+
throw new Error("Telegram Thread display binding changed before title commit.");
|
|
238
|
+
}
|
|
239
|
+
await deps.store.persist();
|
|
240
|
+
assertBinding({ ...binding, displayTitle: title });
|
|
241
|
+
changed++;
|
|
242
|
+
}
|
|
243
|
+
// A prior acknowledged edit may still have dirty metadata after a failed persist.
|
|
244
|
+
assertAuthority();
|
|
245
|
+
if (changed === 0)
|
|
246
|
+
await deps.store.persist();
|
|
247
|
+
assertAuthority();
|
|
248
|
+
return { changed };
|
|
249
|
+
};
|
|
250
|
+
return {
|
|
251
|
+
reconcile() {
|
|
252
|
+
const run = tail.then(apply);
|
|
253
|
+
tail = run.catch(() => undefined);
|
|
254
|
+
return run;
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Thread manual-name dialog state
|
|
3
|
+
* Zones: telegram, thread identity, runtime controls
|
|
4
|
+
* Owns one expiring exact-target interaction per session scope.
|
|
5
|
+
* Excludes Telegram transport, durable Workspace mutation, and name validation.
|
|
6
|
+
*/
|
|
7
|
+
import type { TelegramTarget } from "./target.ts";
|
|
8
|
+
export declare const TELEGRAM_THREAD_NAME_DIALOG_TTL_MS: number;
|
|
9
|
+
export type TelegramThreadNameDialogAction = "reset" | "cancel";
|
|
10
|
+
export interface TelegramThreadNameDialogCandidate {
|
|
11
|
+
scope: string;
|
|
12
|
+
target: TelegramTarget;
|
|
13
|
+
dialogMessageId: number;
|
|
14
|
+
phase: "input";
|
|
15
|
+
expiresAtMs: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function createTelegramThreadNameDialogRuntime(options?: {
|
|
18
|
+
ttlMs?: number;
|
|
19
|
+
nowMs?: () => number;
|
|
20
|
+
}): {
|
|
21
|
+
open(input: {
|
|
22
|
+
scope: string;
|
|
23
|
+
target: TelegramTarget;
|
|
24
|
+
dialogMessageId: number;
|
|
25
|
+
}): TelegramThreadNameDialogCandidate;
|
|
26
|
+
select(input: {
|
|
27
|
+
scope: string;
|
|
28
|
+
target: TelegramTarget;
|
|
29
|
+
dialogMessageId: number;
|
|
30
|
+
action: TelegramThreadNameDialogAction;
|
|
31
|
+
}): {
|
|
32
|
+
kind: "reset" | "cancel" | "expired";
|
|
33
|
+
};
|
|
34
|
+
consumeName(input: {
|
|
35
|
+
scope: string;
|
|
36
|
+
target: TelegramTarget;
|
|
37
|
+
text: string;
|
|
38
|
+
}): {
|
|
39
|
+
kind: "name";
|
|
40
|
+
name: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "none" | "empty";
|
|
43
|
+
};
|
|
44
|
+
clearScope(scope: string): void;
|
|
45
|
+
inspect(target: TelegramTarget): TelegramThreadNameDialogCandidate | undefined;
|
|
46
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Thread manual-name dialog state
|
|
3
|
+
* Zones: telegram, thread identity, runtime controls
|
|
4
|
+
* Owns one expiring exact-target interaction per session scope.
|
|
5
|
+
* Excludes Telegram transport, durable Workspace mutation, and name validation.
|
|
6
|
+
*/
|
|
7
|
+
export const TELEGRAM_THREAD_NAME_DIALOG_TTL_MS = 5 * 60_000;
|
|
8
|
+
function targetKey(target) {
|
|
9
|
+
return `${target.chatId}:${target.threadId ?? "chat"}`;
|
|
10
|
+
}
|
|
11
|
+
function sameTarget(left, right) {
|
|
12
|
+
return left.chatId === right.chatId && left.threadId === right.threadId;
|
|
13
|
+
}
|
|
14
|
+
function cloneCandidate(candidate) {
|
|
15
|
+
return { ...candidate, target: { ...candidate.target } };
|
|
16
|
+
}
|
|
17
|
+
export function createTelegramThreadNameDialogRuntime(options) {
|
|
18
|
+
const ttlMs = options?.ttlMs ?? TELEGRAM_THREAD_NAME_DIALOG_TTL_MS;
|
|
19
|
+
const nowMs = options?.nowMs ?? Date.now;
|
|
20
|
+
const candidates = new Map();
|
|
21
|
+
const current = (scope, target) => {
|
|
22
|
+
const key = targetKey(target);
|
|
23
|
+
const candidate = candidates.get(key);
|
|
24
|
+
if (!candidate || candidate.scope !== scope ||
|
|
25
|
+
!sameTarget(candidate.target, target))
|
|
26
|
+
return undefined;
|
|
27
|
+
if (candidate.expiresAtMs <= nowMs()) {
|
|
28
|
+
candidates.delete(key);
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return candidate;
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
open(input) {
|
|
35
|
+
const candidate = {
|
|
36
|
+
scope: input.scope,
|
|
37
|
+
target: { ...input.target },
|
|
38
|
+
dialogMessageId: input.dialogMessageId,
|
|
39
|
+
phase: "input",
|
|
40
|
+
expiresAtMs: nowMs() + ttlMs,
|
|
41
|
+
};
|
|
42
|
+
candidates.set(targetKey(input.target), candidate);
|
|
43
|
+
return cloneCandidate(candidate);
|
|
44
|
+
},
|
|
45
|
+
select(input) {
|
|
46
|
+
const candidate = current(input.scope, input.target);
|
|
47
|
+
if (!candidate || candidate.dialogMessageId !== input.dialogMessageId) {
|
|
48
|
+
return { kind: "expired" };
|
|
49
|
+
}
|
|
50
|
+
candidates.delete(targetKey(input.target));
|
|
51
|
+
return { kind: input.action };
|
|
52
|
+
},
|
|
53
|
+
consumeName(input) {
|
|
54
|
+
const candidate = current(input.scope, input.target);
|
|
55
|
+
if (!candidate || candidate.phase !== "input")
|
|
56
|
+
return { kind: "none" };
|
|
57
|
+
const name = input.text.trim();
|
|
58
|
+
if (!name)
|
|
59
|
+
return { kind: "empty" };
|
|
60
|
+
candidates.delete(targetKey(input.target));
|
|
61
|
+
return { kind: "name", name };
|
|
62
|
+
},
|
|
63
|
+
clearScope(scope) {
|
|
64
|
+
for (const [key, candidate] of candidates) {
|
|
65
|
+
if (candidate.scope === scope)
|
|
66
|
+
candidates.delete(key);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
inspect(target) {
|
|
70
|
+
const candidate = candidates.get(targetKey(target));
|
|
71
|
+
if (!candidate || candidate.expiresAtMs <= nowMs()) {
|
|
72
|
+
candidates.delete(targetKey(target));
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return cloneCandidate(candidate);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram thread lifecycle reconciler
|
|
3
|
+
* Zones: telegram, multi-instance bus, synchronization control plane
|
|
4
|
+
* Owns pure planning for Telegram thread/tab lifecycle reconciliation and destructive topic cleanup authority
|
|
5
|
+
* Excludes live Telegram API calls, inbound routing, menu rendering, and direct thread-store persistence
|
|
6
|
+
*/
|
|
7
|
+
import type { TelegramTarget } from "./target.ts";
|
|
8
|
+
type ThreadTarget = TelegramTarget & {
|
|
9
|
+
threadId: number;
|
|
10
|
+
};
|
|
11
|
+
type ThreadRecordStatus = "active" | "offline" | "stale" | "pending" | "starting" | "probe-required" | "failed";
|
|
12
|
+
export interface ThreadReconciliationRecord {
|
|
13
|
+
target: ThreadTarget;
|
|
14
|
+
status: ThreadRecordStatus;
|
|
15
|
+
instanceId?: string;
|
|
16
|
+
profileKey?: string;
|
|
17
|
+
ownerKind?: "leader" | "manual-follower" | "pending-topic" | "legacy";
|
|
18
|
+
}
|
|
19
|
+
export interface ThreadReconciliationReservation {
|
|
20
|
+
target: ThreadTarget;
|
|
21
|
+
expiresAtMs?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ThreadReconciliationObservation {
|
|
24
|
+
target: ThreadTarget;
|
|
25
|
+
syncStatus: "open" | "closed" | "deleted" | "unknown";
|
|
26
|
+
observedAtMs: number;
|
|
27
|
+
}
|
|
28
|
+
export interface TelegramThreadPendingProvision {
|
|
29
|
+
id: string;
|
|
30
|
+
owner: "leader" | "manual-follower";
|
|
31
|
+
instanceId: string;
|
|
32
|
+
slot?: string;
|
|
33
|
+
target?: TelegramTarget & {
|
|
34
|
+
threadId: number;
|
|
35
|
+
};
|
|
36
|
+
startedAtMs: number;
|
|
37
|
+
expiresAtMs?: number;
|
|
38
|
+
leaderEpoch?: number | string;
|
|
39
|
+
}
|
|
40
|
+
export interface TelegramThreadCleanupIntent {
|
|
41
|
+
id: string;
|
|
42
|
+
owner: "leader" | "manual-follower";
|
|
43
|
+
instanceId: string;
|
|
44
|
+
runtimeGeneration: string;
|
|
45
|
+
profileKey?: string;
|
|
46
|
+
target: ThreadTarget;
|
|
47
|
+
requestedAtMs: number;
|
|
48
|
+
}
|
|
49
|
+
export interface TelegramUnboundThreadMessageObservation {
|
|
50
|
+
target: TelegramTarget & {
|
|
51
|
+
threadId: number;
|
|
52
|
+
};
|
|
53
|
+
observedAtMs: number;
|
|
54
|
+
messageId?: number;
|
|
55
|
+
leaderEpoch?: number | string;
|
|
56
|
+
}
|
|
57
|
+
export interface TelegramReservedThreadMessageObservation {
|
|
58
|
+
target: TelegramTarget & {
|
|
59
|
+
threadId: number;
|
|
60
|
+
};
|
|
61
|
+
observedAtMs: number;
|
|
62
|
+
messageId?: number;
|
|
63
|
+
leaderEpoch?: number | string;
|
|
64
|
+
}
|
|
65
|
+
export interface ReplacedInstanceBindingInput {
|
|
66
|
+
instanceId: string;
|
|
67
|
+
replacementTarget: ThreadTarget;
|
|
68
|
+
}
|
|
69
|
+
export interface PreviousLeaderCleanupInput {
|
|
70
|
+
currentInstanceId: string;
|
|
71
|
+
}
|
|
72
|
+
export type ThreadReconciliationAction = {
|
|
73
|
+
kind: "mark-topic-active";
|
|
74
|
+
target: TelegramTarget & {
|
|
75
|
+
threadId: number;
|
|
76
|
+
};
|
|
77
|
+
reason: "observed-open";
|
|
78
|
+
} | {
|
|
79
|
+
kind: "mark-topic-stale";
|
|
80
|
+
target: TelegramTarget & {
|
|
81
|
+
threadId: number;
|
|
82
|
+
};
|
|
83
|
+
syncStatus: "closed" | "deleted";
|
|
84
|
+
reason: "observed-closed" | "observed-deleted";
|
|
85
|
+
} | {
|
|
86
|
+
kind: "close-delete-unbound-topic";
|
|
87
|
+
target: TelegramTarget & {
|
|
88
|
+
threadId: number;
|
|
89
|
+
};
|
|
90
|
+
observedAtMs: number;
|
|
91
|
+
messageId?: number;
|
|
92
|
+
reason: "unbound-user-message";
|
|
93
|
+
leaderEpoch?: number | string;
|
|
94
|
+
} | {
|
|
95
|
+
kind: "close-delete-reserved-topic";
|
|
96
|
+
target: TelegramTarget & {
|
|
97
|
+
threadId: number;
|
|
98
|
+
};
|
|
99
|
+
observedAtMs: number;
|
|
100
|
+
messageId?: number;
|
|
101
|
+
reason: "reserved-user-message" | "startup-reservation";
|
|
102
|
+
leaderEpoch?: number | string;
|
|
103
|
+
} | {
|
|
104
|
+
kind: "close-stale-replaced-topic";
|
|
105
|
+
target: TelegramTarget & {
|
|
106
|
+
threadId: number;
|
|
107
|
+
};
|
|
108
|
+
reason: "replaced-instance-binding";
|
|
109
|
+
instanceId?: string;
|
|
110
|
+
leaderEpoch?: number | string;
|
|
111
|
+
} | {
|
|
112
|
+
kind: "close-delete-replaced-follower-topic";
|
|
113
|
+
target: TelegramTarget & {
|
|
114
|
+
threadId: number;
|
|
115
|
+
};
|
|
116
|
+
reason: "replaced-follower";
|
|
117
|
+
instanceId?: string;
|
|
118
|
+
messageId?: number;
|
|
119
|
+
leaderEpoch?: number | string;
|
|
120
|
+
} | {
|
|
121
|
+
kind: "close-delete-previous-leader-topic";
|
|
122
|
+
target: TelegramTarget & {
|
|
123
|
+
threadId: number;
|
|
124
|
+
};
|
|
125
|
+
reason: "previous-leader";
|
|
126
|
+
instanceId?: string;
|
|
127
|
+
messageId?: number;
|
|
128
|
+
leaderEpoch?: number | string;
|
|
129
|
+
} | {
|
|
130
|
+
kind: "close-delete-disconnected-instance-topic";
|
|
131
|
+
target: TelegramTarget & {
|
|
132
|
+
threadId: number;
|
|
133
|
+
};
|
|
134
|
+
reason: "manual-disconnect";
|
|
135
|
+
instanceId?: string;
|
|
136
|
+
messageId?: number;
|
|
137
|
+
leaderEpoch?: number | string;
|
|
138
|
+
} | {
|
|
139
|
+
kind: "close-delete-graceful-shutdown-topic";
|
|
140
|
+
target: TelegramTarget & {
|
|
141
|
+
threadId: number;
|
|
142
|
+
};
|
|
143
|
+
reason: "graceful-shutdown";
|
|
144
|
+
cleanupIntentId: string;
|
|
145
|
+
instanceId: string;
|
|
146
|
+
runtimeGeneration: string;
|
|
147
|
+
leaderEpoch?: number | string;
|
|
148
|
+
} | {
|
|
149
|
+
kind: "cancel-superseded-graceful-shutdown-cleanup";
|
|
150
|
+
target: TelegramTarget & {
|
|
151
|
+
threadId: number;
|
|
152
|
+
};
|
|
153
|
+
reason: "replacement-registration";
|
|
154
|
+
cleanupIntentId: string;
|
|
155
|
+
instanceId: string;
|
|
156
|
+
runtimeGeneration: string;
|
|
157
|
+
leaderEpoch?: number | string;
|
|
158
|
+
} | {
|
|
159
|
+
kind: "close-delete-expired-pending-provision-topic";
|
|
160
|
+
target: TelegramTarget & {
|
|
161
|
+
threadId: number;
|
|
162
|
+
};
|
|
163
|
+
reason: "expired-pending-provision";
|
|
164
|
+
pendingProvisionId: string;
|
|
165
|
+
instanceId?: string;
|
|
166
|
+
leaderEpoch?: number | string;
|
|
167
|
+
};
|
|
168
|
+
export type ThreadReconciliationPhase = "stable" | "provisioning" | "sync-required" | "cleanup-required";
|
|
169
|
+
export type ThreadReconciliationEvent = "settled" | "pending-provision" | "sync-required" | "cleanup-required";
|
|
170
|
+
export interface ThreadReconciliationMachineState {
|
|
171
|
+
phase: ThreadReconciliationPhase;
|
|
172
|
+
event: ThreadReconciliationEvent;
|
|
173
|
+
atMs: number;
|
|
174
|
+
leaderEpoch?: number | string;
|
|
175
|
+
pendingProvisionCount: number;
|
|
176
|
+
syncActionCount: number;
|
|
177
|
+
cleanupActionCount: number;
|
|
178
|
+
}
|
|
179
|
+
export interface ThreadReconciliationTransition {
|
|
180
|
+
from: ThreadReconciliationPhase;
|
|
181
|
+
to: ThreadReconciliationPhase;
|
|
182
|
+
event: ThreadReconciliationEvent;
|
|
183
|
+
atMs: number;
|
|
184
|
+
}
|
|
185
|
+
export interface ThreadReconciliationPlan {
|
|
186
|
+
actions: ThreadReconciliationAction[];
|
|
187
|
+
state?: ThreadReconciliationMachineState;
|
|
188
|
+
transition?: ThreadReconciliationTransition;
|
|
189
|
+
}
|
|
190
|
+
export interface ThreadReconciliationApplyResult {
|
|
191
|
+
changed: boolean;
|
|
192
|
+
incompleteActions?: ThreadReconciliationAction[];
|
|
193
|
+
}
|
|
194
|
+
export interface ThreadReconciliationApplyPorts {
|
|
195
|
+
isCleanupTargetProtected?: (target: ThreadTarget, action: ThreadReconciliationAction) => boolean;
|
|
196
|
+
callApi?: <TResponse>(method: string, body: Record<string, unknown>) => Promise<TResponse>;
|
|
197
|
+
markActiveByTarget?: (target: ThreadTarget) => boolean;
|
|
198
|
+
markStaleByTarget?: (target: ThreadTarget, syncStatus?: "closed" | "deleted", lastSyncError?: string) => boolean;
|
|
199
|
+
persist?: () => Promise<void>;
|
|
200
|
+
removePendingProvisionById?: (id: string) => boolean;
|
|
201
|
+
removeCleanupIntentById?: (id: string) => boolean;
|
|
202
|
+
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
203
|
+
recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
|
|
204
|
+
}
|
|
205
|
+
export interface ThreadReconciliationInput {
|
|
206
|
+
nowMs: number;
|
|
207
|
+
currentLeaderEpoch?: number | string;
|
|
208
|
+
records: readonly ThreadReconciliationRecord[];
|
|
209
|
+
reservations?: readonly ThreadReconciliationReservation[];
|
|
210
|
+
observations?: readonly ThreadReconciliationObservation[];
|
|
211
|
+
pendingProvisions?: readonly TelegramThreadPendingProvision[];
|
|
212
|
+
pendingCleanups?: readonly TelegramThreadCleanupIntent[];
|
|
213
|
+
unboundMessages?: readonly TelegramUnboundThreadMessageObservation[];
|
|
214
|
+
reservedMessages?: readonly TelegramReservedThreadMessageObservation[];
|
|
215
|
+
proactiveReservationCleanup?: boolean;
|
|
216
|
+
replacedBindings?: readonly ReplacedInstanceBindingInput[];
|
|
217
|
+
previousLeaderCleanup?: PreviousLeaderCleanupInput;
|
|
218
|
+
previousState?: ThreadReconciliationMachineState;
|
|
219
|
+
freshCreationGraceMs?: number;
|
|
220
|
+
}
|
|
221
|
+
export interface ThreadReconciliationRuntime {
|
|
222
|
+
getState: () => ThreadReconciliationMachineState | undefined;
|
|
223
|
+
recordPlan: (plan: ThreadReconciliationPlan) => void;
|
|
224
|
+
plan: (input: Omit<ThreadReconciliationInput, "previousState">) => ThreadReconciliationPlan;
|
|
225
|
+
}
|
|
226
|
+
export declare function createThreadReconciliationRuntime(deps: {
|
|
227
|
+
recordRuntimeEvent: (category: string, message: unknown, details?: Record<string, unknown>) => void;
|
|
228
|
+
scheduleSnapshotPersist: () => void;
|
|
229
|
+
}): ThreadReconciliationRuntime;
|
|
230
|
+
export declare function planDisconnectedInstanceThreadCleanup(input: {
|
|
231
|
+
target: TelegramTarget & {
|
|
232
|
+
threadId: number;
|
|
233
|
+
};
|
|
234
|
+
instanceId?: string;
|
|
235
|
+
leaderEpoch?: number | string;
|
|
236
|
+
}): ThreadReconciliationPlan;
|
|
237
|
+
export declare function applyThreadReconciliationPlan(plan: ThreadReconciliationPlan, ports: ThreadReconciliationApplyPorts): Promise<ThreadReconciliationApplyResult>;
|
|
238
|
+
export declare function planThreadReconciliation(input: ThreadReconciliationInput): ThreadReconciliationPlan;
|
|
239
|
+
export {};
|