From a6ae6166a4a31991f6083898d8c2e1bcc59580b9 Mon Sep 17 00:00:00 2001 From: Anto01 Date: Sun, 12 Apr 2026 22:06:07 +0000 Subject: [PATCH] feat: add OpenClaw AtoCore capture plugin --- DEV-LEDGER.md | 5 +- openclaw-plugins/atocore-capture/README.md | 29 ++++++ openclaw-plugins/atocore-capture/index.js | 94 +++++++++++++++++++ .../atocore-capture/openclaw.plugin.json | 29 ++++++ openclaw-plugins/atocore-capture/package.json | 7 ++ 5 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 openclaw-plugins/atocore-capture/README.md create mode 100644 openclaw-plugins/atocore-capture/index.js create mode 100644 openclaw-plugins/atocore-capture/openclaw.plugin.json create mode 100644 openclaw-plugins/atocore-capture/package.json diff --git a/DEV-LEDGER.md b/DEV-LEDGER.md index 3fd509b..3289be3 100644 --- a/DEV-LEDGER.md +++ b/DEV-LEDGER.md @@ -7,8 +7,8 @@ ## Orientation - **live_sha** (Dalidou `/health` build_sha): `8951c62` (R9 fix at e5e9a99 not yet deployed) -- **last_updated**: 2026-04-12 by Claude (Batch 3 R9 fix) -- **main_tip**: `e5e9a99` +- **last_updated**: 2026-04-12 by Codex (branch `codex/openclaw-capture-plugin`) +- **main_tip**: `4f8bec7` - **test_count**: 290 passing (local dev shell) - **harness**: `17/18 PASS` (only p06-tailscale still failing) - **active_memories**: 41 @@ -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`)** 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. - **2026-04-12 Codex (audit branch `codex/audit-batch2`)** audited `69c9717..origin/main` against the current branch tip and live Dalidou. Verified: live build is `8951c62`, retrieval harness improved to **17/18 PASS**, candidate queue is now empty, active memories rose to **41**, and `python3 scripts/auto_triage.py --dry-run --base-url http://127.0.0.1:8100` runs cleanly on Dalidou but only exercised the empty-queue path. Updated R7 to **fixed** (`8951c62`) and R8 to **fixed** (`69c9717`). Kept R9 **open** because project trust-preservation still allows a wrong non-empty registered project from the model to override the interaction scope. Added R13 because the new `286 passing` claim could not be independently reproduced in this audit: `pytest` is absent on both Dalidou and the clean audit worktree. Also corrected stale Orientation fields (live SHA, main tip, harness, active/candidate memory counts). diff --git a/openclaw-plugins/atocore-capture/README.md b/openclaw-plugins/atocore-capture/README.md new file mode 100644 index 0000000..f24c187 --- /dev/null +++ b/openclaw-plugins/atocore-capture/README.md @@ -0,0 +1,29 @@ +# AtoCore Capture Plugin for OpenClaw + +Minimal OpenClaw plugin that mirrors Claude Code's `capture_stop.py` behavior: + +- watches user-triggered assistant turns +- POSTs `prompt` + `response` to `POST /interactions` +- sets `client="openclaw"` +- sets `reinforce=true` +- fails open on network or API errors + +## Config + +Optional plugin config: + +```json +{ + "baseUrl": "http://dalidou:8100", + "minPromptLength": 15, + "maxResponseLength": 50000 +} +``` + +If `baseUrl` is omitted, the plugin uses `ATOCORE_BASE_URL` or defaults to `http://dalidou:8100`. + +## Notes + +- Project detection is intentionally left empty for now. Unscoped capture is acceptable because AtoCore's extraction pipeline handles unscoped interactions. +- 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. diff --git a/openclaw-plugins/atocore-capture/index.js b/openclaw-plugins/atocore-capture/index.js new file mode 100644 index 0000000..dd27258 --- /dev/null +++ b/openclaw-plugins/atocore-capture/index.js @@ -0,0 +1,94 @@ +import { definePluginEntry } from "openclaw/plugin-sdk/core"; + +const DEFAULT_BASE_URL = process.env.ATOCORE_BASE_URL || "http://dalidou:8100"; +const DEFAULT_MIN_PROMPT_LENGTH = 15; +const DEFAULT_MAX_RESPONSE_LENGTH = 50_000; + +function trimText(value) { + return typeof value === "string" ? value.trim() : ""; +} + +function truncateResponse(text, maxLength) { + if (!text || text.length <= maxLength) return text; + return `${text.slice(0, maxLength)}\n\n[truncated]`; +} + +function shouldCapturePrompt(prompt, minLength) { + const text = trimText(prompt); + if (!text) return false; + if (text.startsWith("<")) return false; + return text.length >= minLength; +} + +async function postInteraction(baseUrl, payload, logger) { + try { + const res = await fetch(`${baseUrl.replace(/\/$/, "")}/interactions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(10_000) + }); + if (!res.ok) { + logger?.debug?.("atocore_capture_post_failed", { status: res.status }); + return false; + } + return true; + } catch (error) { + logger?.debug?.("atocore_capture_post_error", { + error: error instanceof Error ? error.message : String(error) + }); + return false; + } +} + +export default definePluginEntry({ + register(api) { + const logger = api.logger; + const pendingBySession = new Map(); + + api.on("before_agent_start", async (event, ctx) => { + if (ctx?.trigger && ctx.trigger !== "user") return; + const config = api.getConfig?.() || {}; + const minPromptLength = Number(config.minPromptLength || DEFAULT_MIN_PROMPT_LENGTH); + const prompt = trimText(event?.prompt || ""); + if (!shouldCapturePrompt(prompt, minPromptLength)) { + pendingBySession.delete(ctx.sessionId); + return; + } + pendingBySession.set(ctx.sessionId, { + prompt, + sessionId: ctx.sessionId, + sessionKey: ctx.sessionKey || "", + project: "" + }); + }); + + api.on("llm_output", async (event, ctx) => { + if (ctx?.trigger && ctx.trigger !== "user") return; + const pending = pendingBySession.get(ctx.sessionId); + if (!pending) return; + + const assistantTexts = Array.isArray(event?.assistantTexts) ? event.assistantTexts : []; + const response = truncateResponse(trimText(assistantTexts.join("\n\n")), Number((api.getConfig?.() || {}).maxResponseLength || DEFAULT_MAX_RESPONSE_LENGTH)); + if (!response) return; + + const config = api.getConfig?.() || {}; + const baseUrl = trimText(config.baseUrl) || DEFAULT_BASE_URL; + const payload = { + prompt: pending.prompt, + response, + client: "openclaw", + session_id: pending.sessionKey || pending.sessionId, + project: pending.project || "", + reinforce: true + }; + + await postInteraction(baseUrl, payload, logger); + pendingBySession.delete(ctx.sessionId); + }); + + api.on("session_end", async (event) => { + if (event?.sessionId) pendingBySession.delete(event.sessionId); + }); + } +}); diff --git a/openclaw-plugins/atocore-capture/openclaw.plugin.json b/openclaw-plugins/atocore-capture/openclaw.plugin.json new file mode 100644 index 0000000..4ca06cd --- /dev/null +++ b/openclaw-plugins/atocore-capture/openclaw.plugin.json @@ -0,0 +1,29 @@ +{ + "id": "atocore-capture", + "name": "AtoCore Capture", + "description": "Captures completed OpenClaw assistant turns to AtoCore interactions for reinforcement.", + "configSchema": { + "type": "object", + "properties": { + "baseUrl": { + "type": "string", + "description": "Override AtoCore base URL. Defaults to ATOCORE_BASE_URL or http://dalidou:8100" + }, + "minPromptLength": { + "type": "integer", + "minimum": 1, + "description": "Minimum user prompt length required before capture" + }, + "maxResponseLength": { + "type": "integer", + "minimum": 100, + "description": "Maximum assistant response length to store" + } + }, + "additionalProperties": false + }, + "uiHints": { + "category": "automation", + "displayName": "AtoCore Capture" + } +} diff --git a/openclaw-plugins/atocore-capture/package.json b/openclaw-plugins/atocore-capture/package.json new file mode 100644 index 0000000..0452fff --- /dev/null +++ b/openclaw-plugins/atocore-capture/package.json @@ -0,0 +1,7 @@ +{ + "name": "@atomaste/atocore-openclaw-capture", + "private": true, + "version": "0.0.0", + "type": "module", + "description": "OpenClaw plugin that captures assistant turns to AtoCore interactions" +}