adp-openclaw 0.0.55 → 0.0.57
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/package.json +1 -1
- package/src/monitor.ts +184 -1
package/package.json
CHANGED
package/src/monitor.ts
CHANGED
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
formatUploadResultAsMarkdown,
|
|
24
24
|
} from "./tool-result-message-blocks.js";
|
|
25
25
|
import crypto from "crypto";
|
|
26
|
+
import fs from "fs";
|
|
27
|
+
import path from "path";
|
|
26
28
|
// @ts-ignore - import JSON file
|
|
27
29
|
import packageJson from "../package.json" with { type: "json" };
|
|
28
30
|
|
|
@@ -116,6 +118,167 @@ function generateRequestId(): string {
|
|
|
116
118
|
return `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
117
119
|
}
|
|
118
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Mark a session's abortedLastRun flag in the sessions store.
|
|
123
|
+
* This tells the SDK to inject an "abort hint" on the next message,
|
|
124
|
+
* preventing the AI from resuming the cancelled task.
|
|
125
|
+
*/
|
|
126
|
+
async function markSessionAborted(params: {
|
|
127
|
+
sessionKey: string;
|
|
128
|
+
runtime: ReturnType<typeof getAdpOpenclawRuntime>;
|
|
129
|
+
cfg?: ClawdbotConfig;
|
|
130
|
+
log?: PluginLogger;
|
|
131
|
+
}): Promise<void> {
|
|
132
|
+
const { sessionKey, runtime, cfg, log } = params;
|
|
133
|
+
try {
|
|
134
|
+
// Use SDK's resolveStorePath to find the sessions.json location
|
|
135
|
+
const storePath = runtime.channel.session.resolveStorePath(cfg?.session?.store);
|
|
136
|
+
if (!storePath || !fs.existsSync(storePath)) {
|
|
137
|
+
log?.warn?.(`[adp-openclaw] Cannot mark session aborted: store not found at ${storePath}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const raw = fs.readFileSync(storePath, "utf-8");
|
|
142
|
+
const store = JSON.parse(raw) as Record<string, { abortedLastRun?: boolean; updatedAt?: number; [key: string]: unknown }>;
|
|
143
|
+
|
|
144
|
+
// Try both the raw sessionKey and the "agent:main:{sessionKey}" variant
|
|
145
|
+
const candidates = [sessionKey, `agent:main:${sessionKey}`];
|
|
146
|
+
let matchedKey: string | undefined;
|
|
147
|
+
for (const key of candidates) {
|
|
148
|
+
if (store[key]) {
|
|
149
|
+
matchedKey = key;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!matchedKey) {
|
|
155
|
+
log?.info?.(`[adp-openclaw] Session key not found in store for abort marking: ${sessionKey}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
store[matchedKey].abortedLastRun = true;
|
|
160
|
+
store[matchedKey].updatedAt = Date.now();
|
|
161
|
+
fs.writeFileSync(storePath, JSON.stringify(store, null, 2), "utf-8");
|
|
162
|
+
log?.info?.(`[adp-openclaw] Marked session ${matchedKey} as abortedLastRun=true`);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
log?.error?.(`[adp-openclaw] Failed to mark session aborted: ${err}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* After an abort, trim the aborted user message and any partial assistant reply
|
|
170
|
+
* from the JSONL transcript file. This prevents the AI from seeing the cancelled
|
|
171
|
+
* request in its context window and resuming it on the next turn.
|
|
172
|
+
*
|
|
173
|
+
* The JSONL transcript is a tree structure (id + parentId). We find the last
|
|
174
|
+
* user message entry and remove it (plus any subsequent entries like partial
|
|
175
|
+
* assistant replies). This is equivalent to SessionManager.branch(parentId).
|
|
176
|
+
*/
|
|
177
|
+
async function trimAbortedMessagesFromTranscript(params: {
|
|
178
|
+
sessionKey: string;
|
|
179
|
+
runtime: ReturnType<typeof getAdpOpenclawRuntime>;
|
|
180
|
+
cfg?: ClawdbotConfig;
|
|
181
|
+
log?: PluginLogger;
|
|
182
|
+
}): Promise<void> {
|
|
183
|
+
const { sessionKey, runtime, cfg, log } = params;
|
|
184
|
+
try {
|
|
185
|
+
const storePath = runtime.channel.session.resolveStorePath(cfg?.session?.store);
|
|
186
|
+
if (!storePath || !fs.existsSync(storePath)) {
|
|
187
|
+
log?.warn?.(`[adp-openclaw] Cannot trim transcript: store not found at ${storePath}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const raw = fs.readFileSync(storePath, "utf-8");
|
|
192
|
+
const store = JSON.parse(raw) as Record<string, { sessionId?: string; sessionFile?: string; [key: string]: unknown }>;
|
|
193
|
+
|
|
194
|
+
// Find matching session entry
|
|
195
|
+
const candidates = [sessionKey, `agent:main:${sessionKey}`];
|
|
196
|
+
let entry: { sessionId?: string; sessionFile?: string; [key: string]: unknown } | undefined;
|
|
197
|
+
for (const key of candidates) {
|
|
198
|
+
if (store[key]) {
|
|
199
|
+
entry = store[key];
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!entry?.sessionId) {
|
|
205
|
+
log?.info?.(`[adp-openclaw] Session entry not found for transcript trimming: ${sessionKey}`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Resolve transcript file path
|
|
210
|
+
let transcriptPath: string;
|
|
211
|
+
if (entry.sessionFile) {
|
|
212
|
+
transcriptPath = entry.sessionFile;
|
|
213
|
+
} else {
|
|
214
|
+
// Fallback: same directory as sessions.json, filename = <sessionId>.jsonl
|
|
215
|
+
transcriptPath = path.join(path.dirname(storePath), `${entry.sessionId}.jsonl`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!fs.existsSync(transcriptPath)) {
|
|
219
|
+
log?.info?.(`[adp-openclaw] Transcript file not found: ${transcriptPath}`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Read and parse JSONL lines
|
|
224
|
+
const content = fs.readFileSync(transcriptPath, "utf-8");
|
|
225
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
226
|
+
|
|
227
|
+
if (lines.length === 0) return;
|
|
228
|
+
|
|
229
|
+
// Parse each line into JSON entries
|
|
230
|
+
type TranscriptEntry = {
|
|
231
|
+
type?: string;
|
|
232
|
+
id?: string;
|
|
233
|
+
parentId?: string | null;
|
|
234
|
+
message?: { role?: string; content?: unknown };
|
|
235
|
+
[key: string]: unknown;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const entries: TranscriptEntry[] = [];
|
|
239
|
+
for (const line of lines) {
|
|
240
|
+
try {
|
|
241
|
+
entries.push(JSON.parse(line));
|
|
242
|
+
} catch {
|
|
243
|
+
entries.push({ _raw: line } as unknown as TranscriptEntry);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Find the last user message entry (searching from the end)
|
|
248
|
+
let lastUserMsgIdx = -1;
|
|
249
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
250
|
+
const e = entries[i];
|
|
251
|
+
if (e.type === "message" && e.message?.role === "user") {
|
|
252
|
+
lastUserMsgIdx = i;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (lastUserMsgIdx < 0) {
|
|
258
|
+
log?.info?.(`[adp-openclaw] No user message found in transcript to trim`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check if the last user message is at or near the end (within last few entries)
|
|
263
|
+
// This ensures we only trim the most recent aborted conversation, not old ones
|
|
264
|
+
if (lastUserMsgIdx < entries.length - 5) {
|
|
265
|
+
log?.info?.(`[adp-openclaw] Last user message is not recent enough to trim (idx=${lastUserMsgIdx}, total=${entries.length})`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Truncate: keep everything before the last user message
|
|
270
|
+
const trimmedLines = lines.slice(0, lastUserMsgIdx);
|
|
271
|
+
const trimmedContent = trimmedLines.join("\n") + "\n";
|
|
272
|
+
|
|
273
|
+
fs.writeFileSync(transcriptPath, trimmedContent, "utf-8");
|
|
274
|
+
|
|
275
|
+
const removedCount = lines.length - lastUserMsgIdx;
|
|
276
|
+
log?.info?.(`[adp-openclaw] Trimmed ${removedCount} aborted entries from transcript (kept ${lastUserMsgIdx} entries)`);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
log?.error?.(`[adp-openclaw] Failed to trim aborted messages from transcript: ${err}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
119
282
|
export async function monitorAdpOpenclaw(params: MonitorParams): Promise<void> {
|
|
120
283
|
const { wsUrl, clientToken, signKey, abortSignal, log, cfg } = params;
|
|
121
284
|
const runtime = getAdpOpenclawRuntime();
|
|
@@ -255,6 +418,7 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
|
|
|
255
418
|
log?.info(`[adp-openclaw] Received: ${inMsg.from}: ${inMsg.text} (conv=${inMsg.conversationId}, rec=${inMsg.recordId || 'none'}, user=${JSON.stringify(inMsg.user || {})})`);
|
|
256
419
|
|
|
257
420
|
// Process the message with full user identity
|
|
421
|
+
const convIdForCleanup = inMsg.conversationId || `fallback-${Date.now()}`;
|
|
258
422
|
try {
|
|
259
423
|
// Build user identity string for From field (like Feishu: "feishu:user_id")
|
|
260
424
|
const userIdentifier = inMsg.user?.userId || inMsg.from;
|
|
@@ -582,6 +746,25 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
|
|
|
582
746
|
const cancelText = lastPartialText ? `${lastPartialText}\n\n[已停止生成]` : "[已停止生成]";
|
|
583
747
|
log?.info(`[adp-openclaw] Generation cancelled, sending outbound_end with partial text`);
|
|
584
748
|
sendOutboundEnd(cancelText);
|
|
749
|
+
|
|
750
|
+
// Mark the session as aborted so the SDK injects an "abort hint"
|
|
751
|
+
// on the next message, preventing the AI from resuming the cancelled task
|
|
752
|
+
await markSessionAborted({
|
|
753
|
+
sessionKey: route.sessionKey,
|
|
754
|
+
runtime,
|
|
755
|
+
cfg,
|
|
756
|
+
log,
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// Remove the aborted user message and partial assistant reply from
|
|
760
|
+
// the JSONL transcript. Without this, the AI sees the old cancelled
|
|
761
|
+
// request in its context and may resume it instead of answering the new one.
|
|
762
|
+
await trimAbortedMessagesFromTranscript({
|
|
763
|
+
sessionKey: route.sessionKey,
|
|
764
|
+
runtime,
|
|
765
|
+
cfg,
|
|
766
|
+
log,
|
|
767
|
+
});
|
|
585
768
|
}
|
|
586
769
|
|
|
587
770
|
// IMPORTANT: After dispatchReplyWithBufferedBlockDispatcher completes,
|
|
@@ -595,7 +778,7 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
|
|
|
595
778
|
}
|
|
596
779
|
} catch (err) {
|
|
597
780
|
// Clean up on error
|
|
598
|
-
activeGenerations.delete(
|
|
781
|
+
activeGenerations.delete(convIdForCleanup);
|
|
599
782
|
log?.error(`[adp-openclaw] Failed to process message: ${err}`);
|
|
600
783
|
}
|
|
601
784
|
break;
|