* perf(sessions): yield during SQLite entry write contention
Keep the Gateway event loop responsive while session-entry patches wait for another SQLite writer. Reuse begin-only asynchronous admission with zero native busy waiting, retaining FIFO ordering, connection custody, commit revalidation, and non-replay of admitted writes. No schema or configuration changes.
* fix(sessions): enforce synchronous callbacks during yielding admission
Name yielding admission separately from retired async-transaction APIs and register its synchronous callback with the source guard. Make the Slack Stop-owner fixture await its competing committed session write instead of depending on updater microtask ordering.
* fix(plugins): keep Doctor native captures out of live state
* test(plugins): use complete metadata snapshot fixtures
* fix(doctor): retain native captures in the profile context
* fix(doctor): include capture resolver in trusted wrapper
Preserve transport error codes through embedded recovery and let one-shot setup verification reject definite connection failures without spending the normal session retry budget. Keep ordinary agent retries unchanged and report actionable server, URL, and DNS guidance through the shared UI/CLI owner.
Remote proof: 167 focused tests; both regressions fail on baseline. Real Gateway/Chromium medians (five fresh runs): closed port 79.766s to 0.720s, DNS NXDOMAIN 69.449s to 0.849s, HTTP 401 control 1.003s to 0.860s. CLI closed-port verification 66.655s to 1.313s. Successful activation, chat, and reload pass. Changed checks pass after removing the obsolete status-mapper export; the exact dead-export gate and setup tests were rerun after that final cleanup.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(gateway): admit shared-venue bursts and explain busy retries
* fix(ui): keep liveness probes out of the service worker cache
* refactor(ui): keep browser transport errors with the socket adapter
* test(ui): model refused socket state transitions
* style(ui): satisfy retry-path lint rules
* chore(ui): retire the removed gateway type assertion allowance
A cloud-worker turn that arrived while the Gateway was installing an updated
worker runtime took its placement turn claim immediately, then queued for its
credential behind the environment lock the refresh held. The refresh's final
authority check saw that claim and failed after the whole bundle transfer, and
stuck-session recovery aborted the waiting turn because install progress was
never reported as run progress.
The runtime refresher now publishes a per-environment in-flight fact before its
first placement read, exposed by the environment service as readRuntimeRefresh.
Turn admission waits on it before claiming (honoring Stop, the turn timeout and
the Gateway restart drain), reports bundle-transfer bytes as run progress, then
re-reads the placement and claims on the refreshed build. Both claim sites also
fence on the fact in the claim store's synchronous pre-commit check, so a
refresh that starts during admission refuses the claim instead of being failed
by it. assertWorkerRuntimeRefresh is unchanged.
Add an opt-in slack-huddles plugin that joins active Slack huddles as a
dedicated signed-in Slack user. Slack has no bot or app huddle API, so the
plugin drives the Slack web client in the OpenClaw Chrome profile on the
shared meeting runtime, with in-page audio capture and a virtual microphone
for agent, bidi, and transcribe modes, mirroring zoom-meetings and
teams-meetings.
Membership is proven only by Slack's in-huddle channel header on the
requested channel. Sessions are workspace-scoped, joins are always muted,
talk-back unmutes only after Slack reports the virtual input, and every
click rechecks live authority. The shared meeting status source gains
optional liveOwnershipSource and afterAudioRoutingSource hooks. They
recheck ownership after awaited device and sink work, and roll back only
this pass's effects. Absent hooks emit no code, so the generated Zoom and
Teams scripts are byte-identical.
The plugin is disabled by default and requires OpenClaw 2026.9.8 or newer,
the first release that can carry the ownership hook.
Release note: Add an opt-in Slack huddles plugin that joins active huddles
as a dedicated signed-in Slack user through the shared meeting runtime.
Requires OpenClaw 2026.9.8 or newer.
* fix(gateway): scope auth policy revocation to each principal
A 2026-09-27 config patch restoring one user's access closed 82 webchat
connections from nine unrelated users, cancelled 16 admitted agent runs
(including a 108-minute run), and aborted four queued chat inputs.
The global identityScopes fingerprint coupled unrelated principals;
the partial identity-aware fix still left global fallback and handshake,
HTTP, cookie, and approval checks.
Capture the verified operator grant principal with its policy generation
at authentication. Reuse that prepared fact for scope derivation,
handshake rechecks, client reconciliation, request/mutation authority,
approval visibility, and retained source custody. Cache the global part
per immutable config and each normalized effective grant per identity.
Changing identity B's grant preserves A's connection, admitted and
delegated run authority, and queued input. Narrowing or removing A's
own grant revokes that authority and remains latched after re-grant;
widening may conservatively revoke A too. Clients without verified
identity grants and node clients ignore identityScopes edits. Global
proxy and Tailscale auth fields still revoke all affected principals.
HTTP requests and plugin cookies use credential/header scopes rather
than identityScopes, including during asynchronous profile preparation.
Keep role policy global because HTTP admission does not already retain
an assigned-role fact. Preserve the existing assignment revocation path.
Update the connection-only grant documentation and exercise real
handshake, dispatch, queued-chat, approval, HTTP, and reload boundaries.
* fix(gateway): move auth policy type to a leaf contract
* test(gateway): migrate merged mutation-authority case to captured auth policy
Main added a retained-authority case that still stamped the removed authPolicyGeneration string; stamp the captured GatewayAuthPolicy for the same verified identity instead. Assertions unchanged.
Closes#160689
## What Problem This Solves
Fixes: `openclaw doctor --session-sqlite inspect` keeps reporting `plugin_migration_source_retained` after deferred plugin migrations complete, and repeated `openclaw doctor --fix` never clears it, when the import receipt includes unindexed session history with `.trajectory-path.json` pointer sidecars.
## User Impact
User impact: after upgrading with deferred plugin migrations (for example Codex or Brave), `doctor --fix` archives each unindexed transcript together with its trajectory pointer, and installs already left with stranded pointers get them archived on the next `doctor --fix`, so the warning clears.
## Why This Change Was Made
A deferred import receipt captures each discovered transcript together with its trajectory and pointer sidecar. When the plugin later completes, settlement no longer rediscovers that history, so its transcripts are moved by the unreferenced-JSONL sweep, which moved only `.jsonl` files. The pointer (a `.json` file) stayed live. The retained check fires while any receipt source is live, so the warning never cleared.
Doctor's archive sweep now moves a transcript's pointer sidecar with it. For installs an earlier release already left in that state, the same sweep also archives a receipt-captured pointer whose receipt-verified transcript is no longer live. Both paths use the existing archive move: identity and byte checks, a migration-manifest entry, and `doctor --session-sqlite restore` recovery. Nothing is deleted. Pointers still referenced by a live index, retained for another owner, in conflict with the receipt, or (during settlement) outside the receipt are left alone.
Update behavior: no schema, receipt or stored-field changes. The installed updater runs first, unchanged. The candidate's Doctor keys on the deferred-plugin session receipt and migration manifests that 2026.9.5/2026.9.6 already write, so the first `doctor --fix` after updating repairs existing stranded installs. Agent/state DB `user_version` and schema are identical to `main` after the same migration.
Related: introduced when deferred-import settlement began archiving receipt-captured unindexed history (#150015, first shipped in 2026.9.5).
No overlap with Pash/Sarah changes.
Thanks @spikewillcocks for the detailed report and receipt analysis.
## Evidence
Real CLI, isolated `--profile p160689base` / `p160689cand` with task-owned `OPENCLAW_HOME`, `OPENCLAW_STATE_DIR` and `OPENCLAW_CONFIG_PATH`; every Doctor run first printed the resolved state dir, config path and session DB path from the same binary. State: a file-era `sessions.json` (1 indexed session) plus 3 unindexed transcripts, each with `.trajectory.jsonl` and `.trajectory-path.json` (pointer format from v2026.9.5), plus unrelated `notes.txt` and `custom-settings.json`. The **published 2026.9.5** package ran `doctor --fix` with `brave` configured but its package unreachable, which deferred the migration and wrote the import receipt. The source checkout then completed the `brave` migration, as in the report.
- **Base** (`origin/main` 9366fd94e1): settlement archived the transcripts and left `history-{1,2,3}.trajectory-path.json` live. `inspect` → `1 issue(s)` `[plugin_migration_source_retained] … await archival. Run openclaw doctor --fix to finish.` A second `doctor --fix` changed nothing and `inspect` still reported it.
- **Candidate, same 2026.9.5 state**: `doctor --fix` archived all 4 pointers with their transcripts into `agents/main/session-sqlite-import-archive/` (for example `archive-tier.history-1.trajectory-path.json.imported-…`, manifest kind `trajectory`, reason `unreferenced-history`). Each archive's SHA-256 matches the receipt. `inspect` → `0 issue(s)`. A second `doctor --fix` wrote a run manifest with `completedMoves=0`, and no other file changed.
- **Candidate on the stranded base state** (the reporter's situation): `doctor --fix` → `Archived 3 legacy transcript artifact(s)`, the three stranded pointers now in the archive with receipt-matching SHA-256. `inspect` → `0 issue(s)`. A second run → `completedMoves=0`.
- **Controls**: `notes.txt` and `custom-settings.json` in the same sessions folder were byte-identical in every run. The regression test covers the explicit settlement path (`settleRetainedDoctorSessionSources`). There, a transcript and pointer written after the receipt (live, not covered by it) stay byte-identical, and so does an unrelated file. When the plugin completes during config preflight instead, Doctor's existing sweep archives every unreferenced JSONL in the folder, including ones written after the receipt. `main` does the same with `late.jsonl`, but leaves `late.trajectory-path.json` orphaned; the candidate moves that pointer with its transcript. A pointer still referenced by a live index or retained for another owner is skipped by the same guards that protect transcripts.
- Schema: after the same migration, base and candidate agent DB `user_version=23`, state DB `user_version=19`, with identical `sqlite_master` hashes.
- Regression test `doctor-session-sqlite.shared-orphan.test.ts` covers both paths. First, settlement of a deferred receipt with unindexed history and a pointer. Second, repair of a pointer an earlier settlement left behind; that half requires a distinct second archive move with receipt-matching bytes. The test fails on `main` because the pointer is still live after settlement. With only the stranded-pointer pass disabled, it fails at the repair step because the pointer stays live. It passes with the fix. Related suites (`deferred-plugin`, `manifests`, `retained-source-verification`, `active-settlement`, `indexless`, `archive-safety`, `recovery-shared-owners`, `discovery`, `recovery`, `receipt-recovery`, `doctor-session-sqlite`) pass: 12 files, 139 tests.
- Test cost: the new test is 1.7s locally and 1.8s in CI (`checks-node-changed-compact-large-19-1`, shard `agentic-commands-doctor-sessions-cron-hosted-1`, run 36491519850). `pnpm test src/commands/doctor-session-sqlite.shared-orphan.test.ts --maxWorkers=1` takes 34.4s wall for the whole file, which is mostly transform; its three tests run in about 6s. It uses no timers, sleeps, polling or process boots, and reuses the file's existing fixture and imports.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Use granted hello scopes to admit canvas lease renewal and stop automatic retries after FORBIDDEN. Preserve read access implied by write/admin scopes and re-evaluate access on reconnect.
#160216 routed src/plugins/runtime.retention.test.ts to the unit-fast Node
partition after reproducing Bun failures. Record why in the runtime-selection
docs, beside the other GC-sensitive proofs.
Root cause, measured on Blacksmith Testbox tbx_01m3n11pjbtgfgcqv8t1jjbh4n with
pinned Bun 1.4.3-canary.1+6b9148b17 on 300ef39911: the registry-instances
case kept a released successor registry past eight forced collections in 3/350
and 6/900 runs. In every retained case, a GCDebugging heap snapshot one task
later still held the oldest-registry control marker but no successor, and each
WeakRef then read empty. No retaining path exists, so this is collector
nondeterminism, not a product leak. Node ran 0/600.
The registry-case failure seen on openclaw/openclaw#160136 (CI run
36380533796) is a separate, already-fixed fixture defect. That run merged onto
56376753f8, before c136ec6314 (#160200) moved gc() out of promise
continuations. On Bun, that base fails the registry case 339/350 and 300ef39911
fails it 0/550.
Routing proof on the same lease: replaying the failing core-unit-fast-2 group
(186 files, bun-compatible policy) exits 0, with all 10 retention cases in the
Node subset. The runtime-inventory guard fails without the routing entry.
Closes#160672
## What Problem This Solves
Fixes: every Active Memory recall on the `claude-cli` runtime writes the whole OpenClaw system prompt into Claude's prompt cache again, even for two recalls of the same agent seconds apart.
## User Impact
User impact: repeated Active Memory recalls of the same agent on `claude-cli` now send a byte-identical system prompt, so the recall's first API call can read it from Claude's cache instead of paying for a 10k+ token cache write each time. Normal `claude-cli` turns are unchanged.
## Why This Change Was Made
Each recall gets a new session key (`<parent>:active-memory:<per-run hash>`) so its session entry stays unique. The Runtime line rendered that key into the system prompt, and Claude CLI receives the whole system prompt as one `appendSystemPrompt`, so the per-run key forced a full cache rewrite. The recall's session key is the only value that differed between two recalls.
One-shot CLI dispatch runs (today only Active Memory recall) now carry the Runtime facts line in their only user turn, through the same `UserPromptSubmit` context Claude CLI already uses for other per-turn facts. This reuses the existing relocatable Runtime region that Chat Completions routes already move into the first user message. The recall model still sees its agent, session, model and channel. Recall session keys, storage and cleanup are unchanged. Normal resumable CLI turns keep the Runtime line in the system prompt.
Thanks @ndakota79 for the detailed cache-counter report and cause analysis.
No overlap with Pash/Sarah changes.
## Evidence
Isolated Gateway (task-owned home, state, config, port) with agents `alpha` and `beta` on `agentRuntime: claude-cli`, Active Memory `mode: always`, and a fake `claude` executable that records the `initialize.appendSystemPrompt` and the `UserPromptSubmit` context it receives. Two `chat.send` turns on `agent:alpha:main` about 60 s apart, then one on `agent:beta:main`.
- Base (`7f0781faa67`): the two alpha recalls' system prompts differ in exactly one line:
```
-Runtime: agent=alpha | session=agent:alpha:main:active-memory:e10ad4c9dc5e | ...
+Runtime: agent=alpha | session=agent:alpha:main:active-memory:9945b3fb179b | ...
```
- Candidate: both alpha recalls send the same system prompt (14,210 bytes, sha256 `f7a45b89f4e84e5f…`). Each recall's user turn carries its own `Runtime: agent=alpha | session=agent:alpha:main:active-memory:<hash> | ... | channel=webchat | ...`. The candidate recall prompt equals the base recall prompt minus the Runtime line.
- Controls: the beta recall still gets its own prompt (its working directory differs) and its own `agent=beta` Runtime facts in the turn. The alpha and beta main (non-recall) turns send the same system prompt and user turn as on base, with the Runtime line still in the system prompt.
- Regression: `src/agents/cli-runner/prepare.test.ts` "keeps per-run helper session identities out of the reusable system prompt" fails on base (system prompts differ) and passes with the fix. `prepare.test.ts` (194), `cli-backend-dispatch.test.ts` (40) and `helpers.system-prompt.test.ts` (15) pass.
- Test cost: the new test runs in 136 ms. `pnpm test src/agents/cli-runner/prepare.test.ts --maxWorkers=1`: 194 passed, 83.3 s wall (Vitest 65.9 s); the same command filtered to the new test: 23.1 s wall (Vitest 5.8 s).
Not verified: a real Claude Code run. The proof checks the bytes OpenClaw sends to the CLI (the `initialize` system prompt and the `UserPromptSubmit` response); it does not show Claude Code's cache counters. The Runtime facts use the same `UserPromptSubmit` additional-context path that already carries `before_prompt_build` hook context (including Active Memory's own recalled context) on `claude-cli` turns.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* perf(nodes): reuse warm workers so node turns start as fast as local ones
Retain settled workers for two minutes with at most two idle children per
node. Negotiate node-worker-idle-retention-v1 across Gateway, node, and
bundle; fresh turn admission and credentials remain mandatory. Protect
background work and preserve durable process cleanup and reconciliation.
Expose reclaimable idle capacity to placement admission and the session
picker. Consolidate lifecycle helpers under the supervisor and use the
shared capacity parser. Document the bounded process-count and memory
tradeoff; include lifecycle, negotiation, and picker regression coverage.
fix(nodes): retry failed idle worker cleanup
Rearm the supervisor idle timer after a transient physical cleanup failure while retaining the durable slot and retirement fence. Cover automatic expiry and idle-limit eviction alongside explicit cleanup with a fake clock.
Fold the cancellation forwarding wrapper into its owner and remove the immediately repeated admission abort check. Preserve the existing authority, settlement, and shutdown guards.
fix(nodes): synchronize idle capacity protocol models
Regenerate the Swift worker-slot model for the optional reclaimableIdle field. The schema and named capability contract remain unchanged.
Inline the single-caller prepared workspace custody wrapper in the supervisor and move its four cases to the public launch entry point. Preserve exact acquisition, abort, shutdown, and release ordering while keeping the complete branch production delta negative.
chore(nodes): prune stale worker assertion allowance
Remove the exact one-entry assertion baseline for worker-command.runtime.ts after the unsafe assertion was eliminated. Keep the scanner and all unrelated allowances unchanged.
fix(nodes): keep supervisor dependencies on protocol owners
Import protocol parsers and types directly from their canonical leaf, including the QA fixture, and remove the redundant supervisor-control re-exports. This breaks the dependency cycle without duplicating the wire contract.
Keep the runner cancellation mock and shutdown callback asynchronous with the real Promise<void> contract, including teardown restoration.
fix(nodes): complete asynchronous cleanup contracts
Join cancellation cleanup in callers and tests while preserving parallel shutdown settlement and expected failure identity. Exclude absent cleanup owners before aggregation and keep stale-connection rejection unchanged.
Use immutable worker input, type-only journal imports, stable sorted idle projections, and receiver-bound fault injection. Remove redundant descriptor bookkeeping and finish the test lint cleanup without changing limits or suppressions.
* test(nodes): disambiguate inventory fixtures
* fix(ui): open voice setup before history admission
The microphone remains enabled for dictation during initial history, but its
short-tap handler returned at the history submission fence before opening
unavailable capability settings. Catalog admission populated a closed picker:
two unavailable DOM rows existed, while accessible Configure menuitems were zero.
Open capability guidance before that fence. Actual Talk creation still waits
for accepted history. Keep the existing unavailable-settings unit case and
all E2E assertions; hold startup and catalog across the target's early click.
Synchronize the four other proven desktop Talk-start races with the existing
waitForTalkReady helper. Leave direct/hold dictation, explicit picker triggers,
mobile Talk and already-synchronized Talk cases unchanged. Document the behavior.
Introducing ordering: 0b52dc467b added the history submission fence before
picker opening; d769e31af2 now awaits foreground stream admission before
history/startup, exposing that window more often. Read the parent-relative
patches and the 94cbd1335d, 6b9da9dc02, f03d64f6a5 and e256cbef8a repairs.
Probe evidence on original production 49351700f3:
- Hold chat.startup and talk.catalog, click Start voice input, release both,
and await visible history: unavailable=2, Configure visible=0,
Configure including hidden=2, picker open=null. The original toBe(2)
assertion fails with expected +0 to be 2; 24.333s case, 197.12s wall.
- The pending-history unit variant fails with dropdown.open=false; the
ready-history variant passes. Extend the existing unit rather than adding
a duplicate case.
- Holding startup across the initial click also leaves talk.client.create
absent in transcript ordering, composer voice selection and both SDP cases;
all four controlled probes fail, while the settings-only sibling passes.
Validation:
- Four changed E2E files: three consecutive passes, 16/16 each;
313.04s, 293.49s and 340.02s command wall including isolated UI builds.
- One-worker dictation before/after: 11/11 each; 234.65s -> 181.54s wall,
file time 63.604s -> 48.819s, changed case 4.663s -> 3.583s.
- One-worker siblings before/after: 5/5 each; 210.09s -> 162.56s wall.
File times: ordering 13.101s -> 7.517s, voice selection 20.346s -> 13.522s,
SDP 13.446s -> 8.394s. These are cost samples, not a speedup claim.
- Existing full browser files exceed 30s because they exercise multiple real
composer/media flows and screenshots; no E2E cases or timers were added.
- Composer microphone, action and catalog owners: 80/80; 85.96s wall.
- Independent Codex autoreview: scoped-clean through P2.
- pnpm tsgo:ui passed in 85.05s; ui-e2e and ui-chat test graphs passed
in 106.07s. Targeted oxlint, oxfmt and git diff --check passed.
- Local check-changed --base origin/main passed its guards, formatting,
graph boundary and i18n, then stopped at inherited TS2741 in
src/gateway/session-row-projection.config-commits.test.ts:76: the fixture
omits the required scheduler. That untouched file is already repaired by
4bd105408c on main. The same gateway-root typecheck passes in 69.67s on
untouched origin/main snapshot 28340c41f8, materialized under this task's
artifacts with physically cloned, identical dependencies. No unrelated
source was changed. The full changed pipeline is not claimed green.
Release-note context: Control UI opens unavailable voice setup when the
microphone is clicked during chat startup, while preserving Talk admission.
* test(ui): rely on Talk readiness without a no-op startup hold
The sibling Talk cases held chat.startup only to release it immediately, which exercises no ordering. waitForTalkReady with the ready-history message is the synchronization, matching the landed e256cbef8a shape.
Detect Bun's native HTTP destroySoon override introduced by upstream #43557
and use the shared ordered half-close path while retaining native response
completion for older Bun transports. Version labels cannot distinguish the
pre-sync and post-sync canaries.
Wait for queued HEAD socket assignment on Bun without finish diagnostics,
then flush through the response callback to avoid Bun's dispatcher socket
cork. Preserve Node's existing ordering and transport operations. Update SDK
rejection documentation; retain all existing wire-level regression cases.
Validation: 31/31 lifecycle tests on Node 24.21.0, stock Bun 1.4.2, pre-sync
and post-sync Bun forks, and the finish-diagnostics build. Core and infra
test typechecks, scoped oxlint, oxfmt, assertion-safety ratchet, and diff
whitespace checks pass. No tests or dependencies added.
Do not move keyboard focus from the composer or another control when an asynchronous recommended option arrives. Preserve initial autofocus and selection within the option card.
Regression cases fail before the fix for focused textareas and buttons. Real isolated Gateway proof retains the full draft and avoids an unintended Talk to my agent handoff. Option-card and custodian-page tests pass, with final option-card checks at 3.01 seconds. Remote changed checks pass against c377d7ca82.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Bootstrap artifact capabilities used a fixed ten-minute backstop while the owning operation could remain authorized for 45-95 minutes. Slow tunnel downloads were cut off mid-body at 600,000 ms, and every retry received an opaque 404.
Derive the transfer lifetime from the existing size-based bootstrap operation owner. Preserve live enrollment and operation revocation, three serial serves, busy exclusivity, and opaque rejection. Cover the streaming deadlines and revocation with fake time, and update the troubleshooting lifetime clause.
Add opt-in Gemini 3.8 Flash and Flash-Lite TTS in the existing Google speech plugin.
Keep the implicit Gemini 3.1 default and older GenerateContent path unchanged. Send Gemini 3.8 transcript text separately from delivery style and structured speaker metadata through stateless Interactions requests, then reuse the existing PCM, WAV, and voice-note outputs.
Preserve the shared Google auth and response helpers, document the model-specific prompting behavior, and cover speaker/no-speaker requests, transcript handling, model selection, and audio output.
Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
* fix(slack): link finished progress cards to the visible work session
Slack progress cards linked "Open in OpenClaw" to the conversation's main
session even when the turn spawned a visible work session, so operators on
groupScope "main" landed on /chat/<agent> instead of the child doing the work.
Carry the child's canonical URL from its producer: accepted visible spawn
receipts keep sessionUrl and label across the embedded, Codex app-server,
and CLI runners; the settled run reports them once through a new optional
GetReplyOptions.onVisibleWorkSessions before final delivery; Slack's native
task card and Block Kit card render "Open work session" for one child,
labeled links in acceptance order (max five) for several, and keep the
conversation link when none was spawned. No request-time session discovery.
To satisfy the line-cap ratchet, the visible-spawn TypeBox schema moves to
sessions-spawn-visible.schema.ts and the new cases live in sibling test
files with shared test-support fixtures.
* fix(slack): match only generated session-link action IDs
The link-only acknowledgement matched any action_id starting with
openclaw:session_link, so an unrelated button such as openclaw:session_linked
was acknowledged and dropped instead of reaching plugin or agent dispatch.
Accept only the bare ID or a numeric suffix, as the renderer emits.
* test(ci): accept measured command ceilings beside startup health
The PR's 771st database-worker file changes compact placement and exposes a
contradiction: capacity checks accept measured command groups' two-worker
ceiling, but the startup-health sibling check expected no ceiling.
Share the complete measured-generation lookup between both assertions and
recognize the existing command policy without changing planner, packing,
worker, or job-level policies. Move the unchanged tooling placement helpers
to the sibling support module, reducing the test's counted lines from 7536
to 7524.
* prototype(plugins): show per-plugin auth alerts from live MCP checks
Place the shared alert beneath the individual plugin hero on main 018639af3b. Read a real Notion HTTP OAuth challenge and use a Notion-specific read-only account probe for the local stdio server. Keep the app inventory isolated and the prototype out of production imports.
* prototype(plugins): simplify auth alerts to one line
* feat(plugins): connect detail auth alerts to MCP OAuth
* fix(plugins): align auth alert with the main content column
* feat(plugins): show installed accounts and credential sections
* fix(plugins): use the supported credential projection copy
* feat(plugins): simplify setup states and retain edit actions
* fix(plugins): reuse MCP ownership across inspections
* fix(plugins): keep MCP cache types in the metadata owner
* test(plugins): preserve real install failure classification in fixtures
* fix(onboarding): distinguish local setup from verified inference
Use a provisioning-specific summary title so Skip for now does not claim that inference is ready. Preserve the separate successful verification message and existing skip behavior.
* fix(onboarding): translate the local setup summary title
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Related: #160668 (fixes the fitting-table case; renderer-valid continuations for tables longer than `maxChars` are out of scope here, so the issue stays open for that part)
## What Problem This Solves
Fixes: with block streaming on (Discord defaults: 800/1200, paragraph), a reply containing a Markdown table that fits in one chunk gets split mid-table, so the channel renders the first part as a code table and posts the remaining rows as raw `| ... |` Markdown.
## User Impact
Streamed replies with a table that fits the chunk limit now arrive with the whole table rendered consistently. Tables larger than `maxChars` still split at row boundaries.
## Why This Change Was Made
The block chunker already refuses to break inside fenced code, but it had no idea about tables: once the buffer passed `minChars`, the last newline inside a still-streaming table was a valid break. The chunker now treats a table that fits in `maxChars` the way it treats a fence when choosing a break:
- Table ranges come from the same markdown-it table grammar the renderers use (`findMarkdownTableRanges` in markdown-core), so single-dash delimiters, single-column tables, rows without edge pipes, quoted tables and tables inside code fences behave like they render.
- While streaming, a table stays open until a full line follows it, so the unfinished line after it (even a bare `> `) may still become a row. Whether a table fits is decided by its parsed rows only, so an unfinished heading after it can't cost it that protection. A trailing header line waiting for its delimiter row is kept whole too. Other lines with pipes aren't protected, so prose containing `|` still streams at its normal newline breaks.
- If the buffer reaches `maxChars` inside a table that fits, the chunker breaks before the table (below `minChars` if it has to), not inside it.
- A table longer than `maxChars` isn't protected, so it still splits at row boundaries as before. Its headerless continuation still renders raw, as on main.
- If a streaming table that starts the chunk exactly fills `maxChars`, the chunker waits only until the next line starts; the final flush also ends the wait. Once the next line starts, the table is emitted whole at the cap, or split at row boundaries if it has grown. The size check counts row trailing spaces, so no chunk can exceed `maxChars`.
Fences, paragraph and newline breaks outside tables, source-range accounting and the forced final flush are unchanged. `src/auto-reply/chunk.ts` now names `FenceSpan` directly because `findFenceSpanAt` is generic over start/end spans.
Earlier attempts (#66344, #66568, #66557, #73981) were closed unmerged. This one is different:
- It uses the renderer's own parser, not a new regex table detector. That covers the repeated review notes about `-{3,}` delimiters, single-column tables, rows without edge pipes and pipes inside code.
- It keeps a streaming table open until a blank line arrives, not until the current buffer tail, which was the P1 in #66344 and #66568.
- Breaks at the table's own edges stay allowed. Only breaks inside a table that fits are refused.
- There's no import cycle through `chunk.ts`, it targets the current `embedded-agent-block-chunker.ts`, and it doesn't add a second limit model: `maxChars` stays the only cap.
Thanks @giodl73-repo for the fresh reproduction.
## Evidence
**Real Gateway, Discord plugin against a Crabline mock of the Discord API, frozen base `3775359a` vs this branch.** The mock model streamed the issue's reply over OpenAI Responses: intro, a 28-row table (983 chars in total), and a closing line, in 17-char deltas. Discord used its native chunking with `streaming.block.enabled: true`. The captured payloads are the `POST /channels/<dm>/messages` bodies:
- Base, Discord default coalescing: message 0 is the intro plus a code table R01–R11. Message 1 is a code table R12–R24 followed by a raw `| R25 | North | 1124 | 2224 |`. Message 2 is raw rows R26–R28 plus the outro.
- Base, coalescing off: block 1 is a code table R01–R24; block 2, 1.2 s later, is raw R25–R28 plus the outro.
- This branch, both coalescing settings: every row R01–R28 is inside code tables. The table only splits at Discord's own 17-line message cap, which reopens the fence. The outro is a separate block, and there are no raw rows.
- Rerun on head `2b3ec27a` with default coalescing: 3 messages, every row inside code tables, no raw rows.
- Oversized control (60 rows, 1943 chars; also rerun on `2b3ec27a`): both base and this branch split cleanly at a row boundary. The headerless continuation still renders raw on both, which is existing behaviour for tables longer than `maxChars`.
**Focused tests**
- The new `EmbeddedBlockChunker > Markdown tables` cases cover a short intro, a blockquote table, a break before the table when the intro leaves no room, and a table exactly `maxChars` long, each at 1/17/43-char deltas. All four fail on base (they get `822,160`-style splits) and pass here. The oversized-table controls (60 rows, and a table whose trailing spaces push it past the cap, streamed and flushed at once) check that every chunk stays within `maxChars` and splits only at row boundaries. They pass on both base and here. The trailing-space control fails if the size check ignores trailing spaces. Two more cases cover the provisional-span edges. Prose lines containing `|` chunk exactly like the same text without pipes, and a table exactly `maxChars` long is emitted as soon as the next line starts even while whitespace keeps streaming. Both fail on the previous revision of this branch. A further case sends an intro, a table exactly `maxChars` long and a directly following `# heading` in one delta as well as streamed. It yields intro, table, heading, and it failed on the revision before this one. The nine new cases take about 2.3 s in total locally; each 1-char cap-edge case takes about 0.45–0.5 s.
- `embedded-agent-block-chunker`, `block-reply-flushing`, `chunking-fences`, `final-answer-delivery`, `text-end-reconciliation`, `btw`, `auto-reply/chunk`, `markdown-core` `tables` and `fences` all pass.
- A chunker probe over 13 table shapes × 5 delta sizes (no-edge-pipe rows, single column, quoted, fenced, newline/sentence preference, `flushOnParagraph`, `minChars: 1`, two tables, prose with pipes) found no split fitting table, no chunk over `maxChars`, and unchanged source coverage.
Pash/Sarah check: #150407 touched `docs/concepts/streaming.md`, but only the Slack progress section. There's no other overlap, and Slack compact progress is unaffected.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Use the existing hosted retry file packing for hybrid tooling so an
indivisible file can share its spare worker without increasing its price.
The exact hourly hybrid plan drops from 80 total rows to 78, within the
unchanged 77 Node plus two dist limit. GitHub remains at 76 total rows.
Add the hybrid hourly cap regression and retain the GitHub ownership and
cap assertions. Preserve the complete file/config/environment inventory,
release descriptors, timing weights, runner policy, and worker limits.
Validation: 26,658 tooling tests and all test types passed on Linux
Testbox; script types and lint passed there. The connection ended after
those gates, so final root-test lint and format checks passed locally.
The integration file passed all 10 cases locally. P2 autoreview is clean.
* fix(node-host): prepare updates on Bun-only POSIX hosts
Read exact registry manifests and stream SHA-512-verified archives in process, then use shared Bun staging inside the private node runtime generation. Preserve npm preparation on Node and Windows, including existing schema and candidate checks.
Cover Bun preparation, private bins, retained generations, invalid registry archives, loopback discovery, and Windows npm selection. Remove the Linux smoke blocker and document the remaining Windows limitation.
* refactor(update): share registry package document reads
Consolidate registry URL construction, HTTP fetching, JSON consumption, deadlines, and response cleanup for update discovery and node runtime preparation. Preserve caller metadata mapping, HTTP error strings, diagnostic labels, and body timeout policies.
Remove the redundant registry status wrapper and share the existing discovery error return. This removes 25 production lines without changing preparation or its regression tests.
* fix(node-host): skip npm freshness probing for Bun preparation
* fix(node-host): create the private Bun node_modules root before staging
* fix(node-host): seed the private Bun global project manifest
* fix(node-host): skip Node engine checks for candidates run on Bun
* docs(bun): link node update preparation history row
* refactor(node-host): move registry archive reads out of install-source-utils
install-source-utils sits under plugin install owners; importing the shared
registry document reader from it closed an import cycle through the provider
and plugin metadata graph. The in-process manifest and archive reads now live
in a leaf module that only the node host imports.
* fix(gateway): keep worker turns working on session hosts that predate new worker tools
A Gateway built from main added the `presence` worker session tool, and
every worker-turn launch to a published openclaw@2026.9.6 session-host
node failed with `INVALID_REQUEST: invalid worker launch descriptor`
followed by `node worker cancellation timed out`. The installed node
supervisor validates `toolAuthority.allowedToolNames` against a closed
vocabulary, and bundle refresh does not replace the supervisor.
The Gateway now negotiates the launch vocabulary per node, following the
existing capability pattern: it advertises
`node-worker-launch-tool-names-v1`, updated nodes declare
`workerHost.launchToolNames` only to Gateways that advertise it, and the
Gateway treats an absent declaration as the frozen 2026.9.6 vocabulary.
Tool authority filters the projected tool set by the destination
vocabulary, so turn authorization and the launch descriptor stay
identical. Unknown declared names are ignored, so future worker tools
need no further capability.
Update behavior: Gateway-first updates keep older nodes hosting turns
without newer tools (one info log names them); node-first updates keep
the declaration unchanged for older Gateways; updated pairs get presence.
* fix(scripts): add worker tool authority to the PR wrapper inventory
src/infra/node-runner-inventory.ts is in the trusted-anchor PR wrapper's runtime import closure and now imports src/worker/tool-authority.ts for launch tool-name negotiation; the extracted wrapper could not resolve it.
* test(gateway): await the shared async node tunnel manager fixture
Main moved createManager into node-worker-tunnel.test-support.ts as an async helper (#160415); the launch-vocabulary lifecycle test still called it synchronously.
* fix(ui): show exact scope approval command
Include the pending request ID in the Limited access approval instructions. The former openclaw devices command only printed help; render openclaw devices approve with the current request ID and align the connection documentation without changing scope or pairing policy.
Proof: real read-only browser requested admin, rendered the actual pending ID, and reconnected with admin scopes 981ms after CLI approval. Denial, expiry, and retry were also exercised. The new regression fails before the fix; sidebar tests pass (9 tests, 3.74s wall). Combined changed checks and UI build pass remotely. Local formatting hooks are replaced by completed remote gates; both private staged-content scans pass.
* test(ui): assert the exact scope approval command in the live e2e
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cron): named-session jobs run in the wrong workspace
Use saved session workspace and cwd bindings for explicit named-session jobs,
retain their managed worktree lease, and preserve workspace context and
restrictions across persistent rollover without changing detached runs.
Co-authored-by: vyctorbrzezowski <51521767+vyctorbrzezowski@users.noreply.github.com>
* fix(cron): named-session jobs run in the wrong workspace
Worked on by:
- @vyctorbrzezowski
Co-authored-by: vyctorbrzezowski <51521767+vyctorbrzezowski@users.noreply.github.com>
OpenClaw-Publication: 85b73558-d465-449a-8257-0d87d3c1ccf8
* fix(cron): prevent cross-session scheduled file access
Bind requester-scheduled saved-workspace access to the authenticated owning
conversation while preserving trusted operator targeting. Prove the path
from creator tool grants through stored jobs and scheduling to real file I/O,
including foreign and archived-session rejection.
Correct mismatched session-key fixtures and make the outbound retry fixture
use the runner session reader and canonical delivery-plan owner.
Co-authored-by: vyctorbrzezowski <51521767+vyctorbrzezowski@users.noreply.github.com>
* fix(cron): reject mismatched worktree owners
Apply the existing session-worktree owner invariant to the already-read registry record, before and after lease acquisition. Prove valid leases and rejected foreign-session or manual-owner bindings through real registry rows, Git worktrees, and final coding-tool file reads.
Co-authored-by: vyctorbrzezowski <51521767+vyctorbrzezowski@users.noreply.github.com>
---------
Co-authored-by: vyctorbrzezowski <51521767+vyctorbrzezowski@users.noreply.github.com>
Worked on by:
- @vyctorbrzezowski
OpenClaw-Publication: 9ef95dec-f3a2-4939-9bee-d50cc60bc93d
Co-authored-by: vyctorbrzezowski <51521767+vyctorbrzezowski@users.noreply.github.com>
A label-only sessions.patch never resolved a pending row target or
published a field receipt, so the acknowledged rename was ignored until a
sessions.changed event or a post-commit sessions.list arrived. Closing the
title editor therefore showed the old name again after the Gateway had
committed the new one. With a slow roster read in flight and the change
event delayed, a real Gateway showed the old title for about one second
after the ack.
Route labels through the existing patch receipt owner: resolve the pending
conversation for label patches, project entry.label from the committed
receipt, and type it in the UI patch result. The receipt carries the
existing connection, agent, session-generation, updatedAt and read-cutoff
fences, so a roster read started before the rename cannot replace it while
newer Gateway snapshots still win.
The earlier permanent revert seen in the rename-composition probe was a
mock artifact: it answered a post-commit list with pre-commit rows and no
snapshotAt. On a real Gateway the event and post-commit list carry newer
snapshotAt clocks and eventually restore the name; the defect was the
visible revert window, not a lasting loss.
Use logical inline-start offsets for the track, markers, and previews so a right-to-left interface does not clip navigation outside the transcript. Keep the existing left-to-right layout unchanged.
Extend the existing layout case to select Arabic UI, not only Arabic message text, and assert horizontal containment. The regression fails before the CSS repair. All nine layout tests pass in 56.98 seconds; real isolated Gateway probes confirm pointer hit-testing in Arabic and Persian across light and dark themes. Remote changed checks pass against c377d7ca82.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix: migrate retained plugin settings before update activation
Complete selected plugin-owned config repairs through the existing update and replacement-install lifecycle before activation. Preserve pending obligations with actual config rollback, retain published package generations when rollback is unconfirmed, and refuse unresolved active inputs without suppressing committed partial updates.
* test: align replacement-install warning fixture snapshots
* fix(plugins): install npm-sourced plugins on Bun-only hosts
Bun-only installs have no Node, so every npm-sourced plugin operation that
spawned `npm` failed. OpenClaw's managed plugin roots rely on npm's lockfile,
peer planner, and lock-based rollback, so keep npm's semantics and run a
pinned npm CLI under Bun instead of switching package managers.
- Add npm 11.20.0 as an exact dependency (npm 12.1.0 fails the managed peer
planner under both Bun and Node).
- One owner, resolveNpmCommand(), serves all plugin install, update,
uninstall, peer-planning, prune, and npm-config call sites: Node keeps
`npm` unchanged; Bun runs `<bun> <bundled npm-cli.js>`.
- npm's bundled minimatch/brace-expansion/ip-address sit below the workspace
security floors and cannot be overridden; a maintainer-approved exception is
bound to npm@11.20.0's exact bundle, and the npm lock mirror verifies those
members against the pnpm-locked npm tarball.
* refactor(plugins): keep npm fund suppression out of the shared spawn helper
Keep managed Bun npm installs quiet through their existing --no-fund arguments and safe install environment. Leave the shared process helper unchanged from main to reduce PR #160224 CI fanout.
* fix(plugins): keep the bundled npm lookup error private
* fix(ui): explain invalid dashboard pairing links
Distinguish expired or consumed dashboard handoffs from a rejected shared Gateway secret. Show the fresh dashboard command and JSON browserUrl fallback, and document recovery without rotating valid credentials.
Proof: real Gateway and Chromium on Blacksmith Testbox reproduced reused and expired links before and after. The new regression fails on the original heading; login-gate tests pass (30 tests, 4.28s wall). Combined changed checks, i18n, and UI build pass remotely. Local formatting hooks are replaced by the completed remote gates; both private staged-content scans pass.
* fix(ui): retire rejected dashboard handoffs before retry
Clear the current connection bootstrap token and profile after the Gateway explicitly rejects that handoff. Correct manual credentials can then reach the existing shared-auth path rather than losing to an already-consumed bootstrap. Preserve fresh handoff precedence and pairing policy.
Proof: a real Gateway and Chromium remained rejected for 30s before the fix and connected in 308-312ms afterward. The regression fails before the fix; gateway-store auth tests pass (27 tests, 2.68s wall). Combined changed checks and UI build pass remotely. Local formatting hooks are replaced by the completed remote gates; both private staged-content scans pass.
Co-authored-by: Peter Steinberger <steipete@gmail.com>