@llblab/pi-kit 0.6.0 → 0.7.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +4 -4
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-actors/README.md +1 -1
  6. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
  7. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
  13. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
  19. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
  20. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  21. package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
  22. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
  23. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
  24. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
  25. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
  26. package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
  27. package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
  28. package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
  29. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
  30. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
  31. package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
  32. package/node_modules/@llblab/pi-actors/package.json +1 -1
  33. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  34. package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
  35. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  36. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  37. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  38. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  39. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  40. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  41. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  42. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  43. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  44. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  45. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  46. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  47. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  50. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  51. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  55. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  56. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  59. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  63. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  64. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  65. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  66. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  68. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  69. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  70. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  71. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  72. package/node_modules/@llblab/pi-telegram/AGENTS.md +13 -8
  73. package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -3
  74. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  75. package/node_modules/@llblab/pi-telegram/README.md +12 -8
  76. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  77. package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
  78. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  79. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  80. package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -6
  81. package/node_modules/@llblab/pi-telegram/docs/public-api.md +12 -5
  82. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  83. package/node_modules/@llblab/pi-telegram/index.ts +4 -1418
  84. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  85. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +28 -1
  86. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  87. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  88. package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
  89. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
  90. package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
  91. package/node_modules/@llblab/pi-telegram/lib/config.ts +177 -24
  92. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
  93. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  94. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  95. package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
  96. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
  97. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +51 -10
  98. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  99. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  100. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
  101. package/node_modules/@llblab/pi-telegram/lib/queue.ts +52 -18
  102. package/node_modules/@llblab/pi-telegram/lib/replies.ts +2 -2
  103. package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
  104. package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
  105. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  106. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +279 -4
  107. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  108. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  109. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  110. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  111. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  112. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  113. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  114. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  115. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  116. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  117. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  118. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  119. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  120. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  121. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  122. package/node_modules/@llblab/skills/package.json +2 -3
  123. package/package.json +6 -5
@@ -0,0 +1,226 @@
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
+ import type { TelegramThreadDisplayMode } from "./config.ts";
9
+ import type {
10
+ TelegramTopicTargetStore,
11
+ TelegramWorkspaceDisplayBinding,
12
+ TelegramWorkspaceThreadBinding,
13
+ } from "./threads.ts";
14
+ import type { TelegramApiCallOptions } from "./telegram-api.ts";
15
+
16
+ function labelText(value: string): string {
17
+ return value.replace(/\s+/gu, " ").trim();
18
+ }
19
+
20
+ function boundedLabel(base: string, suffix = ""): string {
21
+ let prefix = "";
22
+ for (const character of base) {
23
+ if (prefix.length + character.length + suffix.length > 128) break;
24
+ prefix += character;
25
+ }
26
+ return `${prefix}${suffix}`;
27
+ }
28
+
29
+ function directoryLabel(cwd: string, directories: readonly string[]): string {
30
+ const parts = cwd.split("/").filter(Boolean);
31
+ if (!parts.length) return "/";
32
+ for (let depth = 1; depth <= parts.length; depth++) {
33
+ const candidate = labelText(parts.slice(-depth).join("/"));
34
+ const collides = directories.some((other) => other !== cwd &&
35
+ labelText(other.split("/").filter(Boolean).slice(-depth).join("/")).toLowerCase() ===
36
+ candidate.toLowerCase(),
37
+ );
38
+ if (!collides) return candidate;
39
+ }
40
+ return labelText(cwd);
41
+ }
42
+
43
+ /** Missing or ambiguous metadata yields no label rather than inventing identity. */
44
+ export function resolveTelegramWorkspaceDisplayNames(
45
+ bindings: readonly TelegramWorkspaceDisplayBinding[],
46
+ mode: TelegramThreadDisplayMode,
47
+ ): Map<string, string> {
48
+ const labels = new Map<string, string>();
49
+ const directories = Array.from(new Set(bindings.map((binding) => binding.cwd)));
50
+ const directoryCounts = new Map<string, number>();
51
+ for (const binding of bindings) {
52
+ directoryCounts.set(binding.cwd, (directoryCounts.get(binding.cwd) ?? 0) + 1);
53
+ }
54
+ const bases = new Map<string, string>();
55
+ for (const binding of bindings) {
56
+ const slot = binding.slot && /^[A-Z]$/u.test(binding.slot) ? binding.slot : undefined;
57
+ const manualName = binding.manualThreadName
58
+ ? labelText(binding.manualThreadName)
59
+ : undefined;
60
+ if (manualName) {
61
+ labels.set(binding.bindingKey, boundedLabel(manualName));
62
+ } else if (mode === "letters") {
63
+ if (slot) labels.set(binding.bindingKey, slot);
64
+ } else if (mode === "names") {
65
+ const name = binding.threadName ? labelText(binding.threadName) : slot;
66
+ if (name) labels.set(binding.bindingKey, boundedLabel(name));
67
+ } else {
68
+ const base = directoryLabel(binding.cwd, directories);
69
+ bases.set(binding.bindingKey, base);
70
+ const showSuffix = binding.showSlotSuffix ||
71
+ (directoryCounts.get(binding.cwd) ?? 0) > 1;
72
+ if (showSuffix && !slot) continue;
73
+ labels.set(binding.bindingKey, boundedLabel(base, showSuffix ? `_${slot!.toLowerCase()}` : ""));
74
+ }
75
+ }
76
+ // Long or whitespace-normalized paths can collide even after qualification.
77
+ if (mode === "directories") {
78
+ const counts = new Map<string, number>();
79
+ for (const label of labels.values()) {
80
+ const key = label.toLowerCase();
81
+ counts.set(key, (counts.get(key) ?? 0) + 1);
82
+ }
83
+ for (const binding of bindings) {
84
+ const label = labels.get(binding.bindingKey);
85
+ if (binding.manualThreadName || !label ||
86
+ (counts.get(label.toLowerCase()) ?? 0) < 2) continue;
87
+ if (!binding.slot || !/^[A-Z]$/u.test(binding.slot)) {
88
+ labels.delete(binding.bindingKey);
89
+ continue;
90
+ }
91
+ labels.set(binding.bindingKey, boundedLabel(bases.get(binding.bindingKey)!,
92
+ `_${binding.slot.toLowerCase()}`));
93
+ }
94
+ }
95
+ const counts = new Map<string, number>();
96
+ for (const label of labels.values()) {
97
+ const key = label.toLowerCase();
98
+ counts.set(key, (counts.get(key) ?? 0) + 1);
99
+ }
100
+ for (const [key, label] of labels) {
101
+ if ((counts.get(label.toLowerCase()) ?? 0) > 1) labels.delete(key);
102
+ }
103
+ return labels;
104
+ }
105
+
106
+ export function resolveTelegramInitialWorkspaceDisplayName(input: {
107
+ bindings: readonly TelegramWorkspaceDisplayBinding[];
108
+ binding: TelegramWorkspaceDisplayBinding;
109
+ mode: TelegramThreadDisplayMode;
110
+ preserveRetainedManualName?: boolean;
111
+ }): string | undefined {
112
+ const retained = input.bindings.find((binding) =>
113
+ binding.bindingKey === input.binding.bindingKey,
114
+ );
115
+ const binding = retained
116
+ ? {
117
+ ...input.binding,
118
+ ...(retained.showSlotSuffix ? { showSlotSuffix: true as const } : {}),
119
+ ...(input.preserveRetainedManualName !== false && retained.manualThreadName
120
+ ? { manualThreadName: retained.manualThreadName }
121
+ : {}),
122
+ }
123
+ : input.binding;
124
+ return resolveTelegramWorkspaceDisplayNames([
125
+ ...input.bindings.filter((candidate) =>
126
+ candidate.bindingKey !== binding.bindingKey,
127
+ ),
128
+ binding,
129
+ ], input.mode).get(binding.bindingKey);
130
+ }
131
+
132
+ export async function applyTelegramThreadDisplaySetting(
133
+ mode: TelegramThreadDisplayMode,
134
+ deps: {
135
+ getProfileKey(): string | undefined;
136
+ ownsLeader(): boolean;
137
+ getLeaderSetter(): ((mode: TelegramThreadDisplayMode) => Promise<void>) | undefined;
138
+ getFollowerSetter(): ((mode: TelegramThreadDisplayMode) => Promise<void>) | undefined;
139
+ reloadConfig(): Promise<void>;
140
+ },
141
+ ): Promise<void> {
142
+ const profile = deps.getProfileKey();
143
+ const setter = deps.ownsLeader() ? deps.getLeaderSetter() : deps.getFollowerSetter();
144
+ if (!setter) throw new Error("Thread display settings require a connected compatible instance.");
145
+ await setter(mode);
146
+ if (deps.getProfileKey() !== profile) throw new Error("Telegram Thread display setting changed profile.");
147
+ await deps.reloadConfig();
148
+ if (deps.getProfileKey() !== profile) throw new Error("Telegram Thread display setting changed profile.");
149
+ }
150
+
151
+ export interface TelegramThreadDisplayReconcilerDeps {
152
+ store: Pick<TelegramTopicTargetStore,
153
+ "listWorkspaceBindings" | "setWorkspaceDisplayTitle" | "persist">;
154
+ getMode(): TelegramThreadDisplayMode;
155
+ getProfileKey(): string;
156
+ getLeaderEpoch(): string | number | undefined;
157
+ captureBindingAuthority(binding: TelegramWorkspaceThreadBinding): (() => boolean) | undefined;
158
+ callApi<TResponse>(
159
+ method: string,
160
+ body: Record<string, unknown>,
161
+ options?: TelegramApiCallOptions,
162
+ ): Promise<TResponse>;
163
+ }
164
+
165
+ /** Caller owns triggering and reporting; no timer or background retry is created. */
166
+ export function createTelegramThreadDisplayReconciler(
167
+ deps: TelegramThreadDisplayReconcilerDeps,
168
+ ): { reconcile(): Promise<{ changed: number }> } {
169
+ let tail: Promise<unknown> = Promise.resolve();
170
+ const apply = async (): Promise<{ changed: number }> => {
171
+ const epoch = deps.getLeaderEpoch();
172
+ const profile = deps.getProfileKey();
173
+ const mode = deps.getMode();
174
+ const assertAuthority = (): void => {
175
+ if (epoch === undefined || deps.getLeaderEpoch() !== epoch ||
176
+ deps.getProfileKey() !== profile || deps.getMode() !== mode) {
177
+ throw new Error("Telegram Thread display update lost profile, mode, or leader authority.");
178
+ }
179
+ };
180
+ assertAuthority();
181
+ const bindings = deps.store.listWorkspaceBindings();
182
+ const titles = resolveTelegramWorkspaceDisplayNames(bindings, mode);
183
+ let changed = 0;
184
+ for (const binding of bindings) {
185
+ const isBindingCurrent = deps.captureBindingAuthority(binding);
186
+ if (!isBindingCurrent) continue;
187
+ const title = titles.get(binding.bindingKey);
188
+ if (!title) throw new Error("Telegram Thread display identity is missing or ambiguous.");
189
+ const assertBinding = (expected: TelegramWorkspaceThreadBinding): void => {
190
+ assertAuthority();
191
+ const current = deps.store.listWorkspaceBindings().find((candidate) =>
192
+ candidate.bindingKey === expected.bindingKey,
193
+ );
194
+ if (!isDeepStrictEqual(current, expected) || !isBindingCurrent()) {
195
+ throw new Error("Telegram Thread display binding changed.");
196
+ }
197
+ };
198
+ assertBinding(binding);
199
+ if ((binding.displayTitle ?? binding.threadName) === title) continue;
200
+ await deps.callApi("editForumTopic", {
201
+ chat_id: binding.target.chatId,
202
+ message_thread_id: binding.target.threadId,
203
+ name: title,
204
+ }, { maxAttempts: 1 });
205
+ assertBinding(binding);
206
+ if (!deps.store.setWorkspaceDisplayTitle(binding, title)) {
207
+ throw new Error("Telegram Thread display binding changed before title commit.");
208
+ }
209
+ await deps.store.persist();
210
+ assertBinding({ ...binding, displayTitle: title });
211
+ changed++;
212
+ }
213
+ // A prior acknowledged edit may still have dirty metadata after a failed persist.
214
+ assertAuthority();
215
+ if (changed === 0) await deps.store.persist();
216
+ assertAuthority();
217
+ return { changed };
218
+ };
219
+ return {
220
+ reconcile() {
221
+ const run = tail.then(apply);
222
+ tail = run.catch(() => undefined);
223
+ return run;
224
+ },
225
+ };
226
+ }
@@ -0,0 +1,118 @@
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
+
8
+ import type { TelegramTarget } from "./target.ts";
9
+
10
+ export const TELEGRAM_THREAD_NAME_DIALOG_TTL_MS = 5 * 60_000;
11
+
12
+ export type TelegramThreadNameDialogAction = "reset" | "cancel";
13
+
14
+ export interface TelegramThreadNameDialogCandidate {
15
+ scope: string;
16
+ target: TelegramTarget;
17
+ dialogMessageId: number;
18
+ phase: "input";
19
+ expiresAtMs: number;
20
+ }
21
+
22
+ function targetKey(target: TelegramTarget): string {
23
+ return `${target.chatId}:${target.threadId ?? "chat"}`;
24
+ }
25
+
26
+ function sameTarget(left: TelegramTarget, right: TelegramTarget): boolean {
27
+ return left.chatId === right.chatId && left.threadId === right.threadId;
28
+ }
29
+
30
+ function cloneCandidate(
31
+ candidate: TelegramThreadNameDialogCandidate,
32
+ ): TelegramThreadNameDialogCandidate {
33
+ return { ...candidate, target: { ...candidate.target } };
34
+ }
35
+
36
+ export function createTelegramThreadNameDialogRuntime(options?: {
37
+ ttlMs?: number;
38
+ nowMs?: () => number;
39
+ }): {
40
+ open(input: {
41
+ scope: string;
42
+ target: TelegramTarget;
43
+ dialogMessageId: number;
44
+ }): TelegramThreadNameDialogCandidate;
45
+ select(input: {
46
+ scope: string;
47
+ target: TelegramTarget;
48
+ dialogMessageId: number;
49
+ action: TelegramThreadNameDialogAction;
50
+ }): { kind: "reset" | "cancel" | "expired" };
51
+ consumeName(input: {
52
+ scope: string;
53
+ target: TelegramTarget;
54
+ text: string;
55
+ }): { kind: "name"; name: string } | { kind: "none" | "empty" };
56
+ clearScope(scope: string): void;
57
+ inspect(target: TelegramTarget): TelegramThreadNameDialogCandidate | undefined;
58
+ } {
59
+ const ttlMs = options?.ttlMs ?? TELEGRAM_THREAD_NAME_DIALOG_TTL_MS;
60
+ const nowMs = options?.nowMs ?? Date.now;
61
+ const candidates = new Map<string, TelegramThreadNameDialogCandidate>();
62
+ const current = (
63
+ scope: string,
64
+ target: TelegramTarget,
65
+ ): TelegramThreadNameDialogCandidate | undefined => {
66
+ const key = targetKey(target);
67
+ const candidate = candidates.get(key);
68
+ if (!candidate || candidate.scope !== scope ||
69
+ !sameTarget(candidate.target, target)) return undefined;
70
+ if (candidate.expiresAtMs <= nowMs()) {
71
+ candidates.delete(key);
72
+ return undefined;
73
+ }
74
+ return candidate;
75
+ };
76
+ return {
77
+ open(input) {
78
+ const candidate: TelegramThreadNameDialogCandidate = {
79
+ scope: input.scope,
80
+ target: { ...input.target },
81
+ dialogMessageId: input.dialogMessageId,
82
+ phase: "input",
83
+ expiresAtMs: nowMs() + ttlMs,
84
+ };
85
+ candidates.set(targetKey(input.target), candidate);
86
+ return cloneCandidate(candidate);
87
+ },
88
+ select(input) {
89
+ const candidate = current(input.scope, input.target);
90
+ if (!candidate || candidate.dialogMessageId !== input.dialogMessageId) {
91
+ return { kind: "expired" };
92
+ }
93
+ candidates.delete(targetKey(input.target));
94
+ return { kind: input.action };
95
+ },
96
+ consumeName(input) {
97
+ const candidate = current(input.scope, input.target);
98
+ if (!candidate || candidate.phase !== "input") return { kind: "none" };
99
+ const name = input.text.trim();
100
+ if (!name) return { kind: "empty" };
101
+ candidates.delete(targetKey(input.target));
102
+ return { kind: "name", name };
103
+ },
104
+ clearScope(scope) {
105
+ for (const [key, candidate] of candidates) {
106
+ if (candidate.scope === scope) candidates.delete(key);
107
+ }
108
+ },
109
+ inspect(target) {
110
+ const candidate = candidates.get(targetKey(target));
111
+ if (!candidate || candidate.expiresAtMs <= nowMs()) {
112
+ candidates.delete(targetKey(target));
113
+ return undefined;
114
+ }
115
+ return cloneCandidate(candidate);
116
+ },
117
+ };
118
+ }