fix: capture dispatch-stage prompt

This commit is contained in:
2026-04-12 23:29:14 +00:00
parent 72ca823206
commit f2ec5d43de
3 changed files with 18 additions and 13 deletions

View File

@@ -152,6 +152,7 @@ One branch `codex/extractor-eval-loop` for Day 1-5, a second `codex/retrieval-ha
## Session Log
- **2026-04-12 Codex (branch `codex/openclaw-capture-plugin`, polish pass 2)** switched prompt capture from `before_agent_reply.cleanedBody` to `before_dispatch.body` / `content`, because the earlier path still stored Discord wrapper metadata. This should bind capture to the dispatch-stage human message instead of the prompt-builder artifact. Awaiting one more post-restart turn to verify on Dalidou.
- **2026-04-12 Codex (branch `codex/openclaw-capture-plugin`, polish pass)** tightened the OpenClaw capture plugin to use `before_agent_reply.cleanedBody` instead of the raw prompt-build input, which should prevent Discord wrapper metadata from being stored as the interaction prompt. Added `agent_end` cleanup and updated plugin docs. A fresh post-restart user turn is still needed to verify prompt cleanliness on Dalidou.
- **2026-04-12 Codex (branch `codex/openclaw-capture-plugin`)** added a minimal external OpenClaw plugin at `openclaw-plugins/atocore-capture/` that mirrors Claude Code capture semantics: user-triggered assistant turns are POSTed to AtoCore `/interactions` with `client="openclaw"` and `reinforce=true`, fail-open, no extraction in-path. For live verification, temporarily added the local plugin load path to OpenClaw config and restarted the gateway so the plugin can load. Branch truth is ready; end-to-end verification still needs one fresh post-restart OpenClaw user turn to confirm new `client=openclaw` interactions appear on Dalidou.
- **2026-04-12 Claude** Batch 3 (R9 fix): `144dbbd..e5e9a99`. Trust hierarchy for project attribution — interaction scope always wins when set, model project only used for unscoped interactions + registered check. 7 case tests (A-G) cover every combination. Harness 17/18 (no regression). Tests 286->290. Before: wrong registered project could silently override interaction scope. After: interaction.project is the strongest signal; model project is only a fallback for unscoped captures. Not yet guaranteed: nothing prevents the *same* project's model output from being semantically wrong within that project. R9 marked fixed.

View File

@@ -3,7 +3,7 @@
Minimal OpenClaw plugin that mirrors Claude Code's `capture_stop.py` behavior:
- watches user-triggered assistant turns
- uses OpenClaw's cleaned user message (`before_agent_reply.cleanedBody`) so AtoCore stores the real prompt instead of the full inbound wrapper
- uses OpenClaw's dispatch-stage message body (`before_dispatch.body`) so AtoCore stores the real prompt instead of the full inbound wrapper
- POSTs `prompt` + `response` to `POST /interactions`
- sets `client="openclaw"`
- sets `reinforce=true`
@@ -26,6 +26,6 @@ If `baseUrl` is omitted, the plugin uses `ATOCORE_BASE_URL` or defaults to `http
## Notes
- 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 finalized cleaned message body instead of the raw prompt-build input.
- Prompt cleaning is done inside the plugin by reading OpenClaw's dispatch-stage message body instead of the raw prompt-build input.
- 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.

View File

@@ -46,26 +46,28 @@ export default definePluginEntry({
const logger = api.logger;
const pendingBySession = new Map();
api.on("before_agent_reply", async (event, ctx) => {
if (ctx?.trigger && ctx.trigger !== "user") return;
api.on("before_dispatch", async (event, ctx) => {
const config = api.getConfig?.() || {};
const minPromptLength = Number(config.minPromptLength || DEFAULT_MIN_PROMPT_LENGTH);
const prompt = trimText(event?.cleanedBody || "");
const prompt = trimText(event?.body || event?.content || "");
const key = ctx?.sessionKey || event?.sessionKey;
if (!key) return;
if (!shouldCapturePrompt(prompt, minPromptLength)) {
pendingBySession.delete(ctx.sessionId);
pendingBySession.delete(key);
return;
}
pendingBySession.set(ctx.sessionId, {
pendingBySession.set(key, {
prompt,
sessionId: ctx.sessionId,
sessionKey: ctx.sessionKey || "",
sessionId: key,
sessionKey: key,
project: ""
});
});
api.on("llm_output", async (event, ctx) => {
if (ctx?.trigger && ctx.trigger !== "user") return;
const pending = pendingBySession.get(ctx.sessionId);
const key = ctx.sessionKey || ctx.sessionId;
const pending = pendingBySession.get(key);
if (!pending) return;
const assistantTexts = Array.isArray(event?.assistantTexts) ? event.assistantTexts : [];
@@ -87,15 +89,17 @@ export default definePluginEntry({
};
await postInteraction(baseUrl, payload, logger);
pendingBySession.delete(ctx.sessionId);
pendingBySession.delete(key);
});
api.on("agent_end", async (event) => {
if (event?.sessionId) pendingBySession.delete(event.sessionId);
const key = event?.sessionKey || event?.sessionId;
if (key) pendingBySession.delete(key);
});
api.on("session_end", async (event) => {
if (event?.sessionId) pendingBySession.delete(event.sessionId);
const key = event?.sessionKey || event?.sessionId;
if (key) pendingBySession.delete(key);
});
}
});