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.
@@ -0,0 +1,52 @@
1
+ import { MessageStore } from "./message-store.js";
2
+ import { LiveStatusController, type LiveStatusCallbacks } from "./live-status.js";
3
+ /** One unbound cohort per chat, or reuse the exact active-root presentation.
4
+ * Known IDs and desired phase survive restart. Unknown effects are fenced, not
5
+ * retried: the SDK offers no durable query-by-create-key or edit CAS. */
6
+ export declare class QueuedPresentations {
7
+ private readonly storeSource;
8
+ private readonly bot;
9
+ private readonly app;
10
+ private readonly callbacks;
11
+ private readonly enabled;
12
+ private readonly english;
13
+ private entries;
14
+ /** Receipt custody only: never a business busy/admission flag or SDK retry. */
15
+ private repairs;
16
+ private repairTimer?;
17
+ private repairDelay;
18
+ private repairing;
19
+ private disposed;
20
+ constructor(storeSource: MessageStore | (() => MessageStore), bot: string, app: string, callbacks: (chat: string, replyTo: string) => LiveStatusCallbacks, enabled: (chat: string) => boolean, english: (chat: string) => boolean);
21
+ private get store();
22
+ private normalize;
23
+ private records;
24
+ private latest;
25
+ /** Overlay actual receipts before any later UI decision. Fresh metadata,
26
+ * lease/epoch/retirement and every unrelated unknown token remain untouched. */
27
+ private withReceipts;
28
+ private save;
29
+ private observeJournal;
30
+ private acknowledgeJournal;
31
+ private repairFor;
32
+ private createReceipt;
33
+ private editReceipt;
34
+ private scheduleRepair;
35
+ private flushRepairs;
36
+ private usable;
37
+ private current;
38
+ private hydrate;
39
+ receive(chat: string, rowId: number, messageId: string, native?: boolean, activeRunId?: string): LiveStatusController | undefined;
40
+ claim(chat: string, ids: number[], ownerId: string, batchId: string, native?: boolean): LiveStatusController | undefined;
41
+ attempted(live: LiveStatusController | undefined, submissionId: string, ownerId: string, batchId: string): void;
42
+ insertionAccepted(chat: string, rowId: number, expectedRunId: string): Promise<void>;
43
+ settleRun(chat: string, runId: string): Promise<void>;
44
+ admit(live: LiveStatusController | undefined, runId: string, ownerId: string, batchId: string): Promise<void>;
45
+ /** A deliberate same-task continuation has its own real ACK. Never infer
46
+ * this transfer from activity, elapsed time or an unrelated proactive final. */
47
+ advance(live: LiveStatusController | undefined, previousRunId: string | undefined, runId: string, ownerId: string, batchId: string): Promise<void>;
48
+ forTarget(chat: string, runId: string, live?: LiveStatusController): LiveStatusController | undefined;
49
+ forRun(chat: string, runId: string): LiveStatusController | undefined;
50
+ retire(chat: string): void;
51
+ dispose(): void;
52
+ }
@@ -0,0 +1,471 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { LiveStatusController, LiveStatusEffectRejectedError, LiveStatusEffectUnknownError, LiveStatusEffectSuppressedError } from "./live-status.js";
3
+ /** One unbound cohort per chat, or reuse the exact active-root presentation.
4
+ * Known IDs and desired phase survive restart. Unknown effects are fenced, not
5
+ * retried: the SDK offers no durable query-by-create-key or edit CAS. */
6
+ export class QueuedPresentations {
7
+ storeSource;
8
+ bot;
9
+ app;
10
+ callbacks;
11
+ enabled;
12
+ english;
13
+ entries = new Map();
14
+ /** Receipt custody only: never a business busy/admission flag or SDK retry. */
15
+ repairs = new Map();
16
+ repairTimer;
17
+ repairDelay = 1000;
18
+ repairing = false;
19
+ disposed = false;
20
+ constructor(storeSource, bot, app, callbacks, enabled, english) {
21
+ this.storeSource = storeSource;
22
+ this.bot = bot;
23
+ this.app = app;
24
+ this.callbacks = callbacks;
25
+ this.enabled = enabled;
26
+ this.english = english;
27
+ }
28
+ get store() { return typeof this.storeSource === "function" ? this.storeSource() : this.storeSource; }
29
+ normalize(row) {
30
+ // V1 could label expected legacy insertion roots as actual owners. Reopen
31
+ // them as enqueue-only custody; do not repair/promote any business row.
32
+ if (row.runId && (!row.ownerId || !row.batchId)) {
33
+ row.expectedInsertionRoot = row.expectedInsertionRoot || row.insertionRoot || row.runId;
34
+ delete row.runId;
35
+ }
36
+ return row;
37
+ }
38
+ records(chat) { return this.store.listPresentations(this.bot, this.app, chat).map((row) => this.observeJournal(row)); }
39
+ latest(record) {
40
+ const row = this.store.getPresentation(this.bot, this.app, record.chatId, record.id);
41
+ return row ? this.observeJournal(row) : undefined;
42
+ }
43
+ /** Overlay actual receipts before any later UI decision. Fresh metadata,
44
+ * lease/epoch/retirement and every unrelated unknown token remain untouched. */
45
+ withReceipts(record) {
46
+ const repair = this.repairs.get(record.id);
47
+ if (!repair)
48
+ return record;
49
+ const row = { ...record, editEffects: { ...record.editEffects } };
50
+ const create = repair.create;
51
+ if (create?.state === "accepted" && create.cardId) {
52
+ if (row.cardId && row.cardId !== create.cardId)
53
+ throw new Error("conflicting presentation create receipts; no UI retry");
54
+ if (row.createToken === create.token || row.cardId === create.cardId) {
55
+ row.cardId = create.cardId;
56
+ row.create = "accepted";
57
+ }
58
+ }
59
+ else if (create && row.createToken === create.token && !row.cardId) {
60
+ row.create = create.state;
61
+ if (create.state === "never")
62
+ delete row.createToken; // proven zero SDK
63
+ }
64
+ for (const [token, receipt] of repair.edits) {
65
+ if (receipt.state === "clear")
66
+ delete row.editEffects[token];
67
+ else if (row.editEffects[token] || row.lease === receipt.lease)
68
+ row.editEffects[token] = "unknown";
69
+ }
70
+ return row;
71
+ }
72
+ save(record) {
73
+ const row = this.withReceipts(record);
74
+ this.store.savePresentation(this.bot, this.app, row.chatId, row.id, row);
75
+ this.acknowledgeJournal(row);
76
+ }
77
+ observeJournal(record) {
78
+ this.acknowledgeJournal(record);
79
+ return this.withReceipts(this.normalize(record));
80
+ }
81
+ acknowledgeJournal(row) {
82
+ const repair = this.repairs.get(row.id);
83
+ if (repair) {
84
+ const create = repair.create;
85
+ if (create && (create.state === "accepted" ? row.cardId === create.cardId && row.create === "accepted"
86
+ : row.createToken !== create.token || row.create === create.state || row.create === "accepted"))
87
+ repair.create = undefined;
88
+ for (const [token, receipt] of repair.edits)
89
+ if (receipt.state === "clear" ? !row.editEffects?.[token]
90
+ : row.editEffects?.[token] === "unknown" || !row.editEffects?.[token] && row.lease !== receipt.lease)
91
+ repair.edits.delete(token);
92
+ if (!repair.create && !repair.edits.size)
93
+ this.repairs.delete(row.id);
94
+ }
95
+ if (!this.repairs.size) {
96
+ clearTimeout(this.repairTimer);
97
+ this.repairTimer = undefined;
98
+ this.repairDelay = 1000;
99
+ }
100
+ }
101
+ repairFor(record) {
102
+ let repair = this.repairs.get(record.id);
103
+ if (!repair) {
104
+ repair = { record, edits: new Map() };
105
+ this.repairs.set(record.id, repair);
106
+ }
107
+ return repair;
108
+ }
109
+ createReceipt(record, token, state, cardId) {
110
+ const repair = this.repairFor(record);
111
+ if (repair.create?.state !== "accepted" || state === "accepted")
112
+ repair.create = { token, lease: record.lease, state, cardId };
113
+ this.scheduleRepair(true);
114
+ }
115
+ editReceipt(record, token, state) {
116
+ const repair = this.repairFor(record);
117
+ if (repair.edits.get(token)?.state !== "clear" || state === "clear")
118
+ repair.edits.set(token, { lease: record.lease, state });
119
+ this.scheduleRepair(true);
120
+ }
121
+ scheduleRepair(immediate = false) {
122
+ if (this.disposed || this.repairTimer || !this.repairs.size)
123
+ return;
124
+ // A macrotask lets the D2 adopter receive the SDK ID/success first. A
125
+ // failed local write backs off; no timer ever invokes create/edit transport.
126
+ this.repairTimer = setTimeout(() => { this.repairTimer = undefined; this.flushRepairs(); }, immediate ? 0 : this.repairDelay);
127
+ this.repairTimer.unref?.();
128
+ }
129
+ flushRepairs() {
130
+ if (this.repairing || !this.repairs.size)
131
+ return;
132
+ clearTimeout(this.repairTimer);
133
+ this.repairTimer = undefined;
134
+ this.repairing = true;
135
+ let failed = false;
136
+ for (const repair of [...this.repairs.values()].slice(0, 32)) {
137
+ try {
138
+ const row = this.store.getPresentation(this.bot, this.app, repair.record.chatId, repair.record.id);
139
+ if (!row)
140
+ throw new Error("presentation receipt journal row unavailable");
141
+ this.acknowledgeJournal(row);
142
+ if (this.repairs.has(row.id))
143
+ this.save(this.normalize(row));
144
+ }
145
+ catch {
146
+ failed = true;
147
+ // Rotate failures so one unavailable cohort cannot starve another.
148
+ this.repairs.delete(repair.record.id);
149
+ this.repairs.set(repair.record.id, repair);
150
+ }
151
+ }
152
+ this.repairing = false;
153
+ if (failed)
154
+ this.repairDelay = Math.min(30_000, this.repairDelay * 2);
155
+ this.scheduleRepair();
156
+ }
157
+ usable(row) {
158
+ return !row.retired && row.stopEpoch === this.store.getStopGeneration(this.bot, row.chatId)
159
+ && row.view?.state !== "done" && row.view?.state !== "failed";
160
+ }
161
+ current(record) {
162
+ const row = this.latest(record);
163
+ return !!row && this.usable(row) && row.lease === record.lease && this.enabled(record.chatId);
164
+ }
165
+ hydrate(record) {
166
+ const cached = this.entries.get(record.id);
167
+ if (cached && cached.record.lease === record.lease)
168
+ return cached;
169
+ if (cached) {
170
+ cached.live.dispose();
171
+ this.entries.delete(record.id);
172
+ }
173
+ record = { ...record, lease: randomUUID() };
174
+ this.save(record); // fences callbacks from any replaced process/manager
175
+ const base = this.callbacks(record.chatId, record.messageId);
176
+ const desired = (view) => {
177
+ try {
178
+ this.flushRepairs();
179
+ const row = this.latest(record);
180
+ if (!row || row.lease !== record.lease || row.retired || row.stopEpoch !== this.store.getStopGeneration(this.bot, row.chatId))
181
+ return;
182
+ // Elapsed-only ticking is derived from startedAt after restart. Do not
183
+ // rewrite desired custody twice for the same phase/content snapshot.
184
+ if (row.view && JSON.stringify({ ...row.view, elapsed: undefined }) === JSON.stringify({ ...view, elapsed: undefined }))
185
+ return;
186
+ // UI journal outages may suppress UI effects, never business admission.
187
+ // Actual effect checkpoints still MUST succeed before transport.
188
+ this.save({ ...row, view });
189
+ }
190
+ catch (err) {
191
+ base.warn?.("presentation desired checkpoint unavailable", err);
192
+ }
193
+ };
194
+ const live = new LiveStatusController({
195
+ warn: base.warn,
196
+ desired,
197
+ unconfirmedInputs: outcome => {
198
+ try {
199
+ const row = this.latest(record);
200
+ if (!row)
201
+ return { title: `${this.bot} input status unconfirmed`, detail: "输入保管状态未确认;不把 root 终局当作所有输入完成" };
202
+ const ordinary = row.runId && row.ownerId && row.batchId ? (row.ordinaryTriggerIds || row.triggerIds.filter(id => {
203
+ const insertion = this.store.getInsertion(this.bot, row.chatId, id);
204
+ return !insertion || insertion.state === "ordinary";
205
+ })) : [];
206
+ const pending = row.triggerIds.filter(id => {
207
+ const insertion = this.store.getInsertion(this.bot, row.chatId, id);
208
+ return !ordinary.includes(id) || !!insertion && insertion.state !== "ordinary";
209
+ });
210
+ if (!pending.length)
211
+ return undefined;
212
+ const en = this.english(row.chatId);
213
+ return {
214
+ title: en ? `${this.bot} ${ordinary.length ? outcome === "failed" ? "run ended · " : "partial result · " : ""}input confirmation pending`
215
+ : `${this.bot} ${ordinary.length ? outcome === "failed" ? "本轮结束 · " : "部分完成 · " : ""}追加输入回执待确认`,
216
+ detail: en ? `${pending.length} additional input(s) have only local/enqueue receipts; actual receiver, commitment and result are unconfirmed. Root completion covers only ordinary inputs; no automatic replay.`
217
+ : `${pending.length} 条追加输入仅确认本地收件/进入队列;实际接收方、提交及处理结果未确认。root 结果仅覆盖普通输入;保留观察,不重放。`,
218
+ };
219
+ }
220
+ catch {
221
+ return { title: `${this.bot} input status unconfirmed`, detail: "输入保管状态暂不可读;完成范围未确认,不显示全部完成" };
222
+ }
223
+ },
224
+ create: async (view, effect) => {
225
+ const token = randomUUID();
226
+ let attempted = false, settled = false;
227
+ try {
228
+ const result = await base.create(view, { run: send => effect.run(async () => {
229
+ try {
230
+ const row = this.latest(record);
231
+ if (!this.current(record))
232
+ throw new LiveStatusEffectSuppressedError("presentation create suppressed before transport");
233
+ if (row?.create !== "never")
234
+ throw new LiveStatusEffectUnknownError("this presentation create already attempted");
235
+ if (this.records(record.chatId).some(other => other.id !== record.id && !other.retired && other.stopEpoch === row.stopEpoch
236
+ && other.triggerIds.some(id => row.triggerIds.includes(id))
237
+ && ["attempted", "unknown", "accepted"].includes(other.create)))
238
+ throw new LiveStatusEffectUnknownError("overlapping input presentation create unresolved");
239
+ // A failed preflight, even a write-then-throw, is proven zero SDK.
240
+ try {
241
+ this.save({ ...row, create: "attempted", createToken: token });
242
+ }
243
+ catch (err) {
244
+ this.createReceipt(record, token, "never");
245
+ throw err;
246
+ }
247
+ }
248
+ catch (err) {
249
+ if (err instanceof LiveStatusEffectUnknownError || err instanceof LiveStatusEffectSuppressedError)
250
+ throw err;
251
+ throw new LiveStatusEffectSuppressedError("presentation create checkpoint unavailable before SDK");
252
+ }
253
+ attempted = true;
254
+ let id;
255
+ try {
256
+ id = await send();
257
+ }
258
+ catch (err) {
259
+ settled = true;
260
+ this.createReceipt(record, token, err instanceof LiveStatusEffectRejectedError ? "rejected" : "unknown");
261
+ throw err;
262
+ }
263
+ settled = true;
264
+ // Memory first; no post-SDK journal error may swallow this ID or
265
+ // enter the transport catch. D2 adopts before the repair macrotask.
266
+ this.createReceipt(record, token, id ? "accepted" : "unknown", id);
267
+ return id;
268
+ }) });
269
+ // The adapter's effect context has already delivered the ID to D2.
270
+ this.flushRepairs();
271
+ return result;
272
+ }
273
+ catch (err) {
274
+ if (attempted && !settled)
275
+ this.createReceipt(record, token, "unknown"); // outer deadline, not cancellation
276
+ throw err;
277
+ }
278
+ },
279
+ edit: async (id, view, effect) => {
280
+ const token = randomUUID();
281
+ let attempted = false, settled = false;
282
+ try {
283
+ await base.edit(id, view, { run: send => effect.run(async () => {
284
+ try {
285
+ const row = this.latest(record);
286
+ if (!row || row.lease !== record.lease || row.retired || row.stopEpoch !== this.store.getStopGeneration(this.bot, row.chatId)
287
+ || !this.enabled(row.chatId) || row.cardId !== id)
288
+ throw new LiveStatusEffectSuppressedError("presentation edit suppressed before transport");
289
+ try {
290
+ this.save({ ...row, editEffects: { ...row.editEffects, [token]: "attempted" } });
291
+ }
292
+ catch (err) {
293
+ this.editReceipt(record, token, "clear");
294
+ throw err;
295
+ }
296
+ }
297
+ catch (err) {
298
+ if (err instanceof LiveStatusEffectSuppressedError)
299
+ throw err;
300
+ throw new LiveStatusEffectSuppressedError("presentation edit checkpoint unavailable before SDK");
301
+ }
302
+ attempted = true;
303
+ try {
304
+ await send();
305
+ }
306
+ catch (err) {
307
+ settled = true;
308
+ this.editReceipt(record, token, err instanceof LiveStatusEffectRejectedError ? "clear" : "unknown");
309
+ throw err;
310
+ }
311
+ settled = true;
312
+ // Clear only THIS successful token; do not replay the SDK or erase
313
+ // another unknown progress/terminal operation if SQLite is busy.
314
+ this.editReceipt(record, token, "clear");
315
+ }) });
316
+ // D2 has recorded the successful effect before local persistence.
317
+ this.flushRepairs();
318
+ }
319
+ catch (err) {
320
+ if (attempted && !settled)
321
+ this.editReceipt(record, token, "unknown");
322
+ throw err;
323
+ }
324
+ },
325
+ }, {
326
+ botName: this.bot, locale: this.english(record.chatId) ? "en" : "zh", initialPhase: "queued",
327
+ restore: { messageId: record.cardId, view: record.view, startedAt: record.startedAt,
328
+ blockCreate: record.create !== "never", blockEdits: Object.keys(record.editEffects || {}).length > 0 },
329
+ });
330
+ const entry = { record, live };
331
+ this.entries.set(record.id, entry);
332
+ return entry;
333
+ }
334
+ receive(chat, rowId, messageId, native = false, activeRunId) {
335
+ if (native || !rowId || !this.enabled(chat))
336
+ return undefined;
337
+ const rows = this.records(chat).filter(row => this.usable(row));
338
+ // Repeated ingress and steering bursts reuse one card. A foreign/old run
339
+ // is never selected merely because it happens to have a card in this chat.
340
+ let record = rows.find(row => row.triggerIds.includes(rowId))
341
+ || rows.find(row => !row.runId && !row.submissionId)
342
+ || (activeRunId ? rows.find(row => row.runId === activeRunId && row.view?.phase !== "waiting") : undefined);
343
+ if (!record) {
344
+ record = { id: randomUUID(), chatId: chat, messageId, triggerIds: [], stopEpoch: this.store.getStopGeneration(this.bot, chat),
345
+ startedAt: Date.now(), lease: "", create: "never" };
346
+ }
347
+ if (!record.triggerIds.includes(rowId))
348
+ record.triggerIds.push(rowId);
349
+ this.save(record); // local receipt already exists; UI record is separate
350
+ const { live } = this.hydrate(record);
351
+ live.received(this.english(chat) ? "Input durably received locally; acceptance not yet confirmed" : "消息已在本地可靠收件;尚未确认 Gateway 接收");
352
+ live.start();
353
+ return live;
354
+ }
355
+ claim(chat, ids, ownerId, batchId, native = false) {
356
+ if (native || !this.enabled(chat))
357
+ return undefined;
358
+ const row = this.records(chat).find(row => this.usable(row) && !row.runId && (!row.submissionId || row.ownerId === ownerId && row.batchId === batchId) && row.triggerIds.some(id => ids.includes(id)));
359
+ if (!row)
360
+ return undefined;
361
+ // An ordinary batch may only adopt its own intersecting, unbound cohort.
362
+ row.ownerId = ownerId;
363
+ row.batchId = batchId;
364
+ row.triggerIds = [...new Set([...row.triggerIds, ...ids])];
365
+ row.ordinaryTriggerIds = [...ids]; // actual ordinary payload membership, not steer ACKs
366
+ this.save(row);
367
+ const live = this.hydrate(row).live;
368
+ live.resumeQueued();
369
+ return live;
370
+ }
371
+ attempted(live, submissionId, ownerId, batchId) {
372
+ const entry = [...this.entries.values()].find(entry => entry.live === live);
373
+ if (!entry || !submissionId || !this.current(entry.record))
374
+ return;
375
+ const row = this.latest(entry.record);
376
+ if (row.ownerId !== ownerId || row.batchId !== batchId || row.submissionId && row.submissionId !== submissionId)
377
+ return;
378
+ // The actual public submission key seals this cohort. It is NOT a run ID.
379
+ row.submissionId = submissionId;
380
+ this.save(row);
381
+ }
382
+ async insertionAccepted(chat, rowId, expectedRunId) {
383
+ const row = this.records(chat).find(row => this.usable(row) && row.triggerIds.includes(rowId)
384
+ && (!row.runId || row.runId === expectedRunId));
385
+ if (!row || !expectedRunId)
386
+ return;
387
+ // Legacy accepted means enqueue only. expectedRunId is NOT an actual
388
+ // receiver/commitment witness, even when every cohort member has this ACK.
389
+ if (row.expectedInsertionRoot && row.expectedInsertionRoot !== expectedRunId)
390
+ return;
391
+ row.expectedInsertionRoot = expectedRunId;
392
+ row.enqueueAcceptedIds = [...new Set([...(row.enqueueAcceptedIds || []), rowId])];
393
+ this.save(row);
394
+ const live = this.hydrate(row).live;
395
+ const detail = this.english(chat) ? "Input enqueued; actual receiver, commitment and result unconfirmed" : "追加输入已进入队列;实际接收方、提交及处理结果未确认";
396
+ if (row.runId)
397
+ live.received(detail); // ordinary A can still show its own real progress
398
+ else
399
+ await live.showInputWaiting(detail, this.english(chat) ? `${this.bot} enqueued · input confirmation pending` : `${this.bot} 已排队 · 追加输入回执待确认`);
400
+ }
401
+ async settleRun(chat, runId) {
402
+ if (!runId || !this.enabled(chat))
403
+ return;
404
+ for (const row of this.records(chat))
405
+ if (this.usable(row) && row.runId === runId)
406
+ await this.hydrate(row).live.complete();
407
+ }
408
+ async admit(live, runId, ownerId, batchId) {
409
+ if (!live || !runId)
410
+ return;
411
+ const entry = [...this.entries.values()].find(entry => entry.live === live);
412
+ if (!entry || !this.current(entry.record))
413
+ return;
414
+ const row = this.latest(entry.record);
415
+ if (row.ownerId !== ownerId || row.batchId !== batchId || (row.runId && row.runId !== runId))
416
+ return;
417
+ row.runId = runId;
418
+ this.save(row);
419
+ await live.admitted();
420
+ }
421
+ /** A deliberate same-task continuation has its own real ACK. Never infer
422
+ * this transfer from activity, elapsed time or an unrelated proactive final. */
423
+ async advance(live, previousRunId, runId, ownerId, batchId) {
424
+ const entry = [...this.entries.values()].find(entry => entry.live === live);
425
+ if (!entry || !previousRunId || !runId || !this.current(entry.record))
426
+ return;
427
+ const row = this.latest(entry.record);
428
+ if (row.runId !== previousRunId || row.ownerId !== ownerId || row.batchId !== batchId)
429
+ return;
430
+ row.runId = runId;
431
+ this.save(row);
432
+ await live.admitted(true);
433
+ }
434
+ forTarget(chat, runId, live) {
435
+ if (!live)
436
+ return this.forRun(chat, runId);
437
+ const entry = [...this.entries.values()].find(entry => entry.live === live);
438
+ // Existing discussion/custom controllers retain their original contract.
439
+ if (!entry)
440
+ return live;
441
+ const row = this.latest(entry.record);
442
+ return row && this.usable(row) && row.runId === runId ? live : undefined;
443
+ }
444
+ forRun(chat, runId) {
445
+ if (!runId || !this.enabled(chat))
446
+ return undefined;
447
+ const row = this.records(chat).find(row => this.usable(row) && row.runId === runId);
448
+ return row ? this.hydrate(row).live : undefined;
449
+ }
450
+ retire(chat) {
451
+ // Stop old callbacks even when the UI journal is temporarily unavailable.
452
+ for (const entry of this.entries.values())
453
+ if (entry.record.chatId === chat)
454
+ entry.live.dispose();
455
+ for (const row of this.records(chat)) {
456
+ if (row.retired)
457
+ continue;
458
+ this.save({ ...row, retired: true });
459
+ this.entries.get(row.id)?.live.dispose();
460
+ }
461
+ }
462
+ dispose() {
463
+ this.disposed = true;
464
+ clearTimeout(this.repairTimer);
465
+ this.repairTimer = undefined;
466
+ this.flushRepairs(); // one best-effort journal-only shutdown pass, never SDK
467
+ for (const entry of this.entries.values())
468
+ entry.live.dispose();
469
+ this.entries.clear();
470
+ }
471
+ }
@@ -1,6 +1,7 @@
1
1
  /** Consecutive effective-activity silence, not a total foreground lifetime.
2
2
  * Starts with accepted-request collection; does not change Gateway execution limits. */
3
3
  export declare const FOREGROUND_WAIT_MS: number;
4
+ export declare const IDLE_GUARD_MS: number;
4
5
  export type SessionRuntimeStatus = {
5
6
  status: string;
6
7
  running: boolean;
@@ -1,6 +1,10 @@
1
1
  /** Consecutive effective-activity silence, not a total foreground lifetime.
2
2
  * Starts with accepted-request collection; does not change Gateway execution limits. */
3
3
  export const FOREGROUND_WAIT_MS = 10 * 60_000;
4
+ /** Bound one local status-watch callback, never the lifetime of a business run. */
5
+ const configuredIdleGuard = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_IDLE_GUARD_MS || 30_000);
6
+ export const IDLE_GUARD_MS = Number.isFinite(configuredIdleGuard) && configuredIdleGuard > 0 && configuredIdleGuard <= 2_147_483_647
7
+ ? configuredIdleGuard : 30_000;
4
8
  export const isWaitTimeout = (text) => /\btimeout\b|timed\s*out|超时/i.test(text) && !/invalid|validation|schema|malformed|permission denied|forbidden|unauthorized|authentication failed|quota|rate limit|无效参数|参数错误|校验失败/i.test(text);
5
9
  export function normalizeSessionRuntimeStatus(value) {
6
10
  const session = value?.session || value?.sessionInfo;
@@ -0,0 +1,28 @@
1
+ # 1.8.15 — separate input admission, background outcomes and presentation
2
+
3
+ ## Repair scope
4
+
5
+ - Ordinary `chat.send` explicitly selects `queueMode: followup` rather than inheriting a session's stored interrupt/steer policy. SDK insertion keeps its existing durable input identity, stop/generation checks and acceptance semantics.
6
+ - Collectors use non-destructive exact-run event cursors. Replaced observers cannot steal another run's progress or final; synchronous ACK custody protects same-batch finals. Progress-buffer byte budgets preserve authoritative full results until durable handoff.
7
+ - Lifecycle, empty final and message-tool hints trigger exact-run reconciliation, not draft promotion or tool-input replay. Known inter-session input projections and private coordination tool bodies are excluded before output/progress buffering. Genuine assistant quotations and original background results are retained.
8
+ - Root completion does not manufacture insertion commitment/DONE for a merely accepted SDK input. Get is acceptance, not proof of model consumption.
9
+ - Durable queued/waiting presentation begins before slow admission checks. Card pumps coalesce updates, retain known SDK receipts across local checkpoint failures and do not let unknown old effects silently complete new inputs.
10
+ - One silence-watchdog callback has a validated local time bound. A stalled local observer hands its exact outcome to durable/proactive recovery before retiring; it neither stops a business run nor replays input. Cleanup is generation-fenced and cannot clear a successor's guard. Owner release does not wait on card/reaction cleanup. This is not a task-lifetime cutoff.
11
+ - Preserve the existing A4 positive-blocker checks (archive, generation conflict and confirmed busy). Unknown admission never authorizes replay of an attempted or stopped input.
12
+
13
+ ## Verification
14
+
15
+ Integrated in an isolated worktree on main `1badc85`, using the previously handed-off client-v3, B1 and D3-v3 candidates and ordinary-followup policy. The documented B1/D3 overlap retains queued presentation settlement while removing false accepted-to-committed settlement. Only selected public source/test candidates were imported, not an entire QA fixture tree.
16
+
17
+ - Complete offline suite: **63 files, 1701 tests passed**, no skipped or unhandled-error summary.
18
+ - Includes actual Client/WS receiver/Bot/SQLite/outbox fault schedules, ten new watchdog/late-result tests, and parent presentation/insertion interoperability controls.
19
+ - TypeScript bridge + all bundled plugins build; public SDK type contract passed.
20
+ - Uses the current installed local dependency tree; this is not a claim of independently reinstalled exact-lock testing. Published CI performs `npm ci` on Node 24.
21
+
22
+ ## Operations and limits
23
+
24
+ - Bridge runtime update only. The coordinated lma-core protocol remains **2**, package **0.2.0**; no Gateway model/config reset or session-history deletion is required.
25
+ - Preserve existing bridge state with a coherent SQLite backup before the new presentation tables are initialized. Retain only the verified prior runtime needed for rollback; do not replace platform-compatible native dependencies.
26
+ - Windows is independently activated and verified under the actual NSSM data/log paths, not an obsolete home-directory DB.
27
+ - Existing accepted/unknown/stopped inputs are not manually replayed. Do not infer delivery from a process/version, transcript, Get or green tests: original-group platform receipts and a post-deployment human request remain separate live acceptance evidence.
28
+ - Does not claim to fix upstream native steer commit/target atomicity gaps, nor automatically convert an unknown input receipt into consumption proof.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-lark-multi-agent",
3
- "version": "1.8.13",
3
+ "version": "1.8.15",
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": {
@@ -75,7 +75,8 @@
75
75
  "plugins/lma-core/README.md",
76
76
  "docs/releases/1.8.5.md",
77
77
  "docs/releases/1.8.10.md",
78
- "docs/releases/1.8.11.md"
78
+ "docs/releases/1.8.11.md",
79
+ "docs/releases/1.8.15.md"
79
80
  ],
80
81
  "engines": {
81
82
  "node": ">=22"
@@ -83,4 +84,4 @@
83
84
  "publishConfig": {
84
85
  "access": "public"
85
86
  }
86
- }
87
+ }