fix: pair capture on message sending
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
Minimal OpenClaw plugin that mirrors Claude Code's `capture_stop.py` behavior:
|
||||
|
||||
- watches user-triggered assistant turns
|
||||
- uses OpenClaw's dispatch-stage message body (`before_dispatch.body`) so AtoCore stores the real prompt instead of the full inbound wrapper
|
||||
- uses OpenClaw's dispatch-stage message body (`before_dispatch.body`) for the human prompt, then pairs it with the actual outbound assistant message on `message_sending`
|
||||
- POSTs `prompt` + `response` to `POST /interactions`
|
||||
- sets `client="openclaw"`
|
||||
- sets `reinforce=true`
|
||||
@@ -27,5 +27,6 @@ If `baseUrl` is omitted, the plugin uses `ATOCORE_BASE_URL` or defaults to `http
|
||||
|
||||
- Project detection is intentionally left empty for now. Unscoped capture is acceptable because AtoCore's extraction pipeline handles unscoped interactions.
|
||||
- Prompt cleaning is done inside the plugin by reading OpenClaw's dispatch-stage message body instead of the raw prompt-build input.
|
||||
- Turn pairing is done by caching the prompt on dispatch and posting only when OpenClaw emits the outbound assistant message, which is more reliable than pairing against raw model output events.
|
||||
- Extraction is **not** part of the capture path. This plugin only records interactions and lets AtoCore reinforcement run automatically.
|
||||
- The plugin captures only user-triggered turns, not heartbeats or system-only runs.
|
||||
|
||||
@@ -20,6 +20,30 @@ function shouldCapturePrompt(prompt, minLength) {
|
||||
return text.length >= minLength;
|
||||
}
|
||||
|
||||
function buildKeys(...values) {
|
||||
return [...new Set(values.map((v) => trimText(v)).filter(Boolean))];
|
||||
}
|
||||
|
||||
function rememberPending(store, keys, payload) {
|
||||
for (const key of keys) store.set(key, payload);
|
||||
}
|
||||
|
||||
function takePending(store, keys) {
|
||||
for (const key of keys) {
|
||||
const value = store.get(key);
|
||||
if (value) {
|
||||
for (const k of keys) store.delete(k);
|
||||
store.delete(key);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function clearPending(store, keys) {
|
||||
for (const key of keys) store.delete(key);
|
||||
}
|
||||
|
||||
async function postInteraction(baseUrl, payload, logger) {
|
||||
try {
|
||||
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/interactions`, {
|
||||
@@ -50,29 +74,28 @@ export default definePluginEntry({
|
||||
const config = api.getConfig?.() || {};
|
||||
const minPromptLength = Number(config.minPromptLength || DEFAULT_MIN_PROMPT_LENGTH);
|
||||
const prompt = trimText(event?.body || event?.content || "");
|
||||
const key = ctx?.sessionKey || event?.sessionKey;
|
||||
if (!key) return;
|
||||
const keys = buildKeys(ctx?.sessionKey, ctx?.sessionId, event?.sessionKey, event?.sessionId, ctx?.conversationId, event?.conversationId);
|
||||
if (!keys.length) return;
|
||||
if (!shouldCapturePrompt(prompt, minPromptLength)) {
|
||||
pendingBySession.delete(key);
|
||||
clearPending(pendingBySession, keys);
|
||||
return;
|
||||
}
|
||||
pendingBySession.set(key, {
|
||||
rememberPending(pendingBySession, keys, {
|
||||
prompt,
|
||||
sessionId: key,
|
||||
sessionKey: key,
|
||||
sessionId: trimText(ctx?.sessionId || event?.sessionId || ""),
|
||||
sessionKey: trimText(ctx?.sessionKey || event?.sessionKey || ""),
|
||||
conversationId: trimText(ctx?.conversationId || event?.conversationId || ""),
|
||||
project: ""
|
||||
});
|
||||
});
|
||||
|
||||
api.on("llm_output", async (event, ctx) => {
|
||||
if (ctx?.trigger && ctx.trigger !== "user") return;
|
||||
const key = ctx.sessionKey || ctx.sessionId;
|
||||
const pending = pendingBySession.get(key);
|
||||
api.on("message_sending", async (event, ctx) => {
|
||||
const keys = buildKeys(ctx?.sessionKey, ctx?.sessionId, ctx?.conversationId);
|
||||
const pending = takePending(pendingBySession, keys);
|
||||
if (!pending) return;
|
||||
|
||||
const assistantTexts = Array.isArray(event?.assistantTexts) ? event.assistantTexts : [];
|
||||
const response = truncateResponse(
|
||||
trimText(assistantTexts.join("\n\n")),
|
||||
trimText(event?.content || ""),
|
||||
Number((api.getConfig?.() || {}).maxResponseLength || DEFAULT_MAX_RESPONSE_LENGTH)
|
||||
);
|
||||
if (!response) return;
|
||||
@@ -83,23 +106,20 @@ export default definePluginEntry({
|
||||
prompt: pending.prompt,
|
||||
response,
|
||||
client: "openclaw",
|
||||
session_id: pending.sessionKey || pending.sessionId,
|
||||
session_id: pending.sessionKey || pending.sessionId || pending.conversationId,
|
||||
project: pending.project || "",
|
||||
reinforce: true
|
||||
};
|
||||
|
||||
await postInteraction(baseUrl, payload, logger);
|
||||
pendingBySession.delete(key);
|
||||
});
|
||||
|
||||
api.on("agent_end", async (event) => {
|
||||
const key = event?.sessionKey || event?.sessionId;
|
||||
if (key) pendingBySession.delete(key);
|
||||
clearPending(pendingBySession, buildKeys(event?.sessionKey, event?.sessionId));
|
||||
});
|
||||
|
||||
api.on("session_end", async (event) => {
|
||||
const key = event?.sessionKey || event?.sessionId;
|
||||
if (key) pendingBySession.delete(key);
|
||||
clearPending(pendingBySession, buildKeys(event?.sessionKey, event?.sessionId));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user