* fix(status): show 0/1.0m instead of ?/1.0m on a fresh session
On a brand-new /new session the persisted totalTokens is absent
(undefined), so /status rendered the context numerator as ? via
formatTokens(null, ...). A fresh session with no usage is a known
zero, not an unknown total, so normalize undefined-but-not-stale
totals to 0 before formatting while leaving the intentional
totalTokensFresh === false stale guard (which must keep ?) intact.
Fixes#93771
* fix(status): persist fresh-session zero usage
* fix(status): identify fresh empty sessions
* fix(status): persist fresh empty session usage
* fix(status): preserve fork and compaction token state
* fix(status): preserve queued compaction token state
---------
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
* fix(google): keep parallel Gemini tool responses in the turn after the model
On Gemini < 3 vision models, a parallel tool-call turn whose non-last result
returns an image split function responses across user turns. The merge heuristic
only inspected contents[last], so the separate "Tool result image:" turn landed
between two parallel responses and stranded the second one in a fresh turn. The
turn right after the model then carried fewer functionResponse parts than the
model issued functionCall parts, so Gemini returned 400 INVALID_ARGUMENT. Because
the malformed turn is persisted, every later turn re-400s and the session sticks.
Replace the contents[last] heuristic with a run-scoped accumulator: all responses
for one model turn merge into the single user turn after it, and Gemini < 3 image
turns defer to the end of the tool-result run so they trail that response turn.
Covers both google.ts and google-vertex.ts, which share this convertMessages.
* fix(google): align provider transport tool result turns
---------
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
Treat refreshable manifest catalog rows as non-authoritative and load the owning plugin for runtime/cache-backed discovery. Adds focused regression coverage for entries-only and full discovery paths.
* fix(reasoning-tags): strip MiniMax `mm:` namespaced reasoning tags
MiniMax M3 (e.g. via Fireworks) emits its chain-of-thought inline in the
content stream wrapped in `<mm:think>…</mm:think>` rather than in a separate
`reasoning_content` field. The reasoning-tag stripper only recognized the
`antml:` namespace, so `mm:`-namespaced tags slipped through QUICK_TAG_RE and
leaked the model's hidden reasoning into visible chat output.
Accept the `mm:` prefix alongside `antml:` in the shared sanitizer
(reasoning-tags.ts) and in the Telegram reasoning-lane coordinator's tag regex
and prefix list. Adds unit tests covering mm: think/thinking/thought blocks,
truncated-open orphan close recovery, and code-fence preservation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(reasoning): handle MiniMax tags in streams
---------
Co-authored-by: DrHack1 <DrHack1@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
* feat(inbound-meta): expose message_type in trusted inbound metadata (fixes#50482)
Add resolveInboundMessageType() that extracts the media type prefix
(e.g. 'audio' from 'audio/ogg') from MediaType or MediaTypes fields.
Expose it as message_type in the inbound metadata JSON so agents can
distinguish voice messages from typed text for turn-completion heuristics.
* fix(inbound-meta): preserve per-turn source modality
---------
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
The isToolDocBlockStart function checked normalized === normalized.toUpperCase()
but normalized is already uppercased from line 24, making the condition always true.
This caused mixed-case lines ending with ':' to be incorrectly detected as doc block
starts, truncating tool descriptions unnecessarily.
Compare the original line instead to correctly detect all-uppercase headings.
Co-authored-by: Gautam Kumar <gautamkumarofficial@users.noreply.github.com>
* fix(usage): reject invalid explicit dates in usage RPC date parsing
usage.cost and sessions.usage accepted shape-valid but impossible dates such as 2026-02-30: parseDateParts validated only the YYYY-MM-DD regex, so Date.* silently rolled them over (2026-02-30 -> 2026-03-02) and the RPC returned cost/usage for the wrong day. Out-of-range parts now fail a UTC round-trip check, and an explicitly provided unparseable date (bad format or impossible calendar date) returns INVALID_REQUEST instead of silently falling back to the default range. Absent/valid dates are unchanged.
[AI-assisted]
* fix(usage): reject non-string explicit dates
---------
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
Duplicate user-message detection ran over the full branch, so when a prompt
was re-sent within the 60s window its earlier copy in the summarized prefix
and the later copy in the kept tail were both removed: the summarized copy via
summarizedBranchIds and the kept copy as a duplicate. With
truncateAfterCompaction enabled the prompt then vanished from the successor
transcript entirely. Restrict dedup to the kept region so the first surviving
copy is preserved.
* fix: pin plugin workspace dir for sessions.list to avoid O(rows) memo busting
sessions.list was O(rows) slow under concurrent agent/cron load because
each row read a process-global active plugin-registry workspace dir
that was mutated by other turns between rows. The per-row memo key
changed every time, so loadPluginMetadataSnapshot scanned fresh
(~100ms per row).
Fix:
1. Add AsyncLocalStorage-based workspace dir pinning to
runtime-workspace-state.ts — withPinnedActivePluginRegistryWorkspaceDir()
snapshots the current workspace dir for the duration of a callback.
2. Wrap listSessionsFromStoreAsync body in the pin so all per-row
metadata lookups use a stable memo key.
Fixes#90814
* test(plugins): cover request-scoped workspace pins
* fix(plugins): pin canonical runtime workspace reads
* fix(plugins): preserve workspace pins across reloads
---------
Co-authored-by: lsr911 <lsr911@github.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
The 'can preserve asynchronous provider model discovery' test was
flaky because resolveModelAsyncMock in beforeEach delegates to
resolveModelMock. When useAsyncModelResolution=true, the test
asserted resolveModelMock was not called, but the delegation
caused it to be called, failing CI on two lanes.
Fix: use a standalone vi.fn() for the async resolver in this
test, and explicitly reset resolveModelMock before the assertion
to guard against mock state leakage from prior tests.
Fixes#92117
Co-authored-by: lsr911 <lsr911@github.com>
When an assistant message's `content` is a raw string at runtime (JSONL
transcript replay passes it through even though the type declares an array),
the OpenAI-compatible completions path crashes:
- `transformMessages` called `assistantMsg.content.flatMap(...)` ->
`TypeError: ... .flatMap is not a function` (first crash, always hit).
- Two `hasToolHistory` helpers (`openai-transport-stream.ts` and
`openai-completions.ts`) called `content.some(...)` -> `TypeError: ...
.some is not a function` (siblings, surface once the flatMap crash is fixed).
Normalize a string assistant content to an equivalent single text block
before transforming (matching the string->text handling already used in
anthropic-payload-policy.ts), and `Array.isArray`-guard both `hasToolHistory`
helpers so a string assistant simply does not count toward tool history.
Verified end-to-end through the real `buildOpenAICompletionsParams` and
`streamOpenAICompletions` entry points: before the fix a string-content
assistant followed by a toolResult throws TypeError; after the fix params are
produced correctly (string preserved as text, tool history detected). Normal
array content is unaffected.
* fix(respawn): rewrite pnpm versioned entry paths to stable wrapper
During self-update the pnpm versioned directory (node_modules/.pnpm/openclaw@<ver>/)
may be removed. If process.argv contains the versioned path, the respawned child
fails to start because the entrypoint no longer exists.
Detect pnpm versioned realpaths in spawnDetachedGatewayProcess and rewrite them
to the stable node_modules/<pkg>/openclaw.mjs wrapper before spawning.
Fixes#52313
* fix(respawn): scope pnpm entry rewrite to openclaw
---------
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
* fix(wizard): preserve existing default model during setup auth choice
Without preserveExistingDefaultModel: true, the setup wizard
overwrite the user's configured default model when a new provider
auth is selected. This causes existing heartbeat turns to silently
consume paid API quota (e.g. Google Gemini) instead of the user's
original model.
The configure.gateway-auth.ts path already passes this flag; the
setup wizard path was missing it.
Fixes#64129
* fix(wizard): add type assertion for preserveExistingDefaultModel test