adp-openclaw 0.0.56 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/monitor.ts +125 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adp-openclaw",
3
- "version": "0.0.56",
3
+ "version": "0.0.57",
4
4
  "description": "ADP-OpenClaw demo channel plugin (Go WebSocket backend)",
5
5
  "type": "module",
6
6
  "dependencies": {
package/src/monitor.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  } from "./tool-result-message-blocks.js";
25
25
  import crypto from "crypto";
26
26
  import fs from "fs";
27
+ import path from "path";
27
28
  // @ts-ignore - import JSON file
28
29
  import packageJson from "../package.json" with { type: "json" };
29
30
 
@@ -164,6 +165,120 @@ async function markSessionAborted(params: {
164
165
  }
165
166
  }
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
+
167
282
  export async function monitorAdpOpenclaw(params: MonitorParams): Promise<void> {
168
283
  const { wsUrl, clientToken, signKey, abortSignal, log, cfg } = params;
169
284
  const runtime = getAdpOpenclawRuntime();
@@ -640,6 +755,16 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
640
755
  cfg,
641
756
  log,
642
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
+ });
643
768
  }
644
769
 
645
770
  // IMPORTANT: After dispatchReplyWithBufferedBlockDispatcher completes,