openclaw-lark-multi-agent 1.4.1 → 1.4.3
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/config.example.json +1 -1
- package/dist/cli.js +31 -6
- package/dist/feishu-bot.d.ts +19 -10
- package/dist/feishu-bot.js +470 -93
- package/dist/live-status.d.ts +17 -0
- package/dist/live-status.js +27 -0
- package/dist/message-store.d.ts +29 -0
- package/dist/message-store.js +162 -8
- package/dist/openclaw-client.d.ts +4 -0
- package/dist/openclaw-client.js +75 -0
- package/package.json +1 -1
package/dist/live-status.d.ts
CHANGED
|
@@ -23,6 +23,15 @@ export type LiveStatusView = {
|
|
|
23
23
|
/** Terminal state: running / done / failed (lets renderer pick color). */
|
|
24
24
|
state: "running" | "done" | "failed";
|
|
25
25
|
};
|
|
26
|
+
/** Persisted metadata needed to turn the live-status card into the final answer. */
|
|
27
|
+
export type LiveStatusFinalMeta = {
|
|
28
|
+
messageId: string;
|
|
29
|
+
toolCalls: number;
|
|
30
|
+
elapsed: string;
|
|
31
|
+
model?: string;
|
|
32
|
+
locale: "zh" | "en";
|
|
33
|
+
noReply?: boolean;
|
|
34
|
+
};
|
|
26
35
|
export type LiveStatusCallbacks = {
|
|
27
36
|
/** Create the status card; returns the new message id. */
|
|
28
37
|
create: (view: LiveStatusView) => Promise<string | undefined>;
|
|
@@ -71,6 +80,14 @@ export declare class LiveStatusController {
|
|
|
71
80
|
start(initialDetail?: string): void;
|
|
72
81
|
progress(event: ProgressEvent | string): Promise<void>;
|
|
73
82
|
complete(): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Freeze any successful terminal state (text, attachment-only, or NO_REPLY)
|
|
85
|
+
* without first emitting the compact done patch. The caller persists this
|
|
86
|
+
* metadata in the outbox, which owns the final patch/cleanup. If no card was
|
|
87
|
+
* created, the caller falls back to normal new-message delivery.
|
|
88
|
+
*/
|
|
89
|
+
prepareTerminalDelivery(noReply?: boolean): Promise<LiveStatusFinalMeta | undefined>;
|
|
90
|
+
prepareFinalDelivery(): Promise<LiveStatusFinalMeta | undefined>;
|
|
74
91
|
fail(): Promise<void>;
|
|
75
92
|
/**
|
|
76
93
|
* Finish when the model explicitly produced no reply (NO_REPLY) or an empty
|
package/dist/live-status.js
CHANGED
|
@@ -109,6 +109,33 @@ export class LiveStatusController {
|
|
|
109
109
|
return;
|
|
110
110
|
await this.safeEditFinal(this.buildView());
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Freeze any successful terminal state (text, attachment-only, or NO_REPLY)
|
|
114
|
+
* without first emitting the compact done patch. The caller persists this
|
|
115
|
+
* metadata in the outbox, which owns the final patch/cleanup. If no card was
|
|
116
|
+
* created, the caller falls back to normal new-message delivery.
|
|
117
|
+
*/
|
|
118
|
+
async prepareTerminalDelivery(noReply = false) {
|
|
119
|
+
this.state = "done";
|
|
120
|
+
this.noReplyResult = noReply;
|
|
121
|
+
this.finalized = true;
|
|
122
|
+
this.stopTimers();
|
|
123
|
+
if (this.createPromise)
|
|
124
|
+
await this.createPromise.catch(() => { });
|
|
125
|
+
if (!this.messageId)
|
|
126
|
+
return undefined;
|
|
127
|
+
return {
|
|
128
|
+
messageId: this.messageId,
|
|
129
|
+
toolCalls: this.toolCallCount,
|
|
130
|
+
elapsed: this.formatElapsed(),
|
|
131
|
+
model: this.opts.model,
|
|
132
|
+
locale: this.opts.locale === "en" ? "en" : "zh",
|
|
133
|
+
noReply,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async prepareFinalDelivery() {
|
|
137
|
+
return this.prepareTerminalDelivery(false);
|
|
138
|
+
}
|
|
112
139
|
async fail() {
|
|
113
140
|
this.state = "failed";
|
|
114
141
|
this.finalized = true;
|
package/dist/message-store.d.ts
CHANGED
|
@@ -41,6 +41,19 @@ export interface DeliveryOutboxItem {
|
|
|
41
41
|
content: string;
|
|
42
42
|
attachmentsJson: string;
|
|
43
43
|
replyToMessageId: string;
|
|
44
|
+
/** Delivery mode. `patch_live_status` turns the existing live-status card
|
|
45
|
+
* into the final answer instead of creating a second Feishu message. */
|
|
46
|
+
deliveryMode?: "send" | "patch_live_status";
|
|
47
|
+
/** Existing interactive-card message id to patch for patch_live_status. */
|
|
48
|
+
targetMessageId?: string;
|
|
49
|
+
/** Persisted JSON footer metadata (tool calls / elapsed / model / locale). */
|
|
50
|
+
deliveryMetaJson?: string;
|
|
51
|
+
/** Durable stage checkpoint: final text/card is already visible. */
|
|
52
|
+
textDelivered?: boolean;
|
|
53
|
+
/** Durable stage checkpoint: number of attachments already delivered. */
|
|
54
|
+
attachmentCursor?: number;
|
|
55
|
+
/** Final status-card cleanup (done summary after fallback/dedupe) is pending. */
|
|
56
|
+
cleanupPending?: boolean;
|
|
44
57
|
status: "pending" | "delivering" | "delivered" | "failed";
|
|
45
58
|
attempts: number;
|
|
46
59
|
createdAt: number;
|
|
@@ -69,6 +82,8 @@ export declare class MessageStore {
|
|
|
69
82
|
markDeliveredReply(botName: string, chatId: string, triggerMessageRowId: number, replyMessageId?: string): void;
|
|
70
83
|
enqueueDelivery(item: Omit<DeliveryOutboxItem, "id" | "status" | "attempts" | "createdAt" | "updatedAt">): number | null;
|
|
71
84
|
getDeliveryBySource(sessionKey: string, sourceType: string, sourceId: string): DeliveryOutboxItem | null;
|
|
85
|
+
getDeliveryByKey(botName: string, chatId: string, deliveryKey: string): DeliveryOutboxItem | null;
|
|
86
|
+
listDeliveriesByKeyPrefix(botName: string, chatId: string, prefix: string): DeliveryOutboxItem[];
|
|
72
87
|
getPendingDeliveries(chatId?: string, botName?: string, maxCount?: number): DeliveryOutboxItem[];
|
|
73
88
|
markBotSeenInChat(botName: string, chatId: string): void;
|
|
74
89
|
hasBotSeenInChat(botName: string, chatId: string): boolean;
|
|
@@ -78,13 +93,27 @@ export declare class MessageStore {
|
|
|
78
93
|
hasAnyDeliveredToChat(botName: string, chatId: string): boolean;
|
|
79
94
|
hasRecentSimilarDelivery(botName: string, chatId: string, contentHash: string, windowMs: number, sourceTypes?: string[]): boolean;
|
|
80
95
|
hasRecentOverlappingDelivery(botName: string, chatId: string, content: string, attachmentsJson: string, windowMs: number, minShortLength?: number, sourceTypes?: string[]): boolean;
|
|
96
|
+
/** Recover in-flight rows after process restart. A new process cannot own a
|
|
97
|
+
* legitimate delivering row, so every matching row returns to pending—even if
|
|
98
|
+
* the previous process crashed on its nominal final attempt. The restarted
|
|
99
|
+
* process gets another chance to finish rather than silently losing payload. */
|
|
100
|
+
resetDeliveringOnStartup(botName: string): number;
|
|
81
101
|
resetStaleDeliveries(botName: string, staleMs: number, maxAttempts: number): {
|
|
82
102
|
restored: number;
|
|
83
103
|
failed: number;
|
|
84
104
|
};
|
|
85
105
|
claimDelivery(id: number): boolean;
|
|
106
|
+
markDeliveryTextDelivered(id: number): void;
|
|
107
|
+
/** Atomically checkpoint fallback text and the remaining card cleanup. */
|
|
108
|
+
markDeliveryTextDeliveredWithCleanup(id: number): void;
|
|
109
|
+
markDeliveryAttachmentCursor(id: number, cursor: number): void;
|
|
110
|
+
/** Return a failed in-flight delivery to pending until its retry budget is spent. */
|
|
111
|
+
retryDelivery(id: number, maxAttempts: number): boolean;
|
|
112
|
+
markDeliveryCleanupPending(id: number, pending: boolean): void;
|
|
86
113
|
markDeliveryDelivered(id: number): void;
|
|
87
114
|
markDeliveryFailed(id: number): void;
|
|
115
|
+
/** Give a later explicit duplicate another chance after terminal failure. */
|
|
116
|
+
requeueFailedDelivery(id: number): boolean;
|
|
88
117
|
private mapDelivery;
|
|
89
118
|
/**
|
|
90
119
|
* Get messages that haven't been synced to a bot's session yet.
|
package/dist/message-store.js
CHANGED
|
@@ -148,6 +148,12 @@ export class MessageStore {
|
|
|
148
148
|
content TEXT NOT NULL DEFAULT '',
|
|
149
149
|
attachments_json TEXT NOT NULL DEFAULT '[]',
|
|
150
150
|
reply_to_message_id TEXT NOT NULL DEFAULT '',
|
|
151
|
+
delivery_mode TEXT NOT NULL DEFAULT 'send',
|
|
152
|
+
target_message_id TEXT NOT NULL DEFAULT '',
|
|
153
|
+
delivery_meta_json TEXT NOT NULL DEFAULT '{}',
|
|
154
|
+
text_delivered INTEGER NOT NULL DEFAULT 0,
|
|
155
|
+
attachment_cursor INTEGER NOT NULL DEFAULT 0,
|
|
156
|
+
cleanup_pending INTEGER NOT NULL DEFAULT 0,
|
|
151
157
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
152
158
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
153
159
|
created_at INTEGER NOT NULL,
|
|
@@ -183,6 +189,42 @@ export class MessageStore {
|
|
|
183
189
|
catch {
|
|
184
190
|
// Column already exists
|
|
185
191
|
}
|
|
192
|
+
try {
|
|
193
|
+
this.db.exec(`ALTER TABLE delivery_outbox ADD COLUMN delivery_mode TEXT NOT NULL DEFAULT 'send'`);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Column already exists
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
this.db.exec(`ALTER TABLE delivery_outbox ADD COLUMN target_message_id TEXT NOT NULL DEFAULT ''`);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Column already exists
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
this.db.exec(`ALTER TABLE delivery_outbox ADD COLUMN delivery_meta_json TEXT NOT NULL DEFAULT '{}'`);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Column already exists
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
this.db.exec(`ALTER TABLE delivery_outbox ADD COLUMN text_delivered INTEGER NOT NULL DEFAULT 0`);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Column already exists
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
this.db.exec(`ALTER TABLE delivery_outbox ADD COLUMN attachment_cursor INTEGER NOT NULL DEFAULT 0`);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Column already exists
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
this.db.exec(`ALTER TABLE delivery_outbox ADD COLUMN cleanup_pending INTEGER NOT NULL DEFAULT 0`);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Column already exists
|
|
227
|
+
}
|
|
186
228
|
// Backfill stable keys for rows created by earlier outbox experiments before
|
|
187
229
|
// creating indexes that reference the new columns.
|
|
188
230
|
this.db.exec(`
|
|
@@ -194,9 +236,47 @@ export class MessageStore {
|
|
|
194
236
|
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_delivery_outbox_key ON delivery_outbox(bot_name, chat_id, delivery_key)`);
|
|
195
237
|
}
|
|
196
238
|
catch {
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
239
|
+
// Legacy experimental databases may contain duplicate logical deliveries.
|
|
240
|
+
// Keep the earliest owner (and its stage checkpoints) deterministically,
|
|
241
|
+
// remove later duplicates, then REQUIRE the unique index. Continuing
|
|
242
|
+
// without it would allow duplicate visible finals on explicit trigger keys.
|
|
243
|
+
// Merge durable progress into the earliest owner before deleting later
|
|
244
|
+
// duplicates. Prefer the most-complete state so upgrading a dirty legacy DB
|
|
245
|
+
// cannot replay already-visible text/attachments or lose delivered state.
|
|
246
|
+
this.db.exec(`
|
|
247
|
+
UPDATE delivery_outbox AS owner
|
|
248
|
+
SET
|
|
249
|
+
text_delivered = (
|
|
250
|
+
SELECT MAX(d.text_delivered) FROM delivery_outbox d
|
|
251
|
+
WHERE d.bot_name=owner.bot_name AND d.chat_id=owner.chat_id AND d.delivery_key=owner.delivery_key
|
|
252
|
+
),
|
|
253
|
+
attachment_cursor = (
|
|
254
|
+
SELECT MAX(d.attachment_cursor) FROM delivery_outbox d
|
|
255
|
+
WHERE d.bot_name=owner.bot_name AND d.chat_id=owner.chat_id AND d.delivery_key=owner.delivery_key
|
|
256
|
+
),
|
|
257
|
+
cleanup_pending = (
|
|
258
|
+
SELECT MAX(d.cleanup_pending) FROM delivery_outbox d
|
|
259
|
+
WHERE d.bot_name=owner.bot_name AND d.chat_id=owner.chat_id AND d.delivery_key=owner.delivery_key
|
|
260
|
+
),
|
|
261
|
+
attempts = (
|
|
262
|
+
SELECT MAX(d.attempts) FROM delivery_outbox d
|
|
263
|
+
WHERE d.bot_name=owner.bot_name AND d.chat_id=owner.chat_id AND d.delivery_key=owner.delivery_key
|
|
264
|
+
),
|
|
265
|
+
status = CASE
|
|
266
|
+
WHEN EXISTS(SELECT 1 FROM delivery_outbox d WHERE d.bot_name=owner.bot_name AND d.chat_id=owner.chat_id AND d.delivery_key=owner.delivery_key AND d.status='delivered') THEN 'delivered'
|
|
267
|
+
WHEN EXISTS(SELECT 1 FROM delivery_outbox d WHERE d.bot_name=owner.bot_name AND d.chat_id=owner.chat_id AND d.delivery_key=owner.delivery_key AND d.status='pending') THEN 'pending'
|
|
268
|
+
WHEN EXISTS(SELECT 1 FROM delivery_outbox d WHERE d.bot_name=owner.bot_name AND d.chat_id=owner.chat_id AND d.delivery_key=owner.delivery_key AND d.status='delivering') THEN 'pending'
|
|
269
|
+
ELSE 'failed'
|
|
270
|
+
END
|
|
271
|
+
WHERE owner.id IN (
|
|
272
|
+
SELECT MIN(id) FROM delivery_outbox GROUP BY bot_name, chat_id, delivery_key HAVING COUNT(*) > 1
|
|
273
|
+
);
|
|
274
|
+
DELETE FROM delivery_outbox
|
|
275
|
+
WHERE id NOT IN (
|
|
276
|
+
SELECT MIN(id) FROM delivery_outbox GROUP BY bot_name, chat_id, delivery_key
|
|
277
|
+
);
|
|
278
|
+
`);
|
|
279
|
+
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_delivery_outbox_key ON delivery_outbox(bot_name, chat_id, delivery_key)`);
|
|
200
280
|
}
|
|
201
281
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_delivery_outbox_content ON delivery_outbox(bot_name, chat_id, content_hash, created_at)`);
|
|
202
282
|
// Migration: add delivery_outbox reply target if missing
|
|
@@ -404,9 +484,9 @@ export class MessageStore {
|
|
|
404
484
|
const now = Date.now();
|
|
405
485
|
const result = this.db.prepare(`
|
|
406
486
|
INSERT OR IGNORE INTO delivery_outbox
|
|
407
|
-
(session_key, chat_id, bot_name, source_type, source_id, delivery_key, content_hash, content, attachments_json, reply_to_message_id, status, attempts, created_at, updated_at)
|
|
408
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?)
|
|
409
|
-
`).run(item.sessionKey, item.chatId, item.botName, item.sourceType, item.sourceId, item.deliveryKey, item.contentHash, item.content, item.attachmentsJson, item.replyToMessageId || '', now, now);
|
|
487
|
+
(session_key, chat_id, bot_name, source_type, source_id, delivery_key, content_hash, content, attachments_json, reply_to_message_id, delivery_mode, target_message_id, delivery_meta_json, text_delivered, attachment_cursor, cleanup_pending, status, attempts, created_at, updated_at)
|
|
488
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?)
|
|
489
|
+
`).run(item.sessionKey, item.chatId, item.botName, item.sourceType, item.sourceId, item.deliveryKey, item.contentHash, item.content, item.attachmentsJson, item.replyToMessageId || '', item.deliveryMode || 'send', item.targetMessageId || '', item.deliveryMetaJson || '{}', item.textDelivered ? 1 : 0, item.attachmentCursor || 0, item.cleanupPending ? 1 : 0, now, now);
|
|
410
490
|
return result.changes ? Number(result.lastInsertRowid) : null;
|
|
411
491
|
}
|
|
412
492
|
getDeliveryBySource(sessionKey, sourceType, sourceId) {
|
|
@@ -416,6 +496,21 @@ export class MessageStore {
|
|
|
416
496
|
`).get(sessionKey, sourceType, sourceId);
|
|
417
497
|
return row ? this.mapDelivery(row) : null;
|
|
418
498
|
}
|
|
499
|
+
getDeliveryByKey(botName, chatId, deliveryKey) {
|
|
500
|
+
const row = this.db.prepare(`
|
|
501
|
+
SELECT * FROM delivery_outbox
|
|
502
|
+
WHERE bot_name = ? AND chat_id = ? AND delivery_key = ?
|
|
503
|
+
`).get(botName, chatId, deliveryKey);
|
|
504
|
+
return row ? this.mapDelivery(row) : null;
|
|
505
|
+
}
|
|
506
|
+
listDeliveriesByKeyPrefix(botName, chatId, prefix) {
|
|
507
|
+
const rows = this.db.prepare(`
|
|
508
|
+
SELECT * FROM delivery_outbox
|
|
509
|
+
WHERE bot_name = ? AND chat_id = ? AND delivery_key LIKE ?
|
|
510
|
+
ORDER BY id ASC
|
|
511
|
+
`).all(botName, chatId, `${prefix}%`);
|
|
512
|
+
return rows.map((row) => this.mapDelivery(row));
|
|
513
|
+
}
|
|
419
514
|
getPendingDeliveries(chatId, botName, maxCount = 20) {
|
|
420
515
|
if (chatId && botName) {
|
|
421
516
|
const rows = this.db.prepare(`
|
|
@@ -501,17 +596,29 @@ export class MessageStore {
|
|
|
501
596
|
}
|
|
502
597
|
return false;
|
|
503
598
|
}
|
|
599
|
+
/** Recover in-flight rows after process restart. A new process cannot own a
|
|
600
|
+
* legitimate delivering row, so every matching row returns to pending—even if
|
|
601
|
+
* the previous process crashed on its nominal final attempt. The restarted
|
|
602
|
+
* process gets another chance to finish rather than silently losing payload. */
|
|
603
|
+
resetDeliveringOnStartup(botName) {
|
|
604
|
+
const result = this.db.prepare(`
|
|
605
|
+
UPDATE delivery_outbox
|
|
606
|
+
SET status = 'pending', updated_at = ?
|
|
607
|
+
WHERE bot_name = ? AND status = 'delivering'
|
|
608
|
+
`).run(Date.now(), botName);
|
|
609
|
+
return result.changes;
|
|
610
|
+
}
|
|
504
611
|
resetStaleDeliveries(botName, staleMs, maxAttempts) {
|
|
505
612
|
const cutoff = Date.now() - staleMs;
|
|
506
613
|
const fail = this.db.prepare(`
|
|
507
614
|
UPDATE delivery_outbox
|
|
508
615
|
SET status = 'failed', updated_at = ?
|
|
509
|
-
WHERE bot_name = ? AND status = 'delivering' AND updated_at
|
|
616
|
+
WHERE bot_name = ? AND status = 'delivering' AND updated_at <= ? AND attempts >= ?
|
|
510
617
|
`).run(Date.now(), botName, cutoff, maxAttempts);
|
|
511
618
|
const restore = this.db.prepare(`
|
|
512
619
|
UPDATE delivery_outbox
|
|
513
620
|
SET status = 'pending', updated_at = ?
|
|
514
|
-
WHERE bot_name = ? AND status = 'delivering' AND updated_at
|
|
621
|
+
WHERE bot_name = ? AND status = 'delivering' AND updated_at <= ? AND attempts < ?
|
|
515
622
|
`).run(Date.now(), botName, cutoff, maxAttempts);
|
|
516
623
|
return { restored: restore.changes, failed: fail.changes };
|
|
517
624
|
}
|
|
@@ -521,6 +628,38 @@ export class MessageStore {
|
|
|
521
628
|
`).run(Date.now(), id);
|
|
522
629
|
return result.changes === 1;
|
|
523
630
|
}
|
|
631
|
+
markDeliveryTextDelivered(id) {
|
|
632
|
+
this.db.prepare(`
|
|
633
|
+
UPDATE delivery_outbox SET text_delivered = 1, updated_at = ? WHERE id = ?
|
|
634
|
+
`).run(Date.now(), id);
|
|
635
|
+
}
|
|
636
|
+
/** Atomically checkpoint fallback text and the remaining card cleanup. */
|
|
637
|
+
markDeliveryTextDeliveredWithCleanup(id) {
|
|
638
|
+
this.db.prepare(`
|
|
639
|
+
UPDATE delivery_outbox
|
|
640
|
+
SET text_delivered = 1, cleanup_pending = 1, updated_at = ?
|
|
641
|
+
WHERE id = ?
|
|
642
|
+
`).run(Date.now(), id);
|
|
643
|
+
}
|
|
644
|
+
markDeliveryAttachmentCursor(id, cursor) {
|
|
645
|
+
this.db.prepare(`
|
|
646
|
+
UPDATE delivery_outbox SET attachment_cursor = ?, updated_at = ? WHERE id = ?
|
|
647
|
+
`).run(cursor, Date.now(), id);
|
|
648
|
+
}
|
|
649
|
+
/** Return a failed in-flight delivery to pending until its retry budget is spent. */
|
|
650
|
+
retryDelivery(id, maxAttempts) {
|
|
651
|
+
const result = this.db.prepare(`
|
|
652
|
+
UPDATE delivery_outbox
|
|
653
|
+
SET status = 'pending', updated_at = ?
|
|
654
|
+
WHERE id = ? AND status = 'delivering' AND attempts < ?
|
|
655
|
+
`).run(Date.now(), id, maxAttempts);
|
|
656
|
+
return result.changes === 1;
|
|
657
|
+
}
|
|
658
|
+
markDeliveryCleanupPending(id, pending) {
|
|
659
|
+
this.db.prepare(`
|
|
660
|
+
UPDATE delivery_outbox SET cleanup_pending = ?, updated_at = ? WHERE id = ?
|
|
661
|
+
`).run(pending ? 1 : 0, Date.now(), id);
|
|
662
|
+
}
|
|
524
663
|
markDeliveryDelivered(id) {
|
|
525
664
|
this.db.prepare(`
|
|
526
665
|
UPDATE delivery_outbox SET status = 'delivered', updated_at = ? WHERE id = ?
|
|
@@ -531,6 +670,15 @@ export class MessageStore {
|
|
|
531
670
|
UPDATE delivery_outbox SET status = 'failed', updated_at = ? WHERE id = ?
|
|
532
671
|
`).run(Date.now(), id);
|
|
533
672
|
}
|
|
673
|
+
/** Give a later explicit duplicate another chance after terminal failure. */
|
|
674
|
+
requeueFailedDelivery(id) {
|
|
675
|
+
const result = this.db.prepare(`
|
|
676
|
+
UPDATE delivery_outbox
|
|
677
|
+
SET status = 'pending', attempts = 0, updated_at = ?
|
|
678
|
+
WHERE id = ? AND status = 'failed'
|
|
679
|
+
`).run(Date.now(), id);
|
|
680
|
+
return result.changes === 1;
|
|
681
|
+
}
|
|
534
682
|
mapDelivery(row) {
|
|
535
683
|
return {
|
|
536
684
|
id: row.id,
|
|
@@ -544,6 +692,12 @@ export class MessageStore {
|
|
|
544
692
|
content: row.content || '',
|
|
545
693
|
attachmentsJson: row.attachments_json || '[]',
|
|
546
694
|
replyToMessageId: row.reply_to_message_id || '',
|
|
695
|
+
deliveryMode: row.delivery_mode === 'patch_live_status' ? 'patch_live_status' : 'send',
|
|
696
|
+
targetMessageId: row.target_message_id || '',
|
|
697
|
+
deliveryMetaJson: row.delivery_meta_json || '{}',
|
|
698
|
+
textDelivered: Boolean(row.text_delivered),
|
|
699
|
+
attachmentCursor: Number(row.attachment_cursor || 0),
|
|
700
|
+
cleanupPending: Boolean(row.cleanup_pending),
|
|
547
701
|
status: row.status,
|
|
548
702
|
attempts: row.attempts || 0,
|
|
549
703
|
createdAt: row.created_at,
|
|
@@ -105,6 +105,10 @@ export declare class OpenClawClient {
|
|
|
105
105
|
* context-overflow / prompt-too-large, which OpenClaw recovers from via its
|
|
106
106
|
* context-overflow-recovery + auto-compaction path.
|
|
107
107
|
*/
|
|
108
|
+
private isGenericChatError;
|
|
109
|
+
private resolveTerminalChatError;
|
|
110
|
+
private formatTerminalRunError;
|
|
111
|
+
private formatDurationSeconds;
|
|
108
112
|
private isRecoverableAgentError;
|
|
109
113
|
private pickBestCollectedText;
|
|
110
114
|
createSession(params: {
|
package/dist/openclaw-client.js
CHANGED
|
@@ -527,6 +527,7 @@ export class OpenClawClient {
|
|
|
527
527
|
let chatFinalTimer = null;
|
|
528
528
|
let lifecycleEndTimer = null;
|
|
529
529
|
let replayInvalidTimer = null;
|
|
530
|
+
let genericChatErrorTimer = null;
|
|
530
531
|
let pendingRuntimeFailureText = "";
|
|
531
532
|
let lastActivitySummary = "";
|
|
532
533
|
let lastActivityAt = 0;
|
|
@@ -556,6 +557,8 @@ export class OpenClawClient {
|
|
|
556
557
|
clearTimeout(lifecycleEndTimer);
|
|
557
558
|
if (replayInvalidTimer)
|
|
558
559
|
clearTimeout(replayInvalidTimer);
|
|
560
|
+
if (genericChatErrorTimer)
|
|
561
|
+
clearTimeout(genericChatErrorTimer);
|
|
559
562
|
console.warn(`[OpenClaw] collectReply idle timeout for runId=${runId} sessionKey=${sessionKey}`);
|
|
560
563
|
this.abortChat(targetSessionKey || sessionKey, runId).catch((err) => {
|
|
561
564
|
console.warn(`[OpenClaw] abort after collectReply idle timeout failed:`, err.message);
|
|
@@ -633,6 +636,8 @@ export class OpenClawClient {
|
|
|
633
636
|
clearTimeout(lifecycleEndTimer);
|
|
634
637
|
if (replayInvalidTimer)
|
|
635
638
|
clearTimeout(replayInvalidTimer);
|
|
639
|
+
if (genericChatErrorTimer)
|
|
640
|
+
clearTimeout(genericChatErrorTimer);
|
|
636
641
|
resolve(finalText);
|
|
637
642
|
};
|
|
638
643
|
const poller = setInterval(() => {
|
|
@@ -648,6 +653,8 @@ export class OpenClawClient {
|
|
|
648
653
|
clearTimeout(lifecycleEndTimer);
|
|
649
654
|
if (replayInvalidTimer)
|
|
650
655
|
clearTimeout(replayInvalidTimer);
|
|
656
|
+
if (genericChatErrorTimer)
|
|
657
|
+
clearTimeout(genericChatErrorTimer);
|
|
651
658
|
if (idleTimer)
|
|
652
659
|
clearTimeout(idleTimer);
|
|
653
660
|
const salvaged = this.pickBestCollectedText(chatFinalText, text, chatDeltaText, transcriptAssistantText);
|
|
@@ -904,6 +911,28 @@ export class OpenClawClient {
|
|
|
904
911
|
// surfaces the failure / lets auto-retry kick in — instead of
|
|
905
912
|
// surfacing whatever mid-run delta text happened to be collected.
|
|
906
913
|
const errText = String(ev.data?.error || "chat error");
|
|
914
|
+
if (this.isGenericChatError(errText)) {
|
|
915
|
+
// Gateway can emit a terminal chat error without errorMessage,
|
|
916
|
+
// then persist a richer terminal snapshot a moment later. Do
|
|
917
|
+
// not expose the placeholder "chat error" to users. Reconcile
|
|
918
|
+
// through agent.wait, whose snapshot includes timeout phase,
|
|
919
|
+
// stop reason, and the real upstream error when available.
|
|
920
|
+
if (!genericChatErrorTimer) {
|
|
921
|
+
genericChatErrorTimer = setTimeout(() => {
|
|
922
|
+
genericChatErrorTimer = null;
|
|
923
|
+
void this.resolveTerminalChatError(evRunId || runId).then((detail) => {
|
|
924
|
+
clearTimeout(idleTimer);
|
|
925
|
+
clearInterval(poller);
|
|
926
|
+
if (chatFinalTimer)
|
|
927
|
+
clearTimeout(chatFinalTimer);
|
|
928
|
+
if (lifecycleEndTimer)
|
|
929
|
+
clearTimeout(lifecycleEndTimer);
|
|
930
|
+
reject(new Error(`Agent error: ${detail}`));
|
|
931
|
+
});
|
|
932
|
+
}, 350);
|
|
933
|
+
}
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
907
936
|
if (this.isRecoverableAgentError(errText)) {
|
|
908
937
|
pendingRuntimeFailureText = `⚠️ Agent 未正常完成\n原因: ${errText}`;
|
|
909
938
|
resetIdleTimer();
|
|
@@ -917,6 +946,8 @@ export class OpenClawClient {
|
|
|
917
946
|
clearTimeout(chatFinalTimer);
|
|
918
947
|
if (lifecycleEndTimer)
|
|
919
948
|
clearTimeout(lifecycleEndTimer);
|
|
949
|
+
if (genericChatErrorTimer)
|
|
950
|
+
clearTimeout(genericChatErrorTimer);
|
|
920
951
|
// Abort the (possibly still-running) backgrounded run before we
|
|
921
952
|
// reject, so an auto-retry probe does not race a zombie run on the
|
|
922
953
|
// same session. Idempotent/best-effort.
|
|
@@ -939,6 +970,50 @@ export class OpenClawClient {
|
|
|
939
970
|
* context-overflow / prompt-too-large, which OpenClaw recovers from via its
|
|
940
971
|
* context-overflow-recovery + auto-compaction path.
|
|
941
972
|
*/
|
|
973
|
+
isGenericChatError(errText) {
|
|
974
|
+
const t = (errText || "").trim().toLowerCase();
|
|
975
|
+
return t === "chat error" || t === "agent error: chat error";
|
|
976
|
+
}
|
|
977
|
+
async resolveTerminalChatError(runId) {
|
|
978
|
+
try {
|
|
979
|
+
const snapshot = await this.rpc("agent.wait", { runId, timeoutMs: 1500 }, 2500);
|
|
980
|
+
return this.formatTerminalRunError(snapshot);
|
|
981
|
+
}
|
|
982
|
+
catch (err) {
|
|
983
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
984
|
+
return `OpenClaw 运行失败,但未返回具体原因${detail ? `(${detail})` : ""}`;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
formatTerminalRunError(snapshot) {
|
|
988
|
+
const rawError = typeof snapshot?.error === "string" ? snapshot.error.trim() : "";
|
|
989
|
+
const timeoutPhase = typeof snapshot?.timeoutPhase === "string" ? snapshot.timeoutPhase.trim() : "";
|
|
990
|
+
const stopReason = typeof snapshot?.stopReason === "string" ? snapshot.stopReason.trim() : "";
|
|
991
|
+
const status = typeof snapshot?.status === "string" ? snapshot.status.trim() : "";
|
|
992
|
+
const timedOut = status === "timeout" || Boolean(timeoutPhase) || /timed?\s*out|timeout/i.test(rawError);
|
|
993
|
+
if (timedOut) {
|
|
994
|
+
const startedAt = Number(snapshot?.startedAt);
|
|
995
|
+
const endedAt = Number(snapshot?.endedAt);
|
|
996
|
+
const elapsedSeconds = Number.isFinite(startedAt) && Number.isFinite(endedAt) && endedAt > startedAt
|
|
997
|
+
? Math.round((endedAt - startedAt) / 1000)
|
|
998
|
+
: 0;
|
|
999
|
+
const elapsed = elapsedSeconds > 0 ? `(运行约 ${this.formatDurationSeconds(elapsedSeconds)})` : "";
|
|
1000
|
+
const phase = timeoutPhase ? `,阶段:${timeoutPhase}` : "";
|
|
1001
|
+
return `Agent 运行超时${elapsed}${phase},已被 OpenClaw 终止`;
|
|
1002
|
+
}
|
|
1003
|
+
if (rawError)
|
|
1004
|
+
return rawError;
|
|
1005
|
+
if (stopReason === "stop" || stopReason === "rpc")
|
|
1006
|
+
return `Agent 被停止(${stopReason})`;
|
|
1007
|
+
const meta = [status && `状态=${status}`, stopReason && `原因=${stopReason}`, timeoutPhase && `阶段=${timeoutPhase}`].filter(Boolean).join(",");
|
|
1008
|
+
return meta ? `OpenClaw 运行失败:${meta}` : "OpenClaw 运行失败,但未返回具体原因";
|
|
1009
|
+
}
|
|
1010
|
+
formatDurationSeconds(seconds) {
|
|
1011
|
+
if (seconds % 3600 === 0)
|
|
1012
|
+
return `${seconds / 3600} 小时`;
|
|
1013
|
+
if (seconds % 60 === 0)
|
|
1014
|
+
return `${seconds / 60} 分钟`;
|
|
1015
|
+
return `${seconds} 秒`;
|
|
1016
|
+
}
|
|
942
1017
|
isRecoverableAgentError(errText) {
|
|
943
1018
|
const t = (errText || "").toLowerCase();
|
|
944
1019
|
return t.includes("context overflow")
|