* [AI] fix(openai): preserve native tool calls from clean streams without finish_reason
OpenAI-compatible providers that emit delta.tool_calls during streaming but
terminate with data: [DONE] without a final finish_reason chunk (e.g. Evolink
DeepSeek V4) have their tool calls silently stripped.
Introduce sawNativeToolCallDelta (structured provider intent) and
sawStreamDONE (exact SSE data: [DONE] detection via TransformStream).
Pass sawStreamDONE as a getter so the live value is read after stream
consumption. Promotion requires (sawStopFinishReason || (sawNativeToolCallDelta
&& sawStreamDONE?.())). SSE parsing uses line-boundary-aware regex; [DONE]
inside tool arguments or content does not match.
EOF without [DONE] remains fail-closed. DSML still requires sawStopFinishReason.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes#97994
* [AI] test(openai): add fetch-wrapper loopback tests for [DONE] detection proof
Prove sawStreamDONE works through the full transport chain:
- Local HTTP server → TransformStream → OpenAI SDK → processOpenAICompletionsStream
- [DONE] without finish_reason → promoted to toolUse
- EOF without [DONE] → fail-closed (tool calls stripped)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: harden clean SSE terminal detection
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(doctor): gate cross-state-dir legacy imports behind explicit doctor opt-in
A gateway or CLI command started with OPENCLAW_STATE_DIR pointing at a
non-default directory used to import legacy files FROM the default
~/.openclaw state dir (exec-approvals.json, plugin-binding-approvals.json)
and archive the originals with a .migrated suffix. Any isolated, test, or
staging run on a host with production state silently captured and archived
the production files.
Cross-state-dir imports now run only with the new crossStateDirImports
opt-in: openclaw doctor --fix (or an interactive doctor confirm) performs
the import; the implicit CLI/gateway preflight leaves the default dir
untouched and emits a notice pointing at doctor instead.
* test(doctor): include notices in legacy-state detector mocks
* fix(agents): recover claude-cli warm-stdin continuity when no native transcript is written
The headless warm-stdin claude-cli backend (liveSession: "claude-stdio")
never writes a native transcript, so the post-turn flush probe always
fails and the missing-transcript reuse path drops the bound session id.
Part 1 (cli-runner.ts): scope the non-destructive binding behavior to
warm-stdin sessions so they keep their binding instead of clearing it
every turn.
Part 2 (attempt-execution.ts): on a missing transcript, clear the stored
binding (no stale --resume) but still return the bound id as the reuse
candidate so prepare can re-detect the missing transcript and arm
raw-transcript reseed. Returning undefined starved reseed and lost
warm-stdin continuity.
Adds/updates regression coverage in attempt-execution.cli.test.ts and a
complementary reseed test in prepare.test.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(agents): align warm-session continuity coverage
* test(agents): keep cli live-session mock complete
* fix(agents): respect stateless CLI session mode
* style(agents): keep session candidate guard focused
* test(agents): preserve minimal CLI runner fixtures
* fix(agents): preserve exact Claude warm sessions
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* perf(gateway): replace O(n^2) char loop with regex in stripDisallowedChatControlChars
Replace character-by-character iteration with a single regex replace to
avoid event-loop blocking on large messages (1MB+). The regex matches
only disallowed control characters (NUL-BS, VT, FF, SO-US, DEL) while
preserving tab, newline, CR, printable ASCII, and Unicode.
Closes#102915
* perf(gateway): use String.fromCodePoint for regex to avoid lint suppression
Replace the eslint-disable comment and hex-escape regex literal with
String.fromCodePoint() construction. This avoids adding a new entry to
the lint suppression baseline while keeping the same character set and
performance characteristics.
Ref: #102915
* refactor(gateway): consolidate chat sanitizer
---------
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>