* fix: record missing message-tool source replies
Settled message_tool_only runs now record a bounded message-tool-not-called terminal fact when no current-source delivery, silence, or continuation exists. Private final text remains private; long-final recovery and actual deliveries remain unchanged.
* fix: preserve intentional message-tool non-delivery
Reuse the canonical private-final exclusions for policy-denied, heartbeat, and room-event runs before recording message-tool-not-called. Keep stranded retry diagnostics unchanged and cover each lifecycle outcome.
* refactor: remove private-final test-only export
Keep the warning predicate local to its test now that production shares the canonical private-final classifier directly. This satisfies the exact-head dead-code export gate without changing runtime behavior.
Preserve distinct domain-specific skills with reliable triggers during automatic collection cleanup while retaining junk, duplicate, and stale-fragment removal.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Recognized workspace paths in chat now read as their basename behind a
file-type glyph, so a file reference is identifiable before it is read.
Markdown, package manifests, TypeScript and other code, TSX/JSX components,
config/data, shell scripts, and images each get their own mark; anything else
falls back to a plain document. Paths sharing a basename keep the smallest
trailing suffix that tells them apart, and Windows paths keep their own
separator.
Classification wins over authoring syntax: a path written in backticks is a
file link first, so it drops the inline-code chip and renders exactly like a
bare path beside it. Code spans that are not file links keep the chip.
The full path stays addressable: it drives the file panel, the new tooltip,
and the message Copy action (which returns the original Markdown).
Author-written labels in [label](path) links are never rewritten, and text
that is not a recognizable path stays plain prose.
The glyph is painted as a masked ::before like the sibling GitHub mark, so it
stays out of the accessibility tree and out of copied text and follows the
link color in every theme. Extension classification moves to one shared
resolver that the file preview modal now uses too, replacing its own
code/text extension set.
* fix(agents): report best-effort delivery failures
Keep explicit delivery intent through Gateway and agent preflight so final delivery records a send or concrete failure outcome. Persist replay markers only after a route is prepared.
Refs #121804.
* test(gateway): preserve requested delivery contract
Update the multi-channel Gateway regression to assert that best-effort preflight preserves explicit delivery intent for final outcome reporting.
Refs #121804.
* fix(mxc): validate agent workdirs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fc20c06c-e8c0-4649-9efd-ad87fac71a4f
* fix(mxc): treat non-directory parents as unavailable workdirs
validateWorkdir declares that unusable workdirs return null, but only ENOENT was classified as missing. A workdir nested under a file raises ENOTDIR on Linux, which escaped as a raw filesystem error instead of the normal unavailable-workdir result.
The same ENOENT-only classification appeared at three sibling sites in this file, so the shared isMissingPathError guard fixes the whole class rather than the single reported path.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7253887-71c9-4083-a591-91979c981e39
* fix(mxc): sandbox provisioning crashes reading containerWorkdir
MxcFsBridge initialized three fields from this.sandbox, but sandbox is a constructor parameter property. Plugin sources load through jiti, which evaluates field initializers before assigning parameter properties, so this.sandbox was still undefined and every MXC sandbox provisioning failed with SandboxProvisioningError: Cannot read properties of undefined (reading 'containerWorkdir').
Assign the derived fields in the constructor body from the sandbox parameter instead. The repo tsconfig sets useDefineForClassFields=false, so tsgo and Vitest (esbuild) both apply legacy ordering and could not observe the failure; only the jiti-loaded runtime path did.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d7253887-71c9-4083-a591-91979c981e39
---------
Copilot-Session: fc20c06c-e8c0-4649-9efd-ad87fac71a4f
Copilot-Session: d7253887-71c9-4083-a591-91979c981e39
`channels.line.mediaMaxMb` has no range constraint, so a configured `0` or
negative value loads cleanly and then survives the `??` chain in `createLineBot`
into `mediaMaxBytes`. Every non-empty inbound media download is then measured
against a 0-byte budget it cannot satisfy: `saveMediaStream` throws
`Media exceeds 0MB limit`, the LINE handler degrades the attachment to
`[line attachment unavailable]`, and the only trace is a verbose log line that
never names the setting.
Treat a non-positive value as unset at every link of the chain, so it falls back
to the same 10 MB default an unset field already means, and a non-positive
caller override no longer discards a valid account config value. Matrix
(#120466) and Zalo (#120988) resolved the identical contract the same way; LINE
was the last bundled channel resolving this field by hand without the guard.
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* longcat: align baseUrl with docs, fix cacheWrite pricing, add brand icon
- Point baseUrl at the documented https://api.longcat.chat/openai/v1 path
instead of the undocumented unversioned alias route.
- Fix cacheWrite pricing to 0: the LongCat pricing page has no separate
cache-write charge (matches deepseek/moonshot/zai catalog conventions).
- Update the pricing page link to the current docs path.
- Add the official LongCat brand icon (provenance recorded in
ATTRIBUTION.md) and display-name mapping so the Control UI shows proper
branding instead of the letter-badge fallback.
- Migrate the persisted legacy default baseUrl via a plugin-owned doctor
configRepair contract: onboarding persists
models.providers.longcat.baseUrl and the runtime reads the stored value,
so the contract rewrites exactly the former default to /openai/v1,
preserves custom endpoints, and warns via a legacy-config rule.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(longcat): correct provider metadata
* fix(longcat): migrate persisted stock pricing
* style(longcat): format doctor repair
* fix(longcat): preserve doctor migration after compat cleanup
* fix(longcat): avoid map spread in doctor repair
---------
Co-authored-by: yuzehui02 <yuzehui02@meituan.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
* fix: exec timeout field does not state its unit, so callers pass milliseconds
`exec.timeout` is in seconds, but its sibling `yieldMs` is in milliseconds and
the `process` tool's identically named `timeout` is also in milliseconds. Those
two tools are used together in one workflow: exec backgrounds a long command,
then process polls it.
The unit exists only in the field description, and code mode deliberately defers
descriptions - the model sees `timeout?: number` and nothing else. A caller that
guesses milliseconds passes 900000 intending 15 minutes and gets ~10 days, so
the command never times out.
Add `timeoutSeconds` as the canonical field, matching the convention already
used by the config default (`tools.exec.timeoutSeconds`) and the sibling
(`yieldMs`). Keep `timeout` as a deprecated alias; `timeoutSeconds` wins when
both are supplied. Project the new field onto `nodeExecSchema`, which
hand-picks its properties.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix(ci): satisfy test-type and dependency checks
Two CI failures from the test approach, not the change itself.
- check-test-types: TypeBox's TOptional does not surface `description` on its
static type, so the schema assertions failed under tsgo. Read it through a
narrow accessor instead.
- check-dependencies: knip flagged resolveExecTimeoutSeconds as an unused
export because only the test imported it. Move it beside the schema it
resolves, where the exec runtime imports it, so it is production-reachable.
Verified with the same commands CI runs: `run-tsgo.mjs -p
test/tsconfig/tsconfig.core.test.json` reports no errors in these files, and
knip --production reports no unused exports.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix(ci): fit the exec description budget and satisfy oxlint
- `agent-tools.schema.test.ts` enforces a 550-char budget on the exec tool's
model-facing descriptions; the two new ones pushed it to 640. Shortened both.
The rename is what makes this possible: the unit now lives in the field name,
so the description no longer has to carry it. Extended that budget test to
pin `timeoutSeconds` alongside `timeout`.
- oxlint `no-base-to-string`: the test's description accessor typed the field as
`unknown` and stringified it. Typed as `string | undefined` instead.
Verified: budget test passes (137 tests green across both suites), and all six
behavior tests still fail against origin/main.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix: remove the unit-ambiguous exec timeout field instead of deprecating it
Owner decision on the review's "ambiguous model-visible alias" finding: drop
`timeout` entirely rather than keep it as a deprecated alias.
Keeping it visible defeated the purpose. Code mode renders property names and
types and defers descriptions, so a model would still be offered a bare
`timeout` sitting next to a millisecond-based `yieldMs` and could still pick it.
A deprecation marker in a description the model never sees changes nothing.
Backward compatibility is not required here: every tool call is constructed
fresh by the model from the current schema, and a repository search found no
internal caller passing `timeout` to the exec tool. `timeoutSeconds` is now the
only exec timeout field, on both the main and node-only surfaces.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix: migrate the internal exec callers to timeoutSeconds
Removing `timeout` from the exec schema broke three real internal callers that
the earlier survey missed, plus two test files and the runtime's own reads.
`check-test-types` caught it; my local search had filtered `timeout:` matches to
lines mentioning "exec" and discarded these.
- src/auto-reply/reply/bash-command.ts
- src/auto-reply/reply/commands-diagnostics.ts
- src/auto-reply/reply/commands-export-trajectory.ts
all passed `timeout: timeoutSec` and now pass `timeoutSeconds: timeoutSec`.
- bash-tools.exec-run.ts reads the field through the same typeof narrowing the
third call site already used; `params` is loosely typed, so a raw read is
`unknown`.
- Test fixtures updated. A vitest `waitFor({ timeout })` option in
background-abort.test.ts is NOT an exec param and was left alone.
`run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json` now reports zero errors
in changed files; the 5 remaining `rejectSymlinks` errors are pre-existing on
main. 148 tests pass across the affected suites.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* docs: synchronize the exec config table with timeoutSeconds
Addresses the P2 finding. The tools.exec.timeoutSeconds row still described the
per-call override as `timeout` and `timeout: 0`, which no longer exist. The
only remaining bare `timeout` reference in this page is the deliberate
contrast with the process tool's millisecond field.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix: migrate the QA lifecycle caller and the shared ExecToolArgs contract
Two findings from the re-review of 6e8c3560e7, both real.
[P1] test/e2e/qa-lab/runtime/openclaw-exec-process-lifecycle.e2e.test.ts:174
still passed `timeout: 0.05`. After the removal that field is accepted and
ignored, so the call silently used the default timeout and could not produce
the asserted 50 ms `overall-timeout` result. Migrated.
[P2] ExecToolArgs in bash-tools.exec-request-preparation.ts:33 still declared
`timeout?: number` while the runtime reads only `timeoutSeconds`, offering
request-preparation users an accepted-but-inert argument. Renamed.
Why the typecheck missed both: ExecToolArgs is
`Record<string, unknown> & {...}`, and that index signature defeats excess
property checking, so a stale member produces no error at any call site. The
e2e file also sits outside the core test tsconfig I had been running locally.
Left alone deliberately: `processTool.execute({ action: "poll", timeout })` is
milliseconds and correct, and `createBashTool` in sessions/tools/bash.ts is a
separate tool surface with its own timeout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix(test): update the node exec tool property assertion to timeoutSeconds
src/gateway/tool-resolution.exclude.test.ts:413 asserts the exact property list
of the node-forced exec tool and still expected "timeout". Renamed to match
nodeExecSchema.
Found by checks-node-compact-large-5; my local core-test tsconfig does not cover
src/gateway, so it did not surface there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix: accept the legacy exec timeout as an undocumented alias
Removing the field from the schema does not reject it at runtime: the exec
schema accepts unknown properties, so a call built against an older schema was
accepted and then silently ignored, taking the default deadline instead of the
one it asked for.
Route every read through resolveExecTimeoutSeconds, which prefers the canonical
timeoutSeconds and falls back to a numeric timeout. The alias stays out of
execSchema, nodeExecSchema and ExecToolArgs, so no new caller can adopt it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* docs: update the background-process guide to timeoutSeconds
The exec parameter table and the inheritance note still taught `timeout` and
`timeout: 0`, which kept the retired spelling publicly discoverable and
contradicted the exec guide.
`poll`'s own millisecond `timeout` is left alone; it is a different tool and a
different unit, which is the collision this rename removes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh
* fix(exec): reject removed timeout field
* chore: regenerate plugin SDK API baseline
---------
Co-authored-by: Marvinthebored <262704729+Marvinthebored@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
Keep provider-hosted web_search alongside Code Mode's constrained function surface when OpenAI policy authorizes it. Enforce the same authorization at Responses transport egress.
Refs #121803.
* refactor(agents): split claude-live-session into concept modules
* refactor(agents): delete duplicated live-session helpers
* fix(agents): fence live-session close during pending spawn
* chore(lint): ratchet max-lines baseline after live-session split
* style(agents): satisfy lint on live-session split modules
* fix(agents): fence live session close state
* refactor(agents): extract Claude live turn timeouts
* fix(agents): preserve exec policy after Claude live split
* test(agents): complete Claude live policy session fixture
* fix(ui): rename and delete session groups in owned dialogs
The sidebar group menu was the last session surface still asking browser
chrome: Rename group opened window.prompt and Delete group opened
window.confirm. Both are unthemed, unvalidated and unavailable in the
webviews the Control UI also ships in, where they answer silently.
Rename now uses the owned input dialog, prefilled with the name it is
changing and titled with it. showInputDialog gains requireChange, which
holds submission closed while the entry still equals defaultValue, so the
no-op rename the caller used to discard after the fact is not submittable
in the first place. That replaces the caller-side "next === group" bail and
puts the empty check on one predicate shared by the button state and the
submit path.
Delete now uses the owned danger confirm, beside the other destructive
session mutations in the lazily loaded operations module, with Cancel
focused and no opt-out. Its copy said the sessions move to "Ungrouped", a
section id no operator ever sees; the list is labelled Sessions, so it now
says they move back to the session list. Deleting a group really does keep
them: session-groups clears the category on every member rather than
removing anything.
The confirm follows the access check so nobody is asked about a delete the
Gateway would refuse, and the mutation scope is reproven once they answer.
* test(ui): freeze animations in Control UI proof captures
A dialog capture taken during its fade-in shows a half-transparent card over
the page behind it, which proves nothing about the state it was taken for.
Playwright can settle running transitions before the shot.
* fix(ui): stop repeating the group name in the delete confirm
The title said Delete group and the message repeated it with the name, so
the operator read the same three words twice before reaching what actually
happens. The title now carries the group it is about and the message is
left to state the outcome.
* fix(ui): keep stale group deletes retryable
* fix(ui): match the stale delete notice to its sibling
The retry notice led with the negation and used curly quotes the rest of
this dialog family does not, so it read as a different product's copy. It
now states the cause and the next step the same way newGroupStale does for
the same replaced connection.
* fix(ui): open the session companion from one header toggle
The collapsed rail was a button floating over the message area whose click
set the display preference to "pill", so opening the companion took three
interactions and the first two landed on surfaces nobody asked for. On an
idle session the pill had no digest to show and degraded to a bare title
with an X and a chevron.
The control moves into the pane header action row beside background tasks,
workspace files, and split view, where it carries aria-expanded and closes
the panel when pressed again. Opening now expands transiently instead of
persisting "card", matching the automatic path from /btw and the transcript
selection actions; the pill stays as the ambient density for a running
digest. Closing drops the manual-open claim so an idle session falls back to
nothing rather than to an empty pill.
The panel's empty state offers three starter questions instead of a sentence
about being read-only, the digest band no longer renders when there is no
digest, the destructive thread clear moves into an overflow menu, and the
composer input is anchored to its footer.
Closes#121440
* test(ui): cover the session rail header action slot
* fix(ui): label the companion composer without the undefined sr-only class
The composer's screen-reader label used class sr-only, which no stylesheet
defines, so "Ask the session companion" rendered as visible text above the
input. An aria-label on the input carries the same information to assistive
technology and does not depend on a missing utility class.
The class is applied by roughly fifteen other Control UI modules and is
tracked separately; this change only removes the rail's dependency on it.
* refactor(ui): read the mirrored rail mode through one accessor
* docs(ui): state why collapsing the rail keeps observer visibility
* fix(ui): dock the session companion as a side column instead of a card
The companion opened as a floating card over the conversation on any pane
narrower than 1080px, which is most of them once a sidebar or a split pane
takes its share. It covered the transcript the operator was reading and read
as a pop-up chat rather than a surface belonging to the pane.
Docking is now the default whenever the column and a readable thread both
fit, measured against the width left after the workspace and task rails take
theirs — the same way the workspace rail decides. The docked rail claims
layout space, so the thread reflows narrower instead of being painted over,
and it drops the shadow, blur, and animation that made it read as an overlay.
Below that width no pane can hold two columns, so the companion becomes a
full-height sheet on the trailing edge, and a full-screen sheet under 768px.
It still covers the thread there, but as a surface that took the pane over
rather than a card hovering above it.
The starters now sit where the first exchange will land; centring stranded
them mid-column once the rail became full height.
* fix(ui): give the companion the app's send button and a bottom-left opener stack
The rail composer had its own ghost submit icon, which made a second send
pattern in a product that already has one. It now renders chat-send-btn with
icons.arrowUp — the same component and classes the main composer uses, so the
brand circle and every state come from one place instead of a lookalike.
The openers move out of the thread and stack against the composer they feed,
left-aligned, so the eye travels chips to input without crossing the thread.
The explainer keeps the thread's empty space to itself, centred on both axes.
Also drops the now-unused submit colour rule and three svg declarations that
strokeIcon already inlines.
* test(ui): bound the rail against its container, not the old card cap
Three browser assertions encoded the floating card's max-height (680px
desktop, 460px mobile). The docked rail and the narrow sheet are deliberately
full height, so those numbers no longer describe anything.
The invariant they protected is still real and still asserted: a long thread
scrolls inside itself instead of the rail outgrowing what contains it. The
bound is now the pane's own height on desktop and the viewport on mobile,
which is what "do not expand to swallow the layout" actually means.
* fix(ui): stop rail metadata inheriting the thread's scroll layout
Removing the duplicate __empty declaration took its closing brace with it, so
the pr-checks, timestamp and hint selectors merged into the thread's rule.
Each would have rendered as a 96px scrolling bordered flex column instead of
a muted line of metadata.
The narrow sheet also kept the base rule's shadow and blur, which are card
cues on a surface that is meant to read as part of the pane; it drops them,
and the docked rule stops repeating resets it now inherits.
The fixture grows a timestamp and a hint so a browser assertion can hold the
metadata and the thread apart — the empty-companion captures never rendered
either node, which is why this reached review.
* fix(ui): reset the undocked sheet's fade-in and cover the metadata layout
The expanded sheet reset the shared shadow and blur but still inherited the
rail's fade-in, so a narrow-width open animated in like the floating card the
redesign replaces. The reset now lives on the expanded rule and the docked
rule stops repeating what it inherits.
Coverage follows the defect rather than the fix: the browser fixture renders
PR checks alongside the timestamp and hint, and the undocked-sheet assertion
now reads computed shadow, blur, and animation instead of geometry alone.
* test(ui): compare rail metadata against the thread, not a literal
min-height computes to auto on these grid items, so asserting 0px failed on
the metadata the guard was meant to protect. The invariant is that the nodes
do not share the thread's rule, so compare against the thread's own value and
let it change without breaking the guard.
Preserve the internal null Authorization marker when resolving local no-auth provider headers, avoid classifying LM Studio prompt-template guidance as context overflow, and rebuild ai/llm-core package sources during pnpm dev/watch.