openclaw-lark-multi-agent 1.8.17 → 1.8.18

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.
@@ -315,6 +315,7 @@ export declare class MessageStore {
315
315
  */
316
316
  insert(msg: ChatMessage): number;
317
317
  getMessageId(messageId: string): number | null;
318
+ getMessageByRowId(rowId: number): ChatMessage | null;
318
319
  getMessageByMessageId(messageId: string): ChatMessage | null;
319
320
  getLatestMessageId(chatId: string): number;
320
321
  markMessageRecalled(messageId: string, chatId: string, recalledAt?: number, recallType?: string): void;
@@ -1164,6 +1164,10 @@ export class MessageStore {
1164
1164
  const row = this.db.prepare(`SELECT id FROM messages WHERE message_id = ?`).get(messageId);
1165
1165
  return row?.id || this.db.prepare('SELECT message_row_id FROM platform_message_aliases WHERE message_id=?').get(messageId)?.message_row_id || null;
1166
1166
  }
1167
+ getMessageByRowId(rowId) {
1168
+ const row = this.db.prepare('SELECT message_id FROM messages WHERE id = ?').get(rowId);
1169
+ return row ? this.getMessageByMessageId(row.message_id) : null;
1170
+ }
1167
1171
  getMessageByMessageId(messageId) {
1168
1172
  const row = this.db.prepare(`SELECT * FROM messages WHERE message_id = ?`).get(messageId);
1169
1173
  if (!row)
@@ -428,51 +428,70 @@ export class QueuedPresentations {
428
428
  const evidence = this.store.getDeliveredRunReply(this.bot, this.app, chat, runId, epoch);
429
429
  if (!evidence)
430
430
  return;
431
- const eligible = (row) => {
431
+ const witnesses = (row, preserveSealedPayload = false) => {
432
432
  const retry = row.view?.state === "replied" && !!row.linkedReply && !row.linkedReply.rendered &&
433
433
  row.create === "accepted" && !!row.cardId && !Object.keys(row.editEffects || {}).length;
434
- if ((!this.usable(row) && !retry) || row.retired || row.stopEpoch !== epoch || row.linkedReply?.rendered ||
434
+ const preserved = preserveSealedPayload && row.linkedReply?.rendered === 'reply_payload';
435
+ if ((!this.usable(row) && !retry && !preserved) || row.retired || row.stopEpoch !== epoch || row.linkedReply?.rendered && !preserved ||
435
436
  evidence.createdAt < row.startedAt || (row.expectedInsertionRoot !== runId && row.runId !== runId))
436
- return false;
437
- const ordinary = row.runId === runId && !!row.ownerId && !!row.batchId ? row.ordinaryTriggerIds || [] : [];
437
+ return;
438
+ const ordinary = row.runId && row.ownerId && row.batchId ? row.ordinaryTriggerIds || [] : [];
438
439
  const extra = row.triggerIds.filter(id => !ordinary.includes(id));
439
- return extra.length > 0 && extra.every(id => {
440
+ if (!extra.length)
441
+ return;
442
+ const proofs = [];
443
+ for (const id of row.triggerIds) {
444
+ const input = this.store.getMessageByRowId(id);
440
445
  const item = this.store.getInsertion(this.bot, chat, id);
441
- const input = item?.messageId ? this.store.getMessageByMessageId(item.messageId) : undefined;
442
- return row.enqueueAcceptedIds?.includes(id) && item?.state === 'accepted' && item.stopEpoch === epoch &&
443
- item.expectedRunId === runId && (!evidence.sessionId || item.sessionId === evidence.sessionId) &&
444
- input?.id === id && input.chatId === chat && input.senderType === 'human' && input.timestamp <= evidence.createdAt;
445
- });
446
+ const isOrdinary = ordinary.includes(id);
447
+ if (isOrdinary ? item && (item.state !== 'ordinary' || item.stopEpoch !== epoch) :
448
+ !row.enqueueAcceptedIds?.includes(id) || item?.state !== 'accepted' || item.stopEpoch !== epoch)
449
+ return;
450
+ if (!isOrdinary && !item.sessionId)
451
+ return;
452
+ const root = isOrdinary ? row.runId : item.expectedRunId;
453
+ const proof = root ? this.store.getDeliveredRunReply(this.bot, this.app, chat, root, epoch) : undefined;
454
+ if (!proof || input?.id !== id || input.chatId !== chat || input.senderType !== 'human' || input.timestamp > proof.createdAt ||
455
+ item && (item.messageId !== input.messageId || proof.sessionId && item.sessionId && item.sessionId !== proof.sessionId))
456
+ return;
457
+ proofs.push(proof);
458
+ }
459
+ return proofs;
446
460
  };
447
461
  const flights = [];
448
462
  // There is no await in this staging loop: seal every currently eligible
449
463
  // cohort before the first SDK effect can allow new ingress to interleave.
450
464
  for (const snapshot of this.records(chat)) {
451
465
  const row = this.latest(snapshot);
452
- if (!row || !eligible(row) || this.replyFlights.has(row.id) || (row.linkedReply?.retryAfter || 0) > Date.now())
466
+ if (!row || !witnesses(row) || this.replyFlights.has(row.id) || (row.linkedReply?.retryAfter || 0) > Date.now())
453
467
  continue;
454
468
  const entry = this.hydrate(row);
455
469
  const current = this.latest(entry.record);
456
- if (!current || !eligible(current) || current.lease !== entry.record.lease)
470
+ const proofs = current && witnesses(current);
471
+ if (!current || !proofs || current.lease !== entry.record.lease)
457
472
  continue;
473
+ const latestProof = proofs.reduce((a, b) => a.deliveredAt >= b.deliveredAt ? a : b);
474
+ const proofIds = [...new Set(proofs.map(proof => proof.outboxId))].sort((a, b) => a - b);
458
475
  const ids = JSON.stringify(current.triggerIds);
459
- const preservePayload = !!current.cardId && evidence.platformMessageIds.includes(current.cardId);
476
+ const preservePayload = !!current.cardId && proofs.some(proof => proof.platformMessageIds.includes(current.cardId));
460
477
  // Desired persistence may itself have failed while the controller became
461
478
  // terminal. A sealed, unrendered, clear-effect obligation permits only
462
479
  // the controller's own no-flight/no-unknown retry, not business replay.
463
480
  const retry = !!current.linkedReply && !current.linkedReply.rendered && !Object.keys(current.editEffects || {}).length;
464
481
  const attempts = (current.linkedReply?.attempts || 0) + 1;
465
- this.save({ ...current, linkedReply: { runId, outboxId: evidence.outboxId, deliveredAt: evidence.deliveredAt, attempts,
482
+ this.save({ ...current, linkedReply: { runId: latestProof.runId, outboxId: latestProof.outboxId, deliveredAt: latestProof.deliveredAt,
483
+ relatedOutboxIds: proofIds, attempts,
466
484
  retryAfter: Date.now() + Math.min(15 * 60_000, 60_000 * 2 ** Math.min(attempts - 1, 4)),
467
485
  ...(preservePayload ? { rendered: "reply_payload" } : {}) } });
468
486
  const flight = Promise.resolve().then(async () => {
469
487
  const fresh = this.latest(entry.record);
470
- const proof = this.store.getDeliveredRunReply(this.bot, this.app, chat, runId, epoch);
488
+ const freshProofs = fresh && witnesses(fresh, preservePayload);
471
489
  if (this.disposed || !fresh || fresh.retired || fresh.lease !== entry.record.lease ||
472
490
  fresh.stopEpoch !== this.store.getStopGeneration(this.bot, chat) || JSON.stringify(fresh.triggerIds) !== ids ||
473
- fresh.linkedReply?.outboxId !== evidence.outboxId || proof?.outboxId !== evidence.outboxId)
491
+ fresh.linkedReply?.outboxId !== latestProof.outboxId || !freshProofs ||
492
+ JSON.stringify([...new Set(freshProofs.map(proof => proof.outboxId))].sort((a, b) => a - b)) !== JSON.stringify(proofIds))
474
493
  return;
475
- await entry.live.linkedReplyDelivered(evidence.deliveredAt, preservePayload, retry);
494
+ await entry.live.linkedReplyDelivered(latestProof.deliveredAt, preservePayload, retry);
476
495
  }).catch(() => {
477
496
  // Own rejection immediately: a later synchronous cohort read can fail
478
497
  // before this method reaches allSettled. UI outages must not crash LMA.
@@ -0,0 +1,23 @@
1
+ # 1.8.18 — Independently witness every reply in a historical mixed card
2
+
3
+ Extends 1.8.17's presentation-only fix for a live race observed during validation:
4
+ 1.8.16 had already reused an old insertion's waiting card for a newer ordinary
5
+ run. Closing it from only the old run's reply would be incorrect; leaving it
6
+ waiting after both separate replies were delivered would also be misleading.
7
+
8
+ The card now closes only when **each member's own associated run** has a complete
9
+ original-Bot/application/chat/epoch platform reply receipt that postdates that
10
+ member. The newer ordinary reply cannot substitute for the older insertion's
11
+ reply, or vice versa. Elapsed time uses the latest verified delivery. Persist the
12
+ exact related outbox IDs; revalidate all proofs and membership before the effect.
13
+ If any verified reply already occupies the card, retain its business content
14
+ without a compact replacement. Pending/unknown members still prevent closure.
15
+
16
+ No input commitment/consumption, Get/DONE, model-sync, admission, business retry,
17
+ Core/plugin/config or schema changes. Bridge activation only; native dependencies
18
+ are unchanged. Existing unknown-effect fencing and UI-only zero-effect retries
19
+ remain intact. 1.8.17 tags/packages are not replaced; this is a new immutable patch.
20
+
21
+ Validation: bridge/plugins build, public SDK contract, **66 files / 1795 offline
22
+ tests**, including 32 linked-card controls (4 new historical mixed-card cases).
23
+ Live platform-card readback remains a separate deployment gate.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "1.8.17",
3
+ "version": "1.8.18",
4
4
  "description": "Multi-bot Lark/Feishu bridge for OpenClaw, with per-bot model routing and isolated sessions",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -78,7 +78,8 @@
78
78
  "docs/releases/1.8.11.md",
79
79
  "docs/releases/1.8.15.md",
80
80
  "docs/releases/1.8.16.md",
81
- "docs/releases/1.8.17.md"
81
+ "docs/releases/1.8.17.md",
82
+ "docs/releases/1.8.18.md"
82
83
  ],
83
84
  "engines": {
84
85
  "node": ">=22"