@pennyclaw/idle-prune 0.1.0
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 +24 -0
- package/dist/index.js +126 -0
- package/openclaw.plugin.json +18 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Idle Prune
|
|
2
|
+
|
|
3
|
+
Prunes old `tool_result` payloads after idle time by replacing content with a placeholder.
|
|
4
|
+
|
|
5
|
+
## Config
|
|
6
|
+
|
|
7
|
+
```json5
|
|
8
|
+
plugins: {
|
|
9
|
+
entries: {
|
|
10
|
+
"idle-prune": {
|
|
11
|
+
enabled: true,
|
|
12
|
+
config: {
|
|
13
|
+
idleMinutes: 15,
|
|
14
|
+
placeholder: "[pruned due to idle]"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Notes
|
|
22
|
+
- Runs on `message_received` to avoid extra LLM calls.
|
|
23
|
+
- Keeps tool call IDs and metadata intact.
|
|
24
|
+
- Only rewrites the content of tool-result messages.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// index.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
function looksLikeGroupId(from) {
|
|
5
|
+
const lower = from.toLowerCase();
|
|
6
|
+
if (lower.includes(":group:")) return "group";
|
|
7
|
+
if (lower.includes(":channel:")) return "channel";
|
|
8
|
+
if (lower.endsWith("@g.us")) return "group";
|
|
9
|
+
return "dm";
|
|
10
|
+
}
|
|
11
|
+
function stripChannelPrefix(value, channelId) {
|
|
12
|
+
const prefix = `${channelId}:`;
|
|
13
|
+
return value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
|
14
|
+
}
|
|
15
|
+
function pruneToolResultMessage(entry, placeholder) {
|
|
16
|
+
if (!entry || entry.type !== "message") return false;
|
|
17
|
+
const msg = entry.message;
|
|
18
|
+
if (!msg || msg.role !== "tool" && msg.role !== "toolResult") return false;
|
|
19
|
+
const content = msg.content;
|
|
20
|
+
if (typeof content === "string") {
|
|
21
|
+
if (content === placeholder) return false;
|
|
22
|
+
msg.content = placeholder;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(content)) {
|
|
26
|
+
if (content.length === 1 && content[0]?.type === "text" && content[0]?.text === placeholder) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
msg.content = [{ type: "text", text: placeholder }];
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (content && typeof content === "object" && "text" in content) {
|
|
33
|
+
if (content.text === placeholder) return false;
|
|
34
|
+
msg.content = { ...content, text: placeholder };
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
msg.content = [{ type: "text", text: placeholder }];
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
async function pruneTranscript(params) {
|
|
41
|
+
if (!fs.existsSync(params.sessionFile)) return;
|
|
42
|
+
const raw = await fs.promises.readFile(params.sessionFile, "utf-8");
|
|
43
|
+
const lines = raw.split("\n");
|
|
44
|
+
let changed = false;
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
if (!line.trim()) {
|
|
48
|
+
out.push(line);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const entry = JSON.parse(line);
|
|
53
|
+
if (pruneToolResultMessage(entry, params.placeholder)) {
|
|
54
|
+
changed = true;
|
|
55
|
+
}
|
|
56
|
+
out.push(JSON.stringify(entry));
|
|
57
|
+
} catch {
|
|
58
|
+
out.push(line);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!changed) return;
|
|
62
|
+
const tmp = `${params.sessionFile}.idle-prune.tmp`;
|
|
63
|
+
await fs.promises.writeFile(tmp, out.join("\n"), "utf-8");
|
|
64
|
+
await fs.promises.rename(tmp, params.sessionFile);
|
|
65
|
+
params.logger.info(`idle-prune: pruned tool_result content in ${params.sessionFile}`);
|
|
66
|
+
}
|
|
67
|
+
function register(api) {
|
|
68
|
+
const pluginCfg = api.pluginConfig ?? {};
|
|
69
|
+
const idleMinutes = Number.isFinite(pluginCfg.idleMinutes) ? Number(pluginCfg.idleMinutes) : 15;
|
|
70
|
+
const placeholder = typeof pluginCfg.placeholder === "string" && pluginCfg.placeholder.trim() ? pluginCfg.placeholder : "[pruned due to idle]";
|
|
71
|
+
api.on("message_received", async (event, ctx) => {
|
|
72
|
+
if (!idleMinutes || idleMinutes <= 0) return;
|
|
73
|
+
const from = typeof event.from === "string" ? event.from.trim() : "";
|
|
74
|
+
if (!from) return;
|
|
75
|
+
const channelId = typeof ctx.channelId === "string" ? ctx.channelId : "";
|
|
76
|
+
const conversationId = typeof ctx.conversationId === "string" && ctx.conversationId.trim() ? ctx.conversationId.trim() : typeof event?.metadata?.to === "string" ? event.metadata.to : from;
|
|
77
|
+
const peerIdRaw = conversationId || from;
|
|
78
|
+
const peerId = channelId ? stripChannelPrefix(peerIdRaw, channelId) : peerIdRaw;
|
|
79
|
+
const peerKind = looksLikeGroupId(peerIdRaw);
|
|
80
|
+
const route = api.runtime.channel.routing.resolveAgentRoute({
|
|
81
|
+
cfg: api.config,
|
|
82
|
+
channel: channelId || "telegram",
|
|
83
|
+
accountId: ctx.accountId ?? void 0,
|
|
84
|
+
peer: { kind: peerKind, id: peerId }
|
|
85
|
+
});
|
|
86
|
+
const storePath = api.runtime.channel.session.resolveStorePath(
|
|
87
|
+
api.config.session?.store,
|
|
88
|
+
{ agentId: route.agentId }
|
|
89
|
+
);
|
|
90
|
+
if (!fs.existsSync(storePath)) return;
|
|
91
|
+
let store = {};
|
|
92
|
+
try {
|
|
93
|
+
store = JSON.parse(await fs.promises.readFile(storePath, "utf-8"));
|
|
94
|
+
} catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let sessionKey = route.sessionKey;
|
|
98
|
+
if (!store[sessionKey]) {
|
|
99
|
+
const keys = Object.keys(store);
|
|
100
|
+
const match = keys.find((key) => {
|
|
101
|
+
const e = store[key] || {};
|
|
102
|
+
const candidates = [
|
|
103
|
+
e.origin?.from,
|
|
104
|
+
e.origin?.to,
|
|
105
|
+
e.lastTo,
|
|
106
|
+
e.deliveryContext?.to
|
|
107
|
+
].filter(Boolean);
|
|
108
|
+
return candidates.includes(conversationId) || candidates.includes(peerIdRaw) || candidates.includes(peerId);
|
|
109
|
+
});
|
|
110
|
+
if (match) sessionKey = match;
|
|
111
|
+
}
|
|
112
|
+
const entry = store[sessionKey];
|
|
113
|
+
if (!entry || typeof entry.updatedAt !== "number") return;
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
if (now - entry.updatedAt < idleMinutes * 60 * 1e3) return;
|
|
116
|
+
const sessionFile = typeof entry.sessionFile === "string" && entry.sessionFile.trim() || path.join(path.dirname(storePath), `${entry.sessionId}.jsonl`);
|
|
117
|
+
await pruneTranscript({
|
|
118
|
+
sessionFile,
|
|
119
|
+
placeholder,
|
|
120
|
+
logger: api.logger
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
register as default
|
|
126
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "idle-prune",
|
|
3
|
+
"name": "Idle Prune",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Prune tool_result content after idle periods without LLM summarization.",
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"idleMinutes": { "type": "number", "default": 15, "minimum": 1 },
|
|
11
|
+
"placeholder": { "type": "string", "default": "[pruned due to idle]" }
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"uiHints": {
|
|
15
|
+
"idleMinutes": { "label": "Idle minutes" },
|
|
16
|
+
"placeholder": { "label": "Placeholder text" }
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pennyclaw/idle-prune",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenClaw plugin to prune tool_result content after idle",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/index.js",
|
|
9
|
+
"openclaw.plugin.json",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"openclaw",
|
|
14
|
+
"plugin",
|
|
15
|
+
"idle",
|
|
16
|
+
"prune"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup index.ts --format esm --target node18 --out-dir dist"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^8.5.1",
|
|
24
|
+
"typescript": "^5.9.3"
|
|
25
|
+
}
|
|
26
|
+
}
|