The CONNECT handler pipes clientSocket and upstreamSocket together but
only registers an error handler on upstreamSocket. If the client
disconnects abruptly (ECONNRESET), the unhandled error event on
clientSocket causes the Node process to crash.
Add a clientSocket error handler that logs the event and destroys the
upstream socket. Also change clientSocket.end() to clientSocket.destroy()
in the upstream error handler since destroy() is more appropriate for
error cleanup of piped sockets.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
* fix(agents): handle parallel tool call deltas in openai-completions stream
The OpenAI completions streaming parser tracked only a single
`currentBlock` for tool calls and ignored `toolCall.index`. When the
API sends multiple `delta.tool_calls` entries (e.g., parallel tool
call scaffolding from kimi-for-coding), the parser created a new
block for every entry with a differing `id`, spawning phantom tool
calls with empty names and misrouting arguments.
Replace single-block tracking with Maps keyed by `index` and `id`,
matching the correct logic already present in the bundled
`@earendil-works/pi-ai` dependency. This ensures parallel and
interleaved tool call deltas accumulate to the correct block.
Fixes phantom "unknown" tool calls and empty arguments on
parameterized tools for providers that emit multiple tool_call
indices in streaming deltas.
* fix(agents): finalize tool-call blocks in place to keep maps live
ClawSweeper review [P1]: finishCurrentBlock() and finishAllToolCallBlocks()
were creating new block objects and replacing them in output.content,
but toolCallBlocksByIndex / toolCallBlocksById still pointed at the old
objects. Later deltas for those indices would mutate detached blocks,
causing argument loss and incorrect contentIndex in stream events.
Fix by finalizing arguments in place on the existing block objects.
Add regression test for parallel tool calls with split indices:
- two tool-call slots introduced in one chunk
- argument deltas arriving for each index in subsequent chunks
* fix(agents): keep byte counters out of emitted tool-call blocks
ClawSweeper review [P2]: partialArgsBytes was being stored directly on
the tool-call block objects pushed into output.content, exposing parser
scratch state to emitted stream events and final transcripts.
Replace the inline field with a WeakMap keyed by block object, keeping
byte tracking internal to the parser without polluting the public block
shape.
* refactor(agents): extract ToolCallBlock type for map declarations
ClawSweeper review [P1]: NonNullable<typeof currentBlock> at the map
declaration point was unreliable because currentBlock is initialized to
null and flow-narrowed. Define a local ToolCallBlock alias and use it
for toolCallBlocksByIndex, toolCallBlocksById, and toolCallBlockBytes
to give the maps a precise, stable type.
* fix(agents): iterate typed tool-call map in finishAllToolCallBlocks
ClawSweeper review [P1]: output.content elements are typed as
Record<string, unknown>, so block.partialArgs remained unknown even
after checking block.type === "toolCall". Latest CI failed strict
type checking at parseStreamingJson(block.partialArgs).
Fix by iterating toolCallBlocksByIndex.values() instead — the Map
values are already typed as ToolCallBlock, so partialArgs is known
to be a string and parseStreamingJson compiles cleanly.
- Restart the shared Codex app-server client when native server-side compaction times out.
- Retry native compaction once on the fresh app-server while preserving stale-thread cleanup only for `thread not found`.
- Add regression coverage and changelog entry for the preflight compaction recovery path.
Verification:
- `pnpm test extensions/codex/src/app-server/compact.test.ts`
- `env -u OPENCLAW_TESTBOX -u OPENCLAW_TESTBOX_REMOTE_RUN pnpm check:changed`
- `.agents/skills/autoreview/scripts/autoreview --mode local`
CI note: `build-artifacts` is red due inherited latest-main workflow/test drift, reproduced locally outside this PR diff and tracked in the pre-merge PR comment.
* fix(security): escape entry.id in HTML export to prevent attribute XSS
Apply escapeHtmlAttr to entry.id in renderEntry and renderCopyLinkButton
to prevent attribute injection via crafted entry IDs in HTML exports.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
* chore: remove proof helper scripts from branch
ClawSweeper P2: committed proof scripts can provide false-positive
validation. Proof output is in the PR body instead.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
---------
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
When the Slack adapter's startup auth.test call fails (bad token,
transient error, etc.), the bot user id silently stays empty for the
life of the process. The downstream explicit-bot mention check is
`botUserId && mentionedUserIds.includes(botUserId)`, which always
returns false when botUserId is empty. The result is that explicit
<@bot> mentions are silently classified as non-mentions with no log
trace explaining why.
Changes:
- provider.ts: stop swallowing auth.test failures; emit a warn log at
boot so the degraded state is observable. Empty user_id is treated
as a failure too.
- prepare.ts + subteam-mentions.ts: export the existing normalizeSlackId
helper and apply it to both sides of the explicit-bot equality check
(and to the mentioned-ids list). Real Slack ids are already uppercase,
so this is a no-op on healthy traffic, but it locks the invariant down
and removes the asymmetry between collected ids and the ctx bot id.
- prepare.test.ts: add two regression tests pinning the exact symptom:
positive case (botUserId set -> explicit_bot), negative case
(botUserId='' -> not explicit_bot, mention_source not explicit_bot).
🤖 AI-assisted.
Co-authored-by: in-liberty420 <in-liberty420@users.noreply.github.com>
Replace empty .catch(() => {}) on two failDelivery calls with
log.warn() so delivery queue mark-failed errors leave a diagnostic
trail instead of being silently discarded.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
`parseSlashCommandActionArgs` used a naive `startsWith` against the
configured slash prefix. When a skill name shares a prefix with a
built-in command (e.g. a skill named `config-check` vs the built-in
`/config`), the longer name was captured by the shorter built-in
handler and surfaced as an invalid action:
⚠️ /config is disabled. Set commands.config=true to enable.
Any skill whose name starts with a built-in command prefix
(`config-*`, `debug-*`, `models-*`, etc.) was unreachable via slash
invocation from any channel.
Fix: after the prefix match, require that the next character is
whitespace, a colon, or end-of-string. Otherwise the prefix
collided with a longer command name and we return `no-match` so the
longer handler — or the skill router — gets a chance to claim it.
Adds a regression test file `commands-slash-parse.test.ts` covering:
- `/config-check <args>` returns null (the reported case)
- `/configfoo` (no separator) returns null
- `/modelsy` returns null for the `/models` prefix
- `/config:json` still matches (colon is a valid boundary)
- `/config show enabled` still parses cleanly (whitespace boundary)
- empty body still returns the default action
Fixes#84572.
Co-authored-by: infracore <infracore@users.noreply.github.com>
* fix(tui): dismiss watchdog notice when response actually arrives
The streaming watchdog renders 'This response is taking longer than
expected. Send another message to continue.' after 30s without a chat
delta. If a delta or final then arrives — common for runs that are slow
but not stuck — the notice stays in the log alongside the recovered
response and contradicts what the user sees.
Track the notice by runId in the chat log via a new `addPendingSystem`
+ `dismissPendingSystem` pair (mirroring the existing pendingUsers
pattern) and dismiss it from `handleChatEvent` whenever any further chat
event for that run is processed. The watchdog's internal cleanup
(`activeChatRunId` reset, status idle, history reload) is unchanged.
Refs #67052, #69081 (closed). Prior attempt #69026 raised the threshold
and suppressed the notice entirely; this is the narrower fix that keeps
the warning useful for genuinely stuck runs.
* fix(tui): adapt pending notice to repeatable system entries
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The SIGTERM handler's fire-and-forget IIFE can reject if the graceful
drain or tunnel-teardown throws. Without a catch, this becomes an
unhandled promise rejection. Add .catch() that logs the error and
falls back to a hard stop request. Same treatment for SIGUSR1.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
* fix: release cron runtime state after isolated runs
After an isolated cron/subagent run completes, the prepared context retains
references to the full in-memory session store and the registered agent run
context. Over many runs, these retained objects accumulate -- heap snapshots
showed ~2.0 GiB from ~113k copies of the skill prompt string flowing through
skillsSnapshot.prompt -> session entry -> cronSession.store -> cron run context.
Changes:
- Add disposeCronRunContext() to runCronIsolatedAgentTurn's finally block
- Calls clearAgentRunContext(sessionId) to remove the run context from the
global agent-events map
- Nulls cronSession.store to release the in-memory session registry copy
- Export clearAgentRunContext from run-execution.runtime.ts barrel
- The disposal is shallow O(1) -- no deep traversal, no hot-path disk writes
- Session persistence is unaffected (on-disk sessions.json is untouched)
The finally block guarantees cleanup on both success and error paths,
including timeout/abort scenarios.
Includes unit tests for clearAgentRunContext, store disposal, and
sweepStaleRunContexts.
* fix: remove duplicate storePath property in test fixture
* fix: remove unused clearAgentRunContext import from run-executor
* fix(cron): use initial sessionId for disposeCronRunContext in finally block
finalizeCronRun calls adoptCronRunSessionMetadata() which can rotate
sessionEntry.sessionId before the finally block runs. Capturing the
sessionId before the try block ensures clearAgentRunContext clears the
correct registered context instead of the potentially-rotated one.
Also removes unused imports (vi, beforeEach) from the runtime cleanup test.
* chore: trigger CI re-check for proof gate
* chore: retrigger CI proof gate
* test(cron): prove isolated run cleanup path
* fix(cron): keep shared run contexts active
* test(cron): avoid spreading typed-never fixture
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(exec): parse nested approval metadata in followups
(cherry picked from commit 10ff9b318e77cda3d65f40d59bbab0f4a3f59da8)
* docs(changelog): note exec approval nested-paren parser fix
* fix(exec): sanitize denied-reason literals in (...)-delimited approval messages
The exec-approval followup wire format is `Exec denied (gateway id=..., <deniedReason>): cmd`. The producer at `src/agents/bash-tools.exec-host-gateway.ts:606` was emitting `approval-timeout (allowlist-miss)`, which embedded literal parens inside the metadata segment and broke the metadata/body boundary for naive parsers. Switch the literal to a colon-separated form (`approval-timeout: allowlist-miss`) so the surrounding `(...)` delimiter stays unambiguous.
The Gateway node-event surface at `src/gateway/server-node-events.ts:734` interpolates an untrusted `obj.reason` into the same `Exec denied (node=..., <reason>)` format. Strip parens from that field before interpolation so a buggy or hostile node payload cannot smuggle metadata into the body slot.
The robust nested-paren parser already in `src/agents/exec-approval-result.ts` stays as defense in depth. Extend `exec-approval-result.test.ts` to cover the canonical colon-separated `deniedReason` and confirm `formatExecDeniedUserMessage` still maps it to the timeout copy.
* fix(exec): require gateway/node metadata source to reject spoofed approval wrappers
The exec-approval result parser previously accepted any string starting with
"Exec denied (..." or "Exec finished (..." as a structured approval wrapper.
Generic command stdout that happened to start with these tokens would be
classified as kind: "denied" or "finished", letting a tool's output spoof a
resolved-approval event in pi-embedded-subscribe.handlers.tools.ts:1173.
Reported by Aisle as CWE-841 (Improper Enforcement of Behavioral Workflow),
medium severity. The fix validates that the parenthesized metadata starts with
either "gateway id=" or "node=" — both prefixes are emitted by the legitimate
approval generators (bash-tools.exec-host-gateway.ts, bash-tools.exec-host-node.ts,
gateway/server-node-events.ts) and are unlikely to appear in arbitrary command
output. Inputs that fail this check now return kind: "other", which all callers
already handle as a no-op.
* fix(exec): keep sandbox_blocked classification for raw exec-denied messages
After the spoof-guard tightening of parseExecApprovalResultText, inputs that
lack a gateway/node-sourced metadata prefix (such as the synthetic
"exec denied (allowlist-miss):" string used in classifier tests) no longer
return kind: "denied" and therefore no longer trigger formatExecDeniedUserMessage,
so isSandboxBlockedErrorMessage stopped recognising them.
Add a direct \bexec denied\s*\( alternative to SANDBOX_BLOCKED_RE so the
classifier still treats any raw "exec denied (" prefix as sandbox-blocked,
independent of whether the parser accepts the surrounding wrapper. This keeps
classifyProviderRuntimeFailureKind's existing behavior for unstructured exec-
denied messages.
Add documentation for the dynamicAgentCreation feature used to create
isolated agents per Feishu/Lark user. Covers:
- dynamicAgentCreation configuration fields (enabled, workspaceTemplate,
agentDirTemplate, maxAgents)
- Automatic agent/workspace creation flow
- Session isolation with dmScope
- Template variables ({agentId}, {userId})
- Verification steps and example deployment
Refs: feature available since OpenClaw 2026.4.25+
Co-authored-by: li <li@lideMac-mini.local>
* docs(channels/slack,telegram): document ackReactionScope and its DM-excluding default
The Slack and Telegram channel docs documented `ackReaction` but not
`ackReactionScope`, even though the scope (defaulting to
`group-mentions`) silently excludes DMs. People who set `ackReaction`
and expect to see an emoji on DMs are surprised when nothing fires.
This adds:
- The resolution order for `ackReactionScope` (per-account → channel →
`messages.ackReactionScope` → default `group-mentions`).
- The full list of scope values (`all`, `direct`, `group-all`,
`group-mentions`, `off`/`none`).
- A Note callout flagging that the default does not react in DMs and
that `messages.ackReactionScope` requires a gateway restart to take
effect.
- A short JSON example for the common case (`ackReactionScope: "all"`).
Mirrors the structure already used in `docs/channels/matrix.md`.
Found while configuring Slack DMs to show `👀` ack reactions and
discovering that the docs covered the emoji but not the scope gate. AI-assisted.
* fixup: scope is messages-only for Slack & Telegram (not per-account)
Reviewer correctly noted that the Slack and Telegram runtimes only read
`cfg.messages?.ackReactionScope` and the per-account/per-channel
`ackReactionScope` keys don't exist in those schemas (only Discord and
Matrix support them). Drop the misleading resolution-order bullets and
document `messages.ackReactionScope` only.
Verified against:
- extensions/slack/src/monitor/provider.ts:243
- extensions/telegram/src/bot-core.ts:262
- src/config/types.slack.ts (no ackReactionScope in account schema)
- src/config/types.telegram.ts (no ackReactionScope in account schema)
Keeps the DM-default gotcha, the full enum, and the gateway-restart note,
which were the original value of the PR.
---------
Co-authored-by: Dr. Claw <drclaw-iq@users.noreply.github.com>
Recreated from #85108 because the original branch could not be updated by maintainers.
Preserves current-main pnpm install hardening while switching workflow pnpm setup to packageManager, and adds exact version-scoped release-age exclusions for already-locked packages that pnpm 11.2.2 audits during install.
Co-authored-by: Altay <altay@hey.com>