@yu_robotics/remote-cli-router 1.6.37 → 1.6.43

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/README.md CHANGED
@@ -11,6 +11,7 @@ The router server acts as a bridge between Feishu messaging and developer machin
11
11
  - **Code-change rendering** with collapsible, line-aware diff previews inside streaming cards
12
12
  - **Image message forwarding** for standalone images and mixed text-image posts to supported local backends, plus generated Codex images back to Feishu Card 2.0
13
13
  - **Client service management** is handled by the local CLI; the Router remains a separate long-running server process
14
+ - **Queued task cards** appear at execution time for capable clients, with the thread, workspace, task preview, and remaining queue count; older clients retain waiting cards
14
15
  - **WebSocket connections** from local clients
15
16
  - **Feishu long connection** for receiving and sending messages
16
17
 
@@ -127,7 +128,9 @@ curl https://your-domain.com/health
127
128
 
128
129
  ## Advanced Usage
129
130
 
130
- Use one Router for multiple trusted clients. Each client connects from the developer's machine, while the Router owns Feishu configuration, device bindings, routing, and thread switch state. A thread can override its backend with `/backend <index> @`; `/backend <index>` changes the global backend and clears per-thread overrides.
131
+ Use one Router for multiple trusted clients. Each client connects from the developer's machine, while the Router owns Feishu configuration, device bindings, routing, and thread switch state. Feishu thread buttons show automatic names such as `thread-2` as their sequence number, such as `2`, while callbacks retain the full internal name. Continuation cards repeat the thread and working-directory header of the original card. A thread can override its backend with `/backend <index> @`; `/backend <index>` changes the global backend and clears per-thread overrides.
132
+
133
+ Thread switch panels show `Reply from:` with the reply's full thread name and workspace. Under `Switch thread`, a check mark (`✓`) and primary button styling identify the destination of new top-level messages when the card is finalized or clicked. Selected buttons remain clickable. Switching updates only the clicked card; other historical cards retain their last displayed selection.
131
134
 
132
135
  When a client thread is busy, the Router sends a confirmation card before accepting another message into its queue. `/queue` shows queue state, `/queue clear` discards queued messages, and `/abort` stops the active task and clears that thread's queue. Messages received while abort cleanup is in progress wait for the client backend to become safe before execution starts.
133
136
 
@@ -32,6 +32,7 @@ export declare class FeishuLongConnHandler {
32
32
  private threadSwitchStateCleanupTimer;
33
33
  private lastProcessedLengths;
34
34
  private messageLocks;
35
+ private queueCardDecisions;
35
36
  private queueActionStates;
36
37
  private readonly QUEUE_ACTION_STATE_TTL_MS;
37
38
  private readonly TOOL_USE_PATTERN;
@@ -50,7 +51,7 @@ export declare class FeishuLongConnHandler {
50
51
  /**
51
52
  * Set streaming start callback
52
53
  */
53
- setOnStartStreaming(callback: (messageId: string, openId: string, feishuMessageId: string | null, deviceId: string, threadId?: string, pendingNewThread?: boolean) => void): void;
54
+ setOnStartStreaming(callback: (messageId: string, openId: string, feishuMessageId: string | null, deviceId: string, threadId?: string, pendingNewThread?: boolean, queueCardId?: string) => void): void;
54
55
  /**
55
56
  * Callback to resolve threadId from a Feishu card message_id (for parent_id routing)
56
57
  */
@@ -216,6 +217,8 @@ export declare class FeishuLongConnHandler {
216
217
  * are counted, including nested components, text elements, and container children.
217
218
  *
218
219
  * @param elements Array of Feishu Card 2.0 elements
220
+ * @param continuationHeaderElements Header elements repeated at the top of continuation cards
221
+ * @param trailingElements Final panel elements that must remain together
219
222
  * @returns Array of element chunks, each satisfying Feishu's limits
220
223
  */
221
224
  private splitElementsIntoChunks;
@@ -267,8 +270,9 @@ export declare class FeishuLongConnHandler {
267
270
  * @param messageId The Feishu message ID
268
271
  * @param elements Array of Feishu Card 2.0 elements
269
272
  * @param openId User's open_id for creating continuation messages
273
+ * @param threadName Thread name shown in continuation card headers
270
274
  */
271
- updateStreamingMessage(messageId: string, elements: any[], openId?: string): Promise<boolean>;
275
+ updateStreamingMessage(messageId: string, elements: any[], openId?: string, threadName?: string): Promise<boolean>;
272
276
  private _updateStreamingMessage;
273
277
  /**
274
278
  * Finalize streaming message
@@ -278,8 +282,9 @@ export declare class FeishuLongConnHandler {
278
282
  * @param elements Array of Feishu Card 2.0 elements
279
283
  * @param sessionAbbr Optional session abbreviation
280
284
  * @param openId User's open_id for creating continuation messages
285
+ * @param replyThreadId Thread that produced the reply, independent of the selected thread
281
286
  */
282
- finalizeStreamingMessage(messageId: string, elements: any[], sessionAbbr?: string, openId?: string, cwd?: string, threadName?: string, threads?: ThreadSummary[], activeThreadId?: string, queueConfirmation?: QueueConfirmationInfo): Promise<boolean>;
287
+ finalizeStreamingMessage(messageId: string, elements: any[], sessionAbbr?: string, openId?: string, cwd?: string, threadName?: string, threads?: ThreadSummary[], replyThreadId?: string, queueConfirmation?: QueueConfirmationInfo): Promise<boolean>;
283
288
  private _finalizeStreamingMessage;
284
289
  /**
285
290
  * Create Feishu Card 2.0 elements for thread switching buttons.
@@ -288,6 +293,7 @@ export declare class FeishuLongConnHandler {
288
293
  */
289
294
  private createThreadSwitchElements;
290
295
  private threadButtonLabel;
296
+ private createResponseHeaderElements;
291
297
  /**
292
298
  * Sort automatically named threads by their numeric suffix while preserving
293
299
  * the positions and creation order of default and user-named threads.
@@ -312,8 +318,10 @@ export declare class FeishuLongConnHandler {
312
318
  * Reuses handleRegularCommand flow: creates streaming card, sends command to device.
313
319
  */
314
320
  sendCommandFromCardAction(openId: string, content: string, pendingNewThread?: boolean): Promise<void>;
315
- /** Send a queue decision directly to the CLI and prepare a streaming card for confirmed work. */
321
+ /** Register confirmed work; capable clients request a card when execution starts. */
316
322
  sendQueueActionFromCardAction(openId: string, action: 'confirm' | 'cancel', queueId: string, threadId: string, cardMessageId?: string): Promise<'sent' | 'already_processed'>;
323
+ /** Leave a historical queue receipt after output moves to a new card. */
324
+ markQueueCardStarted(cardMessageId: string): Promise<void>;
317
325
  private removeExpiredQueueActionStates;
318
326
  private updateQueueConfirmationCard;
319
327
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"FeishuLongConnHandler.d.ts","sourceRoot":"","sources":["../../src/feishu/FeishuLongConnHandler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FeishuLongConnHandler.d.ts","sourceRoot":"","sources":["../../src/feishu/FeishuLongConnHandler.ts"],"names":[],"mappings":"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"}
@@ -73,6 +73,7 @@ class FeishuLongConnHandler {
73
73
  lastProcessedLengths = new Map();
74
74
  // Per-message serialization locks to prevent concurrent updates from creating duplicates
75
75
  messageLocks = new Map();
76
+ queueCardDecisions = new Map();
76
77
  // Prevent repeated clicks on the same queue confirmation card from sending duplicate commands.
77
78
  queueActionStates = new Map();
78
79
  QUEUE_ACTION_STATE_TTL_MS = 24 * 60 * 60 * 1000;
@@ -933,9 +934,11 @@ Examples:
933
934
  * are counted, including nested components, text elements, and container children.
934
935
  *
935
936
  * @param elements Array of Feishu Card 2.0 elements
937
+ * @param continuationHeaderElements Header elements repeated at the top of continuation cards
938
+ * @param trailingElements Final panel elements that must remain together
936
939
  * @returns Array of element chunks, each satisfying Feishu's limits
937
940
  */
938
- splitElementsIntoChunks(elements) {
941
+ splitElementsIntoChunks(elements, continuationHeaderElements = [], trailingElements = []) {
939
942
  // If empty or very small, return as-is
940
943
  if (elements.length === 0) {
941
944
  return [elements];
@@ -950,17 +953,24 @@ Examples:
950
953
  let currentChunk = [];
951
954
  let currentChunkSize = 0;
952
955
  let currentChunkTaggedNodes = 0;
953
- // Reserve elements for continuation indicators (they add ~2 elements and ~200 bytes)
954
- const continuationIndicatorSize = 200; // Approximate size of indicator element
955
- const continuationIndicatorCount = 2; // Maximum 2 indicators per chunk (start + end)
956
+ // Reserve space for continuation indicators and the repeated card header.
957
+ const continuationHeaderSize = JSON.stringify(continuationHeaderElements).length;
958
+ const continuationHeaderTaggedNodes = continuationHeaderElements.reduce((total, element) => total + this.countTaggedNodes(element), 0);
959
+ const continuationIndicatorSize = 200 + continuationHeaderSize;
960
+ const continuationIndicatorCount = 2 + continuationHeaderTaggedNodes;
956
961
  for (let i = 0; i < elements.length; i++) {
957
962
  const element = elements[i];
958
963
  const elementSize = JSON.stringify(element).length;
959
964
  const elementTaggedNodes = this.countTaggedNodes(element);
965
+ // Keep the switch panel caption and all button rows on the same card.
966
+ const groupSize = element === trailingElements[0]
967
+ ? trailingElements.reduce((sum, item) => sum + JSON.stringify(item).length, 0) : elementSize;
968
+ const groupTaggedNodes = element === trailingElements[0]
969
+ ? trailingElements.reduce((sum, item) => sum + this.countTaggedNodes(item), 0) : elementTaggedNodes;
960
970
  // Check if adding this element would exceed limits
961
971
  // Reserve space for continuation indicators
962
- const wouldExceedElementLimit = currentChunkTaggedNodes + elementTaggedNodes > (this.CARD_ELEMENT_LIMIT - continuationIndicatorCount);
963
- const wouldExceedSizeLimit = currentChunkSize + elementSize + continuationIndicatorSize >
972
+ const wouldExceedElementLimit = currentChunkTaggedNodes + groupTaggedNodes > (this.CARD_ELEMENT_LIMIT - continuationIndicatorCount);
973
+ const wouldExceedSizeLimit = currentChunkSize + groupSize + continuationIndicatorSize >
964
974
  (this.CARD_DATA_SIZE_LIMIT - this.CARD_SIZE_BUFFER);
965
975
  if (currentChunk.length > 0 && (wouldExceedElementLimit || wouldExceedSizeLimit)) {
966
976
  // Start a new chunk
@@ -998,8 +1008,9 @@ Examples:
998
1008
  });
999
1009
  }
1000
1010
  if (!isFirst) {
1001
- // Add "continued from previous message" indicator at the start
1002
- chunks[i].unshift({
1011
+ // Repeat the normal card header before the continuation indicator so
1012
+ // every card in a long response identifies its thread and workspace.
1013
+ chunks[i].unshift(...continuationHeaderElements, {
1003
1014
  tag: 'markdown',
1004
1015
  content: '_⬅️ Continued from previous message..._\n\n',
1005
1016
  });
@@ -1122,8 +1133,8 @@ Examples:
1122
1133
  async uploadImage(data, mimeType) {
1123
1134
  try {
1124
1135
  const image = Buffer.from(data, 'base64');
1125
- if (image.length === 0 || image.length > 10 * 1024 * 1024) {
1126
- console.error('[FeishuHandler] Generated image exceeds the 10MB upload limit');
1136
+ if (image.length === 0 || image.length > 2 * 1024 * 1024) {
1137
+ console.error('[FeishuHandler] Generated image exceeds the 2 MiB upload limit');
1127
1138
  return null;
1128
1139
  }
1129
1140
  const supportedTypes = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/tiff', 'image/bmp', 'image/x-icon']);
@@ -1153,11 +1164,13 @@ Examples:
1153
1164
  * @param messageId The Feishu message ID
1154
1165
  * @param elements Array of Feishu Card 2.0 elements
1155
1166
  * @param openId User's open_id for creating continuation messages
1167
+ * @param threadName Thread name shown in continuation card headers
1156
1168
  */
1157
- async updateStreamingMessage(messageId, elements, openId) {
1158
- return this.withMessageLock(messageId, () => this._updateStreamingMessage(messageId, elements, openId));
1169
+ async updateStreamingMessage(messageId, elements, openId, threadName) {
1170
+ const continuationHeaderElements = this.createResponseHeaderElements(threadName);
1171
+ return this.withMessageLock(messageId, () => this._updateStreamingMessage(messageId, elements, openId, continuationHeaderElements));
1159
1172
  }
1160
- async _updateStreamingMessage(messageId, elements, openId) {
1173
+ async _updateStreamingMessage(messageId, elements, openId, continuationHeaderElements = [], trailingElements = []) {
1161
1174
  try {
1162
1175
  // Get or initialize message chain
1163
1176
  let chain = this.messageChains.get(messageId);
@@ -1166,7 +1179,7 @@ Examples:
1166
1179
  this.messageChains.set(messageId, chain);
1167
1180
  }
1168
1181
  // Split elements into chunks based on Feishu Card 2.0 limits
1169
- const chunks = this.splitElementsIntoChunks(elements);
1182
+ const chunks = this.splitElementsIntoChunks(elements, continuationHeaderElements, trailingElements);
1170
1183
  console.log(`[FeishuHandler] Need ${chunks.length} card(s), currently have ${chain.length} card(s)`);
1171
1184
  // Update the first message with the first chunk
1172
1185
  await this.client.im.message.patch({
@@ -1230,51 +1243,49 @@ Examples:
1230
1243
  * @param elements Array of Feishu Card 2.0 elements
1231
1244
  * @param sessionAbbr Optional session abbreviation
1232
1245
  * @param openId User's open_id for creating continuation messages
1246
+ * @param replyThreadId Thread that produced the reply, independent of the selected thread
1233
1247
  */
1234
- async finalizeStreamingMessage(messageId, elements, sessionAbbr, openId, cwd, threadName, threads, activeThreadId, queueConfirmation) {
1235
- return this.withMessageLock(messageId, () => this._finalizeStreamingMessage(messageId, elements, sessionAbbr, openId, cwd, threadName, threads, activeThreadId, queueConfirmation));
1248
+ async finalizeStreamingMessage(messageId, elements, sessionAbbr, openId, cwd, threadName, threads, replyThreadId, queueConfirmation) {
1249
+ return this.withMessageLock(messageId, () => this._finalizeStreamingMessage(messageId, elements, sessionAbbr, openId, cwd, threadName, threads, replyThreadId, queueConfirmation));
1236
1250
  }
1237
- async _finalizeStreamingMessage(messageId, elements, sessionAbbr, openId, cwd, threadName, threads, activeThreadId, queueConfirmation) {
1251
+ async _finalizeStreamingMessage(messageId, elements, sessionAbbr, openId, cwd, threadName, threads, replyThreadId, queueConfirmation) {
1238
1252
  try {
1239
- // Build header element (thread name + working directory) at the top
1240
- const headerParts = [];
1241
- if (threadName) {
1242
- headerParts.push(`🧵 **${threadName}**`);
1243
- }
1244
- if (cwd) {
1245
- const formattedCwd = cwd.replace(process.env.HOME || '/Users', '~');
1246
- headerParts.push(`📂 \`${formattedCwd}\``);
1247
- }
1248
1253
  // Build completion note
1249
1254
  let noteContent = queueConfirmation ? '⏳ Awaiting queue confirmation' : '✅ Completed';
1250
1255
  if (sessionAbbr) {
1251
1256
  noteContent += ` · Session: ${sessionAbbr}`;
1252
1257
  }
1253
- const finalElements = [];
1254
- if (headerParts.length > 0) {
1255
- finalElements.push({ tag: 'markdown', content: headerParts.join(' · ') });
1256
- finalElements.push({ tag: 'hr' });
1257
- }
1258
+ const headerElements = this.createResponseHeaderElements(threadName, cwd);
1259
+ const finalElements = [...headerElements];
1258
1260
  finalElements.push(...elements, { tag: 'markdown', content: noteContent });
1259
1261
  if (queueConfirmation) {
1260
1262
  finalElements.push(...this.createQueueConfirmationElements(queueConfirmation));
1261
1263
  }
1262
1264
  // Append thread switch buttons when at least one thread exists
1263
1265
  let threadSwitchElements = [];
1266
+ // The reply's owner and the destination of new messages are independent.
1267
+ const activeThreadId = (openId ? this.onResolveActiveThread?.(openId)?.threadId : undefined)
1268
+ ?? threads?.find(thread => thread.name === 'default')?.id;
1269
+ const replyThread = threads?.find(thread => thread.id === replyThreadId);
1270
+ const replyThreadName = threadName ?? replyThread?.name;
1271
+ const replyWorkspace = cwd ? path.posix.basename(cwd.replace(/\\/g, '/')) || cwd : replyThread?.workspaceName;
1272
+ const replyLabel = replyThreadName
1273
+ ? [replyThreadName, replyWorkspace].filter(Boolean).join(' · ')
1274
+ : undefined;
1264
1275
  if (threads && threads.length >= 1) {
1265
- threadSwitchElements = this.createThreadSwitchElements(threads, activeThreadId);
1276
+ threadSwitchElements = this.createThreadSwitchElements(threads, activeThreadId, replyLabel);
1266
1277
  finalElements.push(...threadSwitchElements);
1267
1278
  }
1268
1279
  // Reuse streaming update logic: only create cards, never delete.
1269
1280
  // Whatever layout was built during streaming stays as-is.
1270
- const updated = await this._updateStreamingMessage(messageId, finalElements, openId);
1281
+ const updated = await this._updateStreamingMessage(messageId, finalElements, openId, headerElements, threadSwitchElements);
1271
1282
  if (!updated) {
1272
1283
  return false;
1273
1284
  }
1274
1285
  // Store thread switch card state for later refresh (before cleanup)
1275
1286
  if (threads && threads.length >= 1) {
1276
1287
  const chain = this.messageChains.get(messageId);
1277
- const chunks = this.splitElementsIntoChunks(finalElements);
1288
+ const chunks = this.splitElementsIntoChunks(finalElements, headerElements, threadSwitchElements);
1278
1289
  const lastChunkIndex = chunks.length - 1;
1279
1290
  const lastCardId = chain?.[lastChunkIndex];
1280
1291
  if (lastCardId) {
@@ -1282,6 +1293,7 @@ Examples:
1282
1293
  baseElements: chunks[lastChunkIndex].filter((element) => !threadSwitchElements.includes(element)),
1283
1294
  threads,
1284
1295
  activeThreadId,
1296
+ replyLabel,
1285
1297
  createdAt: Date.now(),
1286
1298
  });
1287
1299
  }
@@ -1301,7 +1313,7 @@ Examples:
1301
1313
  * Renders one button per thread (active is highlighted) plus a "+ New" button,
1302
1314
  * split across rows so desktop clients do not compress every button into one line.
1303
1315
  */
1304
- createThreadSwitchElements(threads, activeThreadId) {
1316
+ createThreadSwitchElements(threads, activeThreadId, replyLabel) {
1305
1317
  const orderedThreads = this.orderThreadsForDisplay(threads);
1306
1318
  const threadColumns = orderedThreads.map((t) => ({
1307
1319
  tag: 'column',
@@ -1311,7 +1323,7 @@ Examples:
1311
1323
  tag: 'button',
1312
1324
  text: { tag: 'plain_text', content: this.threadButtonLabel(t, t.id === activeThreadId) },
1313
1325
  type: t.id === activeThreadId ? 'primary' : 'default',
1314
- disabled: t.id === activeThreadId,
1326
+ disabled: false,
1315
1327
  behaviors: [{ type: 'callback', value: { action: 'switch_thread', threadId: t.id, threadName: t.name } }],
1316
1328
  },
1317
1329
  ],
@@ -1340,14 +1352,34 @@ Examples:
1340
1352
  columns: columns.slice(index, index + this.THREAD_BUTTONS_PER_ROW),
1341
1353
  });
1342
1354
  }
1343
- return [{ tag: 'hr' }, ...rows];
1355
+ const caption = {
1356
+ tag: 'markdown',
1357
+ text_size: 'notation',
1358
+ content: replyLabel ? `Reply from: ${replyLabel}\nSwitch thread` : 'Switch thread',
1359
+ };
1360
+ return [{ tag: 'hr' }, caption, ...rows];
1344
1361
  }
1345
1362
  threadButtonLabel(thread, active) {
1346
1363
  const details = [
1347
1364
  thread.workspaceName,
1348
1365
  thread.backend ? this.backendLabel(thread.backend) : undefined,
1349
1366
  ].filter((detail) => Boolean(detail));
1350
- return `${active ? '★ ' : ''}${thread.name}${details.length > 0 ? ` · ${details.join(' · ')}` : ''}`;
1367
+ const automaticName = /^thread-(\d+)$/.exec(thread.name);
1368
+ const displayName = automaticName ? automaticName[1] : thread.name;
1369
+ return `${active ? '✓ ' : ''}${displayName}${details.length > 0 ? ` · ${details.join(' · ')}` : ''}`;
1370
+ }
1371
+ createResponseHeaderElements(threadName, cwd) {
1372
+ const headerParts = [];
1373
+ if (threadName) {
1374
+ headerParts.push(`🧵 **${threadName}**`);
1375
+ }
1376
+ if (cwd) {
1377
+ const formattedCwd = cwd.replace(process.env.HOME || '/Users', '~');
1378
+ headerParts.push(`📂 \`${formattedCwd}\``);
1379
+ }
1380
+ return headerParts.length > 0
1381
+ ? [{ tag: 'markdown', content: headerParts.join(' · ') }, { tag: 'hr' }]
1382
+ : [];
1351
1383
  }
1352
1384
  /**
1353
1385
  * Sort automatically named threads by their numeric suffix while preserving
@@ -1426,7 +1458,7 @@ Examples:
1426
1458
  }
1427
1459
  const updatedElements = [
1428
1460
  ...state.baseElements,
1429
- ...this.createThreadSwitchElements(state.threads, newActiveThreadId),
1461
+ ...this.createThreadSwitchElements(state.threads, newActiveThreadId, state.replyLabel),
1430
1462
  ];
1431
1463
  await this.client.im.message.patch({
1432
1464
  path: { message_id: cardMessageId },
@@ -1498,7 +1530,7 @@ Examples:
1498
1530
  const syntheticMessageId = (0, uuid_1.v4)();
1499
1531
  await this.handleRegularCommand(openId, syntheticMessageId, content, undefined, pendingNewThread);
1500
1532
  }
1501
- /** Send a queue decision directly to the CLI and prepare a streaming card for confirmed work. */
1533
+ /** Register confirmed work; capable clients request a card when execution starts. */
1502
1534
  async sendQueueActionFromCardAction(openId, action, queueId, threadId, cardMessageId) {
1503
1535
  this.removeExpiredQueueActionStates();
1504
1536
  const previousAction = this.queueActionStates.get(queueId);
@@ -1515,27 +1547,56 @@ Examples:
1515
1547
  let executionMessageId;
1516
1548
  if (action === 'confirm') {
1517
1549
  executionMessageId = (0, uuid_1.v4)();
1518
- const feishuMessageId = await this.sendStreamingStart(openId, '🤔 Queued message confirmed. Waiting for the current task to finish...');
1519
- this.onStartStreaming?.(executionMessageId, openId, feishuMessageId, activeDevice.deviceId, threadId, false);
1550
+ const feishuMessageId = this.connectionHub.supportsQueueStarted(activeDevice.deviceId)
1551
+ ? null
1552
+ : await this.sendStreamingStart(openId, '🤔 Queued message confirmed. Waiting for the current task to finish...');
1553
+ this.onStartStreaming?.(executionMessageId, openId, feishuMessageId, activeDevice.deviceId, threadId, false, cardMessageId);
1520
1554
  }
1521
1555
  const controlMessageId = (0, uuid_1.v4)();
1522
1556
  const suffix = executionMessageId ? ` ${executionMessageId}` : '';
1523
- const success = await this.connectionHub.sendToDevice(activeDevice.deviceId, {
1524
- type: types_1.MessageType.COMMAND,
1525
- messageId: controlMessageId,
1526
- timestamp: Date.now(),
1527
- content: `/queue ${action} ${queueId}${suffix}`,
1528
- openId,
1529
- threadId,
1530
- });
1531
- if (!success) {
1557
+ // An idle thread can start immediately. Its receipt must not be patched
1558
+ // back to "waiting" after the start event has already arrived.
1559
+ let finishDecision;
1560
+ const decision = new Promise((resolve) => { finishDecision = resolve; });
1561
+ if (cardMessageId)
1562
+ this.queueCardDecisions.set(cardMessageId, decision);
1563
+ try {
1564
+ const success = await this.connectionHub.sendToDevice(activeDevice.deviceId, {
1565
+ type: types_1.MessageType.COMMAND,
1566
+ messageId: controlMessageId,
1567
+ timestamp: Date.now(),
1568
+ content: `/queue ${action} ${queueId}${suffix}`,
1569
+ openId,
1570
+ threadId,
1571
+ });
1572
+ if (!success)
1573
+ throw new Error('Failed to send queue action to device');
1574
+ if (cardMessageId)
1575
+ await this.updateQueueConfirmationCard(cardMessageId, action);
1576
+ return 'sent';
1577
+ }
1578
+ catch (error) {
1532
1579
  this.queueActionStates.delete(queueId);
1533
- throw new Error('Failed to send queue action to device');
1580
+ throw error;
1534
1581
  }
1535
- if (cardMessageId) {
1536
- await this.updateQueueConfirmationCard(cardMessageId, action);
1582
+ finally {
1583
+ finishDecision();
1584
+ if (cardMessageId)
1585
+ this.queueCardDecisions.delete(cardMessageId);
1537
1586
  }
1538
- return 'sent';
1587
+ }
1588
+ /** Leave a historical queue receipt after output moves to a new card. */
1589
+ async markQueueCardStarted(cardMessageId) {
1590
+ await this.queueCardDecisions.get(cardMessageId);
1591
+ await this.withMessageLock(cardMessageId, () => this.client.im.message.patch({
1592
+ path: { message_id: cardMessageId },
1593
+ data: {
1594
+ content: JSON.stringify({
1595
+ schema: '2.0',
1596
+ body: { elements: [{ tag: 'markdown', content: '▶️ **Queued task started**\n\nFollow progress in the new execution card at the bottom of the chat.' }] },
1597
+ }),
1598
+ },
1599
+ }));
1539
1600
  }
1540
1601
  removeExpiredQueueActionStates() {
1541
1602
  const expiry = Date.now() - this.QUEUE_ACTION_STATE_TTL_MS;