@llblab/pi-telegram 0.54.4 → 0.55.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 (80) hide show
  1. package/AGENTS.md +4 -2
  2. package/BACKLOG.md +1 -1
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +5 -3
  5. package/dist/lib/activity.d.ts +5 -1
  6. package/dist/lib/activity.js +31 -2
  7. package/dist/lib/bindings.d.ts +3 -1
  8. package/dist/lib/bindings.js +8 -2
  9. package/dist/lib/bus-api.d.ts +2 -2
  10. package/dist/lib/bus-api.js +7 -2
  11. package/dist/lib/bus-follower.d.ts +49 -3
  12. package/dist/lib/bus-follower.js +313 -146
  13. package/dist/lib/bus-leader.d.ts +3 -0
  14. package/dist/lib/bus-leader.js +99 -18
  15. package/dist/lib/bus.d.ts +5 -3
  16. package/dist/lib/bus.js +13 -10
  17. package/dist/lib/delivery.d.ts +4 -0
  18. package/dist/lib/delivery.js +48 -15
  19. package/dist/lib/extension.js +48 -6
  20. package/dist/lib/lifecycle.d.ts +68 -0
  21. package/dist/lib/lifecycle.js +178 -58
  22. package/dist/lib/machine.d.ts +23 -0
  23. package/dist/lib/machine.js +37 -0
  24. package/dist/lib/media.d.ts +114 -28
  25. package/dist/lib/media.js +292 -119
  26. package/dist/lib/model.d.ts +54 -15
  27. package/dist/lib/model.js +98 -53
  28. package/dist/lib/polling.js +2 -0
  29. package/dist/lib/preview.d.ts +128 -113
  30. package/dist/lib/preview.js +448 -366
  31. package/dist/lib/queue.d.ts +2 -2
  32. package/dist/lib/queue.js +3 -3
  33. package/dist/lib/routing.d.ts +6 -1
  34. package/dist/lib/routing.js +334 -210
  35. package/dist/lib/runtime.d.ts +77 -11
  36. package/dist/lib/runtime.js +174 -92
  37. package/dist/lib/sections.js +0 -1
  38. package/dist/lib/telegram-api.d.ts +11 -9
  39. package/dist/lib/telegram-api.js +53 -4
  40. package/dist/lib/text-groups.d.ts +0 -26
  41. package/dist/lib/text-groups.js +51 -98
  42. package/dist/lib/thread-display.js +14 -6
  43. package/dist/lib/threads.d.ts +18 -5
  44. package/dist/lib/threads.js +107 -30
  45. package/dist/lib/turns.d.ts +2 -0
  46. package/dist/lib/turns.js +6 -1
  47. package/dist/lib/updates.d.ts +11 -4
  48. package/dist/lib/updates.js +54 -19
  49. package/dist/package.json +1 -1
  50. package/docs/activity.md +1 -1
  51. package/docs/architecture.md +12 -11
  52. package/docs/delivery.md +7 -5
  53. package/docs/multi-instance-bus.md +18 -8
  54. package/docs/public-api.md +2 -2
  55. package/docs/ui-style.md +2 -2
  56. package/lib/activity.ts +29 -5
  57. package/lib/bindings.ts +9 -1
  58. package/lib/bus-api.ts +11 -3
  59. package/lib/bus-follower.ts +417 -203
  60. package/lib/bus-leader.ts +88 -14
  61. package/lib/bus.ts +17 -15
  62. package/lib/delivery.ts +47 -19
  63. package/lib/extension.ts +51 -9
  64. package/lib/lifecycle.ts +229 -55
  65. package/lib/machine.ts +60 -0
  66. package/lib/media.ts +438 -172
  67. package/lib/model.ts +169 -74
  68. package/lib/polling.ts +2 -0
  69. package/lib/preview.ts +715 -566
  70. package/lib/queue.ts +5 -5
  71. package/lib/routing.ts +325 -238
  72. package/lib/runtime.ts +260 -104
  73. package/lib/sections.ts +0 -2
  74. package/lib/telegram-api.ts +61 -8
  75. package/lib/text-groups.ts +75 -136
  76. package/lib/thread-display.ts +15 -4
  77. package/lib/threads.ts +120 -36
  78. package/lib/turns.ts +8 -1
  79. package/lib/updates.ts +58 -21
  80. package/package.json +1 -1
@@ -68,7 +68,24 @@ function parseTelegramApiTargetInteger(value) {
68
68
  ? parsed
69
69
  : undefined;
70
70
  }
71
- export function createTelegramApiTargetActivityRuntime() {
71
+ /** Optional drain hints are not transport outcomes or request authority. */
72
+ function captureApiDrainObserver(capture) {
73
+ try {
74
+ const notify = capture?.();
75
+ return () => {
76
+ try {
77
+ const emitted = notify?.();
78
+ if (emitted)
79
+ void Promise.resolve(emitted).catch(() => undefined);
80
+ }
81
+ catch { /* Observer failure is inert. */ }
82
+ };
83
+ }
84
+ catch {
85
+ return () => { };
86
+ }
87
+ }
88
+ export function createTelegramApiTargetActivityRuntime(deps = {}) {
72
89
  const pending = new Map();
73
90
  const pendingChats = new Map();
74
91
  const messageScopedMethods = new Set([
@@ -103,6 +120,7 @@ export function createTelegramApiTargetActivityRuntime() {
103
120
  else {
104
121
  pendingChats.set(chatId, (pendingChats.get(chatId) ?? 0) + 1);
105
122
  }
123
+ const notifyDrained = captureApiDrainObserver(deps.captureDrain);
106
124
  let completed = false;
107
125
  return () => {
108
126
  if (completed)
@@ -110,15 +128,19 @@ export function createTelegramApiTargetActivityRuntime() {
110
128
  completed = true;
111
129
  if (!key) {
112
130
  const count = pendingChats.get(chatId);
113
- if (!count || count <= 1)
131
+ if (!count || count <= 1) {
114
132
  pendingChats.delete(chatId);
133
+ notifyDrained();
134
+ }
115
135
  else
116
136
  pendingChats.set(chatId, count - 1);
117
137
  return;
118
138
  }
119
139
  const current = pending.get(key);
120
- if (!current || current.count <= 1)
140
+ if (!current || current.count <= 1) {
121
141
  pending.delete(key);
142
+ notifyDrained();
143
+ }
122
144
  else
123
145
  current.count -= 1;
124
146
  };
@@ -953,6 +975,7 @@ export function buildTelegramAnswerGuestQueryBody(guestQueryId, text, options) {
953
975
  export function createDefaultTelegramBridgeApiRuntime(deps) {
954
976
  const client = createTelegramApiClient(deps.getBotToken, {
955
977
  recordRuntimeEvent: deps.recordRuntimeEvent,
978
+ isThreadedMode: deps.isThreadedMode,
956
979
  });
957
980
  const admittedClient = deps.workspaceAdmission
958
981
  ? createTelegramApiWorkspaceAdmissionClient(client, deps.workspaceAdmission, {
@@ -1064,6 +1087,8 @@ export function createTelegramBridgeApiRuntime(deps) {
1064
1087
  request = Promise.resolve()
1065
1088
  .then(() => deps.client.call(method, body, {
1066
1089
  ...options,
1090
+ // Presence is replaceable, not a queued body to replay after its session/loop owner ends.
1091
+ ...(body.action === "typing" ? { maxAttempts: 1, retrySafety: "non-idempotent" } : {}),
1067
1092
  retryRateLimit: false,
1068
1093
  }))
1069
1094
  .then((result) => {
@@ -1164,7 +1189,7 @@ export function createTelegramBridgeApiRuntime(deps) {
1164
1189
  ...(options?.message_thread_id !== undefined
1165
1190
  ? { message_thread_id: options.message_thread_id }
1166
1191
  : {}),
1167
- }), "typing"),
1192
+ }, { assertAuthority: options?.assertAuthority }), "typing"),
1168
1193
  sendRecordVoiceAction: createTelegramChatActionSender((chatId, action, options) => callRecorded("sendChatAction", {
1169
1194
  chat_id: chatId,
1170
1195
  action,
@@ -1266,9 +1291,31 @@ export function createTelegramBridgeApiRuntime(deps) {
1266
1291
  export function createTelegramApiClient(getBotToken, options = {}) {
1267
1292
  const now = options.now ?? Date.now;
1268
1293
  const recordRuntimeEvent = options.recordRuntimeEvent;
1294
+ const isThreadedMode = options.isThreadedMode;
1295
+ const fenceOutput = (method, body, callOptions) => {
1296
+ if (!isThreadedMode || !/^(send[A-Z]|copyMessages?$|forwardMessages?$)/.test(method))
1297
+ return callOptions;
1298
+ const assertAuthority = () => {
1299
+ const chatId = typeof body.chat_id === "number" || typeof body.chat_id === "string"
1300
+ ? Number(body.chat_id) : undefined;
1301
+ const thread = body.message_thread_id;
1302
+ const threadId = typeof thread === "number" || (typeof thread === "string" && /^[1-9][0-9]*$/.test(thread))
1303
+ ? Number(thread) : undefined;
1304
+ // Business connections and channels are distinct from the bot's private-chat All namespace.
1305
+ if (isThreadedMode() && chatId !== undefined && Number.isSafeInteger(chatId) && chatId > 0 &&
1306
+ !(typeof body.business_connection_id === "string" && body.business_connection_id.length > 0) &&
1307
+ (threadId === undefined || !Number.isSafeInteger(threadId) || threadId <= 0))
1308
+ throw new Error("Telegram All is input-only in Threaded Mode; an explicit Thread target is required.");
1309
+ callOptions?.assertAuthority?.();
1310
+ };
1311
+ assertAuthority();
1312
+ return { ...callOptions, assertAuthority };
1313
+ };
1269
1314
  const draftRetryNotBeforeByTarget = new Map();
1270
1315
  return {
1271
1316
  call: async (method, body, options) => {
1317
+ body = { ...body };
1318
+ options = fenceOutput(method, body, options);
1272
1319
  const token = getBotToken();
1273
1320
  // Cooldown keys retain only the public bot-id prefix, not the credential.
1274
1321
  const botId = token?.match(/^(\d+):/)?.[1];
@@ -1316,6 +1363,8 @@ export function createTelegramApiClient(getBotToken, options = {}) {
1316
1363
  }
1317
1364
  },
1318
1365
  callMultipart: async (method, fields, fileField, filePath, fileName, options) => {
1366
+ fields = { ...fields };
1367
+ options = fenceOutput(method, fields, options);
1319
1368
  return callTelegramMultipart(getBotToken(), method, fields, fileField, filePath, fileName, options);
1320
1369
  },
1321
1370
  downloadFile: async (fileId, suggestedName, tempDir, options) => {
@@ -22,18 +22,6 @@ export interface TelegramTextGroupMessage {
22
22
  forward_from?: TelegramMessageUser;
23
23
  forward_sender_name?: string;
24
24
  }
25
- export interface TelegramTextGroupState<TMessage, TContext = unknown> {
26
- messages: TMessage[];
27
- context?: TContext;
28
- flushTimer?: ReturnType<typeof setTimeout>;
29
- dispatching?: boolean;
30
- suspended?: boolean;
31
- reschedule?: (delayMs?: number) => void;
32
- dispatchNow?: () => Promise<void>;
33
- dispatchPromise?: Promise<void>;
34
- dispatchLimit?: number;
35
- forwardPairCandidate?: TelegramForwardCommentBatchPosition;
36
- }
37
25
  export type TelegramForwardCommentBatchPosition = "comment" | "forward";
38
26
  export interface TelegramTextGroupController<TMessage, TContext = unknown> {
39
27
  prepareUpdateBatch: (updates: readonly {
@@ -65,20 +53,6 @@ export interface TelegramGroupedInputClearerDeps {
65
53
  clearMediaGroups: () => void;
66
54
  clearTextGroups: () => void;
67
55
  }
68
- export declare function queueTelegramTextGroupMessage<TMessage extends TelegramTextGroupMessage, TContext = unknown>(options: {
69
- message: TMessage;
70
- context: TContext;
71
- groups: Map<string, TelegramTextGroupState<TMessage, TContext>>;
72
- debounceMs: number;
73
- minSplitLength: number;
74
- setTimer: (callback: () => void, ms: number) => ReturnType<typeof setTimeout>;
75
- clearTimer: (timer: ReturnType<typeof setTimeout>) => void;
76
- dispatchMessages: (messages: TMessage[], ctx: TContext) => unknown | Promise<unknown>;
77
- forceStart?: boolean;
78
- dispatchImmediately?: boolean;
79
- forwardPairCandidate?: TelegramForwardCommentBatchPosition;
80
- delayMs?: number;
81
- }): boolean;
82
56
  export declare function createTelegramTextGroupController<TMessage extends TelegramTextGroupMessage, TContext = unknown>(options?: TelegramTextGroupControllerOptions): TelegramTextGroupController<TMessage, TContext>;
83
57
  export declare function createTelegramTextGroupDispatchRuntime<TMessage extends TelegramTextGroupMessage, TContext>(deps: {
84
58
  textGroups: TelegramTextGroupController<TMessage, TContext>;
@@ -4,7 +4,7 @@
4
4
  * Owns conservative delayed grouping for Telegram text messages that look like automatic long-message splits
5
5
  */
6
6
  import { setTimeout as waitForTimeout } from "node:timers/promises";
7
- import { createTelegramPendingGroupLifecycle, extractTelegramMessageText, } from "./media.js";
7
+ import { createTelegramPendingGroups, extractTelegramMessageText, } from "./media.js";
8
8
  const TELEGRAM_TEXT_GROUP_DEBOUNCE_MS = 1000;
9
9
  const TELEGRAM_TEXT_GROUP_MIN_SPLIT_LENGTH = 3600;
10
10
  const TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP = 12;
@@ -47,9 +47,9 @@ function canStartTelegramTextGroup(message, minSplitLength) {
47
47
  const text = extractTelegramTextGroupText(message);
48
48
  return text.length >= minSplitLength && !isTelegramTextGroupCommand(text);
49
49
  }
50
- function canAppendTelegramTextGroupMessage(state, message) {
50
+ function canAppendTelegramTextGroupMessage(group, message) {
51
51
  const text = extractTelegramTextGroupText(message);
52
- const previous = state.messages.at(-1);
52
+ const previous = group.messages.at(-1);
53
53
  return (!!previous &&
54
54
  message.message_id > previous.message_id &&
55
55
  message.message_id <=
@@ -57,83 +57,7 @@ function canAppendTelegramTextGroupMessage(state, message) {
57
57
  (text.length > 0 || isTelegramForwardedMessage(message)) &&
58
58
  !isTelegramTextGroupCommand(text));
59
59
  }
60
- export function queueTelegramTextGroupMessage(options) {
61
- const key = getTelegramTextGroupKey(options.message);
62
- if (!key)
63
- return false;
64
- const existing = options.groups.get(key);
65
- const duplicateIndex = existing?.messages.findIndex((message) => message.message_id === options.message.message_id);
66
- if (existing && duplicateIndex !== undefined && duplicateIndex >= 0) {
67
- existing.messages[duplicateIndex] = options.message;
68
- existing.context = options.context;
69
- existing.forwardPairCandidate = options.forwardPairCandidate;
70
- if (!existing.suspended && !existing.dispatching && !existing.flushTimer) {
71
- existing.reschedule?.();
72
- }
73
- return true;
74
- }
75
- if (!existing &&
76
- !options.forceStart &&
77
- !canStartTelegramTextGroup(options.message, options.minSplitLength))
78
- return false;
79
- if (existing && !canAppendTelegramTextGroupMessage(existing, options.message))
80
- return false;
81
- const state = existing ?? { messages: [] };
82
- state.messages.push(options.message);
83
- state.context = options.context;
84
- state.forwardPairCandidate = options.forwardPairCandidate;
85
- const dispatchQueued = () => {
86
- state.flushTimer = undefined;
87
- const queued = options.groups.get(key);
88
- if (!queued || queued.context === undefined)
89
- return Promise.resolve();
90
- if (queued.dispatching)
91
- return queued.dispatchPromise ?? Promise.resolve();
92
- const dispatchCount = queued.dispatchLimit ?? queued.messages.length;
93
- queued.dispatchLimit = undefined;
94
- const dispatchedMessages = queued.messages.slice(0, dispatchCount);
95
- const dispatchedIds = new Set(dispatchedMessages.map((message) => message.message_id));
96
- queued.dispatching = true;
97
- const operation = Promise.resolve(options.dispatchMessages(dispatchedMessages, queued.context)).then(() => {
98
- if (options.groups.get(key) !== queued)
99
- return;
100
- queued.messages = queued.messages.filter((message) => !dispatchedIds.has(message.message_id));
101
- queued.dispatching = false;
102
- queued.dispatchPromise = undefined;
103
- if (queued.messages.length === 0)
104
- options.groups.delete(key);
105
- else if (!queued.flushTimer)
106
- scheduleDispatch();
107
- }, (error) => {
108
- if (options.groups.get(key) === queued) {
109
- queued.dispatching = false;
110
- queued.dispatchPromise = undefined;
111
- if (!queued.flushTimer)
112
- scheduleDispatch();
113
- }
114
- throw error;
115
- });
116
- queued.dispatchPromise = operation;
117
- return operation;
118
- };
119
- const scheduleDispatch = (delayMs = options.debounceMs) => {
120
- if (state.suspended)
121
- return;
122
- state.flushTimer = options.setTimer(() => {
123
- void dispatchQueued().catch(() => undefined);
124
- }, delayMs);
125
- state.flushTimer.unref?.();
126
- };
127
- state.reschedule = scheduleDispatch;
128
- state.dispatchNow = dispatchQueued;
129
- if (state.flushTimer)
130
- options.clearTimer(state.flushTimer);
131
- scheduleDispatch(options.dispatchImmediately ? 0 : (options.delayMs ?? options.debounceMs));
132
- options.groups.set(key, state);
133
- return true;
134
- }
135
60
  export function createTelegramTextGroupController(options = {}) {
136
- const groups = new Map();
137
61
  const plannedForwardCommentStarts = new Set();
138
62
  const plannedForwardCommentEnds = new Set();
139
63
  const debounceMs = options.debounceMs ?? TELEGRAM_TEXT_GROUP_DEBOUNCE_MS;
@@ -155,7 +79,40 @@ export function createTelegramTextGroupController(options = {}) {
155
79
  : (timer) => {
156
80
  timer.abort();
157
81
  });
158
- const lifecycle = createTelegramPendingGroupLifecycle(groups, clearTimer);
82
+ const groups = createTelegramPendingGroups({ debounceMs, requireContext: true, setTimer, clearTimer });
83
+ /** Admission policy for one message; the shared group machine owns timers, batches and dispatch. */
84
+ const queueTextGroupMessage = (options) => {
85
+ const existing = groups.get(options.key);
86
+ const dispatchMessages = (messages, ctx) => options.dispatchMessages(messages, ctx);
87
+ const queued = {
88
+ key: options.key,
89
+ message: options.message,
90
+ context: options.context,
91
+ ...(options.forwardPairCandidate === undefined
92
+ ? {}
93
+ : { meta: options.forwardPairCandidate }),
94
+ dispatchMessages,
95
+ };
96
+ if (existing?.messages.some((message) => message.message_id === options.message.message_id)) {
97
+ groups.queue({ ...queued, delayMs: debounceMs, rearm: "ifIdle" });
98
+ return true;
99
+ }
100
+ if (!existing &&
101
+ !options.forceStart &&
102
+ !canStartTelegramTextGroup(options.message, minSplitLength))
103
+ return false;
104
+ if (existing &&
105
+ !canAppendTelegramTextGroupMessage(existing, options.message))
106
+ return false;
107
+ groups.queue({
108
+ ...queued,
109
+ delayMs: options.dispatchImmediately
110
+ ? 0
111
+ : (options.delayMs ?? debounceMs),
112
+ rearm: "always",
113
+ });
114
+ return true;
115
+ };
159
116
  return {
160
117
  prepareUpdateBatch(updates) {
161
118
  for (let index = 0; index + 1 < updates.length; index += 1) {
@@ -203,22 +160,18 @@ export function createTelegramTextGroupController(options = {}) {
203
160
  const forwarded = isTelegramForwardedMessage(message);
204
161
  const existing = key ? groups.get(key) : undefined;
205
162
  const text = extractTelegramTextGroupText(message);
206
- if (existing && isTelegramTextGroupClearingCommand(text)) {
207
- if (existing.flushTimer)
208
- clearTimer(existing.flushTimer);
209
- groups.delete(key);
210
- }
163
+ if (existing && isTelegramTextGroupClearingCommand(text))
164
+ groups.clearKey(key);
211
165
  const candidatePosition = forwarded
212
166
  ? "forward"
213
167
  : "comment";
214
- const existingCandidate = existing?.forwardPairCandidate;
168
+ const existingCandidate = existing?.meta;
215
169
  const pairCompleted = existingCandidate !== undefined &&
216
170
  existingCandidate !== candidatePosition;
217
171
  const separateFromCandidate = existingCandidate === candidatePosition &&
218
172
  !isTelegramTextGroupCommand(text);
219
- if (separateFromCandidate && existing) {
220
- existing.dispatchLimit = existing.messages.length;
221
- }
173
+ if (separateFromCandidate && existing)
174
+ groups.closeBatch(key);
222
175
  const forceStart = plannedStart ||
223
176
  (forwardCommentWaitMs !== false &&
224
177
  !!key &&
@@ -228,15 +181,13 @@ export function createTelegramTextGroupController(options = {}) {
228
181
  const dispatchImmediately = pairCompleted ||
229
182
  separateFromCandidate ||
230
183
  plannedForwardCommentEnds.delete(identity) ||
231
- (forwarded && !!key && groups.has(key));
232
- return queueTelegramTextGroupMessage({
184
+ (forwarded && !!key && groups.get(key) !== undefined);
185
+ if (!key)
186
+ return false;
187
+ return queueTextGroupMessage({
188
+ key,
233
189
  message,
234
190
  context,
235
- groups,
236
- debounceMs,
237
- minSplitLength,
238
- setTimer,
239
- clearTimer,
240
191
  dispatchMessages,
241
192
  forceStart,
242
193
  dispatchImmediately,
@@ -250,9 +201,11 @@ export function createTelegramTextGroupController(options = {}) {
250
201
  : undefined,
251
202
  });
252
203
  },
253
- ...lifecycle,
204
+ flushMessage: groups.flushMessage,
205
+ suspend: groups.suspend,
206
+ resume: groups.resume,
254
207
  clear: () => {
255
- lifecycle.clear();
208
+ groups.clear();
256
209
  plannedForwardCommentStarts.clear();
257
210
  plannedForwardCommentEnds.clear();
258
211
  },
@@ -4,7 +4,9 @@
4
4
  * Owns mode-specific labels and serialized, authority-fenced title application.
5
5
  * Excludes routing, name allocation, profile mutation, and live-owner discovery.
6
6
  */
7
+ import { homedir } from "node:os";
7
8
  import { isDeepStrictEqual } from "node:util";
9
+ import { normalizeTelegramWorkspacePath } from "./workspace-identity.js";
8
10
  function labelText(value) {
9
11
  return value.replace(/\s+/gu, " ").trim();
10
12
  }
@@ -20,14 +22,16 @@ function boundedLabel(base, suffix = "") {
20
22
  function directorySegments(cwd) {
21
23
  return cwd.split("/").filter(Boolean);
22
24
  }
23
- function distinguishingDirectorySegments(cwd, directories) {
24
- const parts = directorySegments(cwd);
25
+ function distinguishingDirectorySegments(cwd, directories, homeDirectory) {
26
+ // Substitute only exact home identity; qualification must still distinguish ordinary folders named home.
27
+ const segments = (directory) => directory === homeDirectory ? ["home"] : directorySegments(directory);
28
+ const parts = segments(cwd);
25
29
  if (!parts.length)
26
30
  return [];
27
31
  for (let depth = 1; depth <= parts.length; depth++) {
28
32
  const candidate = labelText(parts.slice(-depth).join("/"));
29
33
  const collides = directories.some((other) => other !== cwd &&
30
- labelText(directorySegments(other).slice(-depth).join("/")).toLowerCase() === candidate.toLowerCase());
34
+ labelText(segments(other).slice(-depth).join("/")).toLowerCase() === candidate.toLowerCase());
31
35
  if (!collides)
32
36
  return parts.slice(-depth);
33
37
  }
@@ -45,8 +49,8 @@ export function tokenizeTelegramDirectorySegment(segment) {
45
49
  .split(/[^\p{L}\p{N}]+|\u0000/gu)
46
50
  .filter(Boolean);
47
51
  }
48
- function formatDirectoryLabel(cwd, directories, mode) {
49
- const segments = distinguishingDirectorySegments(cwd, directories);
52
+ function formatDirectoryLabel(cwd, directories, mode, homeDirectory) {
53
+ const segments = distinguishingDirectorySegments(cwd, directories, homeDirectory);
50
54
  if (!segments.length)
51
55
  return "/";
52
56
  const formatted = segments
@@ -99,6 +103,10 @@ function countTelegramDisplayLabels(bindings, labels, mode, liveBindingKeys) {
99
103
  }
100
104
  export function resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBindingKeys = new Set()) {
101
105
  const labels = new Map();
106
+ const homeDirectory = (mode === "directory-snake" || mode === "directory-title") &&
107
+ (process.platform === "linux" || process.platform === "darwin")
108
+ ? normalizeTelegramWorkspacePath(homedir())
109
+ : undefined;
102
110
  const directories = Array.from(new Set(bindings.map((binding) => binding.cwd)));
103
111
  const liveDirectoryCounts = new Map();
104
112
  for (const binding of bindings) {
@@ -127,7 +135,7 @@ export function resolveTelegramWorkspaceDisplayNames(bindings, mode, liveBinding
127
135
  else {
128
136
  const base = mode === "directories"
129
137
  ? directoryLabel(binding.cwd, directories)
130
- : formatDirectoryLabel(binding.cwd, directories, mode);
138
+ : formatDirectoryLabel(binding.cwd, directories, mode, homeDirectory);
131
139
  bases.set(binding.bindingKey, base);
132
140
  const showSuffix = mode === "directories"
133
141
  ? binding.showSlotSuffix ||
@@ -248,6 +248,10 @@ export interface TelegramWorkspaceRestoreIntent {
248
248
  /** Live same-session metadata, never a legacy scoped ACK or permission to dispatch/delete. */
249
249
  export type TelegramWorkspaceLiveRebindIntent = Pick<TelegramWorkspaceRestoreIntent, "request" | "operatorUserId" | "executor" | "revision" | "createdAtMs" | "updatedAtMs"> & {
250
250
  kind: "live-rebind";
251
+ /** Versioned native input gate: terminal cleanup does not itself authorize or settle input. */
252
+ inputRelease?: "held" | "issued" | "accepted";
253
+ /** A session-replacing producer must not drop earlier old-target protection or carry its delete grant forward. */
254
+ oldTargetProtected?: true;
251
255
  recipient: TelegramWorkspaceRestoreRecipient;
252
256
  } & ({
253
257
  phase: "rebound";
@@ -324,9 +328,11 @@ export interface TelegramWorkspaceRestore {
324
328
  list(): TelegramWorkspaceRestoreIntent[];
325
329
  listLiveRebindings(): TelegramWorkspaceLiveRebindIntent[];
326
330
  /** Caller has saved and held the selected input; this publishes the binding, not dispatch readiness. */
327
- commitLiveRebind(request: TelegramWorkspaceRestoreRequest, recipient: TelegramWorkspaceRestoreRecipient, authority: TelegramWorkspaceRestoreAuthority): Promise<TelegramWorkspaceLiveRebindIntent | undefined>;
331
+ commitLiveRebind(request: TelegramWorkspaceRestoreRequest, recipient: TelegramWorkspaceRestoreRecipient, authority: TelegramWorkspaceRestoreAuthority, options?: {
332
+ holdInput: true;
333
+ }): Promise<TelegramWorkspaceLiveRebindIntent | undefined>;
328
334
  /** Caller proves local apply for release or current live clearance for cleanup; unknown issuance never repeats. */
329
- advanceLiveRebind(expected: TelegramWorkspaceLiveRebindIntent, step: "release" | "issue-cleanup" | Extract<TelegramWorkspaceLiveRebindIntent, {
335
+ advanceLiveRebind(expected: TelegramWorkspaceLiveRebindIntent, step: "release" | "issue-cleanup" | "issue-input" | "accept-input" | "protect-old-target" | Extract<TelegramWorkspaceLiveRebindIntent, {
330
336
  phase: "finished";
331
337
  }>["cleanup"], authority: TelegramWorkspaceRestoreAuthority): TelegramWorkspaceLiveRebindIntent | undefined;
332
338
  commit(request: TelegramWorkspaceRestoreRequest, authority: TelegramWorkspaceRestoreAuthority): Promise<TelegramWorkspaceRestoreIntent | undefined>;
@@ -359,8 +365,11 @@ export interface TelegramWorkspaceRestore {
359
365
  inspectTemporaryThreadTarget(expected: TelegramTemporaryThreadEntry, authority: TelegramWorkspaceRestoreAuthority): TelegramTemporaryThreadTargetObservation | undefined;
360
366
  /** Read-only veto for an already-granted in-flight attempt; never another grant, disposable classification or deletion ACK. */
361
367
  isTemporaryThreadCleanupCurrent(expected: TelegramTemporaryThreadEntry, authority: TelegramWorkspaceRestoreAuthority): boolean;
362
- /** Publishes `creating` before the caller's single creation request; an existing source entry is returned, never recreated. */
363
- reserveTemporaryThread(source: TelegramTemporaryThreadEntry["source"], token: string, authority: TelegramWorkspaceRestoreAuthority): {
368
+ /**
369
+ * Publishes `creating` before the caller's single creation request; an existing source entry is returned, never
370
+ * recreated. `group` records an album as the tab's one creating input; it must start at the source update.
371
+ */
372
+ reserveTemporaryThread(source: TelegramTemporaryThreadEntry["source"], token: string, authority: TelegramWorkspaceRestoreAuthority, group?: TelegramTemporaryThreadInput): {
364
373
  reserved: boolean;
365
374
  entry: TelegramTemporaryThreadEntry;
366
375
  } | undefined;
@@ -467,6 +476,8 @@ export interface TelegramTopicTargetStore {
467
476
  /** Exact synchronous live-operation snapshot, including a current publication frame; no mutation, lock or authority grant. */
468
477
  withWorkspaceLiveRebindSnapshot: (expected: TelegramWorkspaceLiveRebindIntent, observe: (snapshot: Readonly<Pick<TelegramTopicTargetFile, "threads" | "workspaceBindings">>) => undefined) => void;
469
478
  /** Current live-origin ownership veto; effect callers still require Workspace admission. False is not work/recipient/deletion authority. */
479
+ hasPreDispatchRebindEvidence: () => boolean;
480
+ listProtectedLiveRebindTargets: () => TelegramTarget[];
470
481
  isWorkspaceLiveRebindCleanupTargetProtected: (expected: TelegramWorkspaceLiveRebindIntent) => boolean;
471
482
  /** Read-only captured identity with an optional exact manual-name result fence; no transport/session/admission grant or lock. */
472
483
  captureWorkspaceThreadRenameObservation: (binding: TelegramWorkspaceThreadBinding, owner: TelegramTopicTargetRecord) => (expectedManualThreadName?: string) => boolean;
@@ -572,7 +583,7 @@ export interface TelegramTopicTargetStore {
572
583
  /** Claim the first reusable inactive thread for an instance, linking it to instanceId. */
573
584
  claimReusableTarget: (instanceId: string, threadName?: string) => TelegramTopicTargetRecord | undefined;
574
585
  }
575
- export declare function createTelegramCleanupTargetProtection(store: Pick<TelegramTopicTargetStore, "list"> & Partial<Pick<TelegramTopicTargetStore, "listReservations" | "listPendingProvisions" | "listPendingCleanups" | "listTemporaryThreadTargets">>, departingRecord?: TelegramTopicTargetRecord): NonNullable<ThreadReconciler.ThreadReconciliationApplyPorts["isCleanupTargetProtected"]>;
586
+ export declare function createTelegramCleanupTargetProtection(store: Pick<TelegramTopicTargetStore, "list"> & Partial<Pick<TelegramTopicTargetStore, "listReservations" | "listPendingProvisions" | "listPendingCleanups" | "listTemporaryThreadTargets" | "listProtectedLiveRebindTargets">>, departingRecord?: TelegramTopicTargetRecord): NonNullable<ThreadReconciler.ThreadReconciliationApplyPorts["isCleanupTargetProtected"]>;
576
587
  export interface TelegramTopicTargetStoreOptions {
577
588
  path: string | (() => string);
578
589
  telegramProfile?: string | (() => string | undefined);
@@ -661,6 +672,8 @@ export declare function getTelegramWorkspaceRestoreSourceCompletionSha256(operat
661
672
  export declare function getTelegramTemporaryThreadInputs(entry: TelegramTemporaryThreadEntry): TelegramTemporaryThreadInput[];
662
673
  /** Every known group is durably cancelled or Forward-completed; still not deletion authority by itself. */
663
674
  export declare function isTelegramTemporaryThreadFullyResolved(entry: TelegramTemporaryThreadEntry): boolean;
675
+ /** Canonical issuance is distinct from cleanup completion, source disposition and peer dispatch readiness. */
676
+ export declare function isTelegramLiveRebindInputAuthorized(value: TelegramWorkspaceLiveRebindIntent): boolean;
664
677
  /** The unified section accepts only lossless current-format Workspace evidence, never tolerant legacy repair. */
665
678
  export declare function parseTelegramWorkspaceStateSection(value: unknown, profile: string): TelegramTopicTargetFile | undefined;
666
679
  export declare function resolveTelegramWorkspaceProvisionRecoveryPath(statePath: string, profileName?: string, layout?: "consolidated"): string;