OpenClaw emits its runtime-context block as a separate custom_message; the
openai-completions adapter folds that into the request as an extra role=user
message after the real user input. extractLatestUserMessage was taking the
last user message unconditionally, so Claude received only the metadata
envelope and replied "your message came through empty".
Walk user messages backward, skip ones starting with the runtime-context
marker, and return the most recent real user message instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bridge was keying claudeSessionId by agentId alone, so every Discord
channel, DM, and cron run for a single agent shared one Claude CLI
session. Two consequences in the wild:
- Cross-channel context bleed: 8.7MB session for `developer` mixed
references from channels 1474327736242798612 and 1498579994044010566
plus the operator DM all in one --resume thread.
- `/new` had no effect on the CLI side. OpenClaw rotated its session
file but the bridge kept --resume-ing the same long-lived
claudeSessionId, eventually crossing the 1M model context (debug log
showed `prompt is too long: 1179616 tokens > 1000000 maximum`).
Changes:
* input-filter: extract `chat_id` from the Conversation-info
untrusted-metadata block (scanning all messages, since runtimeOnly
turns put it in the system prompt) and detect bare `/new`/`/reset`
via the BARE_SESSION_RESET_PROMPT_BASE marker. Add buildSessionKey
`${agentId}::${chatId}` and resolveDispatchPrompt fallback for the
empty user message that OpenClaw sends on bare resets.
* server: use the composite session key for getSession/putSession;
on bareSessionReset, removeSession before dispatching so the CLI
starts a fresh session; on a CLI result_error (typically
prompt_too_long) drop the entry too so the next turn doesn't
re-resume into the poisoned context.
* claude/sdk-adapter: surface CLI terminal errors via a new
`result_error` event (carries reason + sessionId) so the bridge
can react instead of just streaming the synthetic
"Prompt is too long" assistant text and silently re-using the
same session.
* index: convert register() to synchronous (OpenClaw rejects async
register with "plugin register must be synchronous"); replace the
pre-bind port probe with a server-level EADDRINUSE handler.
* .gitignore: ignore node_modules/ and dist/.
Previously dispatchToClaude awaited child.on(close) before yielding the done
event. Claude CLIs Bash tool occasionally leaves ssh/bash grandchildren alive
(e.g. a GUI app that ignores SIGPIPE on the remote end of a piped ssh command);
that kept claude -p alive past end-of-turn, which kept the bridge SSE stream
open, which kept OpenClaw from committing the turn to its session jsonl.
Switch to emitting done as soon as the terminal result stream-json event
arrives. Spawn claude in its own process group (detached:true) and schedule
a best-effort SIGTERM/SIGKILL sweep of leaked descendants; temp-file cleanup
runs asynchronously on actual process close.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Replace --dangerously-skip-permissions with --allowedTools whitelist
to support running Claude Code as root (root blocks the former flag)
- Fix /mcp/execute tool lookup for plugins that register tools via
factory functions (e.g. padded-cell pcexec) where the global registry
names array is empty — now falls back to instantiating factories and
matching by returned tool name
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add .gitignore covering node_modules, .idea, credential files
- Untrack docs/claude/LESSONS_LEARNED.md and OPENCLAW_PLUGIN_DEV.md
(internal dev notes, not part of the plugin repo)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migrate src/ → plugin/ (plugin/core/, plugin/web/, plugin/commands/)
and src/mcp/ → services/ per OpenClaw plugin dev spec
- Add Gemini CLI backend (plugin/core/gemini/sdk-adapter.ts) with GEMINI.md
system-prompt injection
- Inject bootstrap as stateless system prompt on every turn instead of
first turn only: Claude via --system-prompt, Gemini via workspace/GEMINI.md;
eliminates isFirstTurn branch, keeps skills in sync with OpenClaw snapshots
- Fix session-map-store defensive parsing (sessions ?? []) to handle bare {}
reset files without crashing on .find()
- Add docs/TEST_FLOW.md with E2E test scenarios and expected outcomes
- Add docs/claude/BRIDGE_MODEL_FINDINGS.md with contractor-probe results
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude Code sees tools as mcp__openclaw__<name> but skills/instructions
reference the original OpenClaw name (e.g. "memory_search"). Each tool
description now includes a disambiguation line:
[openclaw tool: <name>] If any skill or instruction refers to "<name>",
this is the tool to call.
This ensures Claude can correctly map skill references to the right tool
even when the MCP prefix makes the name appear different.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Claude Code calls an MCP tool via /mcp/execute, the bridge now:
1. Looks up the tool in OpenClaw's global plugin registry (getGlobalPluginRegistry)
2. Calls the tool's factory with proper context (config from globalThis, canonical
session key "agent:<agentId>:direct:bridge" for memory agentId resolution)
3. Executes the real tool implementation and returns the AgentToolResult text
This replaces the manual memory_search/memory_get implementations in memory-handlers.ts
with a generic relay that works for any registered OpenClaw tool. The agentId is now
propagated from dispatchToClaude through the MCP server env (AGENT_ID) to /mcp/execute.
The OpenClaw config is stored in globalThis._contractorOpenClawConfig during plugin
registration (index.ts api.config) since getRuntimeConfigSnapshot() uses module-level
state not shared across bundle boundaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Key fixes:
- Pass --mcp-config on every turn (not just first): MCP server exits with each
claude -p process, so --resume also needs a fresh MCP server
- Pass openclawTools on every turn for the same reason
- Add WORKSPACE env var to MCP server so execute requests include workspace path
- Implement memory_search (keyword search over workspace/memory/*.md + MEMORY.md)
- Implement memory_get (line-range read from workspace memory files)
- Both are workspace-aware, resolved per-request via request body workspace field
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- input-filter: scan workspace for SOUL.md, IDENTITY.md, MEMORY.md etc. on each turn
- bootstrap: when these files exist, instruct Claude to Read them at session start
- This gives the contractor agent its OpenClaw persona (SOUL.md embody, IDENTITY.md fill)
- Memory note added to bootstrap when workspace has memory files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Complete the MCP tool call chain:
- contractor-agent bridge exposes /mcp/execute endpoint for tool callbacks
- openclaw-mcp-server.mjs proxies OpenClaw tool defs to Claude as MCP tools
- sdk-adapter passes --mcp-config on first turn with all OpenClaw tools
- tool-test plugin registers contractor_echo in globalThis tool handler map
- agent-config-writer auto-sets tools.profile=full so OpenClaw sends tool defs
- Fix --mcp-config arg ordering: prompt must come before <configs...> flag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>