openclaw-lark-multi-agent 1.8.13 → 1.8.15

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.
@@ -61,6 +61,11 @@ export interface DeliveryOutboxItem {
61
61
  createdAt: number;
62
62
  updatedAt: number;
63
63
  }
64
+ /** A resolved dispatcher promise is not a platform receipt. */
65
+ export type DeliveryDispatchOutcome = {
66
+ state: "delivered" | "pending" | "unknown" | "failed" | "suppressed";
67
+ outboxIds: number[];
68
+ };
64
69
  export interface PendingErrorNotice {
65
70
  botName: string;
66
71
  chatId: string;
@@ -107,6 +112,9 @@ export declare class MessageStore {
107
112
  /** LMA-owned recovery journal. Never edits OpenClaw state. Unknown attempts
108
113
  * remain durable and are never selected by the ordinary pending drain. */
109
114
  private initReliabilityTables;
115
+ savePresentation(bot: string, app: string, chat: string, cohort: string, record: object): void;
116
+ getPresentation(bot: string, app: string, chat: string, cohort: string): any;
117
+ listPresentations(bot: string, app: string, chat: string): any[];
110
118
  getStopGeneration(bot: string, chat: string): number;
111
119
  bumpStopGeneration(bot: string, chat: string): number;
112
120
  setReactionIntent(bot: string, chat: string, row: number, record: object): void;
@@ -129,6 +137,7 @@ export declare class MessageStore {
129
137
  listAdmissions(sessionKey: string): any[];
130
138
  stopExactRun(sessionKey: string, runId: string): void;
131
139
  isStoppedObservation(sessionKey: string, runId: string): boolean;
140
+ hasUnconfirmedRunDelivery(bot: string, chat: string, runId: string): boolean;
132
141
  settleRunReactionIntents(bot: string, chat: string, runId: string): void;
133
142
  cancelReactionIntents(bot: string, chat: string, epoch: number): void;
134
143
  saveRunObservation(sessionKey: string, runId: string, record: object, nextCheckAt?: number): void;
@@ -149,10 +158,17 @@ export declare class MessageStore {
149
158
  claimInsertionAttempt(bot: string, chat: string, row: number): boolean;
150
159
  getInsertion(bot: string, chat: string, row: number): any;
151
160
  listInsertions(bot: string, chat?: string): any[];
161
+ /** Legacy queue identities are not canonical host input receipts. Until a
162
+ * genuine host proof is supported, only an explicitly ordinary input may
163
+ * participate in ordinary completion/sync; never guess from text or root ID. */
164
+ isOrdinaryInput(bot: string, chat: string, row: number): boolean;
152
165
  isInsertionReserved(bot: string, chat: string, row: number): boolean;
153
166
  stopInsertionAtEpoch(bot: string, chat: string, row: number, epoch: number): void;
154
167
  stopInsertions(bot: string, chat: string): void;
155
168
  hasUnknownDeliveryOperation(outbox: number): boolean;
169
+ /** Includes non-visible uploads/doc creation: absence of a message_id is
170
+ * not proof that no SDK effect has been attempted or accepted. */
171
+ getDeliveryEffectState(outbox: number): 'not_attempted' | 'unknown' | 'accepted' | 'rejected';
156
172
  /** Retain the full known result BEFORE the fallible main-journal checkpoint.
157
173
  * The memory copy survives local SQLite failures while this process is alive;
158
174
  * the independent insert journal also survives an interrupted UPDATE/reopen. */
@@ -298,6 +314,10 @@ export declare class MessageStore {
298
314
  getDeliveryBySource(sessionKey: string, sourceType: string, sourceId: string): DeliveryOutboxItem | null;
299
315
  getDeliveryByKey(botName: string, chatId: string, deliveryKey: string): DeliveryOutboxItem | null;
300
316
  listDeliveriesByKeyPrefix(botName: string, chatId: string, prefix: string): DeliveryOutboxItem[];
317
+ bindDeliveryCompletionTriggers(bot: string, chat: string, key: string, ids: number[]): void;
318
+ /** Include stable correction/supplement rows: the first accepted body alone
319
+ * cannot settle a logical final whose remaining attachments are unknown. */
320
+ getDeliveryDispatchOutcome(bot: string, chat: string, key: string): DeliveryDispatchOutcome;
301
321
  getPendingDeliveries(chatId?: string, botName?: string, maxCount?: number): DeliveryOutboxItem[];
302
322
  markBotSeenInChat(botName: string, chatId: string): void;
303
323
  hasBotSeenInChat(botName: string, chatId: string): boolean;
@@ -28,6 +28,10 @@ export class MessageStore {
28
28
  CREATE TABLE IF NOT EXISTS reaction_custody(bot_name TEXT NOT NULL,chat_id TEXT NOT NULL,row_id INTEGER NOT NULL,record_json TEXT NOT NULL,
29
29
  PRIMARY KEY(bot_name,chat_id,row_id));
30
30
  CREATE TABLE IF NOT EXISTS run_delivery_targets(bot_name TEXT NOT NULL,chat_id TEXT NOT NULL,run_id TEXT NOT NULL,record_json TEXT NOT NULL,PRIMARY KEY(bot_name,chat_id,run_id));
31
+ -- UI-only custody: never read by admission, dispatch or business replay.
32
+ CREATE TABLE IF NOT EXISTS presentation_custody (
33
+ bot_name TEXT NOT NULL,app_id TEXT NOT NULL,chat_id TEXT NOT NULL,cohort_id TEXT NOT NULL,
34
+ record_json TEXT NOT NULL,PRIMARY KEY(bot_name,app_id,chat_id,cohort_id));
31
35
  CREATE TABLE IF NOT EXISTS dispatch_custody(bot_name TEXT NOT NULL,chat_id TEXT NOT NULL,record_json TEXT NOT NULL,PRIMARY KEY(bot_name,chat_id));
32
36
  CREATE TABLE IF NOT EXISTS admission_custody(session_key TEXT NOT NULL,submission_id TEXT PRIMARY KEY,state TEXT NOT NULL,record_json TEXT NOT NULL);
33
37
  CREATE TABLE IF NOT EXISTS run_observations (
@@ -56,6 +60,18 @@ export class MessageStore {
56
60
  WHERE state='capability_blocked' AND json_extract(record_json,'$.attempted')=0;
57
61
  `);
58
62
  }
63
+ savePresentation(bot, app, chat, cohort, record) {
64
+ this.db.prepare(`INSERT OR REPLACE INTO presentation_custody VALUES(?,?,?,?,?)`).run(bot, app, chat, cohort, JSON.stringify(record));
65
+ }
66
+ getPresentation(bot, app, chat, cohort) {
67
+ const row = this.db.prepare(`SELECT record_json FROM presentation_custody WHERE bot_name=? AND app_id=? AND chat_id=? AND cohort_id=?`)
68
+ .get(bot, app, chat, cohort);
69
+ return row ? JSON.parse(row.record_json) : null;
70
+ }
71
+ listPresentations(bot, app, chat) {
72
+ return this.db.prepare(`SELECT record_json FROM presentation_custody WHERE bot_name=? AND app_id=? AND chat_id=?`)
73
+ .all(bot, app, chat).map(row => JSON.parse(row.record_json));
74
+ }
59
75
  getStopGeneration(bot, chat) {
60
76
  return this.db.prepare('SELECT stop_epoch FROM bridge_generations WHERE bot_name=? AND chat_id=?').get(bot, chat)?.stop_epoch || 0;
61
77
  }
@@ -148,14 +164,26 @@ export class MessageStore {
148
164
  isStoppedObservation(sessionKey, runId) {
149
165
  return !!this.db.prepare("SELECT 1 FROM run_observations WHERE session_key=? AND run_id=? AND state='stopped'").get(sessionKey, runId);
150
166
  }
167
+ hasUnconfirmedRunDelivery(bot, chat, runId) {
168
+ const rows = this.db.prepare(`SELECT delivery_key,source_type FROM delivery_outbox WHERE bot_name=? AND chat_id=?
169
+ AND json_extract(delivery_meta_json,'$.provenance.runId')=?`).all(bot, chat, runId);
170
+ const finals = rows.filter(row => row.source_type.startsWith('assistant_visible'));
171
+ return (finals.length ? finals : rows).some(row => this.getDeliveryDispatchOutcome(bot, chat, row.delivery_key).state !== 'delivered');
172
+ }
151
173
  settleRunReactionIntents(bot, chat, runId) {
152
- this.db.prepare(`UPDATE insertion_custody SET state='committed' WHERE bot_name=? AND chat_id=? AND state='accepted'
153
- AND json_extract(record_json,'$.expectedRunId')=? AND json_extract(record_json,'$.stopEpoch')=?`)
154
- .run(bot, chat, runId, this.getStopGeneration(bot, chat));
174
+ if (this.hasUnconfirmedRunDelivery(bot, chat, runId))
175
+ return;
176
+ // A root outcome/visible reply proves nothing about a legacy SDK queue
177
+ // item's commitment or consumption. Only ordinary inputs belong to this
178
+ // bulk completion cohort; even historical committed rows are not repaired
179
+ // or reinterpreted here. No exact host input identity is available for them.
155
180
  this.db.prepare(`UPDATE reaction_custody SET record_json=json_set(record_json,'$.desired','DONE')
156
181
  WHERE bot_name=? AND chat_id=? AND json_extract(record_json,'$.runId')=?
157
182
  AND json_extract(record_json,'$.desired') IN ('Get','Typing')
158
- AND json_extract(record_json,'$.stopEpoch')=?`).run(bot, chat, runId, this.getStopGeneration(bot, chat));
183
+ AND json_extract(record_json,'$.stopEpoch')=?
184
+ AND NOT EXISTS (SELECT 1 FROM insertion_custody i WHERE i.bot_name=reaction_custody.bot_name
185
+ AND i.chat_id=reaction_custody.chat_id AND i.message_row_id=reaction_custody.row_id AND i.state!='ordinary')`)
186
+ .run(bot, chat, runId, this.getStopGeneration(bot, chat));
159
187
  }
160
188
  cancelReactionIntents(bot, chat, epoch) {
161
189
  this.db.prepare(`UPDATE reaction_custody SET record_json=json_set(record_json,'$.desired','','$.stopEpoch',?)
@@ -235,6 +263,13 @@ export class MessageStore {
235
263
  AND state IN ('pending','accepted','unknown','dispatching') ORDER BY next_check_at LIMIT 100`).all(bot, ...(chat ? [chat] : []))
236
264
  .map(r => ({ ...JSON.parse(r.record_json), state: r.state, insertionId: r.insertion_id, rowId: r.message_row_id, chatId: r.chat_id, nextCheckAt: r.next_check_at }));
237
265
  }
266
+ /** Legacy queue identities are not canonical host input receipts. Until a
267
+ * genuine host proof is supported, only an explicitly ordinary input may
268
+ * participate in ordinary completion/sync; never guess from text or root ID. */
269
+ isOrdinaryInput(bot, chat, row) {
270
+ return !this.db.prepare(`SELECT 1 FROM insertion_custody WHERE bot_name=? AND chat_id=?
271
+ AND message_row_id=? AND state!='ordinary'`).get(bot, chat, row);
272
+ }
238
273
  isInsertionReserved(bot, chat, row) {
239
274
  const item = this.getInsertion(bot, chat, row);
240
275
  return !!item && !['ordinary', 'stopped'].includes(item.state);
@@ -250,6 +285,17 @@ export class MessageStore {
250
285
  return !!this.db.prepare(`SELECT 1 FROM delivery_operations o WHERE outbox_id=? AND state IN ('attempted','unknown')
251
286
  AND NOT EXISTS(SELECT 1 FROM delivery_receipt_custody k WHERE k.outbox_id=o.outbox_id AND k.operation_key=o.operation_key) LIMIT 1`).get(outbox);
252
287
  }
288
+ /** Includes non-visible uploads/doc creation: absence of a message_id is
289
+ * not proof that no SDK effect has been attempted or accepted. */
290
+ getDeliveryEffectState(outbox) {
291
+ if (this.hasUnknownDeliveryOperation(outbox))
292
+ return 'unknown';
293
+ if ([...this.knownDeliveryReceipts.values()].some(item => item.outbox === outbox) ||
294
+ this.db.prepare(`SELECT 1 FROM delivery_operations WHERE outbox_id=? AND state='accepted'
295
+ UNION ALL SELECT 1 FROM delivery_receipt_custody WHERE outbox_id=? LIMIT 1`).get(outbox, outbox))
296
+ return 'accepted';
297
+ return this.db.prepare('SELECT 1 FROM delivery_operations WHERE outbox_id=? LIMIT 1').get(outbox) ? 'rejected' : 'not_attempted';
298
+ }
253
299
  /** Retain the full known result BEFORE the fallible main-journal checkpoint.
254
300
  * The memory copy survives local SQLite failures while this process is alive;
255
301
  * the independent insert journal also survives an interrupted UPDATE/reopen. */
@@ -289,7 +335,9 @@ export class MessageStore {
289
335
  this.db.prepare(`INSERT INTO delivery_local_obligations VALUES(?,1,?,?) ON CONFLICT(outbox_id)
290
336
  DO UPDATE SET failures=failures+1,reason=excluded.reason,updated_at=excluded.updated_at`).run(id, reason, Date.now());
291
337
  // Local persistence and unknown audit custody consume no platform retry budget.
292
- this.db.prepare("UPDATE delivery_outbox SET status='pending',attempts=MAX(0,attempts-1),updated_at=? WHERE id=? AND status='delivering'").run(Date.now(), id);
338
+ this.db.prepare(`UPDATE delivery_outbox SET status='pending',
339
+ attempts=CASE WHEN status='delivering' THEN MAX(0,attempts-1) ELSE attempts END,updated_at=?
340
+ WHERE id=? AND status IN ('delivering','delivered')`).run(Date.now(), id);
293
341
  return this.db.prepare('SELECT failures FROM delivery_local_obligations WHERE outbox_id=?').get(id).failures;
294
342
  })();
295
343
  }
@@ -1157,6 +1205,8 @@ export class MessageStore {
1157
1205
  return !!row;
1158
1206
  }
1159
1207
  markDeliveredReply(botName, chatId, triggerMessageRowId, replyMessageId = '') {
1208
+ if (!this.isOrdinaryInput(botName, chatId, triggerMessageRowId))
1209
+ return;
1160
1210
  this.db.prepare(`
1161
1211
  INSERT OR IGNORE INTO delivered_replies (bot_name, chat_id, trigger_message_row_id, delivered_at, reply_message_id)
1162
1212
  VALUES (?, ?, ?, ?, ?)
@@ -1200,6 +1250,29 @@ export class MessageStore {
1200
1250
  `).all(botName, chatId, `${prefix}%`);
1201
1251
  return rows.map((row) => this.mapDelivery(row));
1202
1252
  }
1253
+ bindDeliveryCompletionTriggers(bot, chat, key, ids) {
1254
+ const row = this.getDeliveryByKey(bot, chat, key);
1255
+ if (!row)
1256
+ return;
1257
+ const meta = JSON.parse(row.deliveryMetaJson || '{}');
1258
+ meta.completionTriggerIds = [...new Set([...(meta.completionTriggerIds || []), ...ids])];
1259
+ this.db.prepare('UPDATE delivery_outbox SET delivery_meta_json=? WHERE id=?').run(JSON.stringify(meta), row.id);
1260
+ }
1261
+ /** Include stable correction/supplement rows: the first accepted body alone
1262
+ * cannot settle a logical final whose remaining attachments are unknown. */
1263
+ getDeliveryDispatchOutcome(bot, chat, key) {
1264
+ const root = this.getDeliveryByKey(bot, chat, key);
1265
+ const rows = [...(root ? [root] : []), ...this.listDeliveriesByKeyPrefix(bot, chat, `${key}:`).filter(row => ['authoritative:', 'attachments:', 'recovery:'].some(kind => row.deliveryKey.startsWith(`${key}:${kind}`)))];
1266
+ const outboxIds = rows.map(row => row.id);
1267
+ if (!rows.length)
1268
+ return { state: 'unknown', outboxIds };
1269
+ if (rows.some(row => this.hasUnknownDeliveryOperation(row.id)))
1270
+ return { state: 'unknown', outboxIds };
1271
+ const confirmed = rows.every(row => row.status === 'delivered' &&
1272
+ (!row.content.trim() || this.listDeliveryReceipts(row.id).some(r => r.stage === 'body')) &&
1273
+ JSON.parse(row.attachmentsJson || '[]').every((_, i) => this.listDeliveryReceipts(row.id).some(r => r.stage === `attachment:${i}`)));
1274
+ return { state: confirmed ? 'delivered' : rows.some(row => row.status === 'failed') ? 'failed' : 'pending', outboxIds };
1275
+ }
1203
1276
  getPendingDeliveries(chatId, botName, maxCount = 20) {
1204
1277
  if (chatId && botName) {
1205
1278
  const rows = this.db.prepare(`
@@ -1351,9 +1424,10 @@ export class MessageStore {
1351
1424
  `).run(pending ? 1 : 0, Date.now(), id);
1352
1425
  }
1353
1426
  markDeliveryDelivered(id) {
1354
- this.db.prepare(`
1355
- UPDATE delivery_outbox SET status = 'delivered', updated_at = ? WHERE id = ?
1356
- `).run(Date.now(), id);
1427
+ this.db.transaction(() => {
1428
+ this.db.prepare(`UPDATE delivery_outbox SET status = 'delivered', updated_at = ? WHERE id = ?`).run(Date.now(), id);
1429
+ this.db.prepare('DELETE FROM delivery_local_obligations WHERE outbox_id=?').run(id);
1430
+ })();
1357
1431
  }
1358
1432
  markDeliveryFailed(id) {
1359
1433
  this.db.prepare(`
@@ -1437,8 +1511,12 @@ export class MessageStore {
1437
1511
  AND messages.id <= ?
1438
1512
  AND recalled_messages.message_id IS NULL
1439
1513
  AND message_sync.message_row_id IS NULL
1514
+ -- A held insertion must not be replayed later as group catch-up simply
1515
+ -- because acceptance removed its pending trigger. Custody is not sync.
1516
+ AND NOT EXISTS (SELECT 1 FROM insertion_custody i WHERE i.bot_name=?
1517
+ AND i.chat_id=messages.chat_id AND i.message_row_id=messages.id AND i.state!='ordinary')
1440
1518
  ORDER BY messages.timestamp ASC
1441
- `).all(botName, chatId, triggerRowId);
1519
+ `).all(botName, chatId, triggerRowId, botName);
1442
1520
  return rows.map((r) => ({
1443
1521
  id: r.id,
1444
1522
  chatId: r.chat_id,
@@ -1451,7 +1529,7 @@ export class MessageStore {
1451
1529
  }));
1452
1530
  }
1453
1531
  markMessagesSynced(botName, chatId, messageRowIds, syncBatchId = '') {
1454
- const ids = Array.from(new Set(messageRowIds.filter((id) => Number.isFinite(id) && id > 0)));
1532
+ const ids = Array.from(new Set(messageRowIds.filter((id) => Number.isFinite(id) && id > 0 && this.isOrdinaryInput(botName, chatId, id))));
1455
1533
  if (ids.length === 0)
1456
1534
  return;
1457
1535
  const now = Date.now();
@@ -31,6 +31,10 @@ export declare class InactiveRunObservation extends Error {
31
31
  export declare class SupersededObservation extends Error {
32
32
  constructor(reason?: string);
33
33
  }
34
+ /** Local observer only: the exact background result remains owned by durable recovery. */
35
+ export declare class CollectorStalledError extends Error {
36
+ constructor();
37
+ }
34
38
  export declare class AdmissionUnconfirmed extends Error {
35
39
  readonly submissionId: string;
36
40
  constructor(submissionId: string, cause?: unknown);
@@ -57,9 +61,14 @@ export declare class OpenClawClient {
57
61
  private config;
58
62
  private ws;
59
63
  private pending;
64
+ private admissionResponseHooks;
60
65
  private connected;
61
66
  private connectPromise;
62
67
  private agentEvents;
68
+ private eventProgressSizes;
69
+ private runEventReaders;
70
+ private runEventReservations;
71
+ private completedRunEventCustody;
63
72
  private reconnectDelay;
64
73
  private maxReconnectDelay;
65
74
  private shouldReconnect;
@@ -72,10 +81,12 @@ export declare class OpenClawClient {
72
81
  * `stream=item` mirrors when a runtime emits both representations. */
73
82
  private recentToolEventKeys;
74
83
  private privateInputToolCalls;
84
+ private publicToolCalls;
75
85
  private sessionSettlementCallbacks;
76
86
  private sessionMessageCallbacks;
77
87
  private progressCallbacks;
78
88
  private progressGenerations;
89
+ private progressRuns;
79
90
  /** Live collectReply fences per canonical session key. The most recent
80
91
  * collector for a session owns the shared event bucket; a new batch (or a
81
92
  * queue-custody revocation) fences stale ones synchronously so they cannot
@@ -134,7 +145,18 @@ export declare class OpenClawClient {
134
145
  bindRecoveryStore(store: MessageStore): void;
135
146
  private scheduleRecovery;
136
147
  private recoverExactRuns;
148
+ private runEventKey;
149
+ private retainRunEventCustody;
150
+ private retireCompletedRunEvents;
151
+ private isTerminalBufferedEvent;
152
+ /** Buffers are observation indexes, not raw tool-output/transcript caches.
153
+ * Presentation/evidence callbacks still see the already privacy-filtered wire
154
+ * payload. Keep only the fields collectReply actually consumes here. */
155
+ private projectBufferedEvent;
156
+ private progressEventBytes;
157
+ private enforceEventProgressBudget;
137
158
  private bufferEvent;
159
+ private dispatchBufferedEvents;
138
160
  constructor(config: OpenClawConfig);
139
161
  connect(): Promise<void>;
140
162
  private _doConnect;
@@ -146,11 +168,20 @@ export declare class OpenClawClient {
146
168
  private isInputRecord;
147
169
  private extractVisibleAssistantText;
148
170
  private isVerboseTranscriptEnabled;
171
+ private progressKey;
172
+ private progressForRun;
149
173
  private clearVerboseAssistantState;
150
- /** Internal message tools carry private input, not assistant narration.
151
- * Redact before buffering as well as progress callbacks (including failure
152
- * summaries). Preserve tool identity/phase for activity and acceptance logic. */
174
+ private canonicalToolName;
175
+ /** Inspect supported public wrappers recursively, including JSON arguments.
176
+ * A wrapper containing any private coordination call has private results too;
177
+ * do not try to salvage arbitrary echoed result/error strings. */
178
+ private isPrivateCoordination;
179
+ private knownPublicWrapper;
180
+ /** Redact BEFORE event buffers, logging, observers, cards and verbose sinks.
181
+ * Call custody is session+run+id scoped, so item/command/result mirrors stay
182
+ * private even after the original wrapper arguments have disappeared. */
153
183
  private redactPrivateInputTool;
184
+ private redactPrivateTranscriptTools;
154
185
  /** Render protocol-4 structured tool args/results without `[object Object]`.
155
186
  * Prefer useful, non-secret fields and keep the fallback bounded. */
156
187
  private formatToolValue;
@@ -161,13 +192,11 @@ export declare class OpenClawClient {
161
192
  private handleProactiveSessionMessage;
162
193
  private scheduleReconnect;
163
194
  private rpc;
164
- /**
165
- * Collect agent reply by polling accumulated events.
166
- * Matches by initial runId OR sessionKey to handle multi-turn tool calling
167
- * where OpenClaw creates new runIds for each tool-call round.
168
- * Existing event/anchor matching is retained. Foreground effective silence
169
- * triggers read-only reconciliation, never a task-lifetime cutoff or draft final.
170
- * Explicit terminal/error/disconnect paths keep their existing behavior.
195
+ /** Exact admitted-root observation with non-destructive event cursors.
196
+ * Gateway maps runtime continuations to its public client runId; an unrelated
197
+ * session lifecycle start is not lineage. Separate requester/task continuation
198
+ * lineage remains owned by handoffLinkedContinuation + exact root recovery.
199
+ * Effective silence prompts read-only reconciliation, never a lifetime cutoff.
171
200
  */
172
201
  private collectReply;
173
202
  /**
@@ -183,7 +212,6 @@ export declare class OpenClawClient {
183
212
  private formatTerminalRunError;
184
213
  private formatDurationSeconds;
185
214
  private isRecoverableAgentError;
186
- private pickBestCollectedText;
187
215
  createSession(params: {
188
216
  key: string;
189
217
  model: string;