openclaw-lark-multi-agent 1.4.3 → 1.4.5
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 +38 -19
- package/README.zh-CN.md +14 -0
- package/dist/cli.js +4 -4
- package/dist/compact-progress.d.ts +4 -4
- package/dist/compact-progress.js +5 -5
- package/dist/feishu-bot.d.ts +28 -14
- package/dist/feishu-bot.js +322 -282
- package/dist/message-store.d.ts +4 -3
- package/dist/openclaw-client.d.ts +32 -25
- package/dist/openclaw-client.js +327 -158
- package/package.json +2 -2
- package/plugins/lma-steer/README.md +23 -30
- package/dist/session-file-compactor.d.ts +0 -39
- package/dist/session-file-compactor.js +0 -176
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-lark-multi-agent",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5",
|
|
4
4
|
"description": "Multi-bot Lark/Feishu bridge for OpenClaw, with per-bot model routing and isolated sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@types/better-sqlite3": "^7.6.13",
|
|
24
24
|
"@types/node": "^22.0.0",
|
|
25
25
|
"@vitest/coverage-v8": "^4.1.5",
|
|
26
|
-
"openclaw": "^2026.
|
|
26
|
+
"openclaw": "^2026.8.2",
|
|
27
27
|
"tsx": "^4.19.0",
|
|
28
28
|
"typescript": "^5.7.0",
|
|
29
29
|
"vitest": "^4.1.5"
|
|
@@ -1,45 +1,38 @@
|
|
|
1
|
-
# lma-steer
|
|
1
|
+
# lma-steer (legacy compatibility plugin)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
**active** OpenClaw run at the next tool-call boundary — so a user can nudge or
|
|
6
|
-
correct a long-running task in real time instead of waiting for it to finish.
|
|
3
|
+
This plugin is retained for older `openclaw-lark-multi-agent` deployments that
|
|
4
|
+
called the private `lma.steer` Gateway method.
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
Current LMA releases on OpenClaw 2026.8+ do **not** use this plugin. They submit
|
|
7
|
+
mid-run input through the public protocol API:
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
was queued as a followup, or was rejected. This plugin uses the runtime
|
|
13
|
-
primitives `resolveActiveEmbeddedRunSessionId` + `queueAgentHarnessMessage`,
|
|
14
|
-
which expose that distinction, so the bridge can render an accurate Feishu
|
|
15
|
-
reaction.
|
|
16
|
-
|
|
17
|
-
## Install
|
|
18
|
-
|
|
19
|
-
From an installed `openclaw-lark-multi-agent`:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
openclaw-lark-multi-agent install-steer-plugin
|
|
9
|
+
```text
|
|
10
|
+
chat.send { queueMode: "steer" }
|
|
23
11
|
```
|
|
24
12
|
|
|
25
|
-
|
|
13
|
+
The public path owns admission, while LMA retains its durable fallback trigger
|
|
14
|
+
until a matching `session.message` confirms transcript consumption. Only then
|
|
15
|
+
is the Feishu acknowledgement changed from Typing to Get, so provisional
|
|
16
|
+
acceptance and actual model consumption remain distinct.
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
systemctl --user restart openclaw-gateway.service
|
|
29
|
-
```
|
|
18
|
+
## Legacy installation
|
|
30
19
|
|
|
31
|
-
|
|
20
|
+
Only install this plugin when maintaining an older LMA release that explicitly
|
|
21
|
+
requires `lma.steer`:
|
|
32
22
|
|
|
33
23
|
```bash
|
|
34
|
-
|
|
24
|
+
lma install-steer-plugin
|
|
35
25
|
```
|
|
36
26
|
|
|
37
|
-
|
|
27
|
+
Restart the OpenClaw Gateway after installation. For new deployments, skip this
|
|
28
|
+
step.
|
|
38
29
|
|
|
39
|
-
|
|
30
|
+
## Legacy method
|
|
40
31
|
|
|
32
|
+
- method: `lma.steer`
|
|
41
33
|
- params: `{ sessionKey: string, text: string }`
|
|
42
34
|
- result: `{ status: "steered" | "no_active_run" | "rejected", sessionId?: string }`
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
35
|
+
|
|
36
|
+
Its synchronous `steered` result reflects immediate queue eligibility only; it
|
|
37
|
+
cannot prove later runtime consumption. This limitation is why current LMA uses
|
|
38
|
+
the public native steering path instead.
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
export type ToolTrimResult = {
|
|
2
|
-
ok: boolean;
|
|
3
|
-
reason?: string;
|
|
4
|
-
/** Bytes before / after, and line counts, for reporting. */
|
|
5
|
-
bytesBefore?: number;
|
|
6
|
-
bytesAfter?: number;
|
|
7
|
-
linesBefore?: number;
|
|
8
|
-
linesAfter?: number;
|
|
9
|
-
removedToolResults?: number;
|
|
10
|
-
strippedToolCalls?: number;
|
|
11
|
-
removedEmptyAssistants?: number;
|
|
12
|
-
keptRecentToolCalls?: number;
|
|
13
|
-
backupPath?: string;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Resolve the on-disk OpenClaw transcript file for a session.
|
|
17
|
-
*
|
|
18
|
-
* Session key looks like `agent:<agentId>:<label>` (e.g.
|
|
19
|
-
* `agent:main:lma-claude-oc_xxx`). The transcript lives at
|
|
20
|
-
* `<openclawHome>/agents/<agentId>/sessions/<sessionId>.jsonl`.
|
|
21
|
-
*/
|
|
22
|
-
export declare function resolveSessionFilePath(sessionKey: string, sessionId: string, openclawHome?: string): string | null;
|
|
23
|
-
/**
|
|
24
|
-
* "Tool-trim" compaction: rewrite a session transcript in place, removing the
|
|
25
|
-
* tool-call noise while keeping the conversation intact:
|
|
26
|
-
* - drop every toolResult message entirely;
|
|
27
|
-
* - strip the `toolCall` parts from assistant messages but KEEP their text
|
|
28
|
-
* (the model's "thinking out loud while acting"); if an assistant message
|
|
29
|
-
* had only tool calls and no text, drop the whole line;
|
|
30
|
-
* - fix `stopReason: "toolUse"` -> "stop" on messages that no longer have a
|
|
31
|
-
* tool call, so the loader is not confused.
|
|
32
|
-
*
|
|
33
|
-
* This does NOT call any model, so it works no matter how large the session is
|
|
34
|
-
* (unlike OpenClaw's LLM-summary compaction, which itself fails to fit an
|
|
35
|
-
* oversized session into the model). A backup is always written first.
|
|
36
|
-
*
|
|
37
|
-
* Typical reduction is ~70-90% for tool-heavy sessions.
|
|
38
|
-
*/
|
|
39
|
-
export declare function toolTrimCompactFile(filePath: string, keepRecentToolCalls?: number): ToolTrimResult;
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { existsSync, copyFileSync, readFileSync, writeFileSync, renameSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { homedir } from "os";
|
|
4
|
-
/**
|
|
5
|
-
* Resolve the on-disk OpenClaw transcript file for a session.
|
|
6
|
-
*
|
|
7
|
-
* Session key looks like `agent:<agentId>:<label>` (e.g.
|
|
8
|
-
* `agent:main:lma-claude-oc_xxx`). The transcript lives at
|
|
9
|
-
* `<openclawHome>/agents/<agentId>/sessions/<sessionId>.jsonl`.
|
|
10
|
-
*/
|
|
11
|
-
export function resolveSessionFilePath(sessionKey, sessionId, openclawHome) {
|
|
12
|
-
if (!sessionId)
|
|
13
|
-
return null;
|
|
14
|
-
const home = openclawHome
|
|
15
|
-
|| process.env.OPENCLAW_HOME
|
|
16
|
-
|| process.env.OPENCLAW_CONFIG_DIR
|
|
17
|
-
|| join(homedir(), ".openclaw");
|
|
18
|
-
// agentId is the second segment of agent:<agentId>:<label>; default to "main".
|
|
19
|
-
const parts = sessionKey.split(":");
|
|
20
|
-
const agentId = parts.length >= 3 && parts[0] === "agent" ? parts[1] : "main";
|
|
21
|
-
const file = join(home, "agents", agentId, "sessions", `${sessionId}.jsonl`);
|
|
22
|
-
return existsSync(file) ? file : null;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* "Tool-trim" compaction: rewrite a session transcript in place, removing the
|
|
26
|
-
* tool-call noise while keeping the conversation intact:
|
|
27
|
-
* - drop every toolResult message entirely;
|
|
28
|
-
* - strip the `toolCall` parts from assistant messages but KEEP their text
|
|
29
|
-
* (the model's "thinking out loud while acting"); if an assistant message
|
|
30
|
-
* had only tool calls and no text, drop the whole line;
|
|
31
|
-
* - fix `stopReason: "toolUse"` -> "stop" on messages that no longer have a
|
|
32
|
-
* tool call, so the loader is not confused.
|
|
33
|
-
*
|
|
34
|
-
* This does NOT call any model, so it works no matter how large the session is
|
|
35
|
-
* (unlike OpenClaw's LLM-summary compaction, which itself fails to fit an
|
|
36
|
-
* oversized session into the model). A backup is always written first.
|
|
37
|
-
*
|
|
38
|
-
* Typical reduction is ~70-90% for tool-heavy sessions.
|
|
39
|
-
*/
|
|
40
|
-
export function toolTrimCompactFile(filePath, keepRecentToolCalls = 3) {
|
|
41
|
-
if (!existsSync(filePath))
|
|
42
|
-
return { ok: false, reason: "transcript file not found" };
|
|
43
|
-
const original = readFileSync(filePath, "utf8");
|
|
44
|
-
const lines = original.split("\n");
|
|
45
|
-
const bytesBefore = Buffer.byteLength(original, "utf8");
|
|
46
|
-
// First pass: collect every toolCall id in order, so we can keep the most
|
|
47
|
-
// recent N of them (and their matching toolResults) intact. Recent tool calls
|
|
48
|
-
// are often still relevant to what the agent is doing next.
|
|
49
|
-
const toolCallIds = [];
|
|
50
|
-
for (const line of lines) {
|
|
51
|
-
if (line.trim() === "")
|
|
52
|
-
continue;
|
|
53
|
-
let d;
|
|
54
|
-
try {
|
|
55
|
-
d = JSON.parse(line);
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
if (d?.type !== "message")
|
|
61
|
-
continue;
|
|
62
|
-
const msg = d.message;
|
|
63
|
-
if (msg?.role === "assistant" && Array.isArray(msg.content)) {
|
|
64
|
-
for (const p of msg.content) {
|
|
65
|
-
if (p && typeof p === "object" && p.type === "toolCall" && p.id)
|
|
66
|
-
toolCallIds.push(String(p.id));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const keepN = Math.max(0, Math.floor(keepRecentToolCalls));
|
|
71
|
-
const keepIds = new Set(keepN > 0 ? toolCallIds.slice(-keepN) : []);
|
|
72
|
-
const out = [];
|
|
73
|
-
let removedToolResults = 0;
|
|
74
|
-
let strippedToolCalls = 0;
|
|
75
|
-
let removedEmptyAssistants = 0;
|
|
76
|
-
let keptRecentToolCalls = 0;
|
|
77
|
-
let linesBefore = 0;
|
|
78
|
-
for (const line of lines) {
|
|
79
|
-
if (line.trim() === "") {
|
|
80
|
-
continue;
|
|
81
|
-
} // drop blank lines; we re-join with \n
|
|
82
|
-
linesBefore++;
|
|
83
|
-
let d;
|
|
84
|
-
try {
|
|
85
|
-
d = JSON.parse(line);
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
out.push(line);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (d?.type !== "message") {
|
|
92
|
-
out.push(line);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const msg = d.message;
|
|
96
|
-
const role = msg?.role;
|
|
97
|
-
if (role === "toolResult") {
|
|
98
|
-
// Keep the toolResult only if its toolCall is in the recent keep-set.
|
|
99
|
-
if (msg?.toolCallId && keepIds.has(String(msg.toolCallId))) {
|
|
100
|
-
out.push(line);
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
removedToolResults++;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (role === "assistant" && Array.isArray(msg?.content)) {
|
|
107
|
-
const content = msg.content;
|
|
108
|
-
const hasToolCall = content.some((p) => p && typeof p === "object" && p.type === "toolCall");
|
|
109
|
-
if (hasToolCall) {
|
|
110
|
-
// Keep toolCalls that are in the recent keep-set; strip the rest.
|
|
111
|
-
const kept = content.filter((p) => {
|
|
112
|
-
if (!p || typeof p !== "object")
|
|
113
|
-
return false;
|
|
114
|
-
if (p.type !== "toolCall")
|
|
115
|
-
return true;
|
|
116
|
-
return p.id && keepIds.has(String(p.id));
|
|
117
|
-
});
|
|
118
|
-
const keptToolCall = kept.some((p) => p.type === "toolCall");
|
|
119
|
-
const hasText = kept.some((p) => p.type === "text" && typeof p.text === "string" && p.text.trim() !== "");
|
|
120
|
-
if (!keptToolCall && !hasText) {
|
|
121
|
-
removedEmptyAssistants++;
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (keptToolCall) {
|
|
125
|
-
keptRecentToolCalls++;
|
|
126
|
-
// stopReason stays toolUse since a toolCall remains.
|
|
127
|
-
if (kept.length !== content.length)
|
|
128
|
-
msg.content = kept;
|
|
129
|
-
out.push(JSON.stringify(d));
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
// All toolCalls stripped: keep text only, fix stopReason.
|
|
133
|
-
msg.content = kept;
|
|
134
|
-
if (msg.stopReason === "toolUse")
|
|
135
|
-
msg.stopReason = "stop";
|
|
136
|
-
strippedToolCalls++;
|
|
137
|
-
out.push(JSON.stringify(d));
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
out.push(line);
|
|
142
|
-
}
|
|
143
|
-
// Safety: verify every output line is valid JSON before committing.
|
|
144
|
-
for (const l of out) {
|
|
145
|
-
try {
|
|
146
|
-
JSON.parse(l);
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
return { ok: false, reason: "produced invalid JSON; aborted" };
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
if (removedToolResults === 0 && strippedToolCalls === 0 && removedEmptyAssistants === 0) {
|
|
153
|
-
return { ok: true, reason: "no tool content to trim", bytesBefore, bytesAfter: bytesBefore, linesBefore, linesAfter: out.length, keptRecentToolCalls };
|
|
154
|
-
}
|
|
155
|
-
// Always back up first.
|
|
156
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
157
|
-
const backupPath = `${filePath}.bak-tooltrim-${stamp}`;
|
|
158
|
-
copyFileSync(filePath, backupPath);
|
|
159
|
-
// Atomic write: temp file + rename.
|
|
160
|
-
const body = out.join("\n") + "\n";
|
|
161
|
-
const tmp = `${filePath}.tmp-tooltrim`;
|
|
162
|
-
writeFileSync(tmp, body, "utf8");
|
|
163
|
-
renameSync(tmp, filePath);
|
|
164
|
-
return {
|
|
165
|
-
ok: true,
|
|
166
|
-
bytesBefore,
|
|
167
|
-
bytesAfter: Buffer.byteLength(body, "utf8"),
|
|
168
|
-
linesBefore,
|
|
169
|
-
linesAfter: out.length,
|
|
170
|
-
removedToolResults,
|
|
171
|
-
strippedToolCalls,
|
|
172
|
-
removedEmptyAssistants,
|
|
173
|
-
keptRecentToolCalls,
|
|
174
|
-
backupPath,
|
|
175
|
-
};
|
|
176
|
-
}
|