openclaw-lark-multi-agent 1.8.8 → 1.8.10

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.
@@ -28,6 +28,7 @@ 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
+ 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));
31
32
  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);
32
33
  CREATE TABLE IF NOT EXISTS run_observations (
33
34
  session_key TEXT NOT NULL, run_id TEXT NOT NULL, record_json TEXT NOT NULL,
@@ -81,6 +82,38 @@ export class MessageStore {
81
82
  const row = this.db.prepare('SELECT record_json FROM run_delivery_targets WHERE bot_name=? AND chat_id=? AND run_id=?').get(bot, chat, runId);
82
83
  return row ? JSON.parse(row.record_json) : null;
83
84
  }
85
+ /** Scheduling custody is independent of exact-run result custody. */
86
+ saveDispatchCustody(bot, chat, record) {
87
+ this.db.prepare('INSERT OR REPLACE INTO dispatch_custody VALUES(?,?,?)').run(bot, chat, JSON.stringify(record));
88
+ }
89
+ getDispatchCustody(bot, chat) {
90
+ const row = this.db.prepare('SELECT record_json FROM dispatch_custody WHERE bot_name=? AND chat_id=?').get(bot, chat);
91
+ const item = row ? JSON.parse(row.record_json) : null;
92
+ return item?.stopEpoch === this.getStopGeneration(bot, chat) ? item : null;
93
+ }
94
+ clearDispatchCustody(bot, chat, ownerId, batchId) {
95
+ return this.db.prepare(`DELETE FROM dispatch_custody WHERE bot_name=? AND chat_id=?
96
+ AND json_extract(record_json,'$.id')=? AND json_extract(record_json,'$.batchId')=?`).run(bot, chat, ownerId, batchId).changes === 1;
97
+ }
98
+ /** Atomic release of proven never-attempted reservations only. Unknown and
99
+ * accepted inputs retain their exact target and never enter ordinary drain. */
100
+ releaseDispatchCustody(bot, chat, ownerId, batchId) {
101
+ return this.db.transaction(() => {
102
+ const custody = this.getDispatchCustody(bot, chat);
103
+ if (!custody || !this.clearDispatchCustody(bot, chat, ownerId, batchId))
104
+ return false;
105
+ this.db.prepare(`UPDATE run_delivery_targets SET record_json=json_set(record_json,'$.dispatchReleased',json('true'),
106
+ '$.dispatchOwnerId',?,'$.dispatchBatchId',?,'$.sessionId',?) WHERE bot_name=? AND chat_id=? AND run_id=?`)
107
+ .run(ownerId, batchId, custody.sessionId, bot, chat, custody.runId);
108
+ this.db.prepare(`UPDATE insertion_custody SET state='ordinary',next_check_at=0,
109
+ record_json=json_set(record_json,'$.reason','current session confirmed schedulable; no backend attempt')
110
+ WHERE bot_name=? AND chat_id=? AND state='pending'
111
+ AND COALESCE(json_extract(record_json,'$.attempted'),0)=0
112
+ AND json_extract(record_json,'$.ownerId')=? AND json_extract(record_json,'$.batchId')=?
113
+ AND json_extract(record_json,'$.stopEpoch')=?`).run(bot, chat, ownerId, batchId, this.getStopGeneration(bot, chat));
114
+ return true;
115
+ })();
116
+ }
84
117
  /** Reserved is persisted before the send-attempt callback; dispatching is
85
118
  * persisted before the RPC. Only reserved + unchanged stop generation proves
86
119
  * no business effect and may be restored on startup. Unknown never replays. */
@@ -116,6 +149,9 @@ export class MessageStore {
116
149
  return !!this.db.prepare("SELECT 1 FROM run_observations WHERE session_key=? AND run_id=? AND state='stopped'").get(sessionKey, runId);
117
150
  }
118
151
  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));
119
155
  this.db.prepare(`UPDATE reaction_custody SET record_json=json_set(record_json,'$.desired','DONE')
120
156
  WHERE bot_name=? AND chat_id=? AND json_extract(record_json,'$.runId')=?
121
157
  AND json_extract(record_json,'$.desired') IN ('Get','Typing')
@@ -127,9 +163,16 @@ export class MessageStore {
127
163
  }
128
164
  saveRunObservation(sessionKey, runId, record, nextCheckAt = Date.now()) {
129
165
  this.db.prepare(`INSERT INTO run_observations VALUES(?,?,?,'observing',?)
130
- ON CONFLICT(session_key,run_id) DO UPDATE SET record_json=excluded.record_json,next_check_at=excluded.next_check_at
166
+ ON CONFLICT(session_key,run_id) DO UPDATE SET record_json=json_patch(run_observations.record_json,excluded.record_json),next_check_at=excluded.next_check_at
131
167
  WHERE run_observations.state='observing'`).run(sessionKey, runId, JSON.stringify(record), nextCheckAt);
132
168
  }
169
+ hasYieldedObservation(sessionKey) {
170
+ return !!this.db.prepare(`SELECT 1 FROM run_observations WHERE session_key=? AND json_extract(record_json,'$.yielded')=1 LIMIT 1`).get(sessionKey);
171
+ }
172
+ getRunObservation(sessionKey, runId) {
173
+ const row = this.db.prepare('SELECT record_json,state FROM run_observations WHERE session_key=? AND run_id=?').get(sessionKey, runId);
174
+ return row ? { ...JSON.parse(row.record_json), state: row.state } : null;
175
+ }
133
176
  runObservationState(sessionKey, runId) {
134
177
  return this.db.prepare('SELECT state FROM run_observations WHERE session_key=? AND run_id=?').get(sessionKey, runId)?.state;
135
178
  }
@@ -159,6 +202,26 @@ export class MessageStore {
159
202
  ON CONFLICT(bot_name,chat_id,message_row_id) DO UPDATE SET state=excluded.state,record_json=excluded.record_json,next_check_at=excluded.next_check_at
160
203
  WHERE insertion_custody.state NOT IN ('stopped','committed')`).run(bot, chat, row, id, JSON.stringify(record), state, next);
161
204
  }
205
+ /** Purge still-pending insertion status notices once the insertion resolves
206
+ * (accepted/ordinary): a stale "unknown" notice must not fire after Get. */
207
+ deletePendingInsertionNotices(bot, chat, insertionId) {
208
+ const r = this.db.prepare(`DELETE FROM delivery_outbox WHERE bot_name=? AND chat_id=? AND delivery_key LIKE ? AND status='pending'`)
209
+ .run(bot, chat, `insertion:${insertionId}:%`);
210
+ return r.changes;
211
+ }
212
+ /** Reminder intent and its outbox row commit together. Never write a stale
213
+ * reconciliation snapshot over a later accepted/stopped insertion. */
214
+ queueInsertionUnknownNotice(bot, chat, row, delivery) {
215
+ return this.db.transaction(() => {
216
+ const current = this.getInsertion(bot, chat, row);
217
+ if (current?.state !== 'unknown' || current.stopEpoch !== this.getStopGeneration(bot, chat) || current.unknownNotified ||
218
+ !Number.isFinite(current.unknownSince) || Date.now() - current.unknownSince < 45_000)
219
+ return false;
220
+ this.enqueueDelivery(delivery);
221
+ this.saveInsertion(bot, chat, row, current.insertionId, 'unknown', { ...current, unknownNotified: true }, Date.now() + 15_000);
222
+ return true;
223
+ })();
224
+ }
162
225
  claimInsertionAttempt(bot, chat, row) {
163
226
  return this.db.prepare(`UPDATE insertion_custody SET state='dispatching',record_json=json_set(record_json,'$.attempted',json('true'))
164
227
  WHERE bot_name=? AND chat_id=? AND message_row_id=? AND state='pending'`).run(bot, chat, row).changes === 1;
@@ -176,6 +239,10 @@ export class MessageStore {
176
239
  const item = this.getInsertion(bot, chat, row);
177
240
  return !!item && !['ordinary', 'stopped'].includes(item.state);
178
241
  }
242
+ stopInsertionAtEpoch(bot, chat, row, epoch) {
243
+ this.db.prepare(`UPDATE insertion_custody SET state='stopped' WHERE bot_name=? AND chat_id=? AND message_row_id=?
244
+ AND json_extract(record_json,'$.stopEpoch')=? AND state NOT IN ('committed','ordinary')`).run(bot, chat, row, epoch);
245
+ }
179
246
  stopInsertions(bot, chat) {
180
247
  this.db.prepare("UPDATE insertion_custody SET state='stopped' WHERE bot_name=? AND chat_id=? AND state NOT IN ('committed','ordinary')").run(bot, chat);
181
248
  }
@@ -155,8 +155,8 @@ export declare class OpenClawClient {
155
155
  * Collect agent reply by polling accumulated events.
156
156
  * Matches by initial runId OR sessionKey to handle multi-turn tool calling
157
157
  * where OpenClaw creates new runIds for each tool-call round.
158
- * Existing event/anchor matching is retained. After 30 minutes of silence,
159
- * reconcile the original run instead of aborting or treating a draft as final.
158
+ * Existing event/anchor matching is retained. Foreground effective silence
159
+ * triggers read-only reconciliation, never a task-lifetime cutoff or draft final.
160
160
  * Explicit terminal/error/disconnect paths keep their existing behavior.
161
161
  */
162
162
  private collectReply;
@@ -192,7 +192,7 @@ export declare class OpenClawClient {
192
192
  * expectedSessionId, so describe first, then patch archived=false.
193
193
  */
194
194
  unarchiveSession(key: string): Promise<boolean>;
195
- getSessionStatus(key: string): Promise<any>;
195
+ getSessionStatus(key: string, timeoutMs?: number): Promise<any>;
196
196
  injectAssistantMessage(params: {
197
197
  sessionKey: string;
198
198
  message: string;
@@ -203,6 +203,7 @@ export declare class OpenClawClient {
203
203
  * missing/legacy plugin observations stay pending without blind mutation. */
204
204
  exactRunSettled(runId: string): Promise<boolean>;
205
205
  cancelSteerSession(sessionKey: string, stopEpoch: number): Promise<void>;
206
+ private readinessCause;
206
207
  steer(sessionKey: string, text: string, displayText?: string, identity?: {
207
208
  insertionId: string;
208
209
  expectedRunId: string;
@@ -251,9 +252,19 @@ export declare class OpenClawClient {
251
252
  * Used when queue custody is revoked or a new chat.send takes over, so a
252
253
  * stale poller cannot steal the replacement batch's frames (2026-10-16). */
253
254
  revokeSessionCollectors(sessionKey: string, reason: string): number;
255
+ waitForSessionStop(sessionKey: string): Promise<void>;
254
256
  getSessionRuntimeStatus(sessionKey: string): Promise<SessionRuntimeStatus>;
257
+ /** Strong public current-session activity, independent of expired root
258
+ * terminal caches. Unknown/missing fields never authorize scheduling. */
259
+ getSessionDispatchStatus(sessionKey: string): Promise<{
260
+ idleConfirmed: boolean;
261
+ sessionId?: string;
262
+ archived?: boolean;
263
+ activeRunId?: string;
264
+ busyConfirmed?: boolean;
265
+ }>;
255
266
  private classifyWaitTimeout;
256
- getSessionInfo(sessionKey: string): Promise<any>;
267
+ getSessionInfo(sessionKey: string, timeoutMs?: number): Promise<any>;
257
268
  /**
258
269
  * Ensure session is using the expected model. If not, patch it back.
259
270
  * Returns true if a correction was made.
@@ -283,6 +294,11 @@ export declare class OpenClawClient {
283
294
  private trackChatEventSession;
284
295
  inspectRunError(provenance: ErrorProvenance): Promise<import("./terminal-errors.js").ErrorVerdict>;
285
296
  private extractTextFromChatMessage;
297
+ private continuationChecks;
298
+ /** A child/requester continuation is not automatically its parent's final.
299
+ * Public task parentage selects the original root; only that exact root's
300
+ * committed terminalReply may enter its old trigger/outbox. */
301
+ private handoffLinkedContinuation;
286
302
  private emitProactiveForSession;
287
303
  private suppressSessionKeys;
288
304
  private releaseSuppressedSessionKeysAfter;
@@ -415,9 +431,14 @@ export declare class OpenClawClient {
415
431
  /** Called after chat.send RPC succeeds and OpenClaw has accepted the user message. */
416
432
  onSubmitted?: (runId: string) => void | Promise<void>;
417
433
  /** Identifies collection target; not an acceptance receipt. */
418
- onRunIdentified?: (runId: string) => void | Promise<void>;
434
+ onRunIdentified?: (runId: string, identity?: {
435
+ sessionId?: string;
436
+ }) => void | Promise<void>;
419
437
  onProgress?: (event: ProgressEvent) => void | Promise<void>;
420
438
  onWaitPaused?: (notice: SessionWaitPaused) => void | Promise<void>;
439
+ /** Synchronous durable scheduling handoff; never a task settlement. */
440
+ onYieldHandoff?: (runId: string) => void;
441
+ onCollectionStarted?: (runId: string) => void;
421
442
  }): Promise<string>;
422
443
  private shouldInjectBridgeAttachmentHint;
423
444
  private bridgeAttachmentHint;
@@ -452,9 +473,14 @@ export declare class OpenClawClient {
452
473
  /** Called after chat.send RPC succeeds and OpenClaw has accepted the combined message. */
453
474
  onSubmitted?: (runId: string) => void | Promise<void>;
454
475
  /** Identifies collection target; not an acceptance receipt. */
455
- onRunIdentified?: (runId: string) => void | Promise<void>;
476
+ onRunIdentified?: (runId: string, identity?: {
477
+ sessionId?: string;
478
+ }) => void | Promise<void>;
456
479
  onProgress?: (event: ProgressEvent) => void | Promise<void>;
457
480
  onWaitPaused?: (notice: SessionWaitPaused) => void | Promise<void>;
481
+ /** Synchronous durable scheduling handoff; never a task settlement. */
482
+ onYieldHandoff?: (runId: string) => void;
483
+ onCollectionStarted?: (runId: string) => void;
458
484
  }): Promise<string>;
459
485
  private formatContextLines;
460
486
  private writeContextSyncFile;