openclaw-lark-multi-agent 1.4.7 → 1.4.9

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.
@@ -1,3 +1,4 @@
1
+ import { FOREGROUND_WAIT_MS } from "./session-status.js";
1
2
  const DEFAULT_DELAY_MS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS_DELAY_MS || 800);
2
3
  const DEFAULT_MAX_CHARS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS_MAX_CHARS || 120);
3
4
  // How many recent activity lines to keep in the card content area.
@@ -22,8 +23,11 @@ export class LiveStatusController {
22
23
  startedAt = Date.now();
23
24
  createTimer;
24
25
  tickTimer;
26
+ refreshBudgetTimer;
25
27
  createPromise;
26
28
  finalized = false;
29
+ waitingForResult = false;
30
+ updatesFrozen = false;
27
31
  disabled = false;
28
32
  /** Consecutive safeEdit failures. A single transient Feishu error (e.g. card
29
33
  * patch code=2200 "Internal Error") must NOT permanently disable the card:
@@ -42,9 +46,15 @@ export class LiveStatusController {
42
46
  }
43
47
  get id() { return this.messageId; }
44
48
  start(initialDetail) {
45
- if (this.disabled || this.finalized || this.createTimer || this.messageId)
49
+ if (this.disabled || this.finalized || this.updatesFrozen || this.createTimer || this.messageId)
46
50
  return;
47
51
  this.startedAt = Date.now();
52
+ this.refreshBudgetTimer = setTimeout(() => {
53
+ void this.showWaitingForResult(this.opts.locale === "en"
54
+ ? "Foreground refresh budget reached; background results are still monitored."
55
+ : "前台刷新时间已到,过程卡停止实时刷新;后台继续接收结果。").catch(err => this.callbacks.warn?.("live status freeze failed", err));
56
+ }, this.opts.refreshBudgetMs ?? FOREGROUND_WAIT_MS);
57
+ this.refreshBudgetTimer.unref?.();
48
58
  if (initialDetail && initialDetail.trim()) {
49
59
  this.pushLine("lifecycle", initialDetail.trim());
50
60
  }
@@ -56,6 +66,12 @@ export class LiveStatusController {
56
66
  async progress(event) {
57
67
  if (this.disabled || this.finalized)
58
68
  return;
69
+ if (this.updatesFrozen) {
70
+ // Keep only the cheap local counter for the eventual one-time summary.
71
+ if (typeof event !== "string" && event.kind === "tool" && event.phase === "start")
72
+ this.toolCallCount++;
73
+ return;
74
+ }
59
75
  // Card patch has no 20-edit cap (verified against Feishu im.message.patch),
60
76
  // so we can show a small rolling window of recent activity: tool start, tool
61
77
  // end, and intermediate assistant text each count as one line. We ignore
@@ -96,9 +112,41 @@ export class LiveStatusController {
96
112
  else {
97
113
  return; // lifecycle ticks: ignore (footer timer already advances)
98
114
  }
115
+ this.waitingForResult = false;
99
116
  await this.ensureCreatedNow();
100
117
  await this.safeEdit(this.buildView());
101
118
  }
119
+ /** Freeze recurring/progress edits, not the original result observer.
120
+ * A single final/error summary remains allowed when the real outcome arrives. */
121
+ async showWaitingForResult(detail) {
122
+ if (this.disabled || this.finalized || this.updatesFrozen)
123
+ return;
124
+ this.updatesFrozen = true;
125
+ this.waitingForResult = true;
126
+ this.pushLine("lifecycle", detail);
127
+ this.stopTimers();
128
+ if (this.createPromise)
129
+ await this.createPromise.catch(() => { });
130
+ this.stopTimers();
131
+ if (this.messageId && !this.disabled)
132
+ await this.safeEditFinal(this.buildView());
133
+ }
134
+ /** Release only local observation of a now-idle session, without claiming
135
+ * the previous task succeeded or was aborted. Normal progress is unchanged. */
136
+ async pauseForUnconfirmedResult(detail, title) {
137
+ if (this.finalized)
138
+ return;
139
+ this.pushLine("lifecycle", detail);
140
+ this.finalized = true;
141
+ this.stopTimers();
142
+ if (this.createPromise)
143
+ await this.createPromise.catch(() => { });
144
+ if (!this.messageId || this.disabled)
145
+ return;
146
+ const view = this.buildView();
147
+ view.title = title || (this.opts.locale === "en" ? `${this.opts.botName} result unconfirmed` : `${this.opts.botName} 结果未确认`);
148
+ await this.safeEditFinal(view);
149
+ }
102
150
  async complete() {
103
151
  this.state = "done";
104
152
  this.finalized = true;
@@ -205,6 +253,10 @@ export class LiveStatusController {
205
253
  this.lines = this.lines.slice(this.lines.length - limit);
206
254
  }
207
255
  stopTimers() {
256
+ if (this.refreshBudgetTimer) {
257
+ clearTimeout(this.refreshBudgetTimer);
258
+ this.refreshBudgetTimer = undefined;
259
+ }
208
260
  if (this.createTimer) {
209
261
  clearTimeout(this.createTimer);
210
262
  this.createTimer = undefined;
@@ -244,11 +296,11 @@ export class LiveStatusController {
244
296
  }
245
297
  /** Refresh the elapsed-time footer periodically even with no new activity. */
246
298
  startTicker() {
247
- if (this.tickTimer || this.disabled || this.finalized)
299
+ if (this.tickTimer || this.disabled || this.finalized || this.updatesFrozen)
248
300
  return;
249
301
  const tickMs = this.opts.tickMs ?? DEFAULT_TICK_MS;
250
302
  this.tickTimer = setInterval(() => {
251
- if (this.finalized || this.disabled || !this.messageId)
303
+ if (this.finalized || this.disabled || this.updatesFrozen || !this.messageId)
252
304
  return;
253
305
  void this.safeEdit(this.buildView());
254
306
  }, tickMs);
@@ -257,7 +309,7 @@ export class LiveStatusController {
257
309
  async safeEdit(view, force = false) {
258
310
  if (!this.messageId || this.disabled)
259
311
  return false;
260
- if (!force && this.finalized)
312
+ if (!force && (this.finalized || this.updatesFrozen))
261
313
  return false;
262
314
  const sig = this.signature(view);
263
315
  if (!force && sig === this.lastSentSignature)
@@ -338,6 +390,8 @@ export class LiveStatusController {
338
390
  title = en ? `\u2705 ${this.opts.botName} done` : `\u2705 ${this.opts.botName} \u5df2\u5b8c\u6210`;
339
391
  else if (this.state === "failed")
340
392
  title = en ? `\u26A0\uFE0F ${this.opts.botName} stopped` : `\u26A0\uFE0F ${this.opts.botName} \u6267\u884c\u4e2d\u65ad`;
393
+ else if (this.waitingForResult)
394
+ title = en ? `${this.opts.botName} waiting (card frozen)` : `${this.opts.botName} 等待后续结果(已停止刷新)`;
341
395
  else
342
396
  title = en ? `${this.opts.botName} is working` : `${this.opts.botName} \u6b63\u5728\u6267\u884c`;
343
397
  // On a clean finish (done / NO_REPLY) show only the compact summary. On a
@@ -1,3 +1,4 @@
1
+ import { SessionWaitPaused, type SessionRuntimeStatus } from "./session-status.js";
1
2
  import { OpenClawConfig } from "./config.js";
2
3
  import { ChatMessage } from "./message-store.js";
3
4
  export type ProgressEvent = {
@@ -15,6 +16,9 @@ export type ProgressEvent = {
15
16
  kind: "lifecycle";
16
17
  text: string;
17
18
  };
19
+ export declare class InactiveRunObservation extends Error {
20
+ constructor();
21
+ }
18
22
  export type ChatAttachment = {
19
23
  type?: string;
20
24
  mimeType?: string;
@@ -37,6 +41,8 @@ export declare class OpenClawClient {
37
41
  private reconnectDelay;
38
42
  private maxReconnectDelay;
39
43
  private shouldReconnect;
44
+ /** Read-only reconciliation for a collector whose plugin reports no active run. */
45
+ private runStateChecks;
40
46
  /** Callbacks for tool events (verbose mode) */
41
47
  private toolEventCallbacks;
42
48
  private lastToolNamesByItemId;
@@ -102,8 +108,9 @@ export declare class OpenClawClient {
102
108
  * Collect agent reply by polling accumulated events.
103
109
  * Matches by initial runId OR sessionKey to handle multi-turn tool calling
104
110
  * where OpenClaw creates new runIds for each tool-call round.
105
- * No aggressive timeout — waits for lifecycle end as the source of truth.
106
- * 30-minute safety net only for catastrophic WS disconnection.
111
+ * Existing event/anchor matching is retained. After 30 minutes of silence,
112
+ * reconcile the original run instead of aborting or treating a draft as final.
113
+ * Explicit terminal/error/disconnect paths keep their existing behavior.
107
114
  */
108
115
  private collectReply;
109
116
  /**
@@ -137,13 +144,17 @@ export declare class OpenClawClient {
137
144
  label?: string;
138
145
  }): Promise<any>;
139
146
  /**
140
- * Submit a mid-run message through OpenClaw's public atomic steer path.
141
- * OpenClaw 2026.8 deprecated the synchronous plugin primitive previously used
142
- * by `lma-steer`: its boolean only represented immediate eligibility and could
143
- * still be rejected asynchronously. `chat.send queueMode=steer` owns admission
144
- * and transcript persistence. LMA still treats the RPC result as provisional;
145
- * the Feishu reaction flips to Get only after a matching `session.message`
146
- * confirms that the input was committed/consumed.
147
+ * Submit a mid-run message through the lma-steer plugin.
148
+ *
149
+ * OpenClaw's public `chat.send { queueMode: "steer" }` returns `started` even
150
+ * when it falls through to a later ordinary run; it therefore cannot tell the
151
+ * bridge whether THIS message entered the active run. The plugin resolves the
152
+ * active embedded run and reports `steered` only when it actually queued there.
153
+ * A matching session.message remains the final consumption confirmation.
154
+ *
155
+ * If the plugin is missing, stale, or rejects the injection, return unavailable
156
+ * and let LMA's durable trigger run normally after the current turn. Never use
157
+ * the ambiguous public admission as a realtime-insertion success signal.
147
158
  */
148
159
  steer(sessionKey: string, text: string, displayText?: string): Promise<{
149
160
  status: "steered" | "unavailable";
@@ -175,6 +186,10 @@ export declare class OpenClawClient {
175
186
  /**
176
187
  * Get session info (model, tokens, etc.) for status display.
177
188
  */
189
+ /** Stop claiming an exact run's final output after observation has paused. */
190
+ releasePausedWait(runId: string): void;
191
+ getSessionRuntimeStatus(sessionKey: string): Promise<SessionRuntimeStatus>;
192
+ private classifyWaitTimeout;
178
193
  getSessionInfo(sessionKey: string): Promise<any>;
179
194
  /**
180
195
  * Ensure session is using the expected model. If not, patch it back.
@@ -218,6 +233,7 @@ export declare class OpenClawClient {
218
233
  /** Called after chat.send RPC succeeds and OpenClaw has accepted the user message. */
219
234
  onSubmitted?: (runId: string) => void | Promise<void>;
220
235
  onProgress?: (event: ProgressEvent) => void | Promise<void>;
236
+ onWaitPaused?: (notice: SessionWaitPaused) => void | Promise<void>;
221
237
  }): Promise<string>;
222
238
  private shouldInjectBridgeAttachmentHint;
223
239
  private bridgeAttachmentHint;
@@ -251,6 +267,7 @@ export declare class OpenClawClient {
251
267
  /** Called after chat.send RPC succeeds and OpenClaw has accepted the combined message. */
252
268
  onSubmitted?: (runId: string) => void | Promise<void>;
253
269
  onProgress?: (event: ProgressEvent) => void | Promise<void>;
270
+ onWaitPaused?: (notice: SessionWaitPaused) => void | Promise<void>;
254
271
  }): Promise<string>;
255
272
  private formatContextLines;
256
273
  private writeContextSyncFile;