@llblab/pi-telegram 0.52.2 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +11 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +61 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +249 -126
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +125 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +511 -300
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TelegramThreadDisplayMode } from "./config.ts";
|
|
2
|
-
import type { TelegramTopicTargetStore, TelegramWorkspaceDisplayBinding, TelegramWorkspaceThreadBinding } from "./threads.ts";
|
|
3
2
|
import type { TelegramApiCallOptions } from "./telegram-api.ts";
|
|
3
|
+
import type { TelegramTopicTargetStore, TelegramWorkspaceDisplayBinding, TelegramWorkspaceThreadBinding } from "./threads.ts";
|
|
4
4
|
/** Pure directory tokenization shared by previews, initial titles, and reconciliation. */
|
|
5
5
|
export declare function tokenizeTelegramDirectorySegment(segment: string): string[];
|
|
6
6
|
/** Maps one leader-captured authenticated owner roster onto retained binding identities. */
|
|
@@ -13,7 +13,6 @@ export declare function resolveTelegramLiveWorkspaceBindingKeys(bindings: readon
|
|
|
13
13
|
threadId?: number;
|
|
14
14
|
};
|
|
15
15
|
}[]): ReadonlySet<string>;
|
|
16
|
-
/** Missing or ambiguous metadata yields no label rather than inventing identity. */
|
|
17
16
|
export declare function resolveTelegramWorkspaceDisplayNames(bindings: readonly TelegramWorkspaceDisplayBinding[], mode: TelegramThreadDisplayMode, liveBindingKeys?: ReadonlySet<string>): Map<string, string>;
|
|
18
17
|
export declare function resolveTelegramInitialWorkspaceDisplayName(input: {
|
|
19
18
|
bindings: readonly TelegramWorkspaceDisplayBinding[];
|
|
@@ -27,8 +27,7 @@ function distinguishingDirectorySegments(cwd, directories) {
|
|
|
27
27
|
for (let depth = 1; depth <= parts.length; depth++) {
|
|
28
28
|
const candidate = labelText(parts.slice(-depth).join("/"));
|
|
29
29
|
const collides = directories.some((other) => other !== cwd &&
|
|
30
|
-
labelText(directorySegments(other).slice(-depth).join("/")).toLowerCase() ===
|
|
31
|
-
candidate.toLowerCase());
|
|
30
|
+
labelText(directorySegments(other).slice(-depth).join("/")).toLowerCase() === candidate.toLowerCase());
|
|
32
31
|
if (!collides)
|
|
33
32
|
return parts.slice(-depth);
|
|
34
33
|
}
|
|
@@ -50,16 +49,20 @@ function formatDirectoryLabel(cwd, directories, mode) {
|
|
|
50
49
|
const segments = distinguishingDirectorySegments(cwd, directories);
|
|
51
50
|
if (!segments.length)
|
|
52
51
|
return "/";
|
|
53
|
-
const formatted = segments
|
|
52
|
+
const formatted = segments
|
|
53
|
+
.map((segment) => {
|
|
54
54
|
const tokens = tokenizeTelegramDirectorySegment(segment);
|
|
55
55
|
if (!tokens.length)
|
|
56
56
|
return "";
|
|
57
57
|
if (mode === "directory-snake")
|
|
58
58
|
return tokens.map((token) => token.toLowerCase()).join("_");
|
|
59
|
-
return tokens
|
|
59
|
+
return tokens
|
|
60
|
+
.map((token) => /\p{L}/u.test(token) && token === token.toUpperCase()
|
|
60
61
|
? token
|
|
61
|
-
: `${token.slice(0, 1).toUpperCase()}${token.slice(1).toLowerCase()}`)
|
|
62
|
-
|
|
62
|
+
: `${token.slice(0, 1).toUpperCase()}${token.slice(1).toLowerCase()}`)
|
|
63
|
+
.join(" ");
|
|
64
|
+
})
|
|
65
|
+
.filter(Boolean);
|
|
63
66
|
if (!formatted.length)
|
|
64
67
|
return directoryLabel(cwd, directories);
|
|
65
68
|
return formatted.join(mode === "directory-snake" ? "_" : " / ");
|
|
@@ -75,9 +78,25 @@ export function resolveTelegramLiveWorkspaceBindingKeys(bindings, leaderTarget,
|
|
|
75
78
|
add(leaderTarget);
|
|
76
79
|
for (const follower of followers)
|
|
77
80
|
add(follower.target);
|
|
78
|
-
return new Set(bindings
|
|
81
|
+
return new Set(bindings
|
|
82
|
+
.filter((binding) => targets.has(`${binding.target.chatId}:${binding.target.threadId}`))
|
|
83
|
+
.map((binding) => binding.bindingKey));
|
|
79
84
|
}
|
|
80
85
|
/** Missing or ambiguous metadata yields no label rather than inventing identity. */
|
|
86
|
+
/** Case-insensitive label occurrences; directory modes count only live bindings. */
|
|
87
|
+
function countTelegramDisplayLabels(bindings, labels, mode, liveBindingKeys) {
|
|
88
|
+
const counts = new Map();
|
|
89
|
+
for (const binding of bindings) {
|
|
90
|
+
const label = labels.get(binding.bindingKey);
|
|
91
|
+
if (!label ||
|
|
92
|
+
((mode === "directory-snake" || mode === "directory-title") &&
|
|
93
|
+
!liveBindingKeys.has(binding.bindingKey)))
|
|
94
|
+
continue;
|
|
95
|
+
const key = label.toLowerCase();
|
|
96
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
97
|
+
}
|
|
98
|
+
return counts;
|
|
99
|
+
}
|
|
81
100
|
export function resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBindingKeys = new Set()) {
|
|
82
101
|
const labels = new Map();
|
|
83
102
|
const directories = Array.from(new Set(bindings.map((binding) => binding.cwd)));
|
|
@@ -111,30 +130,29 @@ export function resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBinding
|
|
|
111
130
|
: formatDirectoryLabel(binding.cwd, directories, mode);
|
|
112
131
|
bases.set(binding.bindingKey, base);
|
|
113
132
|
const showSuffix = mode === "directories"
|
|
114
|
-
? binding.showSlotSuffix ||
|
|
133
|
+
? binding.showSlotSuffix ||
|
|
134
|
+
bindings.filter((candidate) => candidate.cwd === binding.cwd)
|
|
135
|
+
.length > 1
|
|
115
136
|
: (liveDirectoryCounts.get(binding.cwd) ?? 0) > 1;
|
|
116
137
|
if (showSuffix && !slot)
|
|
117
138
|
continue;
|
|
118
|
-
const suffix = !showSuffix
|
|
119
|
-
?
|
|
120
|
-
:
|
|
139
|
+
const suffix = !showSuffix
|
|
140
|
+
? ""
|
|
141
|
+
: mode === "directory-title"
|
|
142
|
+
? ` ${slot}`
|
|
143
|
+
: `_${slot.toLowerCase()}`;
|
|
121
144
|
labels.set(binding.bindingKey, boundedLabel(base, suffix));
|
|
122
145
|
}
|
|
123
146
|
}
|
|
124
147
|
// Long or whitespace-normalized paths can collide even after qualification.
|
|
125
|
-
if (mode === "directories" ||
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
}
|
|
148
|
+
if (mode === "directories" ||
|
|
149
|
+
mode === "directory-snake" ||
|
|
150
|
+
mode === "directory-title") {
|
|
151
|
+
const counts = countTelegramDisplayLabels(bindings, labels, mode, liveBindingKeys);
|
|
135
152
|
for (const binding of bindings) {
|
|
136
153
|
const label = labels.get(binding.bindingKey);
|
|
137
|
-
if (binding.manualThreadName ||
|
|
154
|
+
if (binding.manualThreadName ||
|
|
155
|
+
!label ||
|
|
138
156
|
(counts.get(label.toLowerCase()) ?? 0) < 2)
|
|
139
157
|
continue;
|
|
140
158
|
if ((mode === "directory-snake" || mode === "directory-title") &&
|
|
@@ -146,18 +164,12 @@ export function resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBinding
|
|
|
146
164
|
labels.delete(binding.bindingKey);
|
|
147
165
|
continue;
|
|
148
166
|
}
|
|
149
|
-
labels.set(binding.bindingKey, boundedLabel(bases.get(binding.bindingKey), mode === "directory-title"
|
|
167
|
+
labels.set(binding.bindingKey, boundedLabel(bases.get(binding.bindingKey), mode === "directory-title"
|
|
168
|
+
? ` ${binding.slot}`
|
|
169
|
+
: `_${binding.slot.toLowerCase()}`));
|
|
150
170
|
}
|
|
151
171
|
}
|
|
152
|
-
const counts =
|
|
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
|
-
}
|
|
172
|
+
const counts = countTelegramDisplayLabels(bindings, labels, mode, liveBindingKeys);
|
|
161
173
|
for (const [key, label] of labels) {
|
|
162
174
|
if ((counts.get(label.toLowerCase()) ?? 0) > 1)
|
|
163
175
|
labels.delete(key);
|
|
@@ -170,7 +182,8 @@ export function resolveTelegramInitialWorkspaceDisplayName(input) {
|
|
|
170
182
|
? {
|
|
171
183
|
...input.binding,
|
|
172
184
|
...(retained.showSlotSuffix ? { showSlotSuffix: true } : {}),
|
|
173
|
-
...(input.preserveRetainedManualName !== false &&
|
|
185
|
+
...(input.preserveRetainedManualName !== false &&
|
|
186
|
+
retained.manualThreadName
|
|
174
187
|
? { manualThreadName: retained.manualThreadName }
|
|
175
188
|
: {}),
|
|
176
189
|
}
|
|
@@ -182,7 +195,9 @@ export function resolveTelegramInitialWorkspaceDisplayName(input) {
|
|
|
182
195
|
}
|
|
183
196
|
export async function applyTelegramThreadDisplaySetting(mode, deps) {
|
|
184
197
|
const profile = deps.getProfileKey();
|
|
185
|
-
const setter = deps.ownsLeader()
|
|
198
|
+
const setter = deps.ownsLeader()
|
|
199
|
+
? deps.getLeaderSetter()
|
|
200
|
+
: deps.getFollowerSetter();
|
|
186
201
|
if (!setter)
|
|
187
202
|
throw new Error("Thread display settings require a connected compatible instance.");
|
|
188
203
|
await setter(mode);
|
|
@@ -196,7 +211,8 @@ export function createTelegramThreadDisplaySettingsRuntime(deps) {
|
|
|
196
211
|
return {
|
|
197
212
|
isCustom() {
|
|
198
213
|
const target = deps.getTarget();
|
|
199
|
-
return !!target &&
|
|
214
|
+
return (!!target &&
|
|
215
|
+
typeof deps.getBinding(target)?.manualThreadName === "string");
|
|
200
216
|
},
|
|
201
217
|
async setMode(mode) {
|
|
202
218
|
const target = deps.getTarget();
|
|
@@ -220,8 +236,10 @@ export function createTelegramThreadDisplayReconciler(deps) {
|
|
|
220
236
|
const profile = deps.getProfileKey();
|
|
221
237
|
const mode = deps.getMode();
|
|
222
238
|
const assertAuthority = () => {
|
|
223
|
-
if (epoch === undefined ||
|
|
224
|
-
deps.
|
|
239
|
+
if (epoch === undefined ||
|
|
240
|
+
deps.getLeaderEpoch() !== epoch ||
|
|
241
|
+
deps.getProfileKey() !== profile ||
|
|
242
|
+
deps.getMode() !== mode) {
|
|
225
243
|
throw new Error("Telegram Thread display update lost profile, mode, or leader authority.");
|
|
226
244
|
}
|
|
227
245
|
};
|
|
@@ -239,7 +257,9 @@ export function createTelegramThreadDisplayReconciler(deps) {
|
|
|
239
257
|
throw new Error("Telegram Thread display identity is missing or ambiguous.");
|
|
240
258
|
const assertBinding = (expected) => {
|
|
241
259
|
assertAuthority();
|
|
242
|
-
const current = deps.store
|
|
260
|
+
const current = deps.store
|
|
261
|
+
.listWorkspaceBindings()
|
|
262
|
+
.find((candidate) => candidate.bindingKey === expected.bindingKey);
|
|
243
263
|
if (!isDeepStrictEqual(current, expected) || !isBindingCurrent()) {
|
|
244
264
|
throw new Error("Telegram Thread display binding changed.");
|
|
245
265
|
}
|
|
@@ -14,10 +14,38 @@ export interface TelegramThreadNameDialogCandidate {
|
|
|
14
14
|
phase: "input";
|
|
15
15
|
expiresAtMs: number;
|
|
16
16
|
}
|
|
17
|
+
export interface TelegramThreadNameDialogLifetime {
|
|
18
|
+
/** Independent recipient effect authority; never the input handle's deadline or ownership. */
|
|
19
|
+
readonly assertAuthority?: () => void;
|
|
20
|
+
isCurrent(): boolean;
|
|
21
|
+
publish(dialogMessageId: number, assertPublicationCurrent?: () => void): TelegramThreadNameDialogCandidate | undefined;
|
|
22
|
+
select(action: TelegramThreadNameDialogAction): {
|
|
23
|
+
kind: "reset" | "cancel" | "expired";
|
|
24
|
+
};
|
|
25
|
+
consumeName(text: string): {
|
|
26
|
+
kind: "name";
|
|
27
|
+
name: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "none" | "empty";
|
|
30
|
+
};
|
|
31
|
+
reopen(): TelegramThreadNameDialogCandidate | undefined;
|
|
32
|
+
finish(): void;
|
|
33
|
+
}
|
|
17
34
|
export declare function createTelegramThreadNameDialogRuntime(options?: {
|
|
18
35
|
ttlMs?: number;
|
|
19
36
|
nowMs?: () => number;
|
|
20
37
|
}): {
|
|
38
|
+
prepare(input: {
|
|
39
|
+
scope: string;
|
|
40
|
+
target: TelegramTarget;
|
|
41
|
+
isCurrent?: () => boolean;
|
|
42
|
+
assertAuthority?: () => void;
|
|
43
|
+
}): TelegramThreadNameDialogLifetime | undefined;
|
|
44
|
+
capture(input: {
|
|
45
|
+
scope: string;
|
|
46
|
+
target: TelegramTarget;
|
|
47
|
+
dialogMessageId: number;
|
|
48
|
+
}): TelegramThreadNameDialogLifetime | undefined;
|
|
21
49
|
open(input: {
|
|
22
50
|
scope: string;
|
|
23
51
|
target: TelegramTarget;
|
|
@@ -49,14 +77,6 @@ export interface TelegramThreadTitleInput {
|
|
|
49
77
|
profileKey: string;
|
|
50
78
|
threadName?: string;
|
|
51
79
|
}
|
|
52
|
-
export interface TelegramThreadNameInput {
|
|
53
|
-
seed: string;
|
|
54
|
-
cwd?: string;
|
|
55
|
-
role?: "leader" | "follower";
|
|
56
|
-
peers?: readonly string[];
|
|
57
|
-
slot?: string;
|
|
58
|
-
}
|
|
59
|
-
export declare function createTelegramThreadName(input: TelegramThreadNameInput): string;
|
|
60
80
|
export declare function normalizeTelegramTopicTargetThreadName(threadName: string): string;
|
|
61
81
|
export declare function getTelegramTopicIdentityName(threadName: string): string;
|
|
62
82
|
export declare function chooseTelegramThreadName(input: {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Owns name/title value policy and one expiring exact-target dialog per session scope.
|
|
5
5
|
* Excludes occupancy/slot allocation, display-mode projection, transport and API/store effects.
|
|
6
6
|
*/
|
|
7
|
-
import { areTelegramTargetsEqual as sameTarget } from "./target.js";
|
|
7
|
+
import { areTelegramTargetsEqual as sameTarget, } from "./target.js";
|
|
8
8
|
export const TELEGRAM_THREAD_NAME_DIALOG_TTL_MS = 5 * 60_000;
|
|
9
9
|
function targetKey(target) {
|
|
10
10
|
return `${target.chatId}:${target.threadId ?? "chat"}`;
|
|
@@ -16,99 +16,208 @@ export function createTelegramThreadNameDialogRuntime(options) {
|
|
|
16
16
|
const ttlMs = options?.ttlMs ?? TELEGRAM_THREAD_NAME_DIALOG_TTL_MS;
|
|
17
17
|
const nowMs = options?.nowMs ?? Date.now;
|
|
18
18
|
const candidates = new Map();
|
|
19
|
+
const recipientCurrent = (entry) => {
|
|
20
|
+
try {
|
|
21
|
+
return entry.isCurrent ? entry.isCurrent() === true : true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const view = (entry) => cloneCandidate({
|
|
28
|
+
scope: entry.scope,
|
|
29
|
+
target: entry.target,
|
|
30
|
+
dialogMessageId: entry.dialogMessageId,
|
|
31
|
+
phase: "input",
|
|
32
|
+
expiresAtMs: entry.expiresAtMs,
|
|
33
|
+
});
|
|
34
|
+
const createLifetime = (entry) => {
|
|
35
|
+
const key = targetKey(entry.target);
|
|
36
|
+
const claim = {};
|
|
37
|
+
let ended = false;
|
|
38
|
+
const finish = () => {
|
|
39
|
+
ended = true;
|
|
40
|
+
if (candidates.get(key) === entry &&
|
|
41
|
+
(!entry.consumedBy || entry.consumedBy === claim))
|
|
42
|
+
candidates.delete(key);
|
|
43
|
+
};
|
|
44
|
+
const isCurrent = () => {
|
|
45
|
+
if (ended ||
|
|
46
|
+
candidates.get(key) !== entry ||
|
|
47
|
+
(entry.consumedBy && entry.consumedBy !== claim)) {
|
|
48
|
+
ended = true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const current = entry.expiresAtMs > nowMs() && recipientCurrent(entry);
|
|
52
|
+
// Supplied authority/clock observations may synchronously replace this dialog.
|
|
53
|
+
if (!current ||
|
|
54
|
+
entry.expiresAtMs <= nowMs() ||
|
|
55
|
+
candidates.get(key) !== entry ||
|
|
56
|
+
(entry.consumedBy && entry.consumedBy !== claim)) {
|
|
57
|
+
finish();
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
assertAuthority: entry.assertAuthority,
|
|
64
|
+
isCurrent,
|
|
65
|
+
publish(dialogMessageId, assertPublicationCurrent) {
|
|
66
|
+
if (!isCurrent() || entry.phase !== "delivery")
|
|
67
|
+
return undefined;
|
|
68
|
+
// Source authority is needed for this publication only, never retained by future input/effects.
|
|
69
|
+
if (assertPublicationCurrent) {
|
|
70
|
+
assertPublicationCurrent();
|
|
71
|
+
if (entry.expiresAtMs <= nowMs())
|
|
72
|
+
return undefined;
|
|
73
|
+
assertPublicationCurrent();
|
|
74
|
+
if (ended ||
|
|
75
|
+
candidates.get(key) !== entry ||
|
|
76
|
+
entry.phase !== "delivery")
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (!Number.isSafeInteger(dialogMessageId) || dialogMessageId <= 0) {
|
|
80
|
+
finish();
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
entry.dialogMessageId = dialogMessageId;
|
|
84
|
+
entry.phase = "input";
|
|
85
|
+
return view(entry);
|
|
86
|
+
},
|
|
87
|
+
select(action) {
|
|
88
|
+
if (!isCurrent() || entry.phase !== "input")
|
|
89
|
+
return { kind: "expired" };
|
|
90
|
+
if (action === "cancel")
|
|
91
|
+
finish();
|
|
92
|
+
else {
|
|
93
|
+
entry.phase = "consumed";
|
|
94
|
+
entry.consumedBy = claim;
|
|
95
|
+
}
|
|
96
|
+
return { kind: action };
|
|
97
|
+
},
|
|
98
|
+
consumeName(text) {
|
|
99
|
+
if (!isCurrent() || entry.phase !== "input")
|
|
100
|
+
return { kind: "none" };
|
|
101
|
+
const name = text.trim();
|
|
102
|
+
if (!name)
|
|
103
|
+
return { kind: "empty" };
|
|
104
|
+
entry.phase = "consumed";
|
|
105
|
+
entry.consumedBy = claim;
|
|
106
|
+
return { kind: "name", name };
|
|
107
|
+
},
|
|
108
|
+
reopen() {
|
|
109
|
+
if (!isCurrent() ||
|
|
110
|
+
entry.phase !== "consumed" ||
|
|
111
|
+
entry.consumedBy !== claim) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
// A failed effect may restore input, never renew its deadline or issued handle.
|
|
115
|
+
const replacement = {
|
|
116
|
+
...entry,
|
|
117
|
+
phase: "input",
|
|
118
|
+
consumedBy: undefined,
|
|
119
|
+
};
|
|
120
|
+
candidates.set(key, replacement);
|
|
121
|
+
ended = true;
|
|
122
|
+
return view(replacement);
|
|
123
|
+
},
|
|
124
|
+
finish,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
19
127
|
const current = (scope, target) => {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
128
|
+
const entry = candidates.get(targetKey(target));
|
|
129
|
+
if (!entry ||
|
|
130
|
+
entry.scope !== scope ||
|
|
131
|
+
entry.phase !== "input" ||
|
|
132
|
+
!sameTarget(entry.target, target) ||
|
|
133
|
+
!createLifetime(entry).isCurrent())
|
|
24
134
|
return undefined;
|
|
25
|
-
|
|
26
|
-
|
|
135
|
+
return entry;
|
|
136
|
+
};
|
|
137
|
+
const capture = (input) => {
|
|
138
|
+
const { scope, dialogMessageId } = input;
|
|
139
|
+
const target = { ...input.target };
|
|
140
|
+
const entry = current(scope, target);
|
|
141
|
+
if (!entry || entry.dialogMessageId !== dialogMessageId)
|
|
27
142
|
return undefined;
|
|
28
|
-
|
|
29
|
-
return candidate;
|
|
143
|
+
return createLifetime(entry);
|
|
30
144
|
};
|
|
31
145
|
return {
|
|
146
|
+
prepare(input) {
|
|
147
|
+
const scope = input.scope;
|
|
148
|
+
const target = { ...input.target };
|
|
149
|
+
const isCurrent = input.isCurrent, assertAuthority = input.assertAuthority;
|
|
150
|
+
const key = targetKey(target);
|
|
151
|
+
const previous = candidates.get(key);
|
|
152
|
+
const entry = {
|
|
153
|
+
scope,
|
|
154
|
+
target,
|
|
155
|
+
phase: "delivery",
|
|
156
|
+
expiresAtMs: nowMs() + ttlMs,
|
|
157
|
+
isCurrent,
|
|
158
|
+
assertAuthority,
|
|
159
|
+
};
|
|
160
|
+
if (!recipientCurrent(entry) ||
|
|
161
|
+
entry.expiresAtMs <= nowMs() ||
|
|
162
|
+
candidates.get(key) !== previous)
|
|
163
|
+
return undefined;
|
|
164
|
+
candidates.set(key, entry);
|
|
165
|
+
return createLifetime(entry);
|
|
166
|
+
},
|
|
167
|
+
capture,
|
|
32
168
|
open(input) {
|
|
33
|
-
const
|
|
169
|
+
const entry = {
|
|
34
170
|
scope: input.scope,
|
|
35
171
|
target: { ...input.target },
|
|
36
|
-
dialogMessageId: input.dialogMessageId,
|
|
37
172
|
phase: "input",
|
|
173
|
+
dialogMessageId: input.dialogMessageId,
|
|
38
174
|
expiresAtMs: nowMs() + ttlMs,
|
|
39
175
|
};
|
|
40
|
-
candidates.set(targetKey(
|
|
41
|
-
return
|
|
176
|
+
candidates.set(targetKey(entry.target), entry);
|
|
177
|
+
return view(entry);
|
|
42
178
|
},
|
|
43
179
|
select(input) {
|
|
44
|
-
const
|
|
45
|
-
|
|
180
|
+
const action = input.action;
|
|
181
|
+
const lifetime = capture(input);
|
|
182
|
+
if (!lifetime)
|
|
46
183
|
return { kind: "expired" };
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return
|
|
184
|
+
const result = lifetime.select(action);
|
|
185
|
+
lifetime.finish();
|
|
186
|
+
return result;
|
|
50
187
|
},
|
|
51
188
|
consumeName(input) {
|
|
52
|
-
const
|
|
53
|
-
|
|
189
|
+
const text = input.text;
|
|
190
|
+
const entry = current(input.scope, input.target);
|
|
191
|
+
if (!entry)
|
|
54
192
|
return { kind: "none" };
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return
|
|
193
|
+
const lifetime = createLifetime(entry);
|
|
194
|
+
const result = lifetime.consumeName(text);
|
|
195
|
+
if (result.kind === "name")
|
|
196
|
+
lifetime.finish();
|
|
197
|
+
return result;
|
|
60
198
|
},
|
|
61
199
|
clearScope(scope) {
|
|
62
|
-
for (const [key,
|
|
63
|
-
if (
|
|
200
|
+
for (const [key, entry] of candidates) {
|
|
201
|
+
if (entry.scope === scope)
|
|
64
202
|
candidates.delete(key);
|
|
65
203
|
}
|
|
66
204
|
},
|
|
67
205
|
inspect(target) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
206
|
+
const key = targetKey(target);
|
|
207
|
+
const entry = candidates.get(key);
|
|
208
|
+
if (!entry)
|
|
209
|
+
return undefined;
|
|
210
|
+
if (entry.expiresAtMs <= nowMs()) {
|
|
211
|
+
if (candidates.get(key) === entry)
|
|
212
|
+
candidates.delete(key);
|
|
71
213
|
return undefined;
|
|
72
214
|
}
|
|
73
|
-
|
|
215
|
+
if (entry.phase !== "input" || !createLifetime(entry).isCurrent())
|
|
216
|
+
return undefined;
|
|
217
|
+
return view(entry);
|
|
74
218
|
},
|
|
75
219
|
};
|
|
76
220
|
}
|
|
77
|
-
function hashString(value) {
|
|
78
|
-
let hash = 2166136261;
|
|
79
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
80
|
-
hash ^= value.charCodeAt(index);
|
|
81
|
-
hash = Math.imul(hash, 16777619);
|
|
82
|
-
}
|
|
83
|
-
return hash >>> 0;
|
|
84
|
-
}
|
|
85
|
-
function getWorkspaceHint(cwd) {
|
|
86
|
-
if (!cwd)
|
|
87
|
-
return undefined;
|
|
88
|
-
const parts = cwd.split("/").filter(Boolean);
|
|
89
|
-
const last = parts.at(-1)?.trim();
|
|
90
|
-
if (!last)
|
|
91
|
-
return undefined;
|
|
92
|
-
return (last
|
|
93
|
-
.replace(/[^\p{L}\p{N}._-]+/gu, " ")
|
|
94
|
-
.trim()
|
|
95
|
-
.slice(0, 32) || undefined);
|
|
96
|
-
}
|
|
97
|
-
export function createTelegramThreadName(input) {
|
|
98
|
-
const workspace = getWorkspaceHint(input.cwd);
|
|
99
|
-
const roleMark = input.role === "leader"
|
|
100
|
-
? "Leader"
|
|
101
|
-
: input.role === "follower"
|
|
102
|
-
? "Follower"
|
|
103
|
-
: undefined;
|
|
104
|
-
const slot = input.slot ? `Thread ${input.slot}` : undefined;
|
|
105
|
-
const peerSalt = input.peers?.slice().sort().join("|") ?? "";
|
|
106
|
-
const fallback = `Instance ${hashString(`${input.seed}|${input.cwd ?? ""}|${input.role ?? ""}|${peerSalt}|${input.slot ?? ""}`)
|
|
107
|
-
.toString(36)
|
|
108
|
-
.slice(0, 4)}`;
|
|
109
|
-
return ([slot, workspace, roleMark].filter(Boolean).join(" ").slice(0, 96) ||
|
|
110
|
-
fallback);
|
|
111
|
-
}
|
|
112
221
|
export function normalizeTelegramTopicTargetThreadName(threadName) {
|
|
113
222
|
return threadName.replace(/\s+/g, " ").trim().slice(0, 96);
|
|
114
223
|
}
|
|
@@ -126,15 +126,6 @@ export type ThreadReconciliationAction = {
|
|
|
126
126
|
instanceId?: string;
|
|
127
127
|
messageId?: number;
|
|
128
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
129
|
} | {
|
|
139
130
|
kind: "close-delete-graceful-shutdown-topic";
|
|
140
131
|
target: TelegramTarget & {
|
|
@@ -220,6 +211,14 @@ export interface ThreadReconciliationInput {
|
|
|
220
211
|
previousState?: ThreadReconciliationMachineState;
|
|
221
212
|
freshCreationGraceMs?: number;
|
|
222
213
|
}
|
|
214
|
+
/** A Thread record still claims its slot/target while active, starting or pending. */
|
|
215
|
+
export declare function isCurrentThreadRecord(record: {
|
|
216
|
+
readonly status: string;
|
|
217
|
+
}): boolean;
|
|
218
|
+
/** Pure Bot API vocabulary for a deleted/missing Thread; callers own their HTTP-status policy. */
|
|
219
|
+
export declare function isTelegramTopicDeletedErrorMessage(message: string): boolean;
|
|
220
|
+
/** Pure Bot API vocabulary for a closed Thread; closure is not deletion evidence. */
|
|
221
|
+
export declare function isTelegramTopicClosedErrorMessage(message: string): boolean;
|
|
223
222
|
export interface ThreadReconciliationRuntime {
|
|
224
223
|
getState: () => ThreadReconciliationMachineState | undefined;
|
|
225
224
|
recordPlan: (plan: ThreadReconciliationPlan) => void;
|
|
@@ -229,13 +228,40 @@ export declare function createThreadReconciliationRuntime(deps: {
|
|
|
229
228
|
recordRuntimeEvent: (category: string, message: unknown, details?: Record<string, unknown>) => void;
|
|
230
229
|
scheduleSnapshotPersist: () => void;
|
|
231
230
|
}): ThreadReconciliationRuntime;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
231
|
+
/** Momentary preparation evidence only: not an action, lease, issued effect or reusable deletion grant. */
|
|
232
|
+
export interface TelegramLiveRebindCleanupPreparation {
|
|
233
|
+
status: "prepared";
|
|
234
|
+
operationId: string;
|
|
235
|
+
target: ThreadTarget;
|
|
236
|
+
recipientTarget: ThreadTarget;
|
|
237
|
+
leaderEpoch: string;
|
|
238
|
+
}
|
|
239
|
+
export declare function prepareLiveRebindThreadCleanup(input: {
|
|
240
|
+
operationId: string;
|
|
241
|
+
oldTarget: ThreadTarget;
|
|
242
|
+
recipientTarget: ThreadTarget;
|
|
243
|
+
leaderEpoch: string;
|
|
244
|
+
targetProtected: boolean;
|
|
245
|
+
/** Narrow body-free current-work projection; no cached idle or historical disposition proof. */
|
|
246
|
+
work: Readonly<{
|
|
247
|
+
sessionBusy: boolean;
|
|
248
|
+
targetWork: boolean;
|
|
249
|
+
deliveryPending: boolean;
|
|
250
|
+
unknown: boolean;
|
|
251
|
+
}> | undefined;
|
|
252
|
+
}): TelegramLiveRebindCleanupPreparation | undefined;
|
|
253
|
+
export type TelegramLiveRebindCleanupOutcome = "confirmed" | "failed" | "unknown";
|
|
254
|
+
/**
|
|
255
|
+
* One unretried deletion for an already issued live-rebind cleanup grant. Success or confirmed absence is `confirmed`;
|
|
256
|
+
* protection or authority loss before the request and a definite request rejection are `failed`; every other outcome is
|
|
257
|
+
* `unknown`. The caller owns the durable issue marker and terminal record; this never repeats or rolls back.
|
|
258
|
+
*/
|
|
259
|
+
export declare function issueLiveRebindThreadCleanup(prepared: TelegramLiveRebindCleanupPreparation, ports: {
|
|
260
|
+
/** Throws when the issuer's current recipient/work/ownership authority no longer holds. */
|
|
261
|
+
assertCurrent(): void;
|
|
262
|
+
deleteTopic(target: ThreadTarget): Promise<unknown>;
|
|
263
|
+
classifyFailure(error: unknown): "absent" | "rejected" | "unknown";
|
|
264
|
+
}): Promise<TelegramLiveRebindCleanupOutcome>;
|
|
239
265
|
export declare function applyThreadReconciliationPlan(plan: ThreadReconciliationPlan, ports: ThreadReconciliationApplyPorts): Promise<ThreadReconciliationApplyResult>;
|
|
240
266
|
export declare function planThreadReconciliation(input: ThreadReconciliationInput): ThreadReconciliationPlan;
|
|
241
267
|
export {};
|