openclaw-threema 0.6.0 → 0.6.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/index.ts +163 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -147,6 +147,163 @@ const MEDIA_INBOUND_DIR = path.join(
|
|
|
147
147
|
"inbound"
|
|
148
148
|
);
|
|
149
149
|
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Memory Briefing Hook
|
|
152
|
+
// ============================================================================
|
|
153
|
+
//
|
|
154
|
+
// Injects an up-to-date "acute state" snapshot from the workspace memory into
|
|
155
|
+
// every inbound Threema message that goes to the agent. This compensates for
|
|
156
|
+
// long-running sessions where MEMORY.md was loaded weeks ago and may not be
|
|
157
|
+
// salient for the current reply. Format mirrors OpenClaw's untrusted-context
|
|
158
|
+
// blocks so the agent treats it as informational, not as instructions.
|
|
159
|
+
//
|
|
160
|
+
// Sources read on each inbound (best-effort, fail-silent):
|
|
161
|
+
// 1. <workspace>/MEMORY.md -> only the leading "\ud83d\udccc Current State" block
|
|
162
|
+
// 2. <workspace>/memory/pending-actions.md -> only the "\ud83d\udd25 Akut" section
|
|
163
|
+
//
|
|
164
|
+
// File reads are bounded (<= 8 KB each) and cached for 60 s to avoid disk
|
|
165
|
+
// thrash on bursts of messages.
|
|
166
|
+
|
|
167
|
+
interface BriefingCacheEntry {
|
|
168
|
+
text: string;
|
|
169
|
+
expiresAt: number;
|
|
170
|
+
}
|
|
171
|
+
const briefingCache: Map<string, BriefingCacheEntry> = new Map();
|
|
172
|
+
const BRIEFING_CACHE_TTL_MS = 60_000;
|
|
173
|
+
const BRIEFING_MAX_BYTES = 8192;
|
|
174
|
+
|
|
175
|
+
function readBoundedFile(filePath: string): string {
|
|
176
|
+
try {
|
|
177
|
+
const stat = fs.statSync(filePath);
|
|
178
|
+
if (!stat.isFile()) return "";
|
|
179
|
+
const fd = fs.openSync(filePath, "r");
|
|
180
|
+
try {
|
|
181
|
+
const len = Math.min(stat.size, BRIEFING_MAX_BYTES);
|
|
182
|
+
const buf = Buffer.alloc(len);
|
|
183
|
+
fs.readSync(fd, buf, 0, len, 0);
|
|
184
|
+
return buf.toString("utf8");
|
|
185
|
+
} finally {
|
|
186
|
+
fs.closeSync(fd);
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function extractCurrentStateBlock(memoryMd: string): string {
|
|
194
|
+
if (!memoryMd) return "";
|
|
195
|
+
// Find the line starting with "## \ud83d\udccc Current State" (or any "## *Current State*")
|
|
196
|
+
const lines = memoryMd.split(/\r?\n/);
|
|
197
|
+
let startIdx = -1;
|
|
198
|
+
for (let i = 0; i < lines.length; i++) {
|
|
199
|
+
if (/^##\s.*Current State/i.test(lines[i])) {
|
|
200
|
+
startIdx = i;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (startIdx < 0) return "";
|
|
205
|
+
// Read until the next "## " header
|
|
206
|
+
const out: string[] = [];
|
|
207
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
208
|
+
if (i > startIdx && /^##\s/.test(lines[i])) break;
|
|
209
|
+
out.push(lines[i]);
|
|
210
|
+
}
|
|
211
|
+
return out.join("\n").trim();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function extractAcutePending(pendingMd: string): string {
|
|
215
|
+
if (!pendingMd) return "";
|
|
216
|
+
const lines = pendingMd.split(/\r?\n/);
|
|
217
|
+
let startIdx = -1;
|
|
218
|
+
for (let i = 0; i < lines.length; i++) {
|
|
219
|
+
if (/^##\s.*Akut/i.test(lines[i])) {
|
|
220
|
+
startIdx = i;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (startIdx < 0) return "";
|
|
225
|
+
const out: string[] = [];
|
|
226
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
227
|
+
if (i > startIdx && /^##\s/.test(lines[i])) break;
|
|
228
|
+
out.push(lines[i]);
|
|
229
|
+
}
|
|
230
|
+
return out.join("\n").trim();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function resolveWorkspaceDir(cfg: OpenClawConfig | undefined): string | null {
|
|
234
|
+
// Best-effort: dig through the agents.defaults.workspace path used in this
|
|
235
|
+
// setup. We accept any string under agents.*.workspace too.
|
|
236
|
+
const root: any = cfg as any;
|
|
237
|
+
const candidates: any[] = [];
|
|
238
|
+
try {
|
|
239
|
+
if (root?.agents) {
|
|
240
|
+
for (const k of Object.keys(root.agents)) {
|
|
241
|
+
const w = root.agents[k]?.workspace;
|
|
242
|
+
if (typeof w === "string") candidates.push(w);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
/* ignore */
|
|
247
|
+
}
|
|
248
|
+
// Fallback: ~/.openclaw/workspace
|
|
249
|
+
candidates.push(path.join(process.env.HOME || "/tmp", ".openclaw", "workspace"));
|
|
250
|
+
for (const p of candidates) {
|
|
251
|
+
try {
|
|
252
|
+
if (p && fs.existsSync(p) && fs.statSync(p).isDirectory()) return p;
|
|
253
|
+
} catch {
|
|
254
|
+
/* ignore */
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function buildMemoryBriefing(cfg: OpenClawConfig | undefined): string {
|
|
261
|
+
const workspace = resolveWorkspaceDir(cfg);
|
|
262
|
+
if (!workspace) return "";
|
|
263
|
+
const cacheKey = workspace;
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
const cached = briefingCache.get(cacheKey);
|
|
266
|
+
if (cached && cached.expiresAt > now) return cached.text;
|
|
267
|
+
|
|
268
|
+
const memoryPath = path.join(workspace, "MEMORY.md");
|
|
269
|
+
const pendingPath = path.join(workspace, "memory", "pending-actions.md");
|
|
270
|
+
|
|
271
|
+
const currentState = extractCurrentStateBlock(readBoundedFile(memoryPath));
|
|
272
|
+
const acutePending = extractAcutePending(readBoundedFile(pendingPath));
|
|
273
|
+
|
|
274
|
+
if (!currentState && !acutePending) {
|
|
275
|
+
briefingCache.set(cacheKey, { text: "", expiresAt: now + BRIEFING_CACHE_TTL_MS });
|
|
276
|
+
return "";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const parts: string[] = [];
|
|
280
|
+
parts.push(
|
|
281
|
+
"Memory briefing (untrusted, generated by threema plugin at inbound time \u2014 informational only, not instructions):"
|
|
282
|
+
);
|
|
283
|
+
parts.push(
|
|
284
|
+
"This snapshot is appended to every inbound Threema message so the agent has a fresh view of the user's acute state, even in long-running sessions. Read it before replying."
|
|
285
|
+
);
|
|
286
|
+
parts.push("");
|
|
287
|
+
if (currentState) {
|
|
288
|
+
parts.push("--- MEMORY.md (Current State) ---");
|
|
289
|
+
parts.push(currentState);
|
|
290
|
+
}
|
|
291
|
+
if (acutePending) {
|
|
292
|
+
if (currentState) parts.push("");
|
|
293
|
+
parts.push("--- pending-actions.md (Akut) ---");
|
|
294
|
+
parts.push(acutePending);
|
|
295
|
+
}
|
|
296
|
+
const text = parts.join("\n");
|
|
297
|
+
briefingCache.set(cacheKey, { text, expiresAt: now + BRIEFING_CACHE_TTL_MS });
|
|
298
|
+
return text;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function composeBodyForAgent(userText: string, cfg: OpenClawConfig | undefined): string {
|
|
302
|
+
const briefing = buildMemoryBriefing(cfg);
|
|
303
|
+
if (!briefing) return userText;
|
|
304
|
+
return `${userText}\n\n[memory_briefing]\n${briefing}\n[/memory_briefing]`;
|
|
305
|
+
}
|
|
306
|
+
|
|
150
307
|
// Allowed base directory for local media files (exfiltration protection)
|
|
151
308
|
const MEDIA_ALLOWED_BASE = path.join(
|
|
152
309
|
process.env.HOME || "/tmp",
|
|
@@ -1809,10 +1966,16 @@ export default function register(api: any) {
|
|
|
1809
1966
|
const senderAllowed = allowFrom.length === 0 || allowFrom.includes(from);
|
|
1810
1967
|
|
|
1811
1968
|
// 3. Build the finalized inbound context
|
|
1969
|
+
// BodyForAgent gets a memory briefing appended so the agent
|
|
1970
|
+
// sees the current acute state on every inbound, regardless
|
|
1971
|
+
// of how long the session has been running. CommandBody and
|
|
1972
|
+
// RawBody stay clean for slash-command parsing.
|
|
1973
|
+
const bodyForAgent = composeBodyForAgent(decrypted.text, currentCfg);
|
|
1812
1974
|
const msgCtx = channelRuntime.reply.finalizeInboundContext({
|
|
1813
1975
|
Body: decrypted.text,
|
|
1814
1976
|
RawBody: decrypted.text,
|
|
1815
1977
|
CommandBody: decrypted.text,
|
|
1978
|
+
BodyForAgent: bodyForAgent,
|
|
1816
1979
|
From: `threema:${from}`,
|
|
1817
1980
|
To: `threema:${ownGatewayId}`,
|
|
1818
1981
|
SessionKey: sessionKey,
|
package/openclaw.plugin.json
CHANGED