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.
- package/dist/feishu-bot.d.ts +31 -9
- package/dist/feishu-bot.js +525 -327
- package/dist/live-status.d.ts +85 -15
- package/dist/live-status.js +395 -89
- package/dist/message-store.d.ts +20 -0
- package/dist/message-store.js +88 -10
- package/dist/openclaw-client.d.ts +39 -11
- package/dist/openclaw-client.js +520 -407
- package/dist/queued-presentation.d.ts +52 -0
- package/dist/queued-presentation.js +471 -0
- package/dist/session-status.d.ts +1 -0
- package/dist/session-status.js +4 -0
- package/docs/releases/1.8.15.md +28 -0
- package/package.json +4 -3
package/dist/live-status.js
CHANGED
|
@@ -12,6 +12,22 @@ const DEFAULT_TICK_MS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS
|
|
|
12
12
|
// freeze the live-status card for the rest of a long run. We skip the failed
|
|
13
13
|
// tick, keep the ticker alive, and only disable after this many in a row.
|
|
14
14
|
const MAX_CONSECUTIVE_EDIT_FAILURES = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS_MAX_EDIT_FAILURES || 5);
|
|
15
|
+
/** A transport/observation error is not proof that a platform effect failed. */
|
|
16
|
+
export class LiveStatusEffectUnknownError extends Error {
|
|
17
|
+
cause;
|
|
18
|
+
constructor(cause) {
|
|
19
|
+
super("live status effect outcome unknown");
|
|
20
|
+
this.cause = cause;
|
|
21
|
+
this.name = "LiveStatusEffectUnknownError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Only a definitive platform rejection is safe for the terminal retry loop. */
|
|
25
|
+
export class LiveStatusEffectRejectedError extends Error {
|
|
26
|
+
}
|
|
27
|
+
/** Proven local no-effect suppression (settings/epoch/lease), not an unknown
|
|
28
|
+
* transport outcome and not permission to retry an attempted SDK operation. */
|
|
29
|
+
export class LiveStatusEffectSuppressedError extends Error {
|
|
30
|
+
}
|
|
15
31
|
export class LiveStatusController {
|
|
16
32
|
callbacks;
|
|
17
33
|
opts;
|
|
@@ -23,7 +39,19 @@ export class LiveStatusController {
|
|
|
23
39
|
createTimer;
|
|
24
40
|
tickTimer;
|
|
25
41
|
createPromise;
|
|
42
|
+
createSuppressed = false;
|
|
43
|
+
waitingCreateAllowed = false;
|
|
26
44
|
finalized = false;
|
|
45
|
+
terminalOutcome = false;
|
|
46
|
+
disposed = false;
|
|
47
|
+
generation = 0;
|
|
48
|
+
/** Frozen/terminal presentation wins over every older progress snapshot. */
|
|
49
|
+
pinnedView;
|
|
50
|
+
terminalOwner;
|
|
51
|
+
pendingEdit;
|
|
52
|
+
editPump;
|
|
53
|
+
/** Timed-out SDK effects retain receipt ownership, but must not be replayed. */
|
|
54
|
+
unknownEdits = new Set();
|
|
27
55
|
waitingForResult = false;
|
|
28
56
|
updatesFrozen = false;
|
|
29
57
|
disabled = false;
|
|
@@ -34,6 +62,7 @@ export class LiveStatusController {
|
|
|
34
62
|
* A success resets the counter. */
|
|
35
63
|
consecutiveEditFailures = 0;
|
|
36
64
|
state = "running";
|
|
65
|
+
blockedEdits = false;
|
|
37
66
|
/** Total tool calls (tool_start events) seen during the run, for the summary. */
|
|
38
67
|
toolCallCount = 0;
|
|
39
68
|
/** True when finished via noReply(): show a "no content" summary. */
|
|
@@ -41,23 +70,116 @@ export class LiveStatusController {
|
|
|
41
70
|
constructor(callbacks, opts) {
|
|
42
71
|
this.callbacks = callbacks;
|
|
43
72
|
this.opts = opts;
|
|
73
|
+
if (opts.initialPhase)
|
|
74
|
+
this.state = opts.initialPhase;
|
|
75
|
+
const restored = opts.restore;
|
|
76
|
+
if (restored) {
|
|
77
|
+
this.messageId = restored.messageId;
|
|
78
|
+
this.startedAt = restored.startedAt;
|
|
79
|
+
this.blockedEdits = restored.blockEdits;
|
|
80
|
+
if (restored.view) {
|
|
81
|
+
this.state = restored.view.state;
|
|
82
|
+
this.lines = restored.view.lines.map(line => ({ ...line }));
|
|
83
|
+
this.toolCallCount = restored.view.toolCalls;
|
|
84
|
+
this.noReplyResult = restored.view.noReply;
|
|
85
|
+
this.waitingForResult = restored.view.phase === "waiting";
|
|
86
|
+
this.updatesFrozen = this.waitingForResult;
|
|
87
|
+
}
|
|
88
|
+
// Attempted/unknown create survives process loss as a no-create fence.
|
|
89
|
+
if (restored.blockCreate && !this.messageId)
|
|
90
|
+
this.disabled = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Receipt is durable locally, NOT Gateway admission. */
|
|
94
|
+
received(detail) {
|
|
95
|
+
if (this.finalized || this.disposed)
|
|
96
|
+
return;
|
|
97
|
+
this.pushLine("lifecycle", detail);
|
|
98
|
+
this.callbacks.desired?.(this.buildView());
|
|
99
|
+
void this.safeEdit(this.buildView());
|
|
100
|
+
}
|
|
101
|
+
/** A proven never-attempted ordinary recovery may reuse its queued card.
|
|
102
|
+
* Merely observing an expected root never calls this method. */
|
|
103
|
+
resumeQueued() {
|
|
104
|
+
if (this.disposed || this.terminalOutcome)
|
|
105
|
+
return;
|
|
106
|
+
this.finalized = false;
|
|
107
|
+
this.updatesFrozen = false;
|
|
108
|
+
this.waitingForResult = false;
|
|
109
|
+
this.waitingCreateAllowed = false;
|
|
110
|
+
this.state = "queued";
|
|
111
|
+
this.generation++;
|
|
112
|
+
this.pinnedView = undefined;
|
|
113
|
+
this.pendingEdit = undefined;
|
|
114
|
+
this.callbacks.desired?.(this.buildView());
|
|
115
|
+
if (this.messageId) {
|
|
116
|
+
this.startTicker();
|
|
117
|
+
void this.safeEdit(this.buildView());
|
|
118
|
+
}
|
|
119
|
+
else
|
|
120
|
+
this.start();
|
|
121
|
+
}
|
|
122
|
+
/** Enqueue/local receipt remains waiting, not actual-owner/model evidence.
|
|
123
|
+
* Keep one latest pinned view while allowing another input to coalesce. */
|
|
124
|
+
async showInputWaiting(detail, title) {
|
|
125
|
+
if (this.disposed || this.terminalOutcome)
|
|
126
|
+
return;
|
|
127
|
+
this.updatesFrozen = true;
|
|
128
|
+
this.waitingForResult = true;
|
|
129
|
+
this.waitingCreateAllowed = !this.messageId && !this.disabled;
|
|
130
|
+
this.pushLine("lifecycle", detail);
|
|
131
|
+
this.stopTimers();
|
|
132
|
+
const view = this.buildView();
|
|
133
|
+
if (title)
|
|
134
|
+
view.title = title;
|
|
135
|
+
// A quick enqueue receipt must not cancel the only queued card before it
|
|
136
|
+
// ever existed. Still exactly one create flight, with the normal unknown
|
|
137
|
+
// fence and effect-time settings/lease checks, never a terminal retry.
|
|
138
|
+
if (!this.messageId && !this.createPromise && !this.disabled) {
|
|
139
|
+
this.pinView(view);
|
|
140
|
+
this.createPromise = this.ensureCreated();
|
|
141
|
+
}
|
|
142
|
+
await this.finishView(view);
|
|
143
|
+
}
|
|
144
|
+
/** Only the exact ACK owner calls this; admission is not model execution. */
|
|
145
|
+
async admitted(successor = false) {
|
|
146
|
+
if (this.finalized || this.disposed || (!successor && this.state !== "queued"))
|
|
147
|
+
return;
|
|
148
|
+
this.waitingForResult = false;
|
|
149
|
+
this.state = "admitted";
|
|
150
|
+
this.pushLine("lifecycle", this.opts.locale === "en" ? "Gateway accepted; waiting for activity" : "Gateway 已接收,等待执行活动");
|
|
151
|
+
this.callbacks.desired?.(this.buildView());
|
|
152
|
+
await this.safeEdit(this.buildView());
|
|
44
153
|
}
|
|
45
154
|
get id() { return this.messageId; }
|
|
46
155
|
start(initialDetail) {
|
|
47
|
-
if (this.
|
|
156
|
+
if (this.createSuppressed) {
|
|
157
|
+
this.createPromise = undefined;
|
|
158
|
+
this.createSuppressed = false;
|
|
159
|
+
}
|
|
160
|
+
if (this.disabled || this.finalized || (this.updatesFrozen && !this.waitingCreateAllowed) || this.createTimer || this.createPromise || this.messageId)
|
|
48
161
|
return;
|
|
49
|
-
this.
|
|
162
|
+
if (!this.opts.restore)
|
|
163
|
+
this.startedAt = Date.now();
|
|
50
164
|
// Presentation may start before chat.send admission. Only the collector's
|
|
51
165
|
// effective-activity idle decision (or confirmed yield) freezes this card;
|
|
52
166
|
// elapsed ticks are presentation, never evidence of task progress.
|
|
53
167
|
if (initialDetail && initialDetail.trim()) {
|
|
54
168
|
this.pushLine("lifecycle", initialDetail.trim());
|
|
55
169
|
}
|
|
170
|
+
this.callbacks.desired?.(this.buildView());
|
|
56
171
|
this.createTimer = setTimeout(() => {
|
|
57
172
|
this.createTimer = undefined;
|
|
58
173
|
this.createPromise = this.ensureCreated();
|
|
59
174
|
}, this.opts.delayMs ?? DEFAULT_DELAY_MS);
|
|
60
175
|
}
|
|
176
|
+
setMetadata(model, thinkingLevel) {
|
|
177
|
+
if (this.disposed || this.finalized)
|
|
178
|
+
return;
|
|
179
|
+
this.opts.model = model;
|
|
180
|
+
this.opts.thinkingLevel = thinkingLevel;
|
|
181
|
+
this.callbacks.desired?.(this.buildView());
|
|
182
|
+
}
|
|
61
183
|
async progress(event) {
|
|
62
184
|
if (this.disabled || this.finalized)
|
|
63
185
|
return;
|
|
@@ -108,6 +230,8 @@ export class LiveStatusController {
|
|
|
108
230
|
return; // lifecycle ticks: ignore (footer timer already advances)
|
|
109
231
|
}
|
|
110
232
|
this.waitingForResult = false;
|
|
233
|
+
this.state = "running";
|
|
234
|
+
this.callbacks.desired?.(this.buildView());
|
|
111
235
|
await this.ensureCreatedNow();
|
|
112
236
|
await this.safeEdit(this.buildView());
|
|
113
237
|
}
|
|
@@ -120,11 +244,7 @@ export class LiveStatusController {
|
|
|
120
244
|
this.waitingForResult = true;
|
|
121
245
|
this.pushLine("lifecycle", detail);
|
|
122
246
|
this.stopTimers();
|
|
123
|
-
|
|
124
|
-
await this.createPromise.catch(() => { });
|
|
125
|
-
this.stopTimers();
|
|
126
|
-
if (this.messageId && !this.disabled)
|
|
127
|
-
await this.safeEditFinal(this.buildView());
|
|
247
|
+
await this.finishView(this.buildView());
|
|
128
248
|
}
|
|
129
249
|
/** Release only local observation of a now-idle session, without claiming
|
|
130
250
|
* the previous task succeeded or was aborted. Normal progress is unchanged. */
|
|
@@ -132,39 +252,62 @@ export class LiveStatusController {
|
|
|
132
252
|
if (this.finalized)
|
|
133
253
|
return;
|
|
134
254
|
this.pushLine("lifecycle", detail);
|
|
255
|
+
this.waitingForResult = true;
|
|
135
256
|
this.finalized = true;
|
|
136
257
|
this.stopTimers();
|
|
137
|
-
if (this.createPromise)
|
|
138
|
-
await this.createPromise.catch(() => { });
|
|
139
|
-
if (!this.messageId || this.disabled)
|
|
140
|
-
return;
|
|
141
258
|
const view = this.buildView();
|
|
142
259
|
view.title = title || (this.opts.locale === "en" ? `${this.opts.botName} result unconfirmed` : `${this.opts.botName} 结果未确认`);
|
|
143
|
-
await this.
|
|
260
|
+
await this.finishView(view);
|
|
144
261
|
}
|
|
145
262
|
async complete() {
|
|
263
|
+
if (this.disposed || this.terminalOutcome)
|
|
264
|
+
return;
|
|
265
|
+
const pending = this.callbacks.unconfirmedInputs?.("done");
|
|
266
|
+
if (pending) {
|
|
267
|
+
await this.showInputWaiting(pending.detail, pending.title);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
this.terminalOutcome = true;
|
|
146
271
|
this.state = "done";
|
|
147
272
|
this.finalized = true;
|
|
148
273
|
this.stopTimers();
|
|
149
|
-
|
|
150
|
-
await this.createPromise.catch(() => { });
|
|
151
|
-
if (!this.messageId || this.disabled)
|
|
152
|
-
return;
|
|
153
|
-
await this.safeEditFinal(this.buildView());
|
|
274
|
+
await this.finishView(this.buildView());
|
|
154
275
|
}
|
|
155
276
|
/**
|
|
156
277
|
* Freeze any successful terminal state (text, attachment-only, or NO_REPLY)
|
|
157
278
|
* without first emitting the compact done patch. The caller persists this
|
|
158
279
|
* metadata in the outbox, which owns the final patch/cleanup. If no card was
|
|
159
|
-
* created, the caller falls back to normal new-message delivery.
|
|
280
|
+
* created, the caller falls back to normal new-message delivery. Metadata is
|
|
281
|
+
* a target/execution snapshot, never a receipt. B2/outbox callers that may own
|
|
282
|
+
* an unknown or same-card final should supply owner BEFORE attempting it;
|
|
283
|
+
* late IDs/stale effects then delegate reconciliation instead of patching.
|
|
160
284
|
*/
|
|
161
|
-
async prepareTerminalDelivery(noReply = false) {
|
|
162
|
-
this.state
|
|
163
|
-
|
|
164
|
-
|
|
285
|
+
async prepareTerminalDelivery(noReply = false, owner) {
|
|
286
|
+
if (this.disposed || (this.terminalOutcome && this.state !== "done"))
|
|
287
|
+
return undefined;
|
|
288
|
+
const pending = this.callbacks.unconfirmedInputs?.("done");
|
|
289
|
+
if (pending) {
|
|
290
|
+
await this.showInputWaiting(pending.detail, pending.title);
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
if (owner) {
|
|
294
|
+
this.terminalOwner = owner;
|
|
295
|
+
this.generation++; // retire any queued controller-owned terminal effect
|
|
296
|
+
this.pendingEdit = undefined;
|
|
297
|
+
}
|
|
298
|
+
if (!this.terminalOutcome) {
|
|
299
|
+
this.terminalOutcome = true;
|
|
300
|
+
this.state = "done";
|
|
301
|
+
this.noReplyResult = noReply;
|
|
302
|
+
this.finalized = true;
|
|
303
|
+
this.pinView(this.buildView()); // retire queued progress before handing off
|
|
304
|
+
}
|
|
165
305
|
this.stopTimers();
|
|
166
306
|
if (this.createPromise)
|
|
167
307
|
await this.createPromise.catch(() => { });
|
|
308
|
+
return this.finalMeta();
|
|
309
|
+
}
|
|
310
|
+
finalMeta() {
|
|
168
311
|
if (!this.messageId)
|
|
169
312
|
return undefined;
|
|
170
313
|
return {
|
|
@@ -174,21 +317,25 @@ export class LiveStatusController {
|
|
|
174
317
|
model: this.opts.model,
|
|
175
318
|
thinkingLevel: this.opts.thinkingLevel,
|
|
176
319
|
locale: this.opts.locale === "en" ? "en" : "zh",
|
|
177
|
-
noReply,
|
|
320
|
+
noReply: this.noReplyResult,
|
|
178
321
|
};
|
|
179
322
|
}
|
|
180
|
-
async prepareFinalDelivery() {
|
|
181
|
-
return this.prepareTerminalDelivery(false);
|
|
323
|
+
async prepareFinalDelivery(owner) {
|
|
324
|
+
return this.prepareTerminalDelivery(false, owner);
|
|
182
325
|
}
|
|
183
326
|
async fail() {
|
|
327
|
+
if (this.disposed || this.terminalOutcome)
|
|
328
|
+
return;
|
|
329
|
+
const pending = this.callbacks.unconfirmedInputs?.("failed");
|
|
330
|
+
if (pending) {
|
|
331
|
+
await this.showInputWaiting(pending.detail, pending.title);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
this.terminalOutcome = true;
|
|
184
335
|
this.state = "failed";
|
|
185
336
|
this.finalized = true;
|
|
186
337
|
this.stopTimers();
|
|
187
|
-
|
|
188
|
-
await this.createPromise.catch(() => { });
|
|
189
|
-
if (!this.messageId || this.disabled)
|
|
190
|
-
return;
|
|
191
|
-
await this.safeEditFinal(this.buildView());
|
|
338
|
+
await this.finishView(this.buildView());
|
|
192
339
|
}
|
|
193
340
|
/**
|
|
194
341
|
* Finish when the model explicitly produced no reply (NO_REPLY) or an empty
|
|
@@ -196,20 +343,60 @@ export class LiveStatusController {
|
|
|
196
343
|
* activity window, so the user understands the run finished without output.
|
|
197
344
|
*/
|
|
198
345
|
async noReply() {
|
|
346
|
+
if (this.disposed || this.terminalOutcome)
|
|
347
|
+
return;
|
|
348
|
+
const pending = this.callbacks.unconfirmedInputs?.("done");
|
|
349
|
+
if (pending) {
|
|
350
|
+
await this.showInputWaiting(pending.detail, pending.title);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
this.terminalOutcome = true;
|
|
199
354
|
this.state = "done";
|
|
200
355
|
this.noReplyResult = true;
|
|
201
356
|
this.finalized = true;
|
|
202
357
|
this.stopTimers();
|
|
203
|
-
|
|
204
|
-
await this.createPromise.catch(() => { });
|
|
205
|
-
if (!this.messageId || this.disabled)
|
|
206
|
-
return;
|
|
207
|
-
await this.safeEditFinal(this.buildView());
|
|
358
|
+
await this.finishView(this.buildView());
|
|
208
359
|
}
|
|
209
360
|
dispose() {
|
|
361
|
+
this.disposed = true;
|
|
210
362
|
this.finalized = true;
|
|
363
|
+
this.generation++;
|
|
364
|
+
this.pendingEdit = undefined;
|
|
211
365
|
this.stopTimers();
|
|
212
366
|
}
|
|
367
|
+
pinView(view) {
|
|
368
|
+
this.generation++;
|
|
369
|
+
this.callbacks.desired?.(view);
|
|
370
|
+
this.pinnedView = view;
|
|
371
|
+
this.pendingEdit = undefined;
|
|
372
|
+
}
|
|
373
|
+
async finishView(view) {
|
|
374
|
+
// Pin BEFORE awaiting creation: a create already in flight may finish late.
|
|
375
|
+
this.pinView(view);
|
|
376
|
+
if (this.createPromise)
|
|
377
|
+
await this.createPromise.catch(() => { });
|
|
378
|
+
if (this.messageId && !this.disposed)
|
|
379
|
+
await this.reconcilePinnedView();
|
|
380
|
+
}
|
|
381
|
+
async reconcilePinnedView(force = false) {
|
|
382
|
+
if (!this.pinnedView || this.disposed)
|
|
383
|
+
return;
|
|
384
|
+
if (!this.terminalOwner) {
|
|
385
|
+
await this.queueEdit(this.pinnedView, true, force);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const meta = this.finalMeta();
|
|
389
|
+
if (!meta)
|
|
390
|
+
return;
|
|
391
|
+
try {
|
|
392
|
+
// This is a custody signal, NOT retry authority. The owner must retain
|
|
393
|
+
// attempted/unknown effects and reconcile its own desired final payload.
|
|
394
|
+
await this.terminalOwner.onReconcileNeeded(meta);
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
this.callbacks.warn?.("live status external reconciliation failed (not replayed)", err);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
213
400
|
/**
|
|
214
401
|
* Intermediate assistant text arrives as incremental fragments (each flush
|
|
215
402
|
* sends only the newly-added slice). Consecutive fragments with no tool event
|
|
@@ -270,25 +457,61 @@ export class LiveStatusController {
|
|
|
270
457
|
async ensureCreated() {
|
|
271
458
|
if (this.disabled || this.finalized || this.messageId)
|
|
272
459
|
return;
|
|
273
|
-
const view = this.buildView();
|
|
274
|
-
|
|
275
|
-
|
|
460
|
+
const view = this.pinnedView || this.buildView();
|
|
461
|
+
let started = false;
|
|
462
|
+
let settled = false;
|
|
463
|
+
const accept = (id) => {
|
|
464
|
+
if (settled)
|
|
465
|
+
return;
|
|
466
|
+
settled = true;
|
|
276
467
|
if (!id) {
|
|
277
468
|
this.disabled = true;
|
|
278
469
|
return;
|
|
279
470
|
}
|
|
280
471
|
this.messageId = id;
|
|
281
472
|
this.lastSentSignature = this.signature(view);
|
|
282
|
-
this.
|
|
473
|
+
if (this.pinnedView && !this.disposed) {
|
|
474
|
+
void this.reconcilePinnedView();
|
|
475
|
+
}
|
|
476
|
+
else
|
|
477
|
+
this.startTicker();
|
|
478
|
+
};
|
|
479
|
+
const effect = {
|
|
480
|
+
run: async (send) => {
|
|
481
|
+
// A create waiting behind another chat send is no longer needed once
|
|
482
|
+
// the task finished. Do not create a stale running card at effect time.
|
|
483
|
+
if (this.finalized || (this.updatesFrozen && !this.waitingCreateAllowed) || this.disposed) {
|
|
484
|
+
settled = true;
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
started = true;
|
|
488
|
+
const id = await send();
|
|
489
|
+
accept(id); // do not attach receipt ownership to the deadline promise
|
|
490
|
+
return id;
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
try {
|
|
494
|
+
const id = await this.callbacks.create(view, effect);
|
|
495
|
+
if (!settled)
|
|
496
|
+
accept(id); // direct/non-queued callbacks remain supported
|
|
283
497
|
}
|
|
284
498
|
catch (err) {
|
|
285
|
-
|
|
286
|
-
|
|
499
|
+
// No duplicate create, including when the outer deadline expired. The
|
|
500
|
+
// original effect context still adopts a late successful message ID.
|
|
501
|
+
if (err instanceof LiveStatusEffectSuppressedError) {
|
|
502
|
+
this.createSuppressed = true;
|
|
503
|
+
this.createPromise = undefined;
|
|
504
|
+
this.callbacks.warn?.("live status create locally suppressed before transport", err);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (!started)
|
|
508
|
+
this.disabled = true;
|
|
509
|
+
this.callbacks.warn?.("live status create failed (not replayed)", err);
|
|
287
510
|
}
|
|
288
511
|
}
|
|
289
512
|
/** Refresh the elapsed-time footer periodically even with no new activity. */
|
|
290
513
|
startTicker() {
|
|
291
|
-
if (this.tickTimer || this.disabled || this.finalized || this.updatesFrozen)
|
|
514
|
+
if (this.tickTimer || this.disabled || this.finalized || this.updatesFrozen || this.blockedEdits)
|
|
292
515
|
return;
|
|
293
516
|
const tickMs = this.opts.tickMs ?? DEFAULT_TICK_MS;
|
|
294
517
|
this.tickTimer = setInterval(() => {
|
|
@@ -298,58 +521,136 @@ export class LiveStatusController {
|
|
|
298
521
|
}, tickMs);
|
|
299
522
|
this.tickTimer.unref?.();
|
|
300
523
|
}
|
|
301
|
-
async safeEdit(view
|
|
302
|
-
if (!this.messageId || this.disabled)
|
|
303
|
-
return
|
|
304
|
-
|
|
524
|
+
async safeEdit(view) {
|
|
525
|
+
if (!this.messageId || this.disabled || this.finalized || this.updatesFrozen)
|
|
526
|
+
return;
|
|
527
|
+
await this.queueEdit(view, false);
|
|
528
|
+
}
|
|
529
|
+
/** One flight + one latest pending snapshot, regardless of tick/event rate. */
|
|
530
|
+
queueEdit(view, priority, force = false) {
|
|
531
|
+
this.callbacks.desired?.(view);
|
|
532
|
+
if (this.blockedEdits)
|
|
533
|
+
return Promise.resolve();
|
|
534
|
+
if (!this.messageId || this.disposed || (priority && view !== this.pinnedView)
|
|
535
|
+
|| (!priority && (this.disabled || this.finalized || this.updatesFrozen)))
|
|
536
|
+
return Promise.resolve();
|
|
537
|
+
if (!priority)
|
|
538
|
+
this.generation++;
|
|
539
|
+
this.pendingEdit = { view, generation: this.generation, priority, force, started: false, settled: false, accepted: false };
|
|
540
|
+
return this.flushEdits();
|
|
541
|
+
}
|
|
542
|
+
canRunPendingEdit() {
|
|
543
|
+
const request = this.pendingEdit;
|
|
544
|
+
if (!request || this.disposed)
|
|
305
545
|
return false;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
return
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
this.callbacks.warn?.(`live status edit failed (${this.consecutiveEditFailures}/${MAX_CONSECUTIVE_EDIT_FAILURES}${giveUp ? ", disabling" : ", will retry next tick"})`, err);
|
|
324
|
-
if (giveUp) {
|
|
325
|
-
this.disabled = true;
|
|
326
|
-
this.stopTimers();
|
|
546
|
+
// An unknown progress is not retried by ticking. A new terminal/frozen
|
|
547
|
+
// view may supersede it once; an unknown terminal is never blind retried.
|
|
548
|
+
return ![...this.unknownEdits].some(old => !request.priority
|
|
549
|
+
|| (old.priority && old.generation >= request.generation));
|
|
550
|
+
}
|
|
551
|
+
flushEdits() {
|
|
552
|
+
if (this.editPump)
|
|
553
|
+
return this.editPump;
|
|
554
|
+
// Start on a microtask so editPump is installed before callbacks/receipts
|
|
555
|
+
// can re-enter, including immediately resolved test/SDK implementations.
|
|
556
|
+
const pump = Promise.resolve().then(async () => {
|
|
557
|
+
while (this.canRunPendingEdit()) {
|
|
558
|
+
const request = this.pendingEdit;
|
|
559
|
+
this.pendingEdit = undefined;
|
|
560
|
+
if (!request.force && this.signature(request.view) === this.lastSentSignature)
|
|
561
|
+
continue;
|
|
562
|
+
await this.performEdit(request);
|
|
327
563
|
}
|
|
328
|
-
|
|
329
|
-
|
|
564
|
+
});
|
|
565
|
+
const owned = pump.finally(() => {
|
|
566
|
+
if (this.editPump === owned)
|
|
567
|
+
this.editPump = undefined;
|
|
568
|
+
// queueEdit can arrive after the loop's last check, while it still sees
|
|
569
|
+
// this flight. Hand runnable work to a NEW owner and adopt its promise:
|
|
570
|
+
// callers awaiting the old flight must also await the terminal handoff.
|
|
571
|
+
// Do not restart blocked unknown work: that would spin microtasks forever.
|
|
572
|
+
if (this.canRunPendingEdit())
|
|
573
|
+
return this.flushEdits();
|
|
574
|
+
});
|
|
575
|
+
this.editPump = owned;
|
|
576
|
+
return owned;
|
|
330
577
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
578
|
+
async performEdit(request) {
|
|
579
|
+
const current = () => !this.disposed && request.generation === this.generation;
|
|
580
|
+
const accept = () => {
|
|
581
|
+
if (request.settled)
|
|
582
|
+
return;
|
|
583
|
+
request.settled = true;
|
|
584
|
+
request.accepted = true;
|
|
585
|
+
this.unknownEdits.delete(request);
|
|
586
|
+
if (current()) {
|
|
587
|
+
this.lastSentSignature = this.signature(request.view);
|
|
588
|
+
this.consecutiveEditFailures = 0;
|
|
589
|
+
}
|
|
590
|
+
else if (this.pinnedView && !this.disposed) {
|
|
591
|
+
// A deadline frees the ordered chain, not the SDK operation. If old
|
|
592
|
+
// progress applies AFTER done/frozen, repair it on this same card.
|
|
593
|
+
void this.reconcilePinnedView(true);
|
|
594
|
+
}
|
|
595
|
+
void this.flushEdits();
|
|
596
|
+
};
|
|
597
|
+
const effect = {
|
|
598
|
+
run: async (send) => {
|
|
599
|
+
if (!current()) {
|
|
600
|
+
request.settled = true;
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
request.started = true;
|
|
604
|
+
try {
|
|
605
|
+
await send();
|
|
606
|
+
accept();
|
|
607
|
+
}
|
|
608
|
+
catch (err) {
|
|
609
|
+
if (err instanceof LiveStatusEffectRejectedError) {
|
|
610
|
+
request.settled = true;
|
|
611
|
+
request.rejection = err;
|
|
612
|
+
this.unknownEdits.delete(request);
|
|
613
|
+
// A late definitive rejection also releases the unknown gate.
|
|
614
|
+
void this.flushEdits();
|
|
615
|
+
}
|
|
616
|
+
throw err;
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
const attempts = request.priority ? Math.max(1, MAX_CONSECUTIVE_EDIT_FAILURES) : 1;
|
|
621
|
+
for (let i = 0; i < attempts && current(); i++) {
|
|
342
622
|
try {
|
|
343
|
-
await this.callbacks.edit(this.messageId, view);
|
|
344
|
-
|
|
623
|
+
await this.callbacks.edit(this.messageId, request.view, effect);
|
|
624
|
+
if (!request.settled)
|
|
625
|
+
accept(); // direct/non-queued callback
|
|
345
626
|
return;
|
|
346
627
|
}
|
|
347
628
|
catch (err) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (
|
|
629
|
+
// The low-level receipt can beat this catch even if the outer race
|
|
630
|
+
// already chose its deadline. Never retain "unknown" after acceptance.
|
|
631
|
+
if (request.accepted)
|
|
632
|
+
return;
|
|
633
|
+
const failure = request.rejection ?? err;
|
|
634
|
+
if (failure instanceof LiveStatusEffectSuppressedError)
|
|
635
|
+
return;
|
|
636
|
+
const unknown = !(failure instanceof LiveStatusEffectRejectedError);
|
|
637
|
+
if (unknown) {
|
|
638
|
+
this.unknownEdits.add(request);
|
|
639
|
+
this.callbacks.warn?.("live status edit outcome unknown (not replayed)", failure);
|
|
351
640
|
return;
|
|
352
|
-
|
|
641
|
+
}
|
|
642
|
+
this.consecutiveEditFailures++;
|
|
643
|
+
this.callbacks.warn?.("live status edit failed", failure);
|
|
644
|
+
if (request.priority && i + 1 < attempts) {
|
|
645
|
+
await new Promise(resolve => setTimeout(resolve, 300 * (i + 1)));
|
|
646
|
+
request.started = false;
|
|
647
|
+
request.settled = false;
|
|
648
|
+
request.rejection = undefined;
|
|
649
|
+
}
|
|
650
|
+
else if (!request.priority && this.consecutiveEditFailures >= MAX_CONSECUTIVE_EDIT_FAILURES) {
|
|
651
|
+
this.disabled = true;
|
|
652
|
+
this.stopTimers();
|
|
653
|
+
}
|
|
353
654
|
}
|
|
354
655
|
}
|
|
355
656
|
}
|
|
@@ -384,17 +685,21 @@ export class LiveStatusController {
|
|
|
384
685
|
title = en ? `\u26A0\uFE0F ${this.opts.botName} stopped` : `\u26A0\uFE0F ${this.opts.botName} \u6267\u884c\u4e2d\u65ad`;
|
|
385
686
|
else if (this.waitingForResult)
|
|
386
687
|
title = en ? `${this.opts.botName} waiting (card frozen)` : `${this.opts.botName} 等待后续结果(已停止刷新)`;
|
|
688
|
+
else if (this.state === "queued")
|
|
689
|
+
title = en ? `${this.opts.botName} received · queued` : `${this.opts.botName} 已收件 · 排队等待`;
|
|
690
|
+
else if (this.state === "admitted")
|
|
691
|
+
title = en ? `${this.opts.botName} accepted · waiting` : `${this.opts.botName} 已接收 · 等待活动`;
|
|
387
692
|
else
|
|
388
693
|
title = en ? `${this.opts.botName} is working` : `${this.opts.botName} \u6b63\u5728\u6267\u884c`;
|
|
389
694
|
// On a clean finish (done / NO_REPLY) show only the compact summary. On a
|
|
390
695
|
// failure (error / killed / timeout) keep the recent activity window too, so
|
|
391
696
|
// the last few steps before the failure are visible for debugging.
|
|
392
697
|
let lines;
|
|
393
|
-
if (this.state
|
|
394
|
-
lines =
|
|
698
|
+
if (this.state !== "done" && this.state !== "failed") {
|
|
699
|
+
lines = this.lines.map(line => ({ ...line }));
|
|
395
700
|
}
|
|
396
701
|
else if (this.state === "failed") {
|
|
397
|
-
lines = [...this.lines, this.buildSummaryLine(en)];
|
|
702
|
+
lines = [...this.lines.map(line => ({ ...line })), this.buildSummaryLine(en)];
|
|
398
703
|
}
|
|
399
704
|
else {
|
|
400
705
|
lines = [this.buildSummaryLine(en)];
|
|
@@ -408,6 +713,7 @@ export class LiveStatusController {
|
|
|
408
713
|
toolCalls: this.toolCallCount,
|
|
409
714
|
noReply: this.noReplyResult,
|
|
410
715
|
state: this.state,
|
|
716
|
+
phase: this.state === "done" || this.state === "failed" ? this.state : this.waitingForResult ? "waiting" : this.state === "running" ? "working" : this.state,
|
|
411
717
|
};
|
|
412
718
|
}
|
|
413
719
|
buildSummaryLine(en) {
|