mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-19 11:31:03 +00:00
fix/patch-cli-sync
140 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d61d6fc8a9 |
feat: add ACPX agent runtime adapters (#924)
* feat: add acpx claude runtime paths * feat: add acpx adapter preparation * refactor: use acpx adapter preparation * refactor: move openclaw image turns to adapter * fix: keep openclaw independent of host cwd * fix: address acpx review feedback * fix: preserve claude host auth in acpx |
||
|
|
d383b5e344 |
feat(eval): add claude-generated run report artifact (#892)
* feat(eval): add claude-generated run report artifact * fix(eval): install claude code cli for CI evals * fix(eval): bypass claude code tool permissions * Eval metrics configs (#932) * feat(eval): add agisdk comparison metrics configs * fix(eval): keep cdp crashes from aborting run |
||
|
|
0d56815cba |
fix: store server database under BrowserOS dir (#923)
* fix: store server database under browseros dir * fix: address PR review feedback for 923 |
||
|
|
c07d3d95d4 |
feat: add sqlite drizzle persistence (#919)
* feat: add drizzle agent schema * feat: run sqlite drizzle migrations * refactor: remove old sql identity dependency * feat: store harness agents in sqlite * build: package db migrations * refactor: remove sqlite oauth token store * feat: restore oauth token storage * fix: handle empty install id * chore: ignore server runtime state * fix: address review feedback for PR 919 |
||
|
|
1d42a973ea | refactor: extract acpx runtime templates (#918) | ||
|
|
921a797c5b |
feat: add ACPX agent soul and memory support (#917)
* feat: add acpx agent runtime context helpers * feat: add acpx runtime state store * feat: prepare acpx agent runtime context * feat: inject acpx agent command environment * feat: forward acpx agent chat cwd * fix: normalize acpx session record fallback * feat: improve acpx agent soul and memory prompts * fix: address PR review comments for memory-soul-acp * fix: satisfy acpx runtime deepscan checks |
||
|
|
d94597bbf9 |
fix(agent): add CLI model catalog entries (#915)
* fix(agent): add CLI model catalog entries * fix: address PR review comments for acpx-models |
||
|
|
974e7e9b86 |
fix(agents): hide BrowserOS ACP envelope from chat history payloads (TKT-774) (#907)
* fix(agents): hide BrowserOS ACP envelope from chat history payloads (TKT-774) The user-message text persisted on the wire carried two nested envelopes — the outer `<role>You are BrowserOS…</role>` + `<user_request>…</user_request>` block from buildBrowserosAcpPrompt and the inner `## Browser Context` + `<selected_text>` + `<USER_QUERY>` block from formatUserMessage. PR #856 had unwrapped only the outer envelope on history reads, so the user bubble in the agent rail still rendered the inner envelope, and the LLM chat-service path leaked the wrapper all the way back to the sidepanel client through AI SDK's stream sync. Two surgical fixes, both server-only: 1) ACP path (acpx-runtime.ts) — replace unwrapBrowserosAcpPrompt with a comprehensive unwrapBrowserosAcpUserMessage that strips both layers and decodes the </>/& escapes the server applied via escapePromptTagText. Each step is independently defensive (anchors that don't match are skipped) so the helper is idempotent and tolerates partial / older / future-shape envelopes. Applied in userContentToText (history mapper) and inherited by extractLastUserMessage (listing's lastUserMessage). 2) LLM chat path (chat-service.ts) — split the persisted user message from the prompt-time copy. session.agent.appendUserMessage now stores the raw user text; a transient promptUiMessages array is built with the wrapped (formatUserMessage + context-change prefix) form and passed to createAgentUIStreamResponse for the model. onFinish restores the raw form before persisting, so the user-visible message and any future history reads see only the user's typed text. Tests: - acpx-runtime.test.ts: new dedicated unwrapBrowserosAcpUserMessage suite covering fully-wrapped messages, only-outer / only-inner inputs, selected_text blocks with attribute strings, idempotency, literal user-typed angle-bracket round-trip, and an integration test that round-trips the real formatUserMessage output through the unwrap to pin the writer/reader contract. - chat-service.test.ts: existing 'rebuilds a managed-app session' test updated for the new behaviour — asserts the persisted user message is the raw text and the prompt copy passed to the agent carries the Klavis context-change notice. * fix(agents): decode entity escapes before stripping inner envelope (TKT-774) The unwrap was running its inner-envelope strips against the literal-tag form (<USER_QUERY>, <selected_text>) but the persisted payload has those tags entity-escaped (<USER_QUERY>, <selected_text>) — buildBrowserosAcpPrompt runs escapePromptTagText over the entire formatUserMessage payload before adding the outer <role>+<user_request> envelope, so the inner anchors never matched against the on-disk text and the user was still seeing <USER_QUERY> in /agents/:id/sessions/main/history responses. Reorder unwrapBrowserosAcpUserMessage to: outer-strip → decode entities → inner-strips. Test fixtures updated to reflect the actual on-wire form (escaped inner tags); the round-trip test duplicates the escape rule inline so the contract between buildBrowserosAcpPrompt and the unwrap is pinned end-to-end. |
||
|
|
fd5aba249b |
fix: stabilize OpenClaw gateway startup (#888)
* feat(server): add shared process lock helper * feat(container): add container name reconciliation helpers * feat(openclaw): serialize lifecycle across processes * fix(openclaw): reconcile fixed gateway container startup * test(openclaw): cover lifecycle race recovery * fix(server): satisfy process lock error override * fix(openclaw): address review feedback * test(openclaw): align serialization mock with image check |
||
|
|
492f3fcdf2 |
feat(openclaw): prewarm ghcr image in vm (#887)
* feat(openclaw): add gateway image inspection * feat(openclaw): pull gateway image from registry * refactor(vm): decouple readiness from image cache * refactor(openclaw): remove vm cache from runtime factory * feat(openclaw): detect current gateway image * feat(openclaw): prewarm vm runtime and reuse current gateway * feat(openclaw): prewarm runtime on server startup * refactor(vm): remove browseros image cache runtime * refactor(build-tools): remove openclaw tarball pipeline * chore: self-review fixes * fix(openclaw): suppress prewarm pull progress logs * fix(openclaw): address review feedback * fix(openclaw): resolve review findings * fix(dev): stop stale watch supervisors |
||
|
|
8712f89f18 |
feat(agents): durable per-agent chat message queue + composer Stop (#880)
* feat(agents): durable per-agent chat message queue + composer Stop button
* fix(agents): tighten queue UI — smaller Stop, drop empty indicator, live drain attach
User feedback round 1 on the message-queue UX:
1) The Stop button matched the send/voice mics at h-10 w-10 with a
solid destructive fill, which read as alarming. Shrunk to h-8 w-8,
ghost variant with a soft destructive/10 background, smaller
filled square glyph. Reads as a calm 'stop' affordance instead of
a panic button.
2) The QueueItem's leading <QueueItemIndicator> dot was decorative
only — no state, no interaction. Dropped it from QueuePanel along
with the import; queue items now render as a clean preview line
with the trailing X remove action.
3) When the server drained the queue and started the next turn, the
chat panel didn't pick up the live stream until the user
navigated away and back. The hook's resume effect previously
only fired on agent change, not on listing-observed activeTurnId
change. Surface activeTurnId from useHarnessAgents into
useAgentConversation; effect now re-runs when the id changes,
calls /chat/active, and attaches to the new turn — so a queued
message starts streaming the moment the server drain pops it.
* fix(agents): don't reset streaming state from the resume effect's no-op paths
The Stop button was disappearing while the agent was actively
streaming, even though events were still flowing into the chat. Root
cause: the resume effect's `finally` block reset `streaming`,
`turnIdRef`, and `lastSeqRef` unconditionally — including on the
early-return paths (no active turn, or another mechanism already
owns the stream).
Sequence that triggered it:
1) User sends a message → send() sets streamAbortRef + streaming=true
and starts consuming the SSE.
2) User enqueues another message → enqueue mutation invalidates the
listing query.
3) Listing refetches with the live activeTurnId → the resume
effect re-fires (deps include activeTurnIdDep).
4) attemptResume hits `if (streamAbortRef.current) return` because
send() owns it.
5) The finally clause fires anyway and calls setStreaming(false),
clobbering the live state set by send(). The SSE consumer keeps
running (refs are intact) so text keeps streaming, but the React
flag is wrong, so the Stop button gates off.
Fix: track whether *this* run actually started a stream
(`weStartedStream`). The finally only resets state when it does.
Early-return / no-active-turn paths now leave streaming/turnIdRef/
lastSeqRef alone for whoever does own them.
Also widens the Stop button's visibility (`canStop` prop on
ConversationInput) so it stays steady across the brief gap between
turns when a queue drain is mid-flight; the parent computes
`streaming || activeTurnId !== null || queue.length > 0`. The
visibility widening is independent of the streaming-state fix above
— both are now in place.
* revert: drop canStop widening — Stop only shows while streaming
Reverts the canStop prop on ConversationInput and the OR-with-queue
visibility from AgentCommandConversation. Stop is gated solely on
`streaming` again. Between turns (queue draining) the button stays
hidden — only the actively-streaming turn is interruptible from the
composer, which matches what the user actually expects.
* fix(agents): persist the kicking-off prompt on active turns so the resume placeholder isn't empty
When a queued message drained and started a new turn, the chat
panel's resume effect staged a placeholder turn with userText: ''
because the hook had no way to know what message kicked off the
turn — only the agent-side stream was visible, and the user bubble
above it was blank until the user navigated away and back (at which
point the session record's history loaded normally).
Fix: ActiveTurnRegistry.register now accepts an optional `prompt`
that's stashed on the turn and surfaced via describe() / the
ActiveTurnInfo response. AgentHarnessService.startTurn passes the
incoming message into register. /chat/active returns it. The chat
hook's resume effect uses active.prompt as the placeholder
turn's userText, so the user bubble shows the queued message text
the moment streaming begins. Falls back to '' for older clients
that haven't been refetched yet.
* fix(agents): always release streamAbortRef on resume cleanup, even when cancelled
Greptile P1 follow-up. The previous `weStartedStream` guard correctly
stopped the resume effect's no-op early-returns from clobbering an
in-flight `send()` stream — but it also stopped a *cancelled*
mid-stream resume from clearing its own `streamAbortRef`. When the
cleanup fires (e.g. the 5s listing poll captures a new queue-drain
turn id while the SSE for the prior turn is still finishing), the
next effect run hits the `if (streamAbortRef.current) return` guard
against the now-aborted controller and never reattaches, leaving
`streaming === true` with no live stream until the user navigates
away.
Split the finally block: always release `streamAbortRef` when we
owned the controller (so the next run can take over), but only
reset the streaming flag / turn id / lastSeq on a clean exit (the
new run will set those itself, so resetting on cancel would just
flicker).
|
||
|
|
ba60bf466f |
feat(agents): rich command-center rows + home grid + dead-code sweep (#879)
* feat(agents): rich-info command center rows + pin/PATCH/adapter-health backbone
Splits AgentRowCard from a 271-line monolith into a shallow tree of
single-responsibility sub-components under `agent-row/`:
AgentTile, AdapterHealthDot, PinToggle, AgentTitleRow,
AgentSparkline, AgentSummaryChips, AgentLastMessage, CwdChip,
AgentTokenSummary, AgentMetaRow, AgentErrorPanel, AgentActions
Adds the data each row consumes:
- pinned: boolean field on AgentDefinition + FileAgentStore.update
+ new PATCH /agents/:id route. useUpdateHarnessAgent mutation
optimistically updates the listing cache so the star flips
instantly; rolls back on error.
- Listing payload extended with lastUserMessage, cwd, tokens
(cumulative + last7d shape — last7d zero-filled until the
activity ledger lands), turnsByDay/failedByDay (zero-filled),
lastError/lastErrorAt, activeTurnId. AcpxRuntime grows a
getRowSnapshot() that reads cwd + cumulative tokens + last user
message from the session record in one pass.
- Adapter health: in-memory AdapterHealthChecker probes
`claude --version` / `codex --version` with a 2s timeout and
caches results for 5 min. /adapters response carries
{ healthy, reason?, checkedAt }. Tile-corner dot exposes the
state via HoverCard; openclaw inherits health from the gateway
snapshot already on the page.
Sub-components are pure: card itself owns no state. Sort order
becomes pinned-first, then recency. HoverCard is the workhorse for
keeping rows compact while exposing depth (full message, token
breakdown, daily turn list, error stack, adapter reason).
* refactor(agents): tighten command-center row design + cut redundant affordances
User feedback round 1:
1) Two green dots on the tile (health + liveness) was confusing. Health
moves out of the tile entirely and surfaces as an inline 'Unavailable'
chip in the model line — silent when the adapter is healthy, with a
warning amber chip + HoverCard reason when not. The tile now shows
one signal: liveness.
2) The last-user-message HoverCard wasn't telegraphing intent. Drop the
HoverCard. The line is informational, italic, with a leading quote
glyph so the row reads like a conversation snippet. To see the full
message the user opens the chat (which is the action they want next
anyway).
3) Resume + Chat were duplicate CTAs. Single primary action per row:
Resume (filled, accent-orange, with a pulsing dot) replaces Chat
when there's an active turn. Both navigate to /agents/:id but the
row tells the user which action they're taking.
4) Tokens weren't visible because the row gated on last7d.requestCount,
which is zero until the activity ledger ships. Switch to lifetime
tokens (which we have today). Drop the '7d stats:' framing — talking
about a window we can't compute would be misleading. The HoverCard
surfaces input/output split + a footnote that per-window stats land
in a follow-up.
5) CWD was rendering the server's own running directory, which is
meaningless to users. Hide it from the row entirely. The cwd field
still rides in the listing payload for future surfaces (chat panel,
debug view) — only the row stops rendering it.
Aesthetic refinements while we're here:
- Whole card carries state, not just the tile: working rows get an
accent-orange tinted border with a soft glow, error rows tint
destructive, idle rows lift on hover.
- Pin star fades in on hover (group-hover) when unpinned and stays
solid amber when pinned — keeps the rail calm by default.
- Tabular-nums on token figures so columns visually align across rows.
- Drop CwdChip and AdapterHealthDot files: no callers left.
* fix(agents): align row title flush-left whether pinned or not
Pin star moved from leading the title to trailing the badges, and
hidden from layout entirely (`hidden group-hover:inline-flex`) when
unpinned. The previous `opacity-0` rule kept the star reserving its
`size-6` slot, which left every unpinned title indented relative to
the model / preview / meta lines underneath it. Title now flushes
left in both states; pinned star stays solid amber so the signal
isn't hidden, and unpinned reveals an outline star on row hover for
the toggle affordance.
* fix(agents): keep pin-toggle slot reserved so row height is constant
Switching the unpinned star from `hidden group-hover:inline-flex`
to `opacity-0 group-hover:opacity-100`. The hidden/show variant was
collapsing the title row's height when the star wasn't rendered,
which made every card below visibly shift on hover. Always rendering
the button (with opacity-only visibility) keeps the row's vertical
metrics constant; the title still flushes left because the slot is
trailing, not leading.
Card hover effect (-translate-y + shadow-md) restored — the layout
shift wasn't coming from the card hover; it was the pin slot
appearing and disappearing.
* fix(agents): quieten row hover — border-tint only, no lift, no shadow
Drop the `-translate-y-px` and `hover:shadow-md` from the row card
plus the working-state inner ring. The translate + shadow grow
combination was visibly noisy as the cursor moved through the rail —
each row 'lifted' as you passed over it. Hover now just tints the
border in accent-orange/30; working and error states keep their
distinct border colours but no inner ring. Card height and shadow
stay constant in every state, so the rail reads as a calm vertical
list of cards.
* feat(home): rich Recent Agents grid + dead-code sweep
The /home Recent Agents grid was a placeholder shell. Every 'rich'
field on the card (lastMessage, lastMessageTimestamp, activitySummary,
currentTool, costUsd) was wired to undefined because AgentCommandHome
called `buildAgentCardData(agents, status?.status, undefined)` — the
dashboard arg has been hard-coded undefined since the harness
migration. Repointing the grid at `useHarnessAgents` + `useAgentAdapters`
gives every card the same enriched data the rail uses.
What the new card shows per agent:
• Adapter glyph tile + liveness dot (working pulses; asleep is
hollow; error is red)
• Name + Working pill (when active)
• Adapter · model · reasoning summary line, with an inline
Unavailable chip + HoverCard reason when the adapter binary
isn't on $PATH
• Italic last-user-message preview (line-clamp-2, leading quote
glyph) — same visual language as the rail
• Footer: 'X ago' + state chip (Asleep / Attention) OR a Resume
button (orange, with pulsing dot) when activeTurnId is non-null
Sort on the home grid is active-turn → recency. Pinning is NOT a
sort key here (and there's no pin indicator on the card) — pinning
belongs to the rail at /agents; the home page is action-oriented
and trusts active-turn + recency to surface the right agent.
Dead code removed:
• useAgentDashboard.ts (96 lines, no callers; subscribed to the
dead /claw/dashboard/stream from the OpenClaw-only era)
• useAgentCardData.ts (the dashboard-merge shim; passed undefined
every call so all enriched fields landed as undefined)
• AgentCard.tsx (AgentCardExpanded replaced by HomeAgentCard;
AgentCardCompact had no callers — the dock's compact mode was
never used)
• AgentCardData interface dropped from lib/agent-conversations/
types.ts; the new card consumes HarnessAgent directly
Visual language stays continuous between rail and grid: same
<AgentTile>, same <LivenessDot>, same italic-quote message
preview, same orange Resume button with a pulsing dot.
|
||
|
|
df0f45dd29 |
Feat: eval debug dev ci (#869)
* chore(eval): instrument server startup to root-cause dev CI health-check timeouts Three diagnostics + one config swap to investigate why the eval-weekly workflow has been failing on dev since 2026-04-25 with "Server health check timed out" (every worker, every retry). Background: - Last successful weekly eval on dev: 2026-04-18 (sha |
||
|
|
edfc5c751c |
fix: align OpenClaw gateway image with VM cache (#868)
* fix: load OpenClaw gateway image from VM cache * fix: use container port for OpenClaw ACP bridge * fix: address review feedback for PR #868 |
||
|
|
471256f31c | fix: stop passing native permission flags to ACP adapters (#867) | ||
|
|
4c90ca696b | fix(agents): connect OpenClaw ACP inside gateway container (#866) | ||
|
|
f2ac87d7c3 |
feat: show created agents in sidepanel (#865)
* feat(agent): list created agents in sidepanel target catalog * feat(agent): show created agents in sidepanel selector * feat(server): add sidepanel chat route for created agents * feat(agent): route sidepanel agent sends by agent id * chore(agent): retire virtual sidepanel acp targets * fix: address review feedback for PR #865 |
||
|
|
a228c278c6 |
feat(agents): background-resilient chat — turns survive tab disconnect (#863)
* feat(agents): decouple chat turn lifecycle from SSE response
Introduce a per-process ActiveTurnRegistry that owns each agent turn's
lifecycle and a ring-buffered event stream, so chat tabs that close,
refresh, or navigate away no longer cancel the in-flight turn. New
endpoints:
POST /agents/:id/chat starts a turn (now returns 409 when
one is already running, with the
active turnId for attaching)
GET /agents/:id/chat/active reports the running turn for a UI
that just mounted
GET /agents/:id/chat/stream subscribes to a turn; supports
Last-Event-ID resume via per-event
seq ids
POST /agents/:id/chat/cancel explicit cancel — fetch abort no
longer affects the underlying turn
The chat hook now captures X-Turn-Id, tracks lastSeq from SSE id lines,
re-attaches on mount when the server still has an active turn, and
routes Stop through the cancel endpoint. The runtime call uses the
registry's per-turn AbortController instead of the HTTP request signal,
which is the core decoupling that lets turns outlive their initiator.
* feat(agents): add ActiveTurnRegistry primitive backing the new chat lifecycle
The previous commit referenced these files in tests and the harness
service but global gitignore swallowed them on the first add.
The registry owns the per-turn ring buffer (drop-oldest, terminal frame
preserved), the per-turn AbortController, and subscriber fan-out used
by /chat/stream resume.
|
||
|
|
e2ec1991cf |
feat(agents): redesign the agent command center for multi-adapter use (#861)
* feat(agents): redesign agent rail to match the rest of the app
Reshape the `/agents` page so it reads as a sibling of `/scheduled`
and `/soul` and adapts to the multi-adapter world (OpenClaw, Claude
Code, Codex). Visual scaffolding only in this commit — per-agent
liveness state ships as `unknown` until the server-side activity
tracker lands.
- New `AgentsHeader` mirrors `SoulHeader`/`ScheduledTasksHeader`:
accent bot tile, title, descriptive subtitle, "+ New Agent"
button. Replaces the loose top toolbar that mixed page-level and
OpenClaw-lifecycle controls.
- New `GatewayStatusBar` collects the OpenClaw lifecycle pills
(running, control plane connected) plus the Terminal/Refresh
affordances into a single labeled bar that only renders when the
gateway is running AND there is at least one OpenClaw agent in
the merged list.
- New `AgentRowCard` per agent: adapter tile with liveness dot,
name + status badge, adapter/model/reasoning chips, last-used
relative time + truncated workspace path, primary "Chat" button,
overflow menu (Copy id / Rename* / Reset history* / Delete).
Rename + Reset are disabled with "coming soon" tooltips until
the corresponding endpoints ship; Delete is hidden for the
protected `main` agent.
- New `AgentsEmptyState` mirrors the scheduled-tasks empty card.
- New `AdapterIcon` + `LivenessDot` + `agent-display.helpers.ts`
keep the row card focused on layout; helpers cover display name
fallbacks for legacy `oc-<uuid>` titles, workspace label rules,
and a tiny relative-time formatter.
- `AgentList` now sorts by `lastUsedAt` desc with `null`s falling
to the bottom; the gateway's `main` agent is pinned to the top
only while it has zero turns so a fresh install has an obvious
starting point. The list also threads a per-agent activity map
so future commits can light up working/idle/asleep without
reshuffling the API.
- `AgentsPage` swaps to the standard `fade-in slide-in-from-bottom-5
animate-in space-y-6 duration-500` shell and threads a
`harnessAgentLookup` Map down to the row card so adapter chips
and reasoning effort render correctly without a re-fetch.
* feat(agents): wire per-agent liveness end-to-end into the rail
Closes the placeholder `unknown` dot from the redesign's first
commit. The rail now shows real working / idle / asleep / error
states per agent, with `lastUsedAt` driving the recency sort.
Server side:
- `AgentHarnessService` keeps an in-memory activity tracker keyed
by agentId. `notifyTurnStarted` flips an entry to `working`,
`notifyTurnEnded({ok})` either drops it (success) or pins it to
`error` (failure / error event).
- `send()` wraps the runtime stream so the lifecycle hook fires
exactly once on natural close, error event, downstream cancel,
or thrown setup. The runtime itself stays unchanged — fork is
contained at the harness layer.
- New `listAgentsWithActivity()` method enriches every agent with
`{ status, lastUsedAt }`. lastUsedAt is read from the acpx
session record's last persisted item via `runtime.getHistory`,
so it survives server restart even though the activity map
doesn't.
- Status derivation: `working`/`error` take precedence; otherwise
timestamp-based — `idle` until 15 min of silence, then `asleep`.
Never-used agents resolve to `idle` (asleep implies "was active,
went quiet").
- `GET /agents` returns the enriched shape.
Client side:
- `HarnessAgent` UI type extended with optional `status` +
`lastUsedAt` so older deployments still typecheck.
- `useHarnessAgents` flips on `refetchInterval: 5_000` (with
`refetchIntervalInBackground: false` so hidden tabs go quiet)
so the per-row dots and last-used copy stay fresh without a
websocket.
- `AgentsPage` builds an activity map from the harness listing
response and threads it into `AgentList` → `AgentRowCard`. The
sort by `lastUsedAt` desc (already in the row card) now has
real data to operate on.
Tests:
- New `marks an agent working while a turn streams and idle once
it ends` exercises the wrap; uses a held upstream stream so
the in-flight `working` state is observable.
- New `flips to error when a turn emits an error event`.
* fix(agents): dedupe agent rail when /claw/agents and /agents share an id
The agents page was rendering every OpenClaw agent twice — once from
the legacy `/claw/agents` listing (`useOpenClawAgents`) and once from
the harness `/agents` listing (`useHarnessAgents`). Post Step 9
backfill the harness store contains every gateway agent, so the
overlap is the rule, not the exception.
Mirror the dedup the chat-panel layout already does: when a gateway
agent's id appears in the harness listing, drop the legacy entry and
keep the harness one (it has adapter/model/reasoning/status/lastUsedAt
the chat path actually consumes).
* feat(agents): swap GatewayStatusBar refresh icon for a Restart Gateway button + tooltips
The manual refresh became redundant once `useHarnessAgents` and
`useOpenClawStatus` started polling on a 5s interval — every visible
field self-refreshes within seconds. The previous AgentsPageHeader
had a real Restart action that the redesign dropped; reinstate it on
the bar so a wedged gateway is one click away again.
- GatewayStatusBar: dropped the `RotateCcw` refresh icon and the
`onRefresh` prop. Added `onRestart` + `actionInProgress` props;
the button shows a spinner while a gateway lifecycle mutation is
in flight.
- Both Terminal and Restart Gateway buttons get tooltips explaining
what they do — Terminal as a power-user shell escape hatch,
Restart for unsticking a wedged gateway or after manual config
edits.
- AgentsPage: drop the now-unused `refreshAll` helper and the
`refetchStatus`/`refetchAdapters`/`refetchOpenClawAgents`
destructures it depended on. Wire `restartOpenClaw` (already
pulled from `useOpenClawMutations`) through
`runWithPageErrorHandling` like the legacy header did.
* feat(agents): consolidate gateway status into the /agents listing
Folds the gateway lifecycle snapshot into the harness listing so the
agents page polls one endpoint instead of two. Drops the dead
`/claw/status` call from the command center while keeping every UI
affordance the page already shipped (Running / Control plane
connected pills, GatewayStateCards setup/start prompts,
ControlPlaneAlert for degraded states).
Server side:
- `OpenClawProvisioner.getStatus()` (optional) — when wired, returns
the same `GatewayStatusSnapshot` shape `/claw/status` does.
- `AgentHarnessService.getGatewayStatus()` — best-effort wrapper
around the provisioner method; logs and swallows errors so a
transient gateway issue doesn't 500 the listing endpoint.
- `GET /agents` now returns `{agents, gateway}` in a single
`Promise.all`. Both fields are independent — agents enrichment
succeeds even if the gateway snapshot is null.
- `server.ts` wires `getOpenClawService().getStatus()` into the
provisioner accessor object alongside `createAgent` /
`removeAgent` / `listAgents`.
Client side:
- `useHarnessAgents` returns `{harnessAgents, gateway}` (plus the
legacy `agents` mapping). Same 5s `refetchInterval` as before —
one round-trip drives the per-row liveness AND the gateway pills.
- `AgentsPage` drops `useOpenClawStatus` entirely; `status` comes
from the harness query. Loader + error/lifecycle plumbing
rewired around the harness query's loading/error.
- `agents-page-utils.getInlineError` and `getAgentsLoading` lose
the now-redundant `statusError` / `statusLoading` /
`openClawAgentsEnabled` params.
The chat-panel layout (`agent-command-layout.tsx`) still consumes
`useOpenClawStatus(5000)` for now — left intact per the user's "only
the command center" scope. Folding that one in is a separate,
smaller pass once we're sure no regression slipped here.
* test(agents): teach the route fake service about the new listing shape
PR #861 CI surfaced two failures in tests/api/routes/agents.test.ts:
both call \`GET /agents\` and the route handler now invokes
\`service.listAgentsWithActivity()\` + \`service.getGatewayStatus()\`
which the fake created here didn't implement. Add both methods to
the fake (returning idle / null) and update the empty-list assertion
to expect the new \`{agents, gateway}\` envelope.
|
||
|
|
0c84547e8f |
feat(agents): migrate OpenClaw chat onto the unified harness/ACP path (#859)
* chore(acp): smoke-test ACP capabilities against running gateway
Adds apps/server/scripts/acp-smoke.ts which spawns `openclaw acp`
inside the gateway container and exercises every method we plan to
depend on: initialize, newSession, prompt (text + image), cancel,
listSessions, loadSession.
SDK pinned to 0.19.1 (Bun's minimum-release-age policy blocks 0.20+
which were released < 7 days ago).
Findings (full notes in plan outcomes):
- promptCapabilities advertises image:true but the model does NOT see
image bytes — silently dropped at the bridge.
- sessionCapabilities advertises {list:{}} but session/list throws
"Method not found": stale capability advertising.
- loadSession works; replays user/assistant/thought text and
session_info/usage/commands updates. No tool_call replay, as
documented.
- cancel works end-to-end: stopReason=cancelled.
- closeSession/resumeSession are not on ClientSideConnection in
0.19.1; kill child to close, use loadSession for rebind.
Plan revisions triggered by spike are recorded in
plans/browseros-ai/BrowserOS/features/2026-04-28-2310-claude-code-acp-implementation-roadmap.md.
* chore(acp): re-run smoke on SDK 0.21.0 and add mode/config/auth scenarios
After bypassing Bun's minimum-release-age and upgrading the SDK to
0.21.0, restore the previously-skipped resume/close paths and add
three new scenarios: mode (setSessionMode), config (setSessionConfigOption,
correct configId field), and auth (authenticate noop).
Findings, all bridge-side (independent of SDK):
- session/list, session/resume, session/close all throw -32601 on
OpenClaw 2026.4.12 — capability advertising is stale.
- Image content blocks silently dropped; model never sees the bytes.
- setSessionMode and setSessionConfigOption work; latter requires
`configId` (not `optionId`) per the schema.
- loadSession replays user/assistant/thought text + session_info +
usage + available_commands; no tool_call replay (documented).
- authenticate is a noop on OpenClaw (no authMethods advertised).
Plan outcomes updated with full method-support matrix.
* chore(deps): promote @agentclientprotocol/sdk to a runtime dependency
The smoke script in apps/server/scripts/acp-smoke.ts used the SDK as
devDependency. The upcoming ACP bridge (apps/server/src/api/services/acp/)
needs it at runtime, not just for tooling. Move the entry from
devDependencies to dependencies, alphabetically first under @a*.
Pinned to 0.21.0 — same version the smoke script validated against.
README gains a small Dependencies note pointing at the future bridge
location.
No code changes yet. The bridge wiring lands in subsequent commits.
* fix(openclaw): wire LlmProvider.supportsImages through to OpenClaw model config
When BrowserOS sets up a custom OpenAI-compat provider on the gateway,
the agent UI's "Supports Image" flag (LlmProviderConfig.supportsImages)
was being dropped on the floor. As a result the persisted model entry
had no `input` field, OpenClaw defaulted it to ['text'], and image_url
content parts were silently stripped before the model saw them.
Fix:
- Extend OpenClawSetupInput / OpenClawAgentMutationInput on the agent
side (useOpenClaw.ts) and the route body schema + SetupInput +
createAgent input on the server side with `supportsImages?: boolean`.
- AgentsPage forwards `llmOption?.supportsImages` from the selected
LlmProviderConfig in both handleSetup and handleCreate.
- provider-map.resolveSupportedOpenClawProvider emits
`input: ['text', 'image']` on the model entry when the flag is
truthy; otherwise emits the explicit `['text']` so the value is
always pinned (avoids relying on OpenClaw's implicit default).
- applyBrowserosConfig adds `tools.media.image.enabled = true` to the
bootstrap batch so the gateway's image-understanding pipeline is
always wired up — per-model `input` still gates which models see
images, this just enables the global path.
ACP image content blocks are still dropped by the OpenClaw bridge —
that's a separate bridge bug, not addressed here. This commit
restores image support for the OpenAI-compat /v1/chat/completions
path that the upcoming ACP chat panel will use as a carve-out for
image-bearing prompts.
Existing custom-provider configs are NOT auto-migrated; users will
re-acquire image support either by re-running setup or by editing
their model entries' `input` field manually. A migration pass for
legacy installs is not in scope for this commit because the
"supportsImages" intent isn't recoverable from the persisted config
alone — the source of truth is the LlmProvider record on the agent
side.
* feat(agents): add OpenClaw to AgentAdapter union and catalog
Extends AgentAdapter to 'claude' | 'codex' | 'openclaw' and adds the
OpenClaw entry to AGENT_ADAPTER_CATALOG. The new entry has:
- defaultModelId: 'default' — OpenClaw's ACP bridge does not surface
per-session model selection (verified during the ACP spike), so
models live in the OpenClawService config, not in the adapter
catalog. AgentDefinition.modelId carries the gateway-side model
name for display only.
- models: [] — empty list signals "no per-session model picker" in
the UI; isSupportedAgentModel('openclaw', undefined|'default')
returns true via the existing fallback path.
- reasoningEfforts mirror OpenClaw's session-level `thought_level`
config option (off / minimal / low / medium / high / adaptive).
Also extends:
- isAgentAdapter type guard recognizes 'openclaw'
- HarnessAgentAdapter union on the extension side
- agents.test.ts createAgent fake type
- agent-catalog.test.ts asserts on the new entry, empty model list
passthrough behavior, and OpenClaw's reasoning effort set
Lockfile delta is the workspace SDK pin reconciling 0.20.0 (taken
from dev's lock) up to our package.json's 0.21.0 (added in
|
||
|
|
2ff5c12840 |
feat: add sidepanel ACP chat targets (#857)
* feat(agent): add sidepanel chat target catalog * feat(agent): show acp models in sidepanel selector * feat(server): adapt acp events to ui message streams * feat(server): add sidepanel acp chat route * feat(agent): route sidepanel chat through acp targets * chore: self-review fixes * fix: address review feedback for PR #857 |
||
|
|
d87422eea1 | fix: hide BrowserOS ACP wrapper in history (#856) | ||
|
|
1946ca0cf8 | chore: clean up unused agent sdk (#855) | ||
|
|
754f7d0e1d | test: cover terminal limactl resolver errors (#854) | ||
|
|
85bb3f7b42 | fix: avoid eager limactl resolution in server tests (#853) | ||
|
|
cb32b8191d |
fix: show rich ACP harness history from ACPX (#852)
* fix: load ACP harness history from ACPX * fix: address ACP history review comments |
||
|
|
7a92654abc |
feat: add BrowserOS MCP to ACP agents (#851)
* feat: add BrowserOS MCP to ACP agents * fix: bypass ACP agent permissions * fix: address review feedback for PR #851 |
||
|
|
91d3285aa0 |
feat: add ACP agent harness (#849)
* feat: add acp agent runtime spike * feat: add agent harness catalog * feat: persist harness agents in json * feat: persist agent transcripts * feat: route harness service through agent records * feat: expose generic agent harness routes * feat: add harness agent frontend api * feat: create harness agents from agents page * feat: chat with persisted harness agents * chore: remove obsolete agent profile spike * chore: self-review fixes * fix: combine openclaw and harness agents UI * refactor: split agents page components * fix: hide persisted harness turns |
||
|
|
6b9945f933 | feat(dev): use dev dock icon for browser launches (#848) | ||
|
|
6a5a7775a9 |
fix(openclaw): wire LlmProvider.supportsImages through to OpenClaw model config (#846)
When BrowserOS sets up a custom OpenAI-compat provider on the gateway, the agent UI's "Supports Image" flag (LlmProviderConfig.supportsImages) was being dropped on the floor. As a result the persisted model entry had no `input` field, OpenClaw defaulted it to ['text'], and image_url content parts were silently stripped before the model saw them. Fix: - Extend OpenClawSetupInput / OpenClawAgentMutationInput on the agent side (useOpenClaw.ts) and the route body schema + SetupInput + createAgent input on the server side with `supportsImages?: boolean`. - AgentsPage forwards `llmOption?.supportsImages` from the selected LlmProviderConfig in both handleSetup and handleCreate. - provider-map.resolveSupportedOpenClawProvider emits `input: ['text', 'image']` on the model entry when the flag is truthy; otherwise emits the explicit `['text']` so the value is always pinned (avoids relying on OpenClaw's implicit default). - applyBrowserosConfig adds `tools.media.image.enabled = true` to the bootstrap batch so the gateway's image-understanding pipeline is always wired up — per-model `input` still gates which models see images, this just enables the global path. ACP image content blocks are still dropped by the OpenClaw bridge — that's a separate bridge bug, not addressed here. This commit restores image support for the OpenAI-compat /v1/chat/completions path that the upcoming ACP chat panel will use as a carve-out for image-bearing prompts. Existing custom-provider configs are NOT auto-migrated; users will re-acquire image support either by re-running setup or by editing their model entries' `input` field manually. A migration pass for legacy installs is not in scope for this commit because the "supportsImages" intent isn't recoverable from the persisted config alone — the source of truth is the LlmProvider record on the agent side. |
||
|
|
ddbb2cf492 |
feat(agent): composer attachments + server-side outbound message queue (#826)
* feat(agent): attach images and text files to chat messages
Adds end-to-end support for image and text file attachments in the chat
composer, with the staged files round-tripping through the OpenClaw
gateway as OpenAI-compatible content blocks and persisting in the JSONL
so they show up in the historical view.
Server
- HTTP client: new OpenClawChatContentPart union and a buildUserContent
helper that emits multimodal content arrays when messageParts is
supplied, falls back to the legacy string content otherwise.
- Service: chatStream takes an optional messageParts array and forwards
it; BrowserOSChatHistoryItem gains an attachments field.
- JSONL reader: PiContentBlock learns the OpenAI image_url and Anthropic
image source/data shapes; user messages now emit user.attachment
events that the history mapper accumulates onto the next user item.
- Route: validates an inbound attachments[] (kind/mime/size/count),
inlines text-shaped files as <attachment> blocks in the message body,
attaches images via image_url parts. Replaces the immediate 409 on
active monitoring session with a 30s waitForSessionFree(agentId) wait
(registry now exposes onSessionEnd) so cron/hook contention does not
reject a user-chat send outright. Returns 503 if the wait times out.
Client
- New lib/attachments.ts: validateAttachment / compressImageIfNeeded
(canvas downscale to 2048px long edge, JPEG 0.85 re-encode for >1.5
MB inputs) / stageAttachment / stageAttachments that produces the
staged-attachment shape the composer renders and the payload the
server accepts.
- ConversationInput: drag-and-drop, paperclip button, clipboard paste,
staged attachment chip strip with thumbnails for images and a
paperclip+name chip for text files. Send button enables on either
text or attachments. Drop-zone overlay during drag.
- chatWithAgent forwards attachments[]; useAgentConversation.send
accepts a SendInput shape and renders user attachments on the
optimistic streaming turn via MessageAttachments / MessageAttachment.
- ClawChatMessage groups historical attachment parts into a single
MessageAttachments strip, ordered before reasoning/tools/text.
- claw-chat-types adds an attachment ClawChatMessagePart variant; the
history mapper emits attachment parts first and skips the text part
when the user only sent media.
- AgentCommandHome forwards the new SendInput shape — home composer
drops attachments at the boundary in v1 (the conversation page is
where staging is most useful; carrying bytes through the URL bar
is not sensible).
Limits: 10 attachments per message, 5 MB per image (post compression),
1 MB per text file, mime types png/jpeg/webp/gif and text/* +
application/json. PDFs and other binaries are deferred to v2.
* feat(agent): outbound message queue for chats while agent is mid-turn
Lets users keep typing and submitting messages while the agent is still
streaming a previous turn. Each press is appended to a single-flight
queue and dispatched as soon as `streaming` flips false; the queued
state renders as a strip above the composer so the user sees what's
pending vs. what's already sending.
- New `useOutboundQueue` hook owns the queue, the worker effect, and
cancel/retry actions. Single-flight by design — a re-entrancy ref
guard prevents two simultaneous dispatches when `streaming` flickers.
- Composer (`ConversationInput`) accepts optional `outboundQueue`,
`onCancelQueued`, `onRetryQueued` props. When the queue is provided
the send-button gate stops blocking on `streaming`; the spinner stays
as the visual cue that the agent is still busy. Legacy direct-send
callers keep the old streaming-blocks-send semantic.
- Renders an OutboundQueueStrip above the staged-attachment strip with
per-item status (queued / sending / failed), a cancel button on
queued items, and retry + discard on failed items.
- AgentCommandConversation wires `onSend` to `queue.enqueue` and routes
the home composer's `?q=` initial-message handoff through the queue
too, so it inherits the same single-flight serialization.
The server-side `waitForSessionFree` (added with attachments) and this
client-side queue together cover both contention sources: cron / hook
turns and back-to-back user sends. Persistence across reloads is
intentionally out of scope for v1 — losing the queue on extension
reload is documented as a known limitation.
* feat(server): server-side outbound message queue
Replaces the client-only React-state queue from
|
||
|
|
711934555d |
feat(agent): enrich chat UI with tool activity, reasoning duration, and cost (#825)
* feat: pass per-turn cost and token data through chat history items
- Add costUsd, tokensIn, tokensOut to BrowserOSChatHistoryItem (server)
- Pass through from JSONL agent.message events in jsonlEventsToHistoryItems()
- Add same fields to client-side BrowserOSChatHistoryItem and ClawChatMessage
- Map cost/token data in mapHistoryItemToClawMessage()
Data flows: JSONL message.usage → server history item → API response →
client ClawChatMessage. Available for rendering in ClawChatMessage
component (message toolbar, cost badges).
* feat: add message toolbar with copy button and per-turn cost display
Add MessageToolbar to historical assistant messages in ClawChatMessage:
- Copy button copies message text to clipboard via MessageAction
- Per-turn token count (22.7K → 238) and cost ($0.003) shown as muted
tabular-nums text on the right side of the toolbar
- Toolbar appears on hover (opacity transition via group-hover)
- Only shown when the message has text content
- Cost/token display only shown when data is available from JSONL
* fix: toolbar only on assistant messages, always visible, cost only
- Only render toolbar on assistant messages (not user messages)
- Remove hover-only opacity — toolbar is always visible
- Remove token counts (22.7K → 238 is meaningless to users)
- Show only cost as a budget signal ($0.003)
* feat: group all tool activity into single Task collapsible per turn
Replace flat tool rows with a single ai-elements Task collapsible per
assistant turn that lists every tool/MCP call in sequence.
Live streaming (ConversationMessage):
- Aggregate all tool-batch parts into one Task
- Title: "Working… (N actions)" while running, "Agent activity (N actions)" when done
- Default open while turn is in progress
- Wrench icon in trigger
Historical (ClawChatMessage):
- Group all tool-call parts into one Task
- Title includes failed count if any tools errored
- Default collapsed — expandable on click
- Tool name + status icon + error text per row
Both views show one clean collapsible per turn instead of N individual
tool cards. Collapsed reads "5 actions"; expanded shows the timeline.
* feat: include tool calls in chat history responses
Server: jsonlEventsToHistoryItems() now walks ALL events (not just
messages) and pairs agent.tool_use with agent.tool_result by toolCallId.
The resulting tool call list is attached to the next assistant text
message as toolCalls[]. Each entry includes status, input arguments,
output text, error string, and duration computed from event timestamps.
Client:
- BrowserOSChatHistoryItem gets optional toolCalls field
- Tool-call message part type gets durationMs field
- mapHistoryItemToClawMessage() emits tool-call parts BEFORE the text
part (the order the agent produced them)
- ClawChatMessage Task view now shows tool duration in seconds
Result: historical messages now display the full tool activity
timeline grouped into the single Task collapsible per turn (designed
in step 3), instead of showing only the final text response.
* feat: render activity rows as human verbs sourced from tool registry
Tool calls in the chat activity view now read as sentences:
"Opened tab · news.ycombinator.com" instead of "browseros__new_page".
Server (tool-label-registry.ts):
- Curated verb override map for ~70 BrowserOS first-party tools
- Per-tool subject extractors that pull the meaningful argument from
input (URL → host, query → quoted, element → ID, etc.)
- Generic fallback humanizes snake_case for any unmapped tool
- Strips MCP namespace prefixes (browseros__, mcp_)
Server (openclaw-service.ts):
- jsonlEventsToHistoryItems calls buildToolLabel for each tool_use,
attaches label and subject to the BrowserOSChatHistoryToolCall
Client:
- Mirrored label module at lib/tool-labels.ts
- useAgentConversation tool-start handler computes label/subject
from the SSE tool args
- ClawChatMessage and ConversationMessage render label · subject
with foreground/muted styling, no font-mono
- ToolEntry, BrowserOSChatHistoryToolCall, and tool-call message
part types all carry label and optional subject
* fix: drop meaningless tab N subject from page-read tool rows
Page IDs are internal numbers, not URLs. 'Took screenshot · tab 4'
tells the user nothing. Removed subject extractors for take_snapshot,
take_enhanced_snapshot, get_page_content, get_page_links, get_dom,
and take_screenshot. The verb alone is the right signal.
* fix: gate initial loading on historyQuery.isFetched not isLoading
The session and history queries are sequential: the history query is
disabled until session resolves. After session resolves, there's a render
frame where historyQuery.isLoading is still false (the query hasn't
been kicked off yet). isInitialLoading flipped to false during that
window, exposing an empty chat shell with just Task collapsibles and
copy buttons before the messages filled in.
Switching the guard to isFetched closes that window — the loading state
stays true until the first history fetch actually completes.
* fix: render historical messages immediately instead of through Streamdown's idle-callback debounce
Streamdown defaults to mode="streaming" which uses requestIdleCallback (300ms
debounce, 500ms idle timeout) and lazy/Suspense to optimize for token-by-token
live streams. For finalized historical messages this caused tool collapsibles
and copy buttons to paint while text bodies stayed blank for ~300-500ms after
load. Pass mode="static" + parseIncompleteMarkdown=false on the historical
MessageResponse so completed text paints in the same frame as the surrounding
chrome. Live streaming turns still use the default streaming mode.
Also collapse the redundant /agents/:id/session round-trip into the existing
/history endpoint (server already resolves the most recent user-chat session
when sessionKey is omitted) and tighten the initial-loading gate to stay true
across the render frame where the query is enabled but hasn't started fetching.
* feat: surface thinking duration on historical reasoning collapsibles
Server accumulates agent.thinking events per turn from JSONL and attaches a
single reasoning block (joined text + durationMs from first thinking event
to the closing agent.message) on each assistant history item. Reasoning
buffer resets on user.message alongside the tool-call buffer.
Client mirrors the type, emits the reasoning part before tool calls in
mapHistoryItemToClawMessage (chronological: think → act → answer), and
passes duration in seconds to <Reasoning> so the trigger reads "Thought
for N seconds" instead of just "Thinking" on collapsed historical turns.
* fix: read thinking blocks from the correct JSONL field name
OpenClaw stores reasoning blocks as {type:'thinking', thinking:'...'} but
the JSONL parser was reading block.text, so every thinking event was
silently dropped before it ever reached jsonlEventsToHistoryItems. As a
result the reasoning field on history items was always empty even though
the new accumulator was wired up correctly.
Also guard the client mapping: when durationMs is 0 (think + answer
emitted in the same JSONL line, no real elapsed wall-clock) pass
undefined to <Reasoning> so it renders the static "Thinking" trigger
instead of the streaming shimmer / "Thought for 0 seconds".
* fix: reset reasoning buffer on discarded turns and drop dead session hook
Two cleanups from PR review:
1. jsonlEventsToHistoryItems: when an agent.message is discarded (the
"[Chat messages since your last reply" wrapper without a current-message
marker) the tool buffers were already reset but the reasoning buffer
was not. Accumulated thinking from the discarded turn would bleed onto
the next assistant message. Reset pendingReasoningTexts and
pendingReasoningFirstAt alongside the tool buffers.
2. useClawAgentSession, the AgentSessionResponse type, and the unused
session entry in CLAW_CHAT_QUERY_KEYS became dead code after the
session round-trip was folded into the history endpoint. Removed.
|
||
|
|
0035893f33 |
feat: dashboard API, JSONL reader, and OpenClaw observer for enriched home page (#810)
* feat: draft agent chat ui exploration * feat: refine agent chat ui draft * feat: remove outer frame from agent chat workspace * fix: offset agent chat for app sidebar * fix: simplify agent conversation shell * fix: remove redundant chat header actions * fix: unify agent conversation headers * fix: tighten agent chat spacing * fix: bound agent chat composer height * fix: remove agent chat page inset * fix: align agent header height with sidepanel * fix: center agent composer resting state * fix: anchor multiline composer controls * fix: remove focus grid from agent home * fix: remove redundant agent home header * fix: constrain home agent composer * fix: match home composer default posture * feat: add openclaw chat history APIs * feat: add claw chat history hydration * fix: stabilize claw chat viewport layout * fix: use conversation scroll base for claw chat * refactor: split claw chat controller responsibilities * fix: keep active agent turns in memory * fix: normalize openclaw chat sessions * refactor: use HTTP client for agent history instead of CLI client Replace the CLI-based getChatHistory() call in getAgentHistoryPage() with the HTTP client's getSessionHistory() from PR #795. This uses the direct HTTP transport to OpenClaw's /sessions/<key>/history endpoint instead of shelling out through the CLI. - Add filterHttpSessionHistoryMessages() for flat-string content format - Add normalizeHttpHistoryMessages() for OpenClawSessionHistoryMessage shape - Update getAgentHistoryPage() to call getSessionHistory() via httpClient - Remove unused getChatHistory(), filterOpenClawSystemMessages(), normalizeChatHistoryMessages(), and getTextContent() - Update test mocks from cliClient.getChatHistory to httpClient.getSessionHistory - Update MutableOpenClawService type: chatClient -> httpClient * fix: fetch all session messages by iterating OpenClaw pagination OpenClaw's HTTP history endpoint returns a limited page by default. When called without a limit, only the first ~27 messages were returned, causing all newer conversation messages to be silently dropped. Add fetchAllSessionMessages() that iterates through OpenClaw's cursor- based pagination (200 messages per page) until hasMore is false, then feeds the complete message list into the existing BrowserOS normalization and in-memory pagination layer. * refactor: migrate chat history from HTTP gateway to direct JSONL file reads Replace the HTTP-based chat history pipeline (BrowserOS server → OpenClaw gateway /sessions/:key/history pagination loop) with direct JSONL file reads from the host filesystem via Lima's virtiofs mount. - Add OpenClawJsonlReader that reads session JSONL files directly from ~/.browseros/vm/openclaw/.openclaw/agents/<id>/sessions/ - Replace fetchAllSessionMessages() HTTP pagination with single file read - Replace CLI-based listSessions() with sessions.json file reads - Make listSessions, resolveAgentSession, getAgentHistoryPage synchronous - Remove unused toBrowserOSSession, filterHttpSessionHistoryMessages, normalizeHttpHistoryMessages helpers - Update route handlers to drop unnecessary async/await - Update tests to use temp JSONL files instead of mocked HTTP/CLI clients * fix: restore async route handlers for test compatibility with mocked service * fix: address review feedback — path traversal guard, lazy reader, exists flag - Add safePath() to OpenClawJsonlReader that validates resolved paths stay within stateRoot, preventing path traversal via crafted agentId values - Use lazy initialization for jsonlReader (nulled on rebuildRuntimeClients) instead of creating a new instance per property access - Return exists: false from resolveSpecificAgentSession when no session matches instead of fabricating a ghost session with sessionId: '' * feat: add dashboard API and enrich home page agent cards Server: - Add summarizeToolActivity() that converts tool events into natural language descriptions ("Browsed 3 pages, took 2 screenshots") - Add getDashboard() to OpenClawService that aggregates per-agent stats from JSONL: latest message, activity summary, cost, session count - Add GET /claw/dashboard endpoint Client: - Add useAgentDashboard() React Query hook (10s refetch, 5s stale) - Rewrite useAgentCardData from async IndexedDB hook to pure buildAgentCardData() function merging agent entries with dashboard data - Add activity summary and cost to AgentCardExpanded footer - Add activitySummary and costUsd fields to AgentCardData type - Remove IndexedDB dependency from the home page * feat: add OpenClawObserver for real-time per-agent status via gateway WS - Add OpenClawObserver that connects to the OpenClaw gateway WebSocket control plane and subscribes to chat broadcast events - Track per-agent status in real time: working (streaming), idle (turn complete), error (run failed), with current tool name - Auto-connect when gateway control plane becomes available, auto- reconnect on disconnect with 5s backoff - Disconnect observer on stop/shutdown - Wire live status + currentTool into getDashboard() response - Update client: AgentOverview includes status + currentTool, card shows spinning loader + tool name when agent is working - Status resolution: per-agent WS status takes precedence over gateway- level status for working/error states * feat: add SSE dashboard stream for real-time agent status on home page Server: - Add GET /claw/dashboard/stream SSE endpoint that sends an initial snapshot then pushes per-agent status events as they arrive from the OpenClaw observer - Add onAgentStatusChange() to OpenClawService exposing the observer's listener for the route layer - Heartbeat every 15s to keep connections alive Client: - useAgentDashboard() now subscribes to EventSource at /claw/dashboard/stream - SSE snapshot event hydrates the React Query cache immediately - SSE status events patch individual agent status + currentTool in the cache without refetching — agent cards update instantly - Polling fallback raised to 30s since SSE handles real-time * fix: observer WS handshake — wait for challenge before sending connect The OpenClaw gateway sends a connect.challenge event before accepting the connect request. The observer was sending the connect request on ws.open which raced with the challenge. Now waits for the challenge event before sending the handshake. Also add dangerouslyDisableDeviceAuth to the gateway setup config batch so the observer can connect without device identity on new installs. * fix: JSONL reader falls back to most recent file when sessions.json is stale OpenClaw's sessions.json can record a Pi session ID that doesn't match the actual JSONL filename on disk. This happens after context compaction or session restart — the JSONL file gets a new UUID but sessions.json keeps the old one. Previously this caused history to silently disappear (the reader tried to open a non-existent file and returned empty). Now resolveJsonlPath() checks if the mapped file exists and, when it doesn't, scans the sessions directory for the most recently modified .jsonl file as a fallback. * feat: add ClawSession state machine for reliable per-agent status The OpenClawObserver only knows about status changes it witnesses via WS events. If an agent was already running when the observer connected, or after a reconnect, statuses were stuck at "unknown". ClawSession is an in-memory state machine that solves this: 1. Seeds from JSONL on first control plane call — reads the latest events for each agent and infers working/idle. A session is "working" if the last event is a user.message with no subsequent agent.message, or an agent.tool_use with no matching agent.tool_result. 2. Receives live transitions from the WS observer — the observer now delegates all state management to ClawSession instead of maintaining its own status map. 3. Applies a 5-minute staleness threshold — if the last JSONL event is older than 5 minutes, assume idle (handles agent crashes). Consumers (SSE stream, dashboard endpoint) read from ClawSession and get correct state from the first call — no "unknown" period. * fix: remove staleTime so dashboard refetches on every mount * fix: reset stale working status on WS disconnect, eliminate redundant JSONL reads - Observer resets all "working" agents to "unknown" when the WS closes, preventing agents from appearing stuck as Working indefinitely after a gateway restart. ClawSession re-seeds correct state on reconnect. - getDashboard() now derives latestAgentMessage and cost from the already-loaded events array for the latest session instead of calling latestAgentMessage() and getSessionStats() which each re-read the same JSONL file. Reduces file reads from 3x to 1x per agent. |
||
|
|
4284e88625 |
feat: Implement lazy LLM judge for passive monitoring (#777)
* fix: double close on stream controller * feat: initial lazy llm judge impl * feat: added regex-based matching to insert button context * fix: tests & bugfix fix: redundant truthiness check * fix(tests): stabilize server suites on dev |
||
|
|
0b91c735ab | chore: bump server version, offset and patch for release (#814) | ||
|
|
d189b50b03 |
fix: package bundled Lima guest agent (#813)
* fix(build): upload Lima runtime files * fix(build): stage Lima prefix resources * fix(vm): resolve bundled Lima prefix * docs(build): document Lima runtime packaging * chore: self-review fixes * fix: address review feedback for PR #813 |
||
|
|
a407e48209 |
Prefetch runtime VM cache (#811)
* feat: add runtime vm cache sync * feat: configure runtime vm cache sync * feat: prefetch vm cache on startup * feat: await vm cache before vm startup * fix: recheck vm cache after prefetch wait * fix: address vm cache review feedback * build(server): require VM cache manifest env |
||
|
|
1f75b91fba |
feat(openclaw): add Claude CLI as a CLI-backed provider (#791)
* feat(openclaw): add Claude CLI as a CLI-backed provider
Extensible registry of "OpenClaw CLI-backed providers" — tools that run
as subprocesses inside the gateway container rather than via an API key.
Claude CLI is the first entry; Gemini CLI / Codex CLI / etc. are
one-line additions in the same shape.
Backend:
- New openclaw-cli-providers/ module: types, registry, claude-cli entry.
- OpenClawService: generic ensureAllCliProvidersInstalled() (runs on
setup/start/restart/auto-start) and getCliProviderAuthStatus(provider).
- Provider dispatch: resolveProviderForAgent() short-circuits CLI
providers (no env var, no custom-provider merge) before falling
through to the API-key resolver. No changes to openclaw-provider-map.
- Container runtime: PATH + NPM_CONFIG_PREFIX env so tools installed
under /home/node/.npm-global/bin (mounted) are discoverable by
OpenClaw's child-process spawns and persist across restarts.
- New route: GET /claw/providers/:providerId/auth-status returns
installed / loggedIn / account / plan / error.
Frontend:
- New openclaw-cli-providers.tsx: mirrors backend registry (id, models,
authLoginCommand), useOpenClawCliProviderAuthStatus hook (2-s poll
while enabled), OpenClawCliProviderStatusPanel component.
- AgentsPage: synthesized CLI-provider options merged into the Create
Agent dropdown, inline status panel, auth modal mounting the existing
AgentTerminal with provider.authLoginCommand, auto-close on loggedIn.
- AgentTerminal: new optional initialCommand + onSessionExit props
(ref-based so parent re-renders don't rebuild the PTY).
No global ProviderType changes. No custom container image — runtime
install into the mounted home dir persists across restarts.
* fix(openclaw): address review comments for claude-cli provider
- Drop redundant providerId field from OpenClawCliProviderOption (type
already carries the same value).
- Reuse SetupInput type in resolveProviderForAgent instead of inlining.
- Split ensureCliProviderInstalled into probe + install so logs
distinguish "already present" from "freshly installed".
- Narrow union in handleCreate via explicit LlmProviderConfig cast; the
'in'-based narrowing stopped working once the two option shapes
overlapped on required fields.
* fix: green up server-api tests after claude-cli additions
- Update container-runtime.test.ts snapshot to include the new
PATH + NPM_CONFIG_PREFIX env args.
- Add a defensive guard in ensureAllCliProvidersInstalled so test
mocks that swap runtime for a partial stub without execInContainer
simply skip the install step; production runtime always provides it.
No production behavior change.
* fix(openclaw): use claude /login for auth flow and render terminal full-page
`claude auth login` in 2.1.x silently discards stdin, so the pasted OAuth
code never reaches claude. Switch to the REPL's `/login` slash command,
which does accept a pasted token. Also render the auth terminal
full-page instead of inside a Radix Dialog — the focus trap was hiding
keyboard events from xterm's helper textarea. Finally, guard the async
WebSocket in AgentTerminal against React 18 StrictMode's double-invoke
so the first mount's orphaned WS doesn't leak a second live session.
- terminal-session: pass PATH on podman exec so user-installed CLIs
resolve in interactive sessions without manual re-exports.
- claude-cli parseAuthStatus: treat exit-code-1 as a valid "not logged
in" JSON payload instead of a hard error.
* fix(openclaw): drop unnecessary PATH override on podman exec
`podman exec` inherits the container's run-time env (PATH includes
/home/node/.npm-global/bin via `podman run -e PATH=…`), so the extra
`-e PATH` on the exec call was redundant. Reverts the export of
GATEWAY_PATH and the exec flag added in the previous commit.
* feat(openclaw): show CLI-backed providers in Set Up dialog
The Set Up OpenClaw dialog previously listed only API-key LLM
providers. Add the CLI-backed ones (currently just Claude CLI) so
users can bootstrap the gateway with a Claude.ai-subscription-backed
agent without round-tripping through the Create Agent flow first.
When the user picks a CLI provider at setup, skip the apiKey/baseUrl
fields and open the auth terminal immediately after the gateway comes
up, so /login runs in one click.
* fix(openclaw): robust claude auth-status parsing and cleaner CLI UX
parseClaudeAuthStatus was doing JSON.parse on the entire stdout, which
fails when Lima/nerdctl appends a stderr line like `level=fatal
msg="exec failed with exit code 1"` whenever the inner command exits
non-zero (claude auth status exits 1 when not logged in). The panel
then surfaced the raw output as an error. Switch to a line-by-line
scan that picks the first parseable JSON object — handles trailing
noise and nested JSON fields cleanly.
UI polish around the Setup dialog:
- Hide the "uses your API key" hint when the selected provider is
CLI-backed — it is inaccurate and confusing.
- When a CLI provider is picked in Setup, show a short helper line
instead of the status panel (the /auth-status poll would be
pre-gateway and would always fail). Set Up & Start boots the
gateway and then auto-opens the auth terminal in one click.
- Track the active CLI provider across both Setup and Create dialogs
so the auth terminal opens for the right provider regardless of
which dialog triggered it.
* feat(terminal): make selection + copy work under TUI mouse tracking
Interactive TUIs like `claude /login` enable xterm mouse-tracking,
which forwards every click to the app and disables click-drag text
selection. Our terminal had no escape hatch, so users couldn't grab
the OAuth URL.
Three general-purpose fixes (none CLI-specific):
- macOptionClickForcesSelection: Opt+drag always selects on Mac,
regardless of what the running program does with mouse events.
- Cmd/Ctrl+A and Cmd/Ctrl+C custom key handler: select-all and copy
to clipboard via navigator.clipboard, even when the TUI would
swallow the keys.
- Copy button in the terminal header: writes the current selection
to the clipboard, or the full visible viewport if nothing is
selected. One-click escape hatch that works in every state.
Applies to any interactive CLI in our terminal (sudo, vim, claude,
gh auth, etc.), not just the claude login flow.
* fix(terminal): make xterm selection actually visible
Selection was registering internally (xterm-selection layer had
correct width/height rects), but the rectangles rendered in
rgb(252,252,251) — practically invisible against the white
background — so users concluded selection was broken.
Root cause: the theme derived selectionBackground from
`withAlpha(resolveCssColor('--accent-orange'), 0.2)`. When the CSS
var failed to resolve it fell back near-white, and the alpha
compositing against the page background made the result
indistinguishable from the background.
Switch to solid terminal-standard selection colors (VSCode-like
light-blue / dark-indigo). Also set selectionInactiveBackground so
the selection persists when focus moves away (useful while copying).
Drop the now-unused withAlpha helper.
* fix(openclaw): handle pretty-printed JSON in claude auth status parser
claude auth status --json emits multi-line pretty-printed JSON. The previous line-by-line parser never matched, so the UI treated every response as an error and surfaced the raw JSON — even when loggedIn was true. Replace with a brace-matching JSON extractor (string- and escape-aware) that tolerates multi-line JSON, leading banners, trailing lima/nerdctl stderr, and nested objects.
* refactor(openclaw): separate exec streams, argv installs, cleaner async cleanup
Audit-driven cleanup. Net -42 lines, four concrete issues fixed:
1. ContainerRuntime.runInContainer() exposes {exitCode, stdout, stderr}
from the nerdctl exec (ContainerCli.runCommand already tracked them
separately; we were just throwing stderr into the same string). The
40-line hand-rolled brace-matching JSON extractor in claude-cli.ts
existed only because the prior merged-stream output had lima/
nerdctl's 'level=fatal' line fused with claude's JSON. parser is
now JSON.parse(stdout.trim()).
2. Replace shell-based 'sh -lc "npm install -g ${pkg}@latest"' with
argv: execInContainer(['npm','install','-g','${pkg}@${version}']).
Registry values no longer flow through a shell (removes injection
surface from future CLI providers). Pinned version instead of
@latest (adds npmPackageVersion to the provider type).
3. AgentTerminal: replace the 'let cancelled' + out-of-effect
disposeSocketBindings pattern with an AbortController scoped to
the effect and a cleanups[] array. Matches the canonical React 18
async-effect pattern — no partial-cleanup race if StrictMode
unmounts between the async await and the resolve.
4. AgentTerminal: drop the full-buffer fallback in the Copy button
(was copying all 8000 scrollback lines when nothing selected —
surprising). Button now only copies the actual xterm selection,
or no-ops silently. Users who want everything can Cmd+A first.
|
||
|
|
752f42d1fe |
refactor: migrate chat history to direct JSONL file reads via Lima filesystem (#808)
* feat: draft agent chat ui exploration * feat: refine agent chat ui draft * feat: remove outer frame from agent chat workspace * fix: offset agent chat for app sidebar * fix: simplify agent conversation shell * fix: remove redundant chat header actions * fix: unify agent conversation headers * fix: tighten agent chat spacing * fix: bound agent chat composer height * fix: remove agent chat page inset * fix: align agent header height with sidepanel * fix: center agent composer resting state * fix: anchor multiline composer controls * fix: remove focus grid from agent home * fix: remove redundant agent home header * fix: constrain home agent composer * fix: match home composer default posture * feat: add openclaw chat history APIs * feat: add claw chat history hydration * fix: stabilize claw chat viewport layout * fix: use conversation scroll base for claw chat * refactor: split claw chat controller responsibilities * fix: keep active agent turns in memory * fix: normalize openclaw chat sessions * refactor: use HTTP client for agent history instead of CLI client Replace the CLI-based getChatHistory() call in getAgentHistoryPage() with the HTTP client's getSessionHistory() from PR #795. This uses the direct HTTP transport to OpenClaw's /sessions/<key>/history endpoint instead of shelling out through the CLI. - Add filterHttpSessionHistoryMessages() for flat-string content format - Add normalizeHttpHistoryMessages() for OpenClawSessionHistoryMessage shape - Update getAgentHistoryPage() to call getSessionHistory() via httpClient - Remove unused getChatHistory(), filterOpenClawSystemMessages(), normalizeChatHistoryMessages(), and getTextContent() - Update test mocks from cliClient.getChatHistory to httpClient.getSessionHistory - Update MutableOpenClawService type: chatClient -> httpClient * fix: fetch all session messages by iterating OpenClaw pagination OpenClaw's HTTP history endpoint returns a limited page by default. When called without a limit, only the first ~27 messages were returned, causing all newer conversation messages to be silently dropped. Add fetchAllSessionMessages() that iterates through OpenClaw's cursor- based pagination (200 messages per page) until hasMore is false, then feeds the complete message list into the existing BrowserOS normalization and in-memory pagination layer. * refactor: migrate chat history from HTTP gateway to direct JSONL file reads Replace the HTTP-based chat history pipeline (BrowserOS server → OpenClaw gateway /sessions/:key/history pagination loop) with direct JSONL file reads from the host filesystem via Lima's virtiofs mount. - Add OpenClawJsonlReader that reads session JSONL files directly from ~/.browseros/vm/openclaw/.openclaw/agents/<id>/sessions/ - Replace fetchAllSessionMessages() HTTP pagination with single file read - Replace CLI-based listSessions() with sessions.json file reads - Make listSessions, resolveAgentSession, getAgentHistoryPage synchronous - Remove unused toBrowserOSSession, filterHttpSessionHistoryMessages, normalizeHttpHistoryMessages helpers - Update route handlers to drop unnecessary async/await - Update tests to use temp JSONL files instead of mocked HTTP/CLI clients * fix: restore async route handlers for test compatibility with mocked service * fix: address review feedback — path traversal guard, lazy reader, exists flag - Add safePath() to OpenClawJsonlReader that validates resolved paths stay within stateRoot, preventing path traversal via crafted agentId values - Use lazy initialization for jsonlReader (nulled on rebuildRuntimeClients) instead of creating a new instance per property access - Return exists: false from resolveSpecificAgentSession when no session matches instead of fabricating a ghost session with sessionId: '' |
||
|
|
c6c902a4ab |
feat: improve dev watch Lima preflights (#802)
* feat: improve dev watch lima preflights * fix: note vm cache sync duration * fix: address review feedback for PR #802 |
||
|
|
0288cc040d |
feat: use rootless nerdctl in BrowserOS VM (#800)
* feat: use rootless nerdctl in BrowserOS VM * fix: validate openclaw gateway auth before reuse * fix: forward rootless containerd socket * fix: address VM review comments |
||
|
|
d1a3d67e29 | chore(dev): add VM cache setup flow (#798) | ||
|
|
35134518f0 | fix(vm): use system nerdctl in Lima runtime (#797) | ||
|
|
4083155e81 |
feat(container): migrate container runtime to nerdctl over Lima VM
Replace the podman-based runtime with nerdctl running inside the Lima VM introduced in the previous commit. OpenClaw is cut over to the new VM-backed container runtime; legacy podman code paths are removed. - New container CLI (lib/container): nerdctl ContainerCli, ImageLoader with cache-tarball fallback, shared types - OpenClaw: container-runtime-factory orchestrates VM lifecycle + gateway startup; container-runtime.ts rewritten to speak nerdctl; Linux test startup kept disabled behind the factory - Terminal: session + routes moved onto Lima shell transport; server wires the VM-backed runtime via main.ts - Agent UI: simplify AgentsPage/useOpenClaw after route consolidation - Remove podman-runtime, podman-overrides, and their tests - Tests: container-cli, image-loader, container-runtime-factory, and updated openclaw/terminal/main suites |
||
|
|
72ef4f068e |
feat(vm): add Lima-based BrowserOS VM runtime
Introduce a new VM runtime layer using Lima for running containerised workloads on macOS. Lifecycle covers decompress/create/start/stop with stubs for upgrade/reset plus version-mismatch warnings. - Foundation modules: paths, errors, manifest, telemetry - lima.yaml generator + typed limactl wrapper with structured debug logging - ssh ControlMaster transport for fast in-VM commands - Ubuntu 24.04 minimal template, containerd default, 30GiB overlay disk - browseros-dir helpers (getLimaHomeDir, getVmStateDir, getVmDisksDir); OpenClaw dir moves into VM state dir - Test helpers (fake-limactl, fake-ssh, test-env), vm-smoke integration coverage, NODE_ENV propagation for spawned server test groups |
||
|
|
6b6ed1582c |
feat(openclaw): HTTP session history endpoint (JSON + SSE) (#795)
* refactor(openclaw): rename http chat client to http client Session history is about to land on the same HTTP client. 'Chat client' will no longer describe it, so rename the class, file, and service field up front. No behavior change. * feat(openclaw): add session history fetch + sse stream to http client Adds getSessionHistory (JSON) and streamSessionHistory (SSE) to the OpenClaw HTTP client. Both target GET /sessions/<key>/history on the loopback gateway, reusing the same bearer-token auth as streamChat. - 404 from the gateway surfaces as OpenClawSessionNotFoundError so callers can map it to a typed HTTP status. - The SSE path parses named 'history', 'message', and 'error' events into a typed OpenClawSessionHistoryEvent union. - AbortSignal propagates to fetch and cancels the reader mid-stream. * feat(openclaw): expose session history over GET /claw/session/:key/history Wire the new getSessionHistory / streamSessionHistory service methods through a route that defaults to JSON and upgrades to SSE when the client sends Accept: text/event-stream. - OpenClawSessionNotFoundError lives in errors.ts alongside the other OpenClaw errors so routes can import it from one place. - The route propagates c.req.raw.signal into streamSessionHistory so client disconnects cancel the upstream fetch. - Route tests cover the JSON path (with query param forwarding), the 404 path, and the SSE framing. * chore(openclaw): drop NaN from session history route limit param |
||
|
|
4d660874ad |
feat: consolidate build tools package (#785)
* feat(build-tools): scaffold package + cache dir helpers * feat(build-tools): manifest types + R2 helper * feat(build-tools): build-disk script with virt-customize + zstd * feat(build-tools): build-tarball script * feat(build-tools): emit-manifest + cache:sync * ci(build-tools): independent build-vm + build-agent workflows * chore: remove legacy container packages + workflows * fix: address review feedback for PR #785 * fix: stabilize VM build DNS in CI * fix: prioritize arm64 build workflows * fix: keep arm64 VM recipe simple * fix: set VM build DNS in apt command * fix: avoid guest DNS for VM package install * fix: limit VM PR checks to build-tools validation |
||
|
|
5ccdbaf87f |
feat(openclaw): lifecycle progress banner + live podman readiness (#772)
* fix(openclaw): serialize lifecycle operations * feat(openclaw): lifecycle progress banner and live podman readiness check * fix: address review comments for openclaw-lifecycle-progress |
||
|
|
0650f21c80 |
fix(openclaw): allocate gateway host port dynamically + name the two ports distinctly (#771)
* feat(openclaw): dynamically allocate and persist gateway host port The gateway container always listens on OPENCLAW_GATEWAY_CONTAINER_PORT (18789) internally, but that port may be taken on the user's host. Allocate a free host port on each lifecycle transition, persist it to ~/.browseros/openclaw/.openclaw/runtime-state.json, and prefer the persisted value on subsequent starts so the mapping is stable. Split the naming so the two sides of the -p mapping are no longer ambiguous: the shared constant becomes OPENCLAW_GATEWAY_CONTAINER_PORT and the service/spec/chat-client/runtime probes all use hostPort for the mapped host-side port. * fix(openclaw): remove duplicate Podman overrides card from status panels |
||
|
|
e80ec467f4 |
feat: wire lazy monitoring to OpenClaw chat handoff (#768)
* feat: add lazy monitoring substrate * feat: wire lazy monitoring to openclaw chat handoff * test: cover openclaw chat history handoff * fix: reject concurrent monitored chats |