Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,624 @@
---
title: "Codex Harness Context Engine Port"
summary: "Specification for making the bundled Codex app-server harness honor OpenClaw context-engine plugins"
read_when:
- You are wiring context-engine lifecycle behavior into the Codex harness
- You need lossless-claw or another context-engine plugin to work with codex/* embedded harness sessions
- You are comparing embedded OpenClaw and Codex app-server context behavior
---
## Status
Draft implementation specification.
## Goal
Make the bundled Codex app-server harness honor the same OpenClaw context-engine
lifecycle contract that embedded OpenClaw turns already honor.
A session using provider/model `agentRuntime.id: "codex"` or a `codex/*` model
should still let the selected context-engine plugin, such as
`lossless-claw`, control context assembly, post-turn ingest, maintenance, and
OpenClaw-level compaction policy as far as the Codex app-server boundary allows.
## Non-goals
- Do not reimplement Codex app-server internals.
- Do not make Codex native thread compaction produce a lossless-claw summary.
- Do not require non-Codex models to use the Codex harness.
- Do not change ACP/acpx session behavior. This specification is for the
non-ACP embedded agent harness path only.
- Do not make third-party plugins register Codex app-server extension factories;
the existing bundled-plugin trust boundary remains unchanged.
## Current architecture
The embedded run loop resolves the configured context engine once per run before
selecting a concrete low-level harness:
- `src/agents/embedded-agent-runner/run.ts`
- initializes context-engine plugins
- calls `resolveContextEngine(params.config)`
- passes `contextEngine` and `contextTokenBudget` into
`runEmbeddedAttemptWithBackend(...)`
`runEmbeddedAttemptWithBackend(...)` delegates to the selected agent harness:
- `src/agents/embedded-agent-runner/run/backend.ts`
- `src/agents/harness/selection.ts`
The Codex app-server harness is registered by the bundled Codex plugin:
- `extensions/codex/index.ts`
- `extensions/codex/harness.ts`
The Codex harness implementation receives the same `EmbeddedRunAttemptParams`
as built-in OpenClaw attempts:
- `extensions/codex/src/app-server/run-attempt.ts`
That means the required hook point is in OpenClaw-controlled code. The external
boundary is the Codex app-server protocol itself: OpenClaw can control what it
sends to `thread/start`, `thread/resume`, and `turn/start`, and can observe
notifications, but it cannot change Codex's internal thread store or native
compactor.
## Current gap
Built-in OpenClaw attempts call the context-engine lifecycle directly:
- bootstrap/maintenance before the attempt
- assemble before the model call
- afterTurn or ingest after the attempt
- maintenance after a successful turn
- context-engine compaction for engines that own compaction
Relevant OpenClaw code:
- `src/agents/embedded-agent-runner/run/attempt.ts`
- `src/agents/embedded-agent-runner/run/attempt.context-engine-helpers.ts`
- `src/agents/embedded-agent-runner/context-engine-maintenance.ts`
Codex app-server attempts currently run generic agent-harness hooks and mirror
the transcript, but do not call `params.contextEngine.bootstrap`,
`params.contextEngine.assemble`, `params.contextEngine.afterTurn`,
`params.contextEngine.ingestBatch`, `params.contextEngine.ingest`, or
`params.contextEngine.maintain`.
Relevant Codex code:
- `extensions/codex/src/app-server/run-attempt.ts`
- `extensions/codex/src/app-server/thread-lifecycle.ts`
- `extensions/codex/src/app-server/event-projector.ts`
- `extensions/codex/src/app-server/compact.ts`
## Desired behavior
For Codex harness turns, OpenClaw should preserve this lifecycle:
1. Read the mirrored OpenClaw session transcript.
2. Bootstrap the active context engine when a previous session file exists.
3. Run bootstrap maintenance when available.
4. Assemble context using the active context engine.
5. Convert the assembled context into Codex-compatible inputs.
6. Start or resume the Codex thread with developer instructions that include any
context-engine `systemPromptAddition`.
7. Start the Codex turn with the assembled user-facing prompt.
8. Mirror the Codex result back into the OpenClaw transcript.
9. Call `afterTurn` if implemented, otherwise `ingestBatch`/`ingest`, using the
mirrored transcript snapshot.
10. Run turn maintenance after successful non-aborted turns.
11. Preserve Codex native compaction signals and OpenClaw compaction hooks.
## Design constraints
### Codex app-server remains canonical for native thread state
Codex owns its native thread and any internal extended history. OpenClaw should
not try to mutate the app-server's internal history except through supported
protocol calls.
OpenClaw's transcript mirror remains the source for OpenClaw features:
- chat history
- search
- `/new` and `/reset` bookkeeping
- future model or harness switching
- context-engine plugin state
### Context engine assembly must be projected into Codex inputs
The context-engine interface returns OpenClaw `AgentMessage[]`, not a Codex
thread patch. Codex app-server `turn/start` accepts a current user input, while
`thread/start` and `thread/resume` accept developer instructions.
Therefore the implementation needs a projection layer. The safe first version
should avoid pretending it can replace Codex internal history. It should inject
assembled context as deterministic prompt/developer-instruction material around
the current turn.
### Prompt-cache stability matters
For engines like lossless-claw, the assembled context should be deterministic
for unchanged inputs. Do not add timestamps, random ids, or nondeterministic
ordering to generated context text.
### Runtime selection semantics do not change
Harness selection remains as-is:
- `runtime: "openclaw"` selects the built-in OpenClaw harness
- `runtime: "codex"` selects the registered Codex harness
- `runtime: "auto"` lets plugin harnesses claim supported providers
- unmatched `auto` runs use the built-in OpenClaw harness
This work changes what happens after the Codex harness is selected.
## Implementation plan
### 1. Export or relocate reusable context-engine attempt helpers
Today the reusable lifecycle helpers live under the embedded agent runner:
- `src/agents/embedded-agent-runner/run/attempt.context-engine-helpers.ts`
- `src/agents/embedded-agent-runner/run/attempt.prompt-helpers.ts`
- `src/agents/embedded-agent-runner/context-engine-maintenance.ts`
Codex should import harness-neutral helpers rather than reaching into runner
implementation details.
Create a harness-neutral module, for example:
- `src/agents/harness/context-engine-lifecycle.ts`
Move or re-export:
- `runAttemptContextEngineBootstrap`
- `assembleAttemptContextEngine`
- `finalizeAttemptContextEngineTurn`
- `buildAfterTurnRuntimeContext`
- `buildAfterTurnRuntimeContextFromUsage`
- a small wrapper around `runContextEngineMaintenance`
Update built-in harness call sites in the same PR.
The neutral helper names should not mention the built-in harness.
Suggested names:
- `bootstrapHarnessContextEngine`
- `assembleHarnessContextEngine`
- `finalizeHarnessContextEngineTurn`
- `buildHarnessContextEngineRuntimeContext`
- `runHarnessContextEngineMaintenance`
### 2. Add a Codex context projection helper
Add a new module:
- `extensions/codex/src/app-server/context-engine-projection.ts`
Responsibilities:
- Accept the assembled `AgentMessage[]`, original mirrored history, and current
prompt.
- Determine which context belongs in developer instructions vs current user
input.
- Preserve the current user prompt as the final actionable request.
- Render prior messages in a stable, explicit format.
- Avoid volatile metadata.
Proposed API:
```ts
export type CodexContextProjection = {
developerInstructionAddition?: string;
promptText: string;
assembledMessages: AgentMessage[];
prePromptMessageCount: number;
};
export function projectContextEngineAssemblyForCodex(params: {
assembledMessages: AgentMessage[];
originalHistoryMessages: AgentMessage[];
prompt: string;
systemPromptAddition?: string;
}): CodexContextProjection;
```
Recommended first projection:
- Put `systemPromptAddition` into developer instructions.
- Put the assembled transcript context before the current prompt in `promptText`.
- Label it clearly as OpenClaw assembled context.
- Keep current prompt last.
- Exclude duplicate current user prompt if it already appears at the tail.
Example prompt shape:
```text
OpenClaw assembled context for this turn:
<conversation_context>
[user]
...
[assistant]
...
</conversation_context>
Current user request:
...
```
This is less elegant than native Codex history surgery, but it is implementable
inside OpenClaw and preserves context-engine semantics.
Future improvement: if Codex app-server exposes a protocol for replacing or
supplementing thread history, swap this projection layer to use that API.
### 3. Wire bootstrap before Codex thread startup
In `extensions/codex/src/app-server/run-attempt.ts`:
- Read mirrored session history as today.
- Determine whether the session file existed before this run. Prefer a helper
that checks `fs.stat(params.sessionFile)` before mirroring writes.
- Open a `SessionManager` or use a narrow session manager adapter if the helper
requires it.
- Call the neutral bootstrap helper when `params.contextEngine` exists.
Pseudo-flow:
```ts
const hadSessionFile = await fileExists(params.sessionFile);
const sessionManager = SessionManager.open(params.sessionFile);
const historyMessages = sessionManager.buildSessionContext().messages;
await bootstrapHarnessContextEngine({
hadSessionFile,
contextEngine: params.contextEngine,
sessionId: params.sessionId,
sessionKey: sandboxSessionKey,
sessionFile: params.sessionFile,
sessionManager,
runtimeContext: buildHarnessContextEngineRuntimeContext(...),
runMaintenance: runHarnessContextEngineMaintenance,
warn,
});
```
Use the same `sessionKey` convention as the Codex tool bridge and transcript
mirror. Today Codex computes `sandboxSessionKey` from `params.sessionKey` or
`params.sessionId`; use that consistently unless there is a reason to preserve
raw `params.sessionKey`.
### 4. Wire assemble before `thread/start` / `thread/resume` and `turn/start`
In `runCodexAppServerAttempt`:
1. Build dynamic tools first, so the context engine sees the actual available
tool names.
2. Read mirrored session history.
3. Run context-engine `assemble(...)` when `params.contextEngine` exists.
4. Project the assembled result into:
- developer instruction addition
- prompt text for `turn/start`
The existing hook call:
```ts
resolveAgentHarnessBeforePromptBuildResult({
prompt: params.prompt,
developerInstructions: buildDeveloperInstructions(params),
messages: historyMessages,
ctx: hookContext,
});
```
should become context-aware:
1. compute base developer instructions with `buildDeveloperInstructions(params)`
2. apply context-engine assembly/projection
3. run `before_prompt_build` with the projected prompt/developer instructions
This order lets generic prompt hooks see the same prompt Codex will receive. If
we need strict OpenClaw parity, run context-engine assembly before hook
composition, because the built-in harness applies context-engine
`systemPromptAddition` to the final system prompt after its prompt pipeline. The
important invariant is that both context engine and hooks get a deterministic,
documented order.
Recommended order for first implementation:
1. `buildDeveloperInstructions(params)`
2. context-engine `assemble()`
3. append/prepend `systemPromptAddition` to developer instructions
4. project assembled messages into prompt text
5. `resolveAgentHarnessBeforePromptBuildResult(...)`
6. pass final developer instructions to `startOrResumeThread(...)`
7. pass final prompt text to `buildTurnStartParams(...)`
The spec should be encoded in tests so future changes do not reorder it by
accident.
### 5. Preserve prompt-cache stable formatting
The projection helper must produce byte-stable output for identical inputs:
- stable message order
- stable role labels
- no generated timestamps
- no object key order leakage
- no random delimiters
- no per-run ids
Use fixed delimiters and explicit sections.
### 6. Wire post-turn after transcript mirroring
Codex's `CodexAppServerEventProjector` builds a local `messagesSnapshot` for the
current turn. `mirrorTranscriptBestEffort(...)` writes that snapshot into the
OpenClaw transcript mirror.
After mirroring succeeds or fails, call the context-engine finalizer with the
best available message snapshot:
- Prefer full mirrored session context after the write, because `afterTurn`
expects the session snapshot, not only the current turn.
- Fall back to `historyMessages + result.messagesSnapshot` if the session file
cannot be reopened.
Pseudo-flow:
```ts
const prePromptMessageCount = historyMessages.length;
await mirrorTranscriptBestEffort(...);
const finalMessages = readMirroredSessionHistoryMessages(params.sessionFile)
?? [...historyMessages, ...result.messagesSnapshot];
await finalizeHarnessContextEngineTurn({
contextEngine: params.contextEngine,
promptError: Boolean(finalPromptError),
aborted: finalAborted,
yieldAborted,
sessionIdUsed: params.sessionId,
sessionKey: sandboxSessionKey,
sessionFile: params.sessionFile,
messagesSnapshot: finalMessages,
prePromptMessageCount,
tokenBudget: params.contextTokenBudget,
runtimeContext: buildHarnessContextEngineRuntimeContextFromUsage({
attempt: params,
workspaceDir: effectiveWorkspace,
agentDir,
tokenBudget: params.contextTokenBudget,
lastCallUsage: result.attemptUsage,
promptCache: result.promptCache,
}),
runMaintenance: runHarnessContextEngineMaintenance,
sessionManager,
warn,
});
```
If mirroring fails, still call `afterTurn` with the fallback snapshot, but log
that the context engine is ingesting from fallback turn data.
### 7. Normalize usage and prompt-cache runtime context
Codex results include normalized usage from app-server token notifications when
available. Pass that usage into the context-engine runtime context.
If Codex app-server eventually exposes cache read/write details, map them into
`ContextEnginePromptCacheInfo`. Until then, omit `promptCache` rather than
inventing zeros.
### 8. Compaction policy
There are two compaction systems:
1. OpenClaw context-engine `compact()`
2. Codex app-server native `thread/compact/start`
Do not silently conflate them.
#### `/compact` and explicit OpenClaw compaction
When the selected context engine has `info.ownsCompaction === true`, explicit
OpenClaw compaction should prefer the context engine's `compact()` result for
the OpenClaw transcript mirror and plugin state.
When the selected Codex harness has a native thread binding, we may additionally
request Codex native compaction to keep the app-server thread healthy, but this
must be reported as a separate backend action in details.
Recommended behavior:
- If `contextEngine.info.ownsCompaction === true`:
- call context-engine `compact()` first
- then best-effort call Codex native compaction when a thread binding exists
- return the context-engine result as the primary result
- include Codex native compaction status in `details.codexNativeCompaction`
- If the active context engine does not own compaction:
- preserve current Codex native compaction behavior
This likely requires changing `extensions/codex/src/app-server/compact.ts` or
wrapping it from the generic compaction path, depending on where
`maybeCompactAgentHarnessSession(...)` is invoked.
#### In-turn Codex native contextCompaction events
Codex may emit `contextCompaction` item events during a turn. Keep the current
before/after compaction hook emission in `event-projector.ts`, but do not treat
that as a completed context-engine compaction.
For engines that own compaction, emit an explicit diagnostic when Codex performs
native compaction anyway:
- stream/event name: existing `compaction` stream is acceptable
- details: `{ backend: "codex-app-server", ownsCompaction: true }`
This makes the split auditable.
### 9. Session reset and binding behavior
The existing Codex harness `reset(...)` clears the Codex app-server binding from
the OpenClaw session file. Preserve that behavior.
Also ensure context-engine state cleanup continues to happen through existing
OpenClaw session lifecycle paths. Do not add Codex-specific cleanup unless the
context-engine lifecycle currently misses reset/delete events for all harnesses.
### 10. Error handling
Follow built-in OpenClaw semantics:
- bootstrap failures warn and continue
- assemble failures warn and fall back to unassembled pipeline messages/prompt
- afterTurn/ingest failures warn and mark post-turn finalization unsuccessful
- maintenance runs only after successful, non-aborted, non-yield turns
- compaction errors should not be retried as fresh prompts
Codex-specific additions:
- If context projection fails, warn and fall back to the original prompt.
- If transcript mirror fails, still attempt context-engine finalization with
fallback messages.
- If Codex native compaction fails after context-engine compaction succeeds,
do not fail the whole OpenClaw compaction when the context engine is primary.
## Test plan
### Unit tests
Add tests under `extensions/codex/src/app-server`:
1. `run-attempt.context-engine.test.ts`
- Codex calls `bootstrap` when a session file exists.
- Codex calls `assemble` with mirrored messages, token budget, tool names,
citations mode, model id, and prompt.
- `systemPromptAddition` is included in developer instructions.
- Assembled messages are projected into the prompt before current request.
- Codex calls `afterTurn` after transcript mirroring.
- Without `afterTurn`, Codex calls `ingestBatch` or per-message `ingest`.
- Turn maintenance runs after successful turns.
- Turn maintenance does not run on prompt error, abort, or yield abort.
2. `context-engine-projection.test.ts`
- stable output for identical inputs
- no duplicate current prompt when assembled history includes it
- handles empty history
- preserves role order
- includes system prompt addition only in developer instructions
3. `compact.context-engine.test.ts`
- owning context engine primary result wins
- Codex native compaction status appears in details when also attempted
- Codex native failure does not fail owning context-engine compaction
- non-owning context engine preserves current native compaction behavior
### Existing tests to update
- `extensions/codex/src/app-server/run-attempt.test.ts` if present, otherwise
nearest Codex app-server run tests.
- `extensions/codex/src/app-server/event-projector.test.ts` only if compaction
event details change.
- `src/agents/harness/selection.test.ts` should not need changes unless config
behavior changes; it should remain stable.
- Built-in harness context-engine tests should continue to pass unchanged.
### Integration / live tests
Add or extend live Codex harness smoke tests:
- configure `plugins.slots.contextEngine` to a test engine
- configure `agents.defaults.model` to a `codex/*` model
- configure provider/model `agentRuntime.id = "codex"`
- assert test engine observed:
- bootstrap
- assemble
- afterTurn or ingest
- maintenance
Avoid requiring lossless-claw in OpenClaw core tests. Use a small in-repo fake
context engine plugin.
## Observability
Add debug logs around Codex context-engine lifecycle calls:
- `codex context engine bootstrap started/completed/failed`
- `codex context engine assemble applied`
- `codex context engine finalize completed/failed`
- `codex context engine maintenance skipped` with reason
- `codex native compaction completed alongside context-engine compaction`
Avoid logging full prompts or transcript contents.
Add structured fields where useful:
- `sessionId`
- `sessionKey` redacted or omitted according to existing logging practice
- `engineId`
- `threadId`
- `turnId`
- `assembledMessageCount`
- `estimatedTokens`
- `hasSystemPromptAddition`
## Migration / compatibility
This should be backward-compatible:
- If no context engine is configured, legacy context engine behavior should be
equivalent to today's Codex harness behavior.
- If context-engine `assemble` fails, Codex should continue with the original
prompt path.
- Existing Codex thread bindings should remain valid.
- Dynamic tool fingerprinting should not include context-engine output; otherwise
every context change could force a new Codex thread. Only the tool catalog
should affect the dynamic tool fingerprint.
## Open questions
1. Should assembled context be injected entirely into the user prompt, entirely
into developer instructions, or split?
Recommendation: split. Put `systemPromptAddition` in developer instructions;
put assembled transcript context in the user prompt wrapper. This best matches
the current Codex protocol without mutating native thread history.
2. Should Codex native compaction be disabled when a context engine owns
compaction?
Recommendation: no, not initially. Codex native compaction may still be
necessary to keep the app-server thread alive. But it must be reported as
native Codex compaction, not as context-engine compaction.
3. Should `before_prompt_build` run before or after context-engine assembly?
Recommendation: after context-engine projection for Codex, so generic harness
hooks see the actual prompt/developer instructions Codex will receive. If
built-in harness parity requires the opposite, encode the chosen order in
tests and document it here.
4. Can Codex app-server accept a future structured context/history override?
Unknown. If it can, replace the text projection layer with that protocol and
keep the lifecycle calls unchanged.
## Acceptance criteria
- A `codex/*` embedded harness turn invokes the selected context engine's
assemble lifecycle.
- A context-engine `systemPromptAddition` affects Codex developer instructions.
- Assembled context affects the Codex turn input deterministically.
- Successful Codex turns call `afterTurn` or ingest fallback.
- Successful Codex turns run context-engine turn maintenance.
- Failed/aborted/yield-aborted turns do not run turn maintenance.
- Context-engine-owned compaction remains primary for OpenClaw/plugin state.
- Codex native compaction remains auditable as native Codex behavior.
- Existing built-in harness context-engine behavior is unchanged.
- Existing Codex harness behavior is unchanged when no non-legacy context engine
is selected or when assembly fails.

284
docs/plan/ui-channels.md Normal file
View File

@@ -0,0 +1,284 @@
---
summary: Decouple semantic message presentation from channel native UI renderers.
title: Channel presentation refactor plan
read_when:
- Refactoring channel message UI, interactive payloads, or native channel renderers
- Changing message tool capabilities, delivery hints, or cross-context markers
- Debugging Discord Carbon import fanout or channel plugin runtime laziness
---
## Status
Implemented for the shared agent, CLI, plugin capability, and outbound delivery surfaces:
- `ReplyPayload.presentation` carries semantic message UI.
- `ReplyPayload.delivery.pin` carries sent-message pin requests.
- Shared message actions expose `presentation`, `delivery`, and `pin` instead of provider-native `components`, `blocks`, `buttons`, or `card`.
- Core renders or auto-degrades presentation through plugin-declared outbound capabilities.
- Discord, Slack, Telegram, Mattermost, MS Teams, and Feishu renderers consume the generic contract.
- Discord channel control-plane code no longer imports Carbon-backed UI containers.
Canonical docs now live in [Message Presentation](/plugins/message-presentation).
Keep this plan as historical implementation context; update the canonical guide
for contract, renderer, or fallback behavior changes.
## Problem
Channel UI is currently split across several incompatible surfaces:
- Core owns a Discord-shaped cross-context renderer hook through `buildCrossContextComponents`.
- Discord `channel.ts` can import native Carbon UI through `DiscordUiContainer`, which pulls runtime UI dependencies into the channel plugin control plane.
- The agent and CLI expose native payload escape hatches such as Discord `components`, Slack `blocks`, Telegram or Mattermost `buttons`, and Teams or Feishu `card`.
- `ReplyPayload.channelData` carries both transport hints and native UI envelopes.
- The generic `interactive` model exists, but it is narrower than the richer layouts already used by Discord, Slack, Teams, Feishu, LINE, Telegram, and Mattermost.
This makes core aware of native UI shapes, weakens plugin runtime laziness, and gives agents too many provider-specific ways to express the same message intent.
## Goals
- Core decides the best semantic presentation for a message from declared capabilities.
- Extensions declare capabilities and render semantic presentation into native transport payloads.
- Web Control UI remains separate from chat native UI.
- Native channel payloads are not exposed through the shared agent or CLI message surface.
- Unsupported presentation features auto-degrade to the best text representation.
- Delivery behavior such as pinning a sent message is generic delivery metadata, not presentation.
## Non goals
- No backwards compatibility shim for `buildCrossContextComponents`.
- No public native escape hatches for `components`, `blocks`, `buttons`, or `card`.
- No core imports of channel-native UI libraries.
- No provider-specific SDK seams for bundled channels.
## Target model
Add a core-owned `presentation` field to `ReplyPayload`.
```ts
type MessagePresentationTone = "neutral" | "info" | "success" | "warning" | "danger";
type MessagePresentation = {
tone?: MessagePresentationTone;
title?: string;
blocks: MessagePresentationBlock[];
};
type MessagePresentationBlock =
| { type: "text"; text: string }
| { type: "context"; text: string }
| { type: "divider" }
| { type: "buttons"; buttons: MessagePresentationButton[] }
| { type: "select"; placeholder?: string; options: MessagePresentationOption[] };
type MessagePresentationButton = {
label: string;
value?: string;
url?: string;
style?: "primary" | "secondary" | "success" | "danger";
};
type MessagePresentationOption = {
label: string;
value: string;
};
```
`interactive` becomes a subset of `presentation` during migration:
- `interactive` text block maps to `presentation.blocks[].type = "text"`.
- `interactive` buttons block maps to `presentation.blocks[].type = "buttons"`.
- `interactive` select block maps to `presentation.blocks[].type = "select"`.
The external agent and CLI schemas now use `presentation`; `interactive` remains an internal legacy parser/rendering helper for existing reply producers.
The public producer-facing API treats `interactive` as deprecated. Runtime
support remains so existing approval helpers and older plugins continue to
work while new code emits `presentation`.
## Delivery metadata
Add a core-owned `delivery` field for send behavior that is not UI.
```ts
type ReplyPayloadDelivery = {
pin?:
| boolean
| {
enabled: boolean;
notify?: boolean;
required?: boolean;
};
};
```
Semantics:
- `delivery.pin = true` means pin the first successfully delivered message.
- `notify` defaults to `false`.
- `required` defaults to `false`; unsupported channels or failed pinning auto-degrade by continuing delivery.
- Manual `pin`, `unpin`, and `list-pins` message actions remain for existing messages.
Current Telegram ACP topic binding should move from `channelData.telegram.pin = true` to `delivery.pin = true`.
## Runtime capability contract
Add presentation and delivery render hooks to the runtime outbound adapter, not the control-plane channel plugin.
```ts
type ChannelPresentationCapabilities = {
supported: boolean;
buttons?: boolean;
selects?: boolean;
context?: boolean;
divider?: boolean;
tones?: MessagePresentationTone[];
limits?: {
actions?: {
maxActions?: number;
maxActionsPerRow?: number;
maxRows?: number;
maxLabelLength?: number;
maxValueBytes?: number;
supportsStyles?: boolean;
supportsDisabled?: boolean;
supportsLayoutHints?: boolean;
};
selects?: {
maxOptions?: number;
maxLabelLength?: number;
maxValueBytes?: number;
};
text?: {
maxLength?: number;
encoding?: "characters" | "utf8-bytes" | "utf16-units";
markdownDialect?: "plain" | "markdown" | "html" | "slack-mrkdwn" | "discord-markdown";
supportsEdit?: boolean;
};
};
};
type ChannelDeliveryCapabilities = {
pinSentMessage?: boolean;
};
type ChannelOutboundAdapter = {
presentationCapabilities?: ChannelPresentationCapabilities;
renderPresentation?: (params: {
payload: ReplyPayload;
presentation: MessagePresentation;
ctx: ChannelOutboundSendContext;
}) => ReplyPayload | null;
deliveryCapabilities?: ChannelDeliveryCapabilities;
pinDeliveredMessage?: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
to: string;
threadId?: string | number | null;
messageId: string;
notify: boolean;
}) => Promise<void>;
};
```
Core behavior:
- Resolve target channel and runtime adapter.
- Ask for presentation capabilities.
- Degrade unsupported blocks and apply generic capability limits before
rendering.
- Call `renderPresentation`.
- If no renderer exists, convert presentation to text fallback.
- After successful send, call `pinDeliveredMessage` when `delivery.pin` is requested and supported.
## Channel mapping
Discord:
- Render `presentation` to components v2 and Carbon containers in runtime-only modules.
- Keep accent color helpers in light modules.
- Remove `DiscordUiContainer` imports from channel plugin control-plane code.
Slack:
- Render `presentation` to Block Kit.
- Remove agent and CLI `blocks` input.
Telegram:
- Render text, context, and dividers as text.
- Render actions and select as inline keyboards when configured and allowed for the target surface.
- Use text fallback when inline buttons are disabled.
- Move ACP topic pinning to `delivery.pin`.
Mattermost:
- Render actions as interactive buttons where configured.
- Render other blocks as text fallback.
MS Teams:
- Render `presentation` to Adaptive Cards.
- Keep manual pin/unpin/list-pins actions.
- Optionally implement `pinDeliveredMessage` if Graph support is reliable for the target conversation.
Feishu:
- Render `presentation` to interactive cards.
- Keep manual pin/unpin/list-pins actions.
- Optionally implement `pinDeliveredMessage` for sent-message pinning if API behavior is reliable.
LINE:
- Render `presentation` to Flex or template messages where possible.
- Fall back to text for unsupported blocks.
- Remove LINE UI payloads from `channelData`.
Plain or limited channels:
- Convert presentation to text with conservative formatting.
## Refactor steps
1. Reapply the Discord release fix that splits `ui-colors.ts` from Carbon-backed UI and removes `DiscordUiContainer` from `extensions/discord/src/channel.ts`.
2. Add `presentation` and `delivery` to `ReplyPayload`, outbound payload normalization, delivery summaries, and hook payloads.
3. Add `MessagePresentation` schema and parser helpers in a narrow SDK/runtime subpath.
4. Replace message capabilities `buttons`, `cards`, `components`, and `blocks` with semantic presentation capabilities.
5. Add runtime outbound adapter hooks for presentation render and delivery pinning.
6. Replace cross-context component construction with `buildCrossContextPresentation`.
7. Delete `src/infra/outbound/channel-adapters.ts` and remove `buildCrossContextComponents` from channel plugin types.
8. Change `maybeApplyCrossContextMarker` to attach `presentation` instead of native params.
9. Update plugin-dispatch send paths to consume only semantic presentation and delivery metadata.
10. Remove agent and CLI native payload params: `components`, `blocks`, `buttons`, and `card`.
11. Remove SDK helpers that create native message-tool schemas, replacing them with presentation schema helpers.
12. Remove UI/native envelopes from `channelData`; keep only transport metadata until each remaining field is reviewed.
13. Migrate Discord, Slack, Telegram, Mattermost, MS Teams, Feishu, and LINE renderers.
14. Update docs for message CLI, channel pages, plugin SDK, and capability cookbook.
15. Run import fanout profiling for Discord and affected channel entrypoints.
Steps 1-11 and 13-14 are implemented in this refactor for the shared agent, CLI, plugin capability, and outbound adapter contracts. Step 12 remains a deeper internal cleanup pass for provider-private `channelData` transport envelopes. Step 15 remains follow-up validation if we want quantified import-fanout numbers beyond the type/test gate.
## Tests
Add or update:
- Presentation normalization tests.
- Presentation auto-degrade tests for unsupported blocks.
- Cross-context marker tests for plugin dispatch and core delivery paths.
- Channel render matrix tests for Discord, Slack, Telegram, Mattermost, MS Teams, Feishu, LINE, and text fallback.
- Message tool schema tests proving native fields are gone.
- CLI tests proving native flags are gone.
- Discord entrypoint import-laziness regression covering Carbon.
- Delivery pin tests covering Telegram and generic fallback.
## Open questions
- Should `delivery.pin` be implemented for Discord, Slack, MS Teams, and Feishu in the first pass, or only Telegram first?
- Should `delivery` eventually absorb existing fields such as `replyToId`, `replyToCurrent`, `silent`, and `audioAsVoice`, or stay focused on post-send behaviors?
- Should presentation support images or file references directly, or should media remain separate from UI layout for now?
## Related
- [Channels overview](/channels)
- [Message presentation](/plugins/message-presentation)