* fix(acp): scope /acp sessions listing for non-owner senders
/acp sessions listed every ACP session on the gateway for any sender
allowlisted via commands.allowFrom, exposing other senders' session
labels, agent ids, runtime state, and thread bindings. The handler now
returns only the current bound or requester session for non-owner
senders, while owner identity and operator.admin clients keep the full
gateway-wide listing, matching the documented contract.
Fixes#103055
* test(acp): cover empty and missing-session cases for /acp sessions scoping
* fix(acp): avoid non-owner session scans
* docs(acp): remove duplicated session scope text
* test(acp): cover internal session visibility
* fix(acp): require current ACP metadata
* test(acp): reject sessions target tokens
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(acp): persist cancelled partial replies
* fix(acp): persist delivered output for cancelled turns
Normalize terminal status at the ACP manager boundary and settle routed/direct delivery outcomes before persisting cancelled bound turns.
Co-authored-by: shaoohh <150606856+shaoohh@users.noreply.github.com>
* fix(acp): require confirmed cancelled-turn delivery
Persist cancelled-turn output only when the core dispatcher reports successful delivery. Keep canonical ACP history independent of outbound-only hook rewrites and prove backend cancellation with the real dispatcher.
Co-authored-by: shaoohh <150606856+shaoohh@users.noreply.github.com>
* test(acp): satisfy cancellation proof lint
Keep the pending-delivery race assertion behavior while avoiding a return value from the Promise executor.
Co-authored-by: shaoohh <150606856+shaoohh@users.noreply.github.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Run-mode subagent cleanup retired the session bundle MCP runtime even when an
approved exec follow-up turn still held an active materialization lease, so the
follow-up turn failed with "bundle-mcp runtime disposed for session ...".
Retire run-mode subagent runtimes with preserveActiveLeases so retirement defers
while a lease is held, and complete the deferred retirement when the run's
materialized tool runtime releases its lease.
* fix(usage-bar): clear dead watcher reference after transient error
When a usage bar template file watcher hits a transient error, the error
handler closes the FSWatcher but leaves entry.watcher pointing to the
closed instance. loadUsageBarTemplate treats a truthy cached.watcher
as "still watching" and never re-reads the file, so a template that
became invalid stays stuck at the default forever.
Clear entry.watcher after closing so the next access that needs a
re-read can create a fresh watcher.
This matches the pattern from #109682 (config hot-reload watcher
recovery).
* fix(usage-bar): invalidate cached template on watcher error
Clear both entry.watcher and entry.template when the FSWatcher errors
so the next loadUsageBarTemplate call re-reads from disk and creates a
fresh watcher. Previously only the watcher reference was cleared,
leaving a valid cached template that never observed future file edits.
This addresses the remaining valid-cache recovery gap from #109682.
* fix(usage-bar): fix no-promise-executor-return lint in test
* fix(config): bound state-directory .env file reads with size limit
Replace unbounded fs.readFileSync with readRegularFileSync capped at
1 MiB (MAX_STATE_DIR_DOTENV_BYTES) so an oversized .env file is
rejected before loading the entire file into memory.
Resolve symlinks via fs.realpathSync before the bounded read so
symlinked .env files keep working — matching the marketplace.ts
pattern for bounded manifest reads.
* fix(config): add diagnostic when oversized state .env is skipped
Log a warning when the state-directory .env file exceeds the 1 MiB
limit so operators know a configured file was skipped — matching the
pattern from #108200 (plugin catalog bounded read diagnostic).
* fix(config): bound global runtime dotenv file reads with size limit
Replace unbounded fs.readFileSync in readDotEnvFile with readRegularFileSync
capped at 1 MiB so CLI/Gateway startup dotenv loading also gets the bounded
read protection — not just the state-dir service-env path.
This addresses the P1 review finding that the original fix only capped the
helper-level reader while the shared readDotEnvFile used by
loadGlobalRuntimeDotEnvFiles was still unbounded.
* fix(config): bound external catalog file reads with size limit
Replace unbounded fs.readFileSync with readRegularFileSync capped at
16 MiB (MAX_EXTERNAL_CATALOG_BYTES) to prevent memory exhaustion from
oversized or malicious plugin catalog files.
Resolve symlinks via fs.realpathSync before the bounded read so
symlinked catalog files keep working — matching the marketplace.ts
pattern for bounded manifest reads.
* fix(config): add diagnostic when oversized catalog is skipped
Log a warning when an external catalog file exceeds the 16 MiB limit
so operators know a configured file was skipped. Add regression test
verifying the oversized catalog is skipped and selection continues.
* fix(config): fix false-positive oversized catalog test
Replace the broken spy-on-object-literal mock with a real sparse file
that genuinely triggers readRegularFileSync rejection via stat.size.
The previous vi.spyOn({ readRegularFileSync }, ...) intercepted a
throwaway object, never the actual module import, so the test passed
regardless of whether the fix was applied or not.
Also fix the no-unused-expressions lint error on the env-primary
assertion that used a discarded ternary (? undefined : undefined)
instead of a proper expect assertion.
Reload the current on-disk config once replacement native or polling watchers finish their initial scan, so edits made during recovery backoff are not lost.
* fix(cron): support current and session: targets in initial delivery resolution
resolveInitialCronDelivery only checked sessionTarget === 'isolated' for
default announce delivery. All other cron paths (delivery-plan.ts:106-114,
normalize.ts:680-684, jobs.ts:288-298) already treat 'isolated', 'current',
and 'session:' as equivalent. This gap meant cron jobs created with
--session-target current or --session-target session:... would not get
default announce delivery at creation time.
Real behavior proof:
Input: sessionTarget='current', payload.kind='agentTurn'
OLD: undefined <- missing default announce delivery
NEW: {mode:'announce'} <- consistent with other cron code paths
Co-Authored-By: Claude <noreply@anthropic.com>
* test(cron): add typed initial delivery resolution tests for all session targets
* test(cron): document direct-service delivery contract and cover service callers
* refactor(cron): centralize detached-delivery-target predicate into shared helper
Extract isDetachedDeliveryTarget(sessionTarget, payloadKind) in normalize.ts
and use it from all three callers:
- normalizeCronJobInput (write-time default, normalize.ts)
- resolveCronDeliveryPlan (read-time default, delivery-plan.ts)
- resolveInitialCronDelivery (service-bypass default, initial-delivery.ts)
This removes the third manually synchronized copy that the original PR
added, addressing ClawSweeper P1 finding.
* fix(cron): align initial delivery targets
Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>
* test(cron): consolidate initial delivery coverage
Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>
* test(cron): keep delivery matrix type-safe
Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>
* chore: keep release note in PR metadata
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): use fatal UTF-8 decoding in provider response readers
* fix(agents): use fatal UTF-8 decoding only for JSON responses, preserve compatibility for text
Add optional per-job pacing bounds across the cron API, CLI, tool schema, public output, and SQLite job envelope, requiring at least one bound. Allow only the currently running paced job to record a one-shot next_check proposal and carry it through isolated-run completion.
After successful runs, clamp the proposal to the job bounds and persist an exact one-shot slot marker so maintenance preserves only that timestamp. Clear the marker on runs, edits, and schedule normalization; preserve existing no-proposal, skip, timeout, and error scheduling behavior.
* fix: bound misc unbounded fs.readFile calls; remove unused fs import
* fix: decode buffer to string before passing to string consumers
readRegularFile and readRegularFileSync return { buffer, stat },
not a string. All 4 new call sites passed the raw object to functions
expecting a string (JSON.parse, RegExp.test, template literals, etc.),
causing TS2345 type errors and runtime failures.
Fix each call by extracting .buffer and calling .toString('utf8')
before passing the result to string consumers.
* style: fix oxfmt formatting in config-set-input.ts
* fix: bound config and trajectory metadata reads
Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>
* refactor: isolate bounded read ownership
Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(wizard): treat not-directory paths as missing in migration snapshots
The setup migration snapshot and recovery paths checked for ENOENT only
when catching fs errors. ENOTDIR (returned when a path component that
should be a directory is a file) was not recognized, so a not-directory
migration root made fs.readdir throw instead of resolving to no recovery
record, and interrupted the onboarding migration snapshot hash.
Migrate both local isMissingPathError helpers to the shared
isNotFoundPathError guard (matches ENOENT and ENOTDIR), following the
same migration applied to plugins in #107691.
* test(wizard): cover ENOTDIR migration snapshots
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): record token usage when a Responses turn ends incomplete
The agent-side Responses processor only had a terminal branch for
response.completed. A stream that ends with response.incomplete — the
max_output_tokens and content_filter cases, and what Azure emits on early
truncation — matched no branch at all, so the event was dropped: usage was
never recorded and stopReason was never set. The turn reports zero tokens and
zero cost, which is the drift in #100954.
#109615 fixed the same split on the package-side processor by finalizing
completed and incomplete through one finalizeResponse. This does the same for
the agent path, which #109615 did not touch: both terminal events now record
usage, cost and service-tier pricing through one helper.
The helper moves to its own module rather than growing openai-responses-transport.ts,
which is a legacy file the max-lines ratchet will not let grow; extracting the
block drops it from 2502 to 2444 lines.
Content-filtered turns are mapped to a provider error instead of a plain length
stop, matching what the package side already does, so the two terminal surfaces
do not disagree.
* fix(agents): keep Responses output backfill on completed turns
Terminal handling now covers response.incomplete, which also routed those
events into backfillCompletedResponseOutput. That reconstruction exists to
recover a final answer when item events never arrived; an incomplete turn has
no final answer, so replaying its partial output persisted truncated text the
streaming path never emitted. Usage and stop-reason recording still apply to
both terminal events.
* refactor(ai): share one Responses terminal usage mapper
The agent transport mapped terminal usage buckets, cost, and stop reasons in
parallel with the package-side processor, so the two could drift on token
buckets, service-tier pricing, or future terminal-event semantics.
Both now call one canonical mapper. openai-responses-shared.ts cannot be
imported across the package boundary, so the mapper lives in its own module
re-exported through the existing internal/openai entry point; no new subpath
is introduced. The agent module keeps only the reasoning-token accounting the
package path does not track.
Merging the two revealed a real disagreement on totalTokens: the package took
the reported total, the agent summed the split buckets. The canonical rule is
now max(bucket sum, reported total), which keeps the reported value while
covering both payloads that omit total_tokens and payloads whose cached_tokens
exceed input_tokens, where clamping leaves the reported total short.
* fix(agents): preserve partial incomplete Responses output
Co-authored-by: Yigtwxx <yigiterdogan023@gmail.com>
* test(agents): cover incomplete terminal output backfill boundaries
The head's partial-output preservation is only safe while it stays a recovery
path. Pin both sides of that guard: text that already streamed must not be
replayed from the terminal payload, and a non-length incomplete stop must not
surface partial text as an answer.
* fix(agents): avoid shadowing Responses options
Co-authored-by: Yigtwxx <yigiterdogan023@gmail.com>
* style(agents): apply oxfmt to the incomplete backfill test
* test(agents): register incomplete Responses suite
Co-authored-by: Yiğit ERDOĞAN <yigiterdogan023@gmail.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* Bound bundle command file reads with size cap
* fix: use buffer.toString for readRegularFileSync result
* fix: log oversized bundle command file diagnostic instead of silent skip
The catch block now captures the error and emits a console.warn with the file path and error detail, so upgrades do not silently remove oversized installed commands.
* test: verify oversized bundle command file is skipped and siblings continue
PR #110594: Add focused regression coverage:
- Test: an oversized bundle command markdown file (>1 MB) is skipped via catch + continue
- Test: normal sibling command files still load correctly
- Verifies console.warn diagnostic is emitted for the oversized file
* refactor(plugins): log rejected bundle commands
Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>
* style(plugins): format bundle warning
Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): preserve filename case for agent file writes on Windows
toRelativePathUnderRoot passed root and candidate through
normalizeWindowsPathForComparison, which lowercases, and then returned the
resulting relative path. Callers build files out of that path, so an agent
asking for src/Components/MyComponent.tsx got src\components\mycomponent.tsx
on disk. NTFS is case-preserving, so nothing fails locally, but git records
the lowercased name and the imports the agent wrote break on Linux and in CI.
Lowercasing is not even case-safe for every name: "İstanbul.md" lowercases to
"i̇stanbul.md" (U+0130 becomes U+0069 U+0307), one code point longer and not
reversible, so the filename is corrupted rather than merely recased.
The lowercasing was never needed for the boundary math: path.win32.relative
already matches the root case-insensitively and returns the tail in its
original case. Extended-length prefix stripping is still needed, or a \?\
candidate relativizes to ..\..\..\?\C:\... and reads as an escape, so this
adds normalizeWindowsPathPreservingCase next to the comparison variant. It
mirrors that helper step for step, including the trim, minus the lowercasing;
a test pins the equivalence so the two cannot drift.
The containment decision is unchanged: relative(lower(a), lower(b)) and
relative(a, b) return the same structure, and that structure is all
validateRelativePathWithinBoundary inspects.
Sibling surfaces checked: the other two callers of
normalizeWindowsPathForComparison use it as a comparison key and are correct
as-is (installed-plugin-index-record-reader.ts:215 compares with ===,
fs-safe's isPathInside discards the relative and returns a boolean).
path-policy.ts was the only site returning the normalized value.
* test(agents): verify Windows filename case end to end
Co-authored-by: Yigtwxx <yigiterdogan023@gmail.com>
* style(agents): apply oxfmt to the workspace path case test
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(webchat): reply-to a message with hydrated reply context
Control UI replies now carry the target transcript id as replyToId on
chat.send. The Gateway resolves the replied-to message from session
history and hydrates the channel-agnostic ReplyToId/ReplyToBody/
ReplyToSender envelope fields, so agents receive reply_to_id,
has_reply_context, and the untrusted reply-target block exactly like
Discord replies (mirrors #90263). Reply targets without a persisted
transcript id keep the inline-quote fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(webchat): keep non-reply chat.send dispatch ordering and satisfy CI gates
* docs(webchat): match reply-context doc to webchat conversation-info policy
* fix(webchat): hydrate reply bodies from display-visible content only
---------
Co-authored-by: openclaw-clawsweeper[bot] <openclaw-clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>