openclaw-lark-multi-agent 1.1.2 → 1.2.1

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 CHANGED
@@ -37,6 +37,7 @@ All of them connect to the same OpenClaw Gateway while keeping sessions, queues,
37
37
  - Feishu image download and OpenClaw multimodal attachment forwarding
38
38
  - Bridge attachment marker protocol for generated files/images/documents
39
39
  - Feishu CardKit v2 Markdown rendering, including native table elements for pipe tables
40
+ - Live status card: in non-verbose mode each run shows a single self-updating Feishu interactive card (title + recent activity window with per-line timestamps + elapsed/model footer, refreshed once per second), which collapses to a compact one-line summary on a clean finish and retains the recent activity on failure for debugging
40
41
  - Bridge-level slash commands and escaped OpenClaw slash commands
41
42
  - `/discuss` mode for barrier-style multi-bot group discussion, including per-round markers and no-reply status notices
42
43
  - `/chairman` role: a single per-group chairman that answers plain messages when no bot is in Free mode, and acts as host, challenger, and summarizer inside `/discuss`
@@ -231,10 +232,13 @@ Bridge-level commands use a single slash and are handled by this project:
231
232
  - `/status` — show bot model, token usage, and session state
232
233
  - `/compact` — compact the OpenClaw session
233
234
  - `/reset` — reset the OpenClaw session
235
+ - `/stop` — force-stop a stuck run for this bot in this chat and unlock the queue
234
236
  - `/verbose` — toggle tool-call messages for this bot in this chat
237
+ - `/livestatus [on|off]` — toggle, or explicitly enable/disable, the self-updating run-status card shown in non-verbose mode (default on)
235
238
  - `/free [on|off]` — toggle, or explicitly enable/disable, this bot's Free mode in the current group chat
236
239
  - `/mute` — toggle this bot's mute mode in the current group chat
237
240
  - `/mode` — show this bot's current mode in the current chat
241
+ - `/model [id]` — show or switch this bot's bound model (persisted)
238
242
  - `/discuss on|off|status|stop|rounds N` — control group-level multi-bot discussion mode (requires a chairman to enable; default 10 rounds)
239
243
  - `/chairman [@Bot|off]` — set, view, or clear the single chairman for this group
240
244
  - `/locale [zh|en]` — set or view this group's language
@@ -323,7 +327,7 @@ Persistent constraints (such as "do not call Feishu send tools directly" and the
323
327
  `/discuss` is an explicit group-level multi-agent discussion scheduler. It is separate from Free mode:
324
328
 
325
329
  - `/free` controls whether a single bot may answer plain human messages.
326
- - `/discuss on` requires a chairman to be set first (`/chairman @Bot`). It lets one coordinator take over plain human messages and run all Free-mode bots plus the chairman in barrier-style rounds.
330
+ - `/discuss on` requires a chairman to be set first (`/chairman @Bot`). It lets one coordinator take over plain human messages and run all non-muted bots plus the chairman in barrier-style rounds. Free mode is ignored inside discussion: every bot that is not muted participates regardless of its Free setting.
327
331
  - Targeted mentions still fall through to normal routing, so `@GPT hello` works even while discuss mode is enabled.
328
332
  - Each participant receives the same round prompt and does not see other participants' replies from the current round until the next round.
329
333
  - The chairman speaks last each round: it gives its own view, challenges weak points, mediates disagreements, and decides whether to continue or conclude.
@@ -396,6 +400,23 @@ The bridge subscribes to Feishu `im.message.recalled_v1` events. When a user rec
396
400
 
397
401
  Version 1 behavior intentionally does **not** abort an OpenClaw run that has already started processing the recalled message, and it does not recall bot replies that were already sent. The first goal is to make recall reliable for queued/not-yet-processed messages.
398
402
 
403
+ ## Session health and error recovery
404
+
405
+ The bridge distinguishes a failed *run* from a dead *session*:
406
+
407
+ - During an active run, a health monitor polls the OpenClaw session. Only true
408
+ session-death states (`killed` / `dead` / `crashed`, matched on word
409
+ boundaries) are treated as unhealthy; run-level outcomes such as `aborted`,
410
+ `error`, or `timeout` are not, because the session usually remains usable for
411
+ the next message. A short re-check guards against transient blips.
412
+ - When a session is genuinely unhealthy, the bridge warns the user but does not
413
+ force a `/reset`: the current message is still attempted, and only an in-run
414
+ confirmation of death stops the wait. Users decide whether to retry.
415
+ - Recoverable agent errors are not surfaced as failures. When OpenClaw emits a
416
+ recoverable lifecycle error (for example `Context overflow: prompt too large`)
417
+ it auto-compacts and keeps the same run alive; the bridge defers instead of
418
+ rejecting, so a later real final reply wins. Only a genuine stall falls back to
419
+ the deferred error via the idle timeout. Non-recoverable errors still fail fast.
399
420
 
400
421
  ## Markdown, tables, and attachments
401
422
 
@@ -408,6 +429,50 @@ Assistant replies are sent as Feishu CardKit v2 cards. Markdown is preprocessed
408
429
 
409
430
  For generated files/images/documents, agents should use the bridge attachment marker protocol instead of calling Feishu messaging tools directly. The bridge strips the marker from the visible reply, validates the file path under the configured attachment directory, uploads/sends the attachment, and records it in local context. Markdown documents can be converted into Feishu cloud documents through this path.
410
431
 
432
+ ## Live status
433
+
434
+ In non-verbose mode, each run shows a single self-updating Feishu interactive
435
+ card so users can see progress without the noise of per-tool-call messages.
436
+ Verbose mode and live status are mutually exclusive: when `/verbose` is on, the
437
+ bridge emits the existing per-tool messages instead of a live status card.
438
+
439
+ While running, the card shows:
440
+
441
+ - a title (`<bot> is working`);
442
+ - a rolling window of the most recent activity lines (tool start `▸`, tool end
443
+ `✓`, and intermediate assistant text `•`), each prefixed with the relative
444
+ time `mm:ss` since the run started;
445
+ - a footer with elapsed time and the bound model name.
446
+
447
+ The card is created lazily (a fast reply that finishes within the create delay
448
+ never spawns a card, to avoid flicker) and is updated with `im.message.patch`,
449
+ which has no 20-edit cap. A 1-second ticker advances the elapsed timer even when
450
+ no new activity arrives.
451
+
452
+ When the run ends:
453
+
454
+ - a clean finish (normal reply or `NO_REPLY`) collapses the card to a single
455
+ compact grey line: status emoji + total tool calls + total elapsed;
456
+ - a failure (provider error, killed/unhealthy session, delivery error, or idle
457
+ timeout) keeps the recent activity window plus the summary, with an orange
458
+ header, so the steps leading up to the failure stay visible for debugging.
459
+
460
+ The final answer is always delivered separately through the normal interactive-card
461
+ delivery path, so Markdown renders correctly; the live status card is a distinct
462
+ message and never replaces or blocks the final reply.
463
+
464
+ Live status is on by default and can be toggled per bot/chat with
465
+ `/livestatus [on|off]`. It can be disabled globally by setting
466
+ `OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS=0`. Tunable defaults:
467
+
468
+ | Environment variable | Default | Meaning |
469
+ | --- | --- | --- |
470
+ | `OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS` | `1` | Master switch; `0` disables live status for all bots |
471
+ | `OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS_DELAY_MS` | `800` | Delay before creating the card, so fast replies do not flicker one |
472
+ | `OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS_TICK_MS` | `1000` | How often the elapsed-time footer auto-refreshes |
473
+ | `OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS_HISTORY` | `6` | Number of recent activity lines kept in the card |
474
+ | `OPENCLAW_LARK_MULTI_AGENT_LIVE_STATUS_MAX_CHARS` | `120` | Per-line character cap before truncation |
475
+
411
476
  ## Data model
412
477
 
413
478
  SQLite state lives in the configured data directory. Important tables:
@@ -421,7 +486,7 @@ SQLite state lives in the configured data directory. Important tables:
421
486
  - `delivery_outbox` — durable user-visible delivery ledger with claim/dedupe state
422
487
  - `recalled_messages` — recalled user messages excluded from pending work and future context
423
488
  - `processed_events` — Feishu event de-duplication
424
- - `bot_chat_settings` — per-bot/per-chat settings such as verbose mode
489
+ - `bot_chat_settings` — per-bot/per-chat settings such as verbose mode, mode, and live-status toggle
425
490
 
426
491
  ## Development
427
492
 
@@ -0,0 +1,75 @@
1
+ /** Coarse phase of the compaction flow, drives the card's primary line. */
2
+ export type CompactPhase = "native" | "tool-trim";
3
+ /** Terminal outcome used by the renderer to pick color/emoji. */
4
+ export type CompactState = "running" | "done" | "failed" | "noop";
5
+ /** Structured snapshot the renderer turns into a Feishu interactive card. */
6
+ export type CompactProgressView = {
7
+ state: CompactState;
8
+ /** Current phase while running (ignored once terminal). */
9
+ phase: CompactPhase;
10
+ /** Elapsed time like "0:42". */
11
+ elapsed: string;
12
+ /** Terminal detail line, e.g. "12.3M→4.1M (-67%)" or an error/skip reason. */
13
+ detail?: string;
14
+ };
15
+ export type CompactProgressCallbacks = {
16
+ /** Create the status card; returns the new message id (undefined disables). */
17
+ create: (view: CompactProgressView) => Promise<string | undefined>;
18
+ /** Patch the existing status card. */
19
+ edit: (messageId: string, view: CompactProgressView) => Promise<void>;
20
+ warn?: (message: string, err?: unknown) => void;
21
+ };
22
+ export type CompactProgressOptions = {
23
+ locale?: "zh" | "en";
24
+ /** Delay before the card first appears, to avoid flashing on fast compactions. */
25
+ delayMs?: number;
26
+ /** Footer/auto-refresh cadence in ms. */
27
+ tickMs?: number;
28
+ };
29
+ /**
30
+ * Lifecycle: start() → (optional) toToolTrim() when native gives up → one of
31
+ * done()/noop()/fail(). The card is created lazily after `delayMs` so a fast
32
+ * compaction that finishes first never shows a card at all. All terminal calls
33
+ * are idempotent and safe even if the card was never created.
34
+ */
35
+ export declare class CompactProgressController {
36
+ private readonly callbacks;
37
+ private readonly opts;
38
+ private messageId?;
39
+ private startedAt;
40
+ private phase;
41
+ private state;
42
+ private detail?;
43
+ private createTimer?;
44
+ private tickTimer?;
45
+ private createPromise?;
46
+ private finalized;
47
+ private disabled;
48
+ private lastSignature;
49
+ constructor(callbacks: CompactProgressCallbacks, opts?: CompactProgressOptions);
50
+ get id(): string | undefined;
51
+ /** Begin the flow; schedules lazy card creation after delayMs. */
52
+ start(): void;
53
+ /** Native compaction gave up (timeout/no-op); switch the card to fast-trim. */
54
+ toToolTrim(): Promise<void>;
55
+ /** Finished successfully; patch the card to the done state with a detail line. */
56
+ done(detail?: string): Promise<void>;
57
+ /** Nothing to compact; patch the card to a neutral "no-op" state. */
58
+ noop(detail?: string): Promise<void>;
59
+ /** Failed; patch the card to the failed state with an error detail line. */
60
+ fail(detail?: string): Promise<void>;
61
+ /** Tear down timers without sending anything (e.g. on unexpected disposal). */
62
+ dispose(): void;
63
+ private finish;
64
+ private refresh;
65
+ private ensureCreatedNow;
66
+ private ensureCreated;
67
+ /** Refresh the elapsed-time footer periodically even with no phase change. */
68
+ private startTicker;
69
+ private safeEdit;
70
+ private stopTimers;
71
+ /** Dedupe key; includes elapsed so the ticking footer still advances. */
72
+ private signature;
73
+ private formatElapsed;
74
+ buildView(): CompactProgressView;
75
+ }
@@ -0,0 +1,177 @@
1
+ // A tiny live-status controller dedicated to the /compact (and auto-compact)
2
+ // flow, so the user gets a visible "正在压缩…" card that ticks while a large
3
+ // session is being compacted — instead of staring at nothing for tens of
4
+ // seconds while native compaction grinds (and sometimes times out).
5
+ //
6
+ // It deliberately does NOT reuse LiveStatusController: that one models a tool
7
+ // run (activity window, tool-call count, model footer). Compaction has just a
8
+ // few coarse phases and a single elapsed timer, so a focused, self-contained
9
+ // controller keeps the card clean and the logic trivial to test.
10
+ const DEFAULT_DELAY_MS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_COMPACT_STATUS_DELAY_MS || 500);
11
+ const DEFAULT_TICK_MS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_COMPACT_STATUS_TICK_MS || 1000);
12
+ /**
13
+ * Lifecycle: start() → (optional) toToolTrim() when native gives up → one of
14
+ * done()/noop()/fail(). The card is created lazily after `delayMs` so a fast
15
+ * compaction that finishes first never shows a card at all. All terminal calls
16
+ * are idempotent and safe even if the card was never created.
17
+ */
18
+ export class CompactProgressController {
19
+ callbacks;
20
+ opts;
21
+ messageId;
22
+ startedAt = Date.now();
23
+ phase = "native";
24
+ state = "running";
25
+ detail;
26
+ createTimer;
27
+ tickTimer;
28
+ createPromise;
29
+ finalized = false;
30
+ disabled = false;
31
+ lastSignature = "";
32
+ constructor(callbacks, opts = {}) {
33
+ this.callbacks = callbacks;
34
+ this.opts = opts;
35
+ }
36
+ get id() { return this.messageId; }
37
+ /** Begin the flow; schedules lazy card creation after delayMs. */
38
+ start() {
39
+ if (this.disabled || this.finalized || this.createTimer || this.messageId)
40
+ return;
41
+ this.startedAt = Date.now();
42
+ this.createTimer = setTimeout(() => {
43
+ this.createTimer = undefined;
44
+ this.createPromise = this.ensureCreated();
45
+ }, this.opts.delayMs ?? DEFAULT_DELAY_MS);
46
+ this.createTimer.unref?.();
47
+ }
48
+ /** Native compaction gave up (timeout/no-op); switch the card to fast-trim. */
49
+ async toToolTrim() {
50
+ if (this.finalized || this.phase === "tool-trim")
51
+ return;
52
+ this.phase = "tool-trim";
53
+ await this.refresh();
54
+ }
55
+ /** Finished successfully; patch the card to the done state with a detail line. */
56
+ async done(detail) {
57
+ await this.finish("done", detail);
58
+ }
59
+ /** Nothing to compact; patch the card to a neutral "no-op" state. */
60
+ async noop(detail) {
61
+ await this.finish("noop", detail);
62
+ }
63
+ /** Failed; patch the card to the failed state with an error detail line. */
64
+ async fail(detail) {
65
+ await this.finish("failed", detail);
66
+ }
67
+ /** Tear down timers without sending anything (e.g. on unexpected disposal). */
68
+ dispose() {
69
+ this.finalized = true;
70
+ this.stopTimers();
71
+ }
72
+ async finish(state, detail) {
73
+ if (this.finalized)
74
+ return;
75
+ this.state = state;
76
+ this.detail = detail;
77
+ this.finalized = true;
78
+ this.stopTimers();
79
+ // Wait for any in-flight lazy create so we can patch (not orphan) the card.
80
+ if (this.createPromise)
81
+ await this.createPromise.catch(() => { });
82
+ if (!this.messageId || this.disabled)
83
+ return;
84
+ await this.safeEdit(true);
85
+ }
86
+ async refresh() {
87
+ if (this.finalized || this.disabled)
88
+ return;
89
+ await this.ensureCreatedNow();
90
+ await this.safeEdit();
91
+ }
92
+ async ensureCreatedNow() {
93
+ if (this.createTimer) {
94
+ clearTimeout(this.createTimer);
95
+ this.createTimer = undefined;
96
+ this.createPromise = this.ensureCreated();
97
+ }
98
+ if (this.createPromise)
99
+ await this.createPromise.catch(() => { });
100
+ }
101
+ async ensureCreated() {
102
+ if (this.disabled || this.finalized || this.messageId)
103
+ return;
104
+ try {
105
+ const id = await this.callbacks.create(this.buildView());
106
+ if (!id) {
107
+ this.disabled = true;
108
+ return;
109
+ }
110
+ this.messageId = id;
111
+ this.lastSignature = this.signature();
112
+ this.startTicker();
113
+ }
114
+ catch (err) {
115
+ this.disabled = true;
116
+ this.callbacks.warn?.("compact progress create failed", err);
117
+ }
118
+ }
119
+ /** Refresh the elapsed-time footer periodically even with no phase change. */
120
+ startTicker() {
121
+ if (this.tickTimer || this.disabled || this.finalized)
122
+ return;
123
+ const tickMs = this.opts.tickMs ?? DEFAULT_TICK_MS;
124
+ this.tickTimer = setInterval(() => {
125
+ if (this.finalized || this.disabled || !this.messageId)
126
+ return;
127
+ void this.safeEdit();
128
+ }, tickMs);
129
+ this.tickTimer.unref?.();
130
+ }
131
+ async safeEdit(force = false) {
132
+ if (!this.messageId || this.disabled)
133
+ return;
134
+ if (!force && this.finalized)
135
+ return;
136
+ const sig = this.signature();
137
+ if (!force && sig === this.lastSignature)
138
+ return;
139
+ try {
140
+ await this.callbacks.edit(this.messageId, this.buildView());
141
+ this.lastSignature = sig;
142
+ }
143
+ catch (err) {
144
+ this.disabled = true;
145
+ this.stopTimers();
146
+ this.callbacks.warn?.("compact progress edit failed", err);
147
+ }
148
+ }
149
+ stopTimers() {
150
+ if (this.createTimer) {
151
+ clearTimeout(this.createTimer);
152
+ this.createTimer = undefined;
153
+ }
154
+ if (this.tickTimer) {
155
+ clearInterval(this.tickTimer);
156
+ this.tickTimer = undefined;
157
+ }
158
+ }
159
+ /** Dedupe key; includes elapsed so the ticking footer still advances. */
160
+ signature() {
161
+ return `${this.state}|${this.phase}|${this.formatElapsed()}|${this.detail || ""}`;
162
+ }
163
+ formatElapsed() {
164
+ const totalSec = Math.max(0, Math.floor((Date.now() - this.startedAt) / 1000));
165
+ const m = Math.floor(totalSec / 60);
166
+ const s = totalSec % 60;
167
+ return `${m}:${String(s).padStart(2, "0")}`;
168
+ }
169
+ buildView() {
170
+ return {
171
+ state: this.state,
172
+ phase: this.phase,
173
+ elapsed: this.formatElapsed(),
174
+ detail: this.detail,
175
+ };
176
+ }
177
+ }
@@ -34,6 +34,8 @@ export declare class FeishuBot {
34
34
  private delayedFailureTimers;
35
35
  /** Last time a real assistant-visible reply was successfully handed to the delivery pipeline. */
36
36
  private lastRealDeliveryAt;
37
+ /** Per-chat: whether we've already sent the high-context alert this cycle. */
38
+ private contextAlerted;
37
39
  /** Active chatSend trigger target so final replies and proactive session.message share one delivery key. */
38
40
  private activeDeliveryTargets;
39
41
  private adminOpenId;
@@ -117,6 +119,51 @@ export declare class FeishuBot {
117
119
  private isBridgeControlReply;
118
120
  private isLegacyBridgeControlMessage;
119
121
  private isRuntimeFailureText;
122
+ /**
123
+ * Is this main-run error worth auto-retrying (by resuming the session via the
124
+ * probe loop)? Mainly transient failures — request/response timeouts and
125
+ * aborted/incomplete runs. We do NOT auto-retry clearly terminal errors
126
+ * (auth/permission/quota/billing/model-not-supported), which would just fail
127
+ * again. The error text comes from collectReply's `Agent error: <msg>`.
128
+ */
129
+ private isRetryableAgentError;
130
+ /**
131
+ * Heuristic: does this reply look truncated / mid-task? Bias toward false
132
+ * positives (“宁可错杀不放过”) — a false positive only costs one invisible
133
+ * confirmation round, and the session self-check is the real arbiter. We gate
134
+ * only on cheap text shape here, never on stopReason (the upstream rarely
135
+ * provides one for normal-looking-but-truncated finals).
136
+ */
137
+ private looksTruncated;
138
+ /** Current context usage percent for a session (totalTokens / contextTokens),
139
+ * or 0 if it cannot be determined. Same basis as /status. */
140
+ private getContextUsagePercent;
141
+ /**
142
+ * After a reply is delivered, alert the user once if context usage crossed the
143
+ * threshold (default 80%), so they can compact before a session bloats into the
144
+ * timeout/lock/overflow failure modes. We only alert on a fresh crossing: once
145
+ * alerted we stay quiet until usage drops back below the threshold (e.g. after
146
+ * a compact), then re-arm.
147
+ */
148
+ private maybeAlertHighContext;
149
+ /** True when the agent's reply is exactly a completion phrase (zh or en,
150
+ * tolerant of trailing punctuation/whitespace and case). */
151
+ /**
152
+ * True when the agent's reply means “done”. We accept the exact phrase as well
153
+ * as common wrappers (“已经结束了” / “好的,结束了” / “yes, DONE”), but reject it
154
+ * when a negation/not-finished cue is present (“还没结束” / “not done”), so a
155
+ * “still working” answer never counts as completion.
156
+ */
157
+ private isDonePhrase;
158
+ /**
159
+ * Auto-retry loop: if `reply` looks truncated, ask the same session whether it
160
+ * finished. The session is the arbiter: if it replies exactly the done phrase,
161
+ * the ORIGINAL reply was the real result and we deliver that. Otherwise the
162
+ * session kept working and its new output becomes the latest reply, which we
163
+ * re-check. Loops until the session confirms completion, a reply no longer
164
+ * looks truncated, the budget is spent, or the session cannot answer.
165
+ */
166
+ private autoRetryUntilComplete;
120
167
  private cancelDelayedFailure;
121
168
  private setActiveDeliveryTarget;
122
169
  private scheduleDelayedFailure;
@@ -153,6 +200,14 @@ export declare class FeishuBot {
153
200
  private sendLiveStatusCard;
154
201
  private replyLiveStatusCard;
155
202
  private patchLiveStatusCard;
203
+ /**
204
+ * Render the /compact progress card. While running it shows a phase line plus a
205
+ * ticking elapsed footer so the user can see compaction is in progress; when
206
+ * terminal it collapses to a single compact line (success/skip/fail).
207
+ */
208
+ private buildCompactProgressCard;
209
+ private replyCompactCard;
210
+ private patchCompactCard;
156
211
  private sendTextMessage;
157
212
  private replyTextMessage;
158
213
  private editTextMessage;
@@ -202,6 +257,17 @@ export declare class FeishuBot {
202
257
  /**
203
258
  * Handle /compact command: compress session context.
204
259
  */
260
+ /**
261
+ * Compact a session with a fallback chain:
262
+ * 1. OpenClaw's native (LLM-summary) compaction — best semantics.
263
+ * 2. If that did not compact (or threw), fall back to "tool-trim": rewrite
264
+ * the transcript file to drop tool calls/results while keeping the
265
+ * conversation. This needs no model, so it works no matter how large the
266
+ * session is (the native path itself fails to fit an oversized session
267
+ * into the model). A backup is always written before any file change.
268
+ * Returns a structured result describing what happened.
269
+ */
270
+ private compactWithFallback;
205
271
  private handleCompactCommand;
206
272
  /**
207
273
  * Handle /reset command: fire sessions.reset and confirm.