@enderfga/claw-orchestrator 7.14.0 → 7.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +162 -135
  2. package/configs/autoloop-reviewer-prompt.md +1 -1
  3. package/configs/engines/README.md +31 -11
  4. package/dist/bin/acp-server.js +7 -12
  5. package/dist/bin/acp-server.js.map +1 -1
  6. package/dist/bin/cli.js +84 -46
  7. package/dist/bin/cli.js.map +1 -1
  8. package/dist/bin/mcp-server.js +2 -54
  9. package/dist/bin/mcp-server.js.map +1 -1
  10. package/dist/src/acp-server.d.ts +17 -9
  11. package/dist/src/acp-server.js +149 -30
  12. package/dist/src/acp-server.js.map +1 -1
  13. package/dist/src/autoloop/agent-tools.d.ts +2 -0
  14. package/dist/src/autoloop/agent-tools.js +7 -0
  15. package/dist/src/autoloop/agent-tools.js.map +1 -1
  16. package/dist/src/autoloop/dispatcher.d.ts +6 -0
  17. package/dist/src/autoloop/dispatcher.js +72 -9
  18. package/dist/src/autoloop/dispatcher.js.map +1 -1
  19. package/dist/src/autoloop/planner-tools.js +2 -0
  20. package/dist/src/autoloop/planner-tools.js.map +1 -1
  21. package/dist/src/claude-goal-session.d.ts +147 -0
  22. package/dist/src/claude-goal-session.js +355 -0
  23. package/dist/src/claude-goal-session.js.map +1 -0
  24. package/dist/src/council.d.ts +8 -0
  25. package/dist/src/council.js +45 -12
  26. package/dist/src/council.js.map +1 -1
  27. package/dist/src/dashboard/index.html +28 -14
  28. package/dist/src/embedded-server.js +27 -8
  29. package/dist/src/embedded-server.js.map +1 -1
  30. package/dist/src/engine-spawn.js +3 -1
  31. package/dist/src/engine-spawn.js.map +1 -1
  32. package/dist/src/fanout.d.ts +13 -3
  33. package/dist/src/fanout.js +10 -0
  34. package/dist/src/fanout.js.map +1 -1
  35. package/dist/src/inbox-manager.d.ts +9 -0
  36. package/dist/src/inbox-manager.js +8 -2
  37. package/dist/src/inbox-manager.js.map +1 -1
  38. package/dist/src/index.d.ts +2 -0
  39. package/dist/src/index.js +135 -53
  40. package/dist/src/index.js.map +1 -1
  41. package/dist/src/kernel/agent-step.d.ts +6 -0
  42. package/dist/src/kernel/agent-step.js.map +1 -1
  43. package/dist/src/kernel/conditions.d.ts +1 -1
  44. package/dist/src/kernel/conditions.js +1 -1
  45. package/dist/src/kernel/engine.d.ts +28 -8
  46. package/dist/src/kernel/engine.js +93 -19
  47. package/dist/src/kernel/engine.js.map +1 -1
  48. package/dist/src/kernel/nodes/agent.js +2 -0
  49. package/dist/src/kernel/nodes/agent.js.map +1 -1
  50. package/dist/src/kernel/nodes/council.js +9 -4
  51. package/dist/src/kernel/nodes/council.js.map +1 -1
  52. package/dist/src/kernel/nodes/fanout.js +5 -0
  53. package/dist/src/kernel/nodes/fanout.js.map +1 -1
  54. package/dist/src/kernel/nodes/goal.d.ts +19 -5
  55. package/dist/src/kernel/nodes/goal.js +81 -34
  56. package/dist/src/kernel/nodes/goal.js.map +1 -1
  57. package/dist/src/kernel/nodes/subflow.js +9 -1
  58. package/dist/src/kernel/nodes/subflow.js.map +1 -1
  59. package/dist/src/kernel/nodes/ultraapp.d.ts +1 -0
  60. package/dist/src/kernel/nodes/ultraapp.js +1 -0
  61. package/dist/src/kernel/nodes/ultraapp.js.map +1 -1
  62. package/dist/src/kernel/nodes/verifier.js +5 -1
  63. package/dist/src/kernel/nodes/verifier.js.map +1 -1
  64. package/dist/src/kernel/projections.d.ts +1 -0
  65. package/dist/src/kernel/projections.js +7 -1
  66. package/dist/src/kernel/projections.js.map +1 -1
  67. package/dist/src/kernel/templates/index.d.ts +16 -11
  68. package/dist/src/kernel/templates/index.js +14 -8
  69. package/dist/src/kernel/templates/index.js.map +1 -1
  70. package/dist/src/kernel/types.d.ts +37 -13
  71. package/dist/src/kernel/types.js.map +1 -1
  72. package/dist/src/logger.d.ts +6 -0
  73. package/dist/src/logger.js +14 -0
  74. package/dist/src/logger.js.map +1 -1
  75. package/dist/src/mcp-annotations.d.ts +13 -0
  76. package/dist/src/mcp-annotations.js +100 -0
  77. package/dist/src/mcp-annotations.js.map +1 -0
  78. package/dist/src/models.d.ts +7 -0
  79. package/dist/src/models.js +9 -0
  80. package/dist/src/models.js.map +1 -1
  81. package/dist/src/openai-compat.d.ts +11 -5
  82. package/dist/src/openai-compat.js +55 -54
  83. package/dist/src/openai-compat.js.map +1 -1
  84. package/dist/src/persistent-acp-session.d.ts +7 -0
  85. package/dist/src/persistent-acp-session.js +26 -2
  86. package/dist/src/persistent-acp-session.js.map +1 -1
  87. package/dist/src/persistent-codex-app-session.d.ts +2 -0
  88. package/dist/src/persistent-codex-app-session.js +18 -9
  89. package/dist/src/persistent-codex-app-session.js.map +1 -1
  90. package/dist/src/persistent-codex-session.d.ts +14 -3
  91. package/dist/src/persistent-codex-session.js +22 -19
  92. package/dist/src/persistent-codex-session.js.map +1 -1
  93. package/dist/src/persistent-custom-session.d.ts +9 -0
  94. package/dist/src/persistent-custom-session.js +41 -21
  95. package/dist/src/persistent-custom-session.js.map +1 -1
  96. package/dist/src/persistent-grok-session.js +1 -1
  97. package/dist/src/persistent-grok-session.js.map +1 -1
  98. package/dist/src/persistent-session.d.ts +9 -0
  99. package/dist/src/persistent-session.js +23 -0
  100. package/dist/src/persistent-session.js.map +1 -1
  101. package/dist/src/run-ledger.d.ts +3 -3
  102. package/dist/src/session-manager.d.ts +45 -1
  103. package/dist/src/session-manager.js +199 -34
  104. package/dist/src/session-manager.js.map +1 -1
  105. package/dist/src/types.d.ts +34 -11
  106. package/dist/src/types.js.map +1 -1
  107. package/dist/src/ultraapp/build.d.ts +5 -11
  108. package/dist/src/ultraapp/build.js +5 -11
  109. package/dist/src/ultraapp/build.js.map +1 -1
  110. package/dist/src/ultraapp/council-adapter.d.ts +5 -2
  111. package/dist/src/ultraapp/council-adapter.js +6 -6
  112. package/dist/src/ultraapp/council-adapter.js.map +1 -1
  113. package/dist/src/ultraapp/files.d.ts +7 -0
  114. package/dist/src/ultraapp/files.js +7 -1
  115. package/dist/src/ultraapp/files.js.map +1 -1
  116. package/dist/src/ultraapp/manager.d.ts +23 -3
  117. package/dist/src/ultraapp/manager.js +170 -72
  118. package/dist/src/ultraapp/manager.js.map +1 -1
  119. package/dist/src/ultraapp/narrator.js +1 -1
  120. package/dist/src/ultraapp/narrator.js.map +1 -1
  121. package/dist/src/ultraapp/versions.d.ts +27 -5
  122. package/dist/src/ultraapp/versions.js +58 -13
  123. package/dist/src/ultraapp/versions.js.map +1 -1
  124. package/openclaw.plugin.json +4 -13
  125. package/package.json +13 -6
  126. package/skills/SKILL.md +52 -24
  127. package/skills/references/acp.md +140 -61
  128. package/skills/references/autoloop.md +148 -38
  129. package/skills/references/claude-cli-tracking.md +9 -6
  130. package/skills/references/cli.md +139 -108
  131. package/skills/references/council.md +44 -22
  132. package/skills/references/dashboard.md +37 -21
  133. package/skills/references/getting-started.md +49 -15
  134. package/skills/references/inbox.md +13 -8
  135. package/skills/references/mcp.md +25 -17
  136. package/skills/references/multi-engine.md +161 -145
  137. package/skills/references/observability.md +90 -58
  138. package/skills/references/openai-compat.md +172 -82
  139. package/skills/references/sessions.md +101 -65
  140. package/skills/references/tools.md +217 -158
  141. package/skills/references/ultra.md +7 -7
  142. package/skills/references/ultraapp.md +101 -56
  143. package/skills/references/verification.md +112 -56
  144. package/skills/references/workflow.md +275 -97
  145. package/assets/banner.jpg +0 -0
@@ -1,8 +1,8 @@
1
1
  # OpenAI-Compatible Bridge
2
2
 
3
- > **Cost note**: the bridge drives the Claude Code CLI, so usage counts against your Claude account. It does not bypass Anthropic's billing or subscription limits. Heavy agent traffic can consume extra-usage credits at API rates. Check your usage at [claude.ai/settings/usage](https://claude.ai/settings/usage).
3
+ > **Cost note**: by default the bridge drives the Claude Code CLI, so usage counts against your Claude account. It does not bypass Anthropic's billing or subscription limits. Heavy agent traffic can consume extra-usage credits at API rates. Check your usage at [claude.ai/settings/usage](https://claude.ai/settings/usage).
4
4
 
5
- The embedded server exposes a drop-in OpenAI-compatible endpoint so any client that speaks `/v1/chat/completions` can talk to a persistent Claude Code (or Codex / Antigravity / Grok / OpenCode) session. The bridge is designed to serve **two kinds of clients as first-class citizens**:
5
+ The embedded server (`clawo serve`, or the OpenClaw plugin) exposes a drop-in OpenAI-compatible endpoint so any client that speaks `/v1/chat/completions` can talk to a persistent Claude Code (or Codex / Antigravity / Grok Build) session; the request's `model` picks the engine — see [Model routing](#model-routing). The bridge is designed to serve **two kinds of clients as first-class citizens**:
6
6
 
7
7
  1. **Upstream agents** that maintain their own conversation state and forward only the latest user turn — OpenClaw's main agent loop, cron jobs, subagents, programmatic clients.
8
8
  2. **OpenAI-compatible webchat / labeling tools** that re-send the full transcript on every turn — ChatGPT-Next-Web, Open WebUI, LobeChat, data-labeling pipelines.
@@ -11,14 +11,43 @@ Both modes share the same wire protocol; the difference is how a "new conversati
11
11
 
12
12
  ## Endpoint
13
13
 
14
- | | |
15
- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
16
- | **URL** | `http://127.0.0.1:18796/v1/chat/completions` |
17
- | **Models endpoint** | `GET /v1/models` |
18
- | **Inspection endpoint** | `GET /v1/sessions` (lists active openai-compat sessions with caching stats) |
19
- | **Auth** | `Authorization: Bearer <token>`. By default the server generates a token and writes it to `~/.openclaw/server-token` (mode 0600). Set `OPENCLAW_SERVER_TOKEN=<value>` to choose the token, or `OPENCLAW_SERVER_TOKEN=disabled` to turn auth off (single-user hosts only) |
20
- | **Wire format** | OpenAI Chat Completions, both streaming (SSE) and non-streaming |
21
- | **Default model** | `claude-sonnet-4-6` when the request has no `model` |
14
+ | | |
15
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
+ | **URL** | `http://127.0.0.1:18796/v1/chat/completions` (`clawo serve --port` / `--host`, or `OPENCLAW_SERVER_HOST`, change the address) |
17
+ | **Models endpoint** | `GET /v1/models` — the model registry, as an OpenAI model list |
18
+ | **Inspection endpoint** | `GET /v1/sessions` (lists active openai-compat sessions with caching stats) |
19
+ | **Auth** | `Authorization: Bearer <token>`. By default the server reuses the token in `~/.openclaw/server-token`, or generates one and writes it there (mode 0600). Set `OPENCLAW_SERVER_TOKEN=<value>` to choose the token, or `OPENCLAW_SERVER_TOKEN=disabled` to turn auth off (single-user hosts only) |
20
+ | **Wire format** | OpenAI Chat Completions, both streaming (SSE) and non-streaming |
21
+ | **Usage** | `usage` (on the final chunk when streaming) counts this request's tokens — what the turn added to the session's running totals, which `GET /v1/sessions` reports |
22
+ | **Default model** | `claude-sonnet-4-6` when the request has no `model` |
23
+
24
+ The request fields the bridge reads are `model`, `messages`, `stream`, `user` and `tools`. `max_tokens` is
25
+ validated (it must be a positive number) but does not limit the reply; `temperature` and other sampling fields are
26
+ ignored — the CLI runs with its own settings.
27
+
28
+ A streamed reply opens with a `role` chunk, sends a `: keepalive` comment every 30 seconds while the turn runs,
29
+ ends with a chunk carrying `finish_reason` (and `usage`, when available), then `data: [DONE]`. When
30
+ the request carries `tools`, the reply is held back until the turn finishes, so the tool calls can be parsed out of
31
+ it, and then sent as `tool_calls` chunks (finish reason `tool_calls`) or as one content chunk — on every engine,
32
+ whether or not it streams deltas.
33
+
34
+ ## Model routing
35
+
36
+ The `model` field picks the engine as well as the model, through the registry in `src/models.ts`:
37
+
38
+ 1. **`agy/<model>`** runs `<model>` on Antigravity (`agy`).
39
+ 2. **A registered id or alias** runs on the engine it is registered to — `opus`, `sonnet`, `haiku` and the
40
+ `claude-*` ids on `claude`, the `gpt-*` ids, `o3`, `o4-mini` and `codex-mini-latest` on `codex`, Antigravity's
41
+ Gemini ids on `agy`, `grok` and the `grok-*` ids on `grok`. `GET /v1/models` lists the current ones.
42
+ 3. **An unregistered name** falls back on its prefix: `gpt*`, `o3*`, `o4*` and `codex*` run on `codex`, `grok*` on
43
+ `grok`.
44
+ 4. **Anything else** — including vendor-prefixed names such as `anthropic/claude-sonnet-4-6` — runs on `claude`,
45
+ with the name passed through.
46
+
47
+ Two legacy engines are still reachable this way: the registry's `gemini-*-preview` ids (aliases `gemini-pro`
48
+ and `gemini-flash`) and any unregistered name containing `gemini` run on `gemini`, and `composer-*`, `cursor*`
49
+ and `auto` run on `cursor`. Nothing else selects an engine, so `opencode`, `codex-app`, `acp` and custom
50
+ engines are not reachable through the bridge.
22
51
 
23
52
  ## Session keying
24
53
 
@@ -26,8 +55,8 @@ Each request is mapped to a long-running session. Once a session exists, subsequ
26
55
 
27
56
  1. **`X-Session-Id` header** — explicit, highest precedence
28
57
  2. **`user` field in the request body** — OpenAI standard field, treated as a stable caller identifier
29
- 3. **`sys-<sha1(model + systemPrompt)[0..12]>`** — automatic fallback so unkeyed callers don't all collapse onto a single shared session
30
- 4. **`'default'`** — only when there is no system prompt AND no model (degenerate empty body)
58
+ 3. **`sys-<sha1(model + systemPrompt + tools)[0..12]>`** — automatic fallback so unkeyed callers don't all collapse onto a single shared session
59
+ 4. **`'default'`** — only when there is no system prompt, no model and no tools (degenerate empty body)
31
60
 
32
61
  Without the hash fallback, unkeyed callers (for example OpenClaw's main agent, cron jobs and subagents behind one gateway) would share one session: their requests would queue behind each other and could receive another caller's system prompt.
33
62
 
@@ -35,22 +64,43 @@ The model is mixed into the hash so that two callers with the same system prompt
35
64
 
36
65
  The full plugin-side session name is `openai-<key>`. The key becomes a directory
37
66
  name — the bridge starts each session in `os.tmpdir()/openclaw-compat-<name>` —
38
- so a key that is not already `[A-Za-z0-9._-]` is replaced by a hash of itself.
39
- An ordinary id is used unchanged; a key containing path separators cannot choose
40
- where the session runs.
41
-
42
- The tool list is fingerprinted into the hash fallback by name, a description
43
- prefix, and the **parameter schema** (with object keys normalised, so a
44
- re-serialised identical schema still resolves to the same session). On the
45
- Claude engine the schemas are written into the session's system prompt when the
46
- session is created and are not re-sent per turn, so a changed schema has to
47
- resolve to a new session for the model to see it.
67
+ so a key that is not 1–64 characters of `[A-Za-z0-9._-]` without `..` is replaced
68
+ by a hash of itself (`k-<16 hex>`). An ordinary id is used unchanged; a key
69
+ containing path separators cannot choose where the session runs.
70
+
71
+ The tool list is fingerprinted into the hash fallback by name, the first 64
72
+ characters of the description, and the **parameter schema** (with object keys
73
+ normalised, so a re-serialised identical schema still resolves to the same
74
+ session). On the Claude engine the schemas are written into the session's system
75
+ prompt when the session is created and are not re-sent per turn, so a changed
76
+ schema has to resolve to a new session for the model to see it. An explicit key
77
+ (`X-Session-Id` or `user`) carries no fingerprint — see
78
+ [Known limitations](#known-limitations).
79
+
80
+ ### How a bridge session runs
81
+
82
+ - **Working directory**: the empty temp directory above, so the CLI picks up no
83
+ project files, `CLAUDE.md` or git state from wherever the server was started.
84
+ - **Permission mode**: `bypassPermissions`.
85
+ - **On `claude`**: with `tools` in the request, the CLI's own built-in tools are
86
+ switched off (`--tools ""`) and its system prompt is replaced
87
+ (`--system-prompt`) by a preamble that says it has no tools of its own, the
88
+ tool definitions (sent in each message instead under
89
+ `OPENAI_COMPAT_TOOLS_PER_MESSAGE`), and the caller's system prompt. Without `tools`, the caller's
90
+ system prompt is appended (`--append-system-prompt`). Either is fixed when the
91
+ session is created.
92
+ - **On the other engines**: the caller's system prompt and the tool definitions
93
+ travel in the message; see [Tool definitions](#tool-definitions-and-where-they-live).
94
+ - **Compaction**: before a turn on an existing session whose context is over 80%
95
+ full, the bridge compacts it.
96
+ - **Persistence**: bridge sessions are not written to the on-disk session
97
+ registry. An idle session is stopped when its TTL runs out and is not resumed.
48
98
 
49
99
  ## Operator modes
50
100
 
51
101
  ### Default mode — agent / programmatic clients
52
102
 
53
- When the env var is **not set**, the bridge assumes upstream callers maintain their own conversation transcript and only forward the latest user turn. Sessions are reused indefinitely. The only signal that starts a new conversation is the explicit reset header:
103
+ When `OPENAI_COMPAT_NEW_CONVO_HEURISTIC` is **not `1`**, the bridge assumes upstream callers maintain their own conversation transcript and only forward the latest user turn. A session is reused for as long as it lives — until it sits idle past its TTL. The only signal that starts a new conversation is the explicit reset header:
54
104
 
55
105
  ```
56
106
  X-Session-Reset: 1
@@ -60,13 +110,15 @@ X-Session-Reset: 1
60
110
 
61
111
  When the header is present, the existing session for this key is stopped and a fresh one is created. Use this from a client that wants "new chat" semantics under your own control — e.g. when your UI's "Clear History" button is pressed.
62
112
 
113
+ A turn treated as a new conversation (the reset header, or the webchat heuristic below) runs in a session that is stopped as soon as it has answered. The next request for the key starts another session and replays whatever earlier turns its `messages[]` carries (see [Conversation history](#conversation-history-on-the-way-in)). A client that forwards only its latest turn carries none, so the reset turn's exchange is not in that session.
114
+
63
115
  ### Webchat mode — `OPENAI_COMPAT_NEW_CONVO_HEURISTIC=1`
64
116
 
65
117
  When the env var is set to `1`, the bridge additionally restores a legacy heuristic: a request whose `messages` array contains exactly one non-system message (i.e. the conversation has no assistant turns yet) is treated as a fresh conversation. This is the only signal that webchat frontends (ChatGPT-Next-Web, Open WebUI, LobeChat) emit when the user clicks "New Chat" — they clear their UI transcript and post `[system, user]`.
66
118
 
67
119
  Without this flag, those frontends would silently continue the previous CLI session and surface stale context the user thought they had cleared.
68
120
 
69
- The env var is read on every request, so ops can flip it via `launchctl setenv` (or equivalent) without restarting the server.
121
+ Set it in the server's environment. It is read per request, but a running process does not see a later `launchctl setenv` or shell export, so restart the server after changing it.
70
122
 
71
123
  | Mode | Best for | New-conversation signals |
72
124
  | ----------------- | ----------------------------------------------------------- | --------------------------------------------------- |
@@ -75,26 +127,26 @@ The env var is read on every request, so ops can flip it via `launchctl setenv`
75
127
 
76
128
  ## Status webhook
77
129
 
78
- When `OPENAI_COMPAT_STATUS_URL` is set (full HTTP URL), each chat completion sends best-effort `POST` requests with `Content-Type: application/json` and body:
130
+ When `OPENAI_COMPAT_STATUS_URL` is set to a plain `http://` URL, each chat completion sends best-effort `POST` requests with `Content-Type: application/json` and body:
79
131
 
80
- | Field | Type | Meaning |
81
- | ---------- | -------------- | ----------------------------------------------------------------------------------------------------- |
82
- | `state` | string | `thinking` (turn started), `working` (a tool is running), or `idle` (turn finished or stream closed). |
83
- | `activity` | string | Short human-readable line, e.g. `Processing request...`, `Reading: foo.ts`, `Running: npm test...`. |
84
- | `tool` | string \| null | Tool name when `state === working`, otherwise `null`. |
132
+ | Field | Type | Meaning |
133
+ | ---------- | -------------- | --------------------------------------------------------------------------------------------------------------------------- |
134
+ | `state` | string | `thinking` (turn started), `working` (a tool is running), or `idle` (turn finished or failed). |
135
+ | `activity` | string | Short human-readable line, e.g. `Processing request...`, `Reading: foo.ts`, `Running: npm test`, `Ready`, `Request failed`. |
136
+ | `tool` | string \| null | Tool name when `state === working`, otherwise `null`. |
85
137
 
86
- Failures are ignored (no retries). Use this from a small local HTTP handler that forwards status into your webchat UI.
138
+ Failures are ignored (no retries) and never affect the completion: a request the endpoint has not answered within 2 s is dropped, and a URL the bridge cannot POST to — anything but `http://`, or malformed — sends nothing. Use this from a small local HTTP handler that forwards status into your webchat UI.
87
139
 
88
140
  ## Tool definitions and where they live
89
141
 
90
142
  When the request carries `tools`, the schemas have to reach the CLI somehow. Which
91
143
  mechanism is used depends on whether the engine keeps the conversation itself.
92
144
 
93
- | Engine | Turn 1 | Later turns |
94
- | ----------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
95
- | `claude` | Schemas go into the session system prompt (`--system-prompt`) | Nothing injected — the system prompt persists |
96
- | `codex`, `codex-app`, `agy`, `opencode`, `grok` | Full schema block prepended to the message | A short reminder of the calling convention, no schemas — but only once the conversation id has been captured; until then the full block is sent again |
97
- | `gemini`, one-shot `custom` | Full schema block prepended to the message | Full schema block again — these have no resume surface, so nothing persists between sends |
145
+ | Engine | Turn 1 | Later turns |
146
+ | -------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
147
+ | `claude` | Schemas go into the session system prompt (`--system-prompt`) | Nothing injected — the system prompt persists |
148
+ | `codex`, `agy`, `grok` (and legacy `cursor`) | Full schema block prepended to the message | A short reminder of the calling convention, no schemas — but only once the conversation id has been captured; until then the full block is sent again |
149
+ | legacy `gemini` | Full schema block prepended to the message | Full schema block again — it has no resume surface, so nothing persists between sends |
98
150
 
99
151
  The engines in the middle row resume a conversation by id, so everything injected
100
152
  stays in the transcript. Re-sending the full block each turn would grow the prompt
@@ -105,9 +157,10 @@ without it the CLI starts doing the work directly.
105
157
 
106
158
  A fresh session always gets the full block, so a thread is never created without
107
159
  the definitions — including when a session was evicted and is being recreated. A
108
- caller that changes its tool list mid-conversation also gets the full block,
109
- because the tool list is part of the session-name hash, so a different list
110
- resolves to a different session.
160
+ caller with no explicit key (no `X-Session-Id`, no `user`) that changes its tool
161
+ list mid-conversation also gets the full block, because the tool list is part of
162
+ the hashed key, so a different list resolves to a different session. With an
163
+ explicit key it does not — see [Known limitations](#known-limitations).
111
164
 
112
165
  The caller's own system prompt follows exactly the same rule on these engines: it
113
166
  is prepended to the message, and skipped only while the conversation it was sent
@@ -115,19 +168,36 @@ to is still the one being resumed. A turn that creates a conversation always
115
168
  carries it — including a `X-Session-Reset: 1` turn, which stops the existing
116
169
  session and starts a new one. "The conversation is being resumed" means the
117
170
  engine has actually announced an id (codex's `thread.started`, agy's `init` event,
118
- cursor's, grok's and opencode's session id), not merely that the session is in the
119
- manager's map: a first turn that failed before announcing one leaves a session
120
- that resumes nothing, and later turns keep receiving the full prompt.
171
+ grok's session id), not merely that the session is in the manager's map: a first
172
+ turn that failed before announcing one leaves a session that resumes nothing, and
173
+ later turns keep receiving the full prompt.
121
174
 
122
175
  See [Known limitations](#known-limitations) for the one request shape where
123
176
  `X-Session-Reset` is not honoured.
124
177
 
125
- `OPENAI_COMPAT_TOOLS_PER_MESSAGE=1` opts out: it re-sends the full block on every
178
+ `OPENAI_COMPAT_TOOLS_PER_MESSAGE=1` (also `true` or `yes`) opts out: it re-sends the full block on every
126
179
  turn for `claude` too, which is what makes a changing tool set work inside one
127
180
  session (in that mode the tool list is deliberately left out of the session hash).
128
181
  It costs the per-turn growth described above — only use it if the tool set really
129
182
  does change mid-conversation.
130
183
 
184
+ ## Tool calls on the way out
185
+
186
+ The tool block tells the model to answer a tool call with a JSON array wrapped in
187
+ `<tool_calls>` tags, `[{"name": "…", "arguments": {…}}]`. When the request carries
188
+ `tools`, the bridge parses every such block out of the reply:
189
+
190
+ - Each call becomes an OpenAI `tool_calls` entry with a fresh `call_<id>`, and the
191
+ response's `finish_reason` is `tool_calls`. Arguments given as an object are
192
+ serialised to a JSON string; a string that is not valid JSON is wrapped as
193
+ `{"input": "<string>"}`.
194
+ - Text outside the blocks becomes the message `content` (`null` when there is
195
+ none). A block that does not parse as JSON stays in the text.
196
+ - `<tool_result>` / `<tool_results>` and `<conversation_history>` blocks the
197
+ model echoes back are stripped from the text.
198
+
199
+ A request without `tools` gets the reply as plain text, unparsed.
200
+
131
201
  ## Conversation history on the way in
132
202
 
133
203
  The caller's `messages[]` can carry the whole conversation: earlier `user` turns and the engine's
@@ -166,7 +236,7 @@ What goes into the block:
166
236
 
167
237
  Replayed text has the `<` of every structural tag escaped (`</user>` becomes `&lt;/user>`): the
168
238
  block's own `<conversation_history>` / `<user>` / `<assistant>`, and also `<tool_results>` /
169
- `<tool_result>`, `<system>` and `<tool_calls>`. Forms padded with invisible or zero-width
239
+ `<tool_result>`, `<system>`, `<available_tools>` and `<tool_calls>`. Forms padded with invisible or zero-width
170
240
  characters (`</​user>`, `<user︀>`) are caught too. This stops a user message from closing its own
171
241
  turn or forging an `assistant` turn, a `<system>` block, a tool result, or a tool call. Limits:
172
242
 
@@ -198,22 +268,23 @@ With no block in front of it there is nothing to forge, so those turns go out un
198
268
  The most recent `user` turn in the block always keeps at least its first 200 characters, so the
199
269
  block is never left with only `assistant` turns.
200
270
 
201
- The cap exists because most engines (`codex`, `agy`, `grok`, `opencode`, `cursor`, `gemini`,
202
- one-shot `custom`) receive the prompt as a single command-line argument, and Linux limits one
203
- argument to 128 KiB (`MAX_ARG_STRLEN`). Going over fails the request with a 500. The 5 MiB request
204
- body limit does not protect against this. `claude`, `codex-app` and a persistent `custom` engine
205
- write over stdin instead. The cap bounds only the part of the prompt that grows with the
206
- transcript; the tool block, the system prompt and the caller's own turn are added on top.
271
+ The cap exists because `agy` and `grok` (and the legacy `gemini` and `cursor` engines) receive the
272
+ prompt as a single command-line argument, and Linux limits one argument to 128 KiB
273
+ (`MAX_ARG_STRLEN`). Going over fails the request with a 500. The 5 MiB request body limit does not
274
+ protect against this. `claude` and `codex` write the prompt over stdin instead. The cap bounds only
275
+ the part of the prompt that grows with the transcript; the tool block, the system prompt and the
276
+ caller's own turn are added on top.
207
277
 
208
278
  - **A send that threw records nothing.** The fingerprint is written only after the send returns. A
209
- send that returns an error is answered with 502 and still records, since the CLI received the
210
- prompt; only a thrown send leaves no record, so the next request replays the turns.
279
+ send that returns an error is answered with 502 (an SSE error frame when streaming) and still
280
+ records, since the CLI received the prompt; only a thrown send leaves no record, so the next request
281
+ replays the turns.
211
282
  - **A second request that arrives while the first is still in flight replays.** It sees no
212
283
  fingerprint yet, so the transcript goes out again into the session that already holds it: a
213
284
  duplicate rather than a loss, plus a lost cache prefix.
214
- - **Cost is O(n) per turn for engines that never resume.** `gemini` and one-shot custom engines have
215
- no native conversation, so the block is rebuilt and sent on every turn (capped). The same applies
216
- to any caller that creates a new session per turn.
285
+ - **Cost is O(n) per turn for engines that never resume.** The legacy `gemini` engine has no native
286
+ conversation, so the block is rebuilt and sent on every turn (capped). The same applies to any
287
+ caller that creates a new session per turn.
217
288
  - **`X-Session-Reset` replays the transcript.** A reset turn means the engine holds nothing, so the
218
289
  history goes out in full. A client that sends the header on every request and also re-sends
219
290
  `messages[]` pays for the transcript every time.
@@ -227,7 +298,7 @@ different exchange. Three shapes where the two come apart, all reachable with de
227
298
  | shape | what happens |
228
299
  | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
229
300
  | a caller whose session key hashes its latest message | every repeat of the same short confirmation resolves to whichever session that phrase opened first — often a different subject entirely |
230
- | a caller that sends no `X-Session-Id` at all | the key falls back to a hash of model + system prompt + tools, so all of that caller's concurrent chats share one name |
301
+ | a caller that sends no `X-Session-Id` and no `user` | the key falls back to a hash of model + system prompt + tools, so all of that caller's concurrent chats share one name |
231
302
  | `engine: 'claude'` — the default | `nativeThreadIsLive()` has no id to check and returns `true` for anything in the session map, so the name is the only evidence there is |
232
303
 
233
304
  So the bridge records, per session, a fingerprint of the `user` turns it has sent there, and
@@ -262,11 +333,10 @@ array:
262
333
  | **is** resuming a live conversation | only the `tool` messages after the array's last `assistant` message — everything before it is already in the transcript |
263
334
 
264
335
  Whether the engine is resuming is resolved with the same `nativeThreadIsLive()` check as the
265
- middle row of [Tool definitions](#tool-definitions-and-where-they-live). Engines with no resume
266
- surface (`gemini`, one-shot `custom`) are never in the second row, because
267
- `engineHasNativeConversation()` gates the check. `claude` (and a persistent `custom` engine) holds
268
- its context in a live process and has no separate id to check, so it is in the second row whenever
269
- the session exists.
336
+ middle row of [Tool definitions](#tool-definitions-and-where-they-live). The legacy `gemini` engine
337
+ has no resume surface, so it is never in the second row: `engineHasNativeConversation()` gates the
338
+ check. `claude` holds its context in a live process and has no separate id to check, so it is in the
339
+ second row whenever the session exists.
270
340
 
271
341
  The trailing role of the array does not enter into it — `[..., tool]`, `[..., tool, user]` and
272
342
  `[..., tool, assistant]` are read the same way, and the caller's latest `user` text, when there is
@@ -309,22 +379,29 @@ sends one round at a time.
309
379
  - **`X-Session-Reset` is ignored on a request whose last non-system message is a `tool` result.** On
310
380
  that shape the reset stops nothing and creates nothing, and the turn is treated as a resumed one:
311
381
  the system prompt is skipped if the thread is live, and tool results are scoped as for a live
312
- thread. Every other shape,
313
- `[..., tool, user]` included, honours it. To reset in the middle of a tool loop, send the header on
314
- a turn that ends in a `user` message.
382
+ thread. Every other shape, `[..., tool, user]` included, honours it. To reset in the middle of a
383
+ tool loop, send the header on a turn that ends in a `user` message.
384
+ - **With an explicit key, a changed system prompt or tool list does not reach a live session.**
385
+ `X-Session-Id` and `user` are used as the key as they are, with no fingerprint of the system prompt
386
+ or the tools, so the request lands on the existing session. On `claude` both were fixed when that
387
+ session was created. On `codex`, `codex-app`, `agy`, `opencode` and `grok`, once the thread is live,
388
+ the system prompt is not re-sent and the tools get only the short reminder. Send
389
+ `X-Session-Reset: 1` with the changed request, or use a new key. `OPENAI_COMPAT_TOOLS_PER_MESSAGE=1`
390
+ re-sends the tool schemas every turn instead; it does not change the system prompt.
315
391
 
316
392
  ## Environment variables
317
393
 
318
394
  | Variable | Default | Purpose |
319
395
  | ----------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
320
- | `OPENCLAW_SERVER_TOKEN` | (auto-generated) | Overrides the auto-generated bearer token; `disabled` turns auth off. The active token is written to `~/.openclaw/server-token` for the CLI. |
396
+ | `OPENCLAW_SERVER_TOKEN` | (auto-generated) | Overrides the auto-generated bearer token; `disabled` turns auth off. Without it, the token in `~/.openclaw/server-token` is reused, or a new one is generated and written there for the CLI. |
397
+ | `OPENCLAW_SERVER_HOST` | `127.0.0.1` | Bind address when `clawo serve --host` is not given. Use `0.0.0.0` for remote access. |
321
398
  | `OPENCLAW_RATE_LIMIT` | `300` | Max requests per IP per 60-second sliding window. |
322
399
  | `OPENCLAW_CORS_ORIGINS` | (loopback only) | Set to `*` to allow all origins (the `/v1/*` paths already do this). |
323
400
  | `OPENAI_COMPAT_NEW_CONVO_HEURISTIC` | (unset) | Set to `1` to enable webchat mode (see above). |
324
401
  | `OPENAI_COMPAT_TOOLS_PER_MESSAGE` | (unset) | Set to `1` to re-send the full tool schemas on every turn (see [Tool definitions](#tool-definitions-and-where-they-live)). Needed only when the tool set changes mid-conversation; costs per-turn prompt growth. |
325
- | `OPENAI_COMPAT_STATUS_URL` | (unset) | If set, the bridge POSTs JSON status updates to this URL (fire-and-forget, 2s timeout). See [Status webhook](#status-webhook). |
326
- | `OPENCLAW_SERVE_MAX_SESSIONS` | `32` | Max concurrent OpenAI-compat sessions in serve mode. The plugin default is 5; serve mode raises it because each distinct caller gets its own `sys-<hash>` session. |
327
- | `OPENCLAW_SERVE_TTL_MINUTES` | `60` | Idle TTL for OpenAI-compat sessions in serve mode. Idle sessions are reaped by a 60s background loop and are not resumed from disk. |
402
+ | `OPENAI_COMPAT_STATUS_URL` | (unset) | If set, the bridge POSTs JSON status updates to this `http://` URL (fire-and-forget, 2s timeout). See [Status webhook](#status-webhook). |
403
+ | `OPENCLAW_SERVE_MAX_SESSIONS` | `32` | Max concurrent sessions in `clawo serve`, bridge sessions included. The plugin default is 5; serve mode raises it because each distinct caller gets its own `sys-<hash>` session. |
404
+ | `OPENCLAW_SERVE_TTL_MINUTES` | `60` | Idle TTL for sessions in `clawo serve` (the plugin default is 120). Idle sessions are reaped by a 60s background loop; bridge sessions are not resumed from disk. |
328
405
 
329
406
  ## Inspection: `GET /v1/sessions`
330
407
 
@@ -345,7 +422,7 @@ Sample response:
345
422
  "key": "sys-a3f81c9d0b27",
346
423
  "session_name": "openai-sys-a3f81c9d0b27",
347
424
  "model": "claude-opus-5-5",
348
- "cwd": "/home/user/projects",
425
+ "cwd": "/tmp/openclaw-compat-openai-sys-a3f81c9d0b27",
349
426
  "created": "2026-04-09T03:12:18.441Z",
350
427
  "turns": 68,
351
428
  "turns_succeeded": 14,
@@ -392,8 +469,8 @@ for M in opus sonnet; do
392
469
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
393
470
  -d "{\"model\":\"$M\",\"messages\":[{\"role\":\"system\",\"content\":\"SAME\"},{\"role\":\"user\",\"content\":\"hi\"}]}" > /dev/null
394
471
  done
395
- curl -s http://127.0.0.1:18796/v1/sessions -H "Authorization: Bearer $TOKEN" | jq '.data | length'
396
- # Expected: 2
472
+ curl -s http://127.0.0.1:18796/v1/sessions -H "Authorization: Bearer $TOKEN" | jq '.data[] | {key, model}'
473
+ # Expected: two new rows with distinct keys, one claude-opus-5-5 and one claude-sonnet-5-5.
397
474
  ```
398
475
 
399
476
  **3. `X-Session-Reset: 1` resets cleanly.**
@@ -428,25 +505,38 @@ done
428
505
 
429
506
  ## Error responses
430
507
 
431
- Errors use the OpenAI error envelope:
508
+ Errors from the bridge itself use the OpenAI error envelope:
432
509
 
433
510
  ```json
434
511
  { "error": { "message": "...", "type": "invalid_request_error" } }
435
512
  ```
436
513
 
437
- | Status | When |
438
- | ------ | ---------------------------------------------------------------------- |
439
- | 400 | `messages` empty/missing, no user message, invalid `max_tokens` |
440
- | 413 | Request body over 5 MiB |
441
- | 401 | Missing or wrong bearer token (when auth enabled) |
442
- | 415 | POST without `Content-Type: application/json` |
443
- | 429 | Rate limited (`OPENCLAW_RATE_LIMIT` exceeded) |
444
- | 503 | Failed to start a new session (model unavailable, CLI crashed at boot) |
445
- | 502 | The CLI finished the turn with an error (`type: upstream_error`) |
446
- | 500 | Mid-turn failure |
514
+ | Status | `type` | When |
515
+ | ------ | ----------------------- | --------------------------------------------------------------------------------------------- |
516
+ | 400 | `invalid_request_error` | `messages` empty/missing, no user message, invalid `max_tokens` |
517
+ | 404 | `invalid_request_error` | Unknown `/v1/*` path |
518
+ | 503 | `server_error` | Failed to start a new session (session limit reached, model unavailable, CLI crashed at boot) |
519
+ | 502 | `upstream_error` | The CLI finished the turn with an error |
520
+ | 500 | `server_error` | Mid-turn failure |
521
+
522
+ The checks that run before a request reaches the bridge answer in the server's own shape,
523
+ `{ "ok": false, "error": "..." }`:
524
+
525
+ | Status | When |
526
+ | ------ | ------------------------------------------------- |
527
+ | 400 | Body is not valid JSON |
528
+ | 401 | Missing or wrong bearer token (when auth enabled) |
529
+ | 413 | Request body over 5 MiB |
530
+ | 415 | POST without `Content-Type: application/json` |
531
+ | 429 | Rate limited (`OPENCLAW_RATE_LIMIT` exceeded) |
532
+
533
+ A streamed request has already received `200` by the time the turn runs, so a turn that ends in an error
534
+ (`upstream_error`) or fails mid-turn (`server_error`) arrives as a `data: {"error": {...}}` frame followed by
535
+ `data: [DONE]`.
447
536
 
448
537
  ## Related
449
538
 
450
539
  - [getting-started.md](./getting-started.md) — install + auth setup
451
540
  - [sessions.md](./sessions.md) — what a session is and how the lifecycle works under the hood
541
+ - [multi-engine.md](./multi-engine.md) — the engines behind the model names, and how each CLI is invoked
452
542
  - [tools.md](./tools.md) — the full plugin tool surface (council, ultraplan, etc.)