* feat(skills): diagnose skill_workshop hidden by tool policy (#87570)
Workshop can be enabled and auto-capturing while tools.profile hides the
skill_workshop tool; every inspection surface looked healthy. plugins
inspect and openclaw doctor now name the excluding policy layer (global/
agent/provider profile, allowlist, denylist) and the exact alsoAllow
grant to add, via a shared resolveSkillWorkshopToolPolicyAvailability
helper that /learn's guard now reuses instead of composing policy
itself. Diagnosis only; no policy behavior change.
* ci: retrigger
* fix(sessions): persist sender metadata in user turn transcript JSONL
Thread senderId/senderName/senderUsername/senderE164 from the channel
inbound context into the persisted user-turn transcript message so that
group chat session JSONL records include __openclaw sender identity.
Ref #90531
* fix(sessions): exclude senderE164 from persisted transcript for privacy
Remove phone-number field from the persisted __openclaw sender envelope,
keeping only senderId, senderName, and senderUsername. Privacy-sensitive
E.164 metadata can be added back by maintainers if needed.
Ref #90531
* fix(infra): spread base fields in applyExecPolicyLayer return values
The `as TBase & ExecPolicyLayer` casts failed because the returned
objects did not spread `...base`, losing generic TBase fields.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(infra): use ...base spread instead of ...baseWithoutMode
The baseWithoutMode destructuring produces Omit<TBase, "mode"> which is
not assignable to TBase & ExecPolicyLayer when the as-cast is removed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(infra): preserve baseWithoutMode in second applyExecPolicyLayer branch
The second branch (security/ask override without a mode change) must
exclude base.mode from the spread so that mode is not leaked into results
when only security or ask fields are being overridden.
Without this fix, the spread ...base carries mode through to the returned
object, breaking callers that expect applyExecPolicyLayer to clear stale
mode when applying explicit security/ask policy fields.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sessions): scope persisted sender attribution
* fix(sessions): preserve sender metadata through hooks
* refactor(sessions): keep sender metadata path lean
* fix(sessions): preserve sender metadata in runtime writes
* fix(sessions): preserve queued sender attribution
* test(sessions): use complete message fixtures
* refactor(sessions): rely on narrowed user message type
* test(sessions): use shared temp directory helper
* test(sessions): align sender metadata assertions
* fix(sessions): honor sender metadata redaction hooks
* test(agents): use automatic temp cleanup
* test(sessions): cover queued turn provenance
* test(auto-reply): expect room sender metadata
* refactor(sessions): isolate queued transcript context
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(goals): keep active session goals in per-turn context (#100409)
Active goals now inject one bounded user-role context line on every
non-heartbeat turn, refreshed at queued/interrupt admission via
provenance-tracked generated lines so operator stops take effect
immediately. Adds qa/scenarios/goals continuance scenarios and
goals.* coverage IDs.
* docs: regenerate docs map for goal context section
* fix(qa): use requiredChannelDriver instead of execution.channel pin in goal scenarios
* feat(commands): add /learn to draft skills from recent work (#100408)
/learn rewrites the turn into a standards-guided Skill Workshop authoring
instruction: the agent gathers named sources (or distills the current
conversation) and files ONE pending skill proposal via skill_workshop.
Approval flow unchanged; sandboxed/tool-restricted agents get a clear
unavailable reply. Extracts the harness OpenClaw-tools predicate into
shared helpers and reserves the command name against plugin shadowing.
* docs: regenerate docs map for /learn section
* fix(auto-reply): render chat history since last reply as per-message prose
The inbound chat-history block dumped batched history as a raw JSON array, which models read poorly compared to the chat-window block's per-message prose. Reuse the existing formatChatWindowMessage renderer for history entries so both blocks share one shape, keep the untrusted framing label, and keep media rendered as a bare content-type tag so local paths and URLs stay redacted. Teach the metadata stripper to consume the new prose block form.
* fix(auto-reply): preserve every media content type in chat-history prose
The prose chat-history renderer only forwarded the first attachment's
content type per history message, dropping the rest for entries with
multiple media items. Join all bounded content types instead, and
regenerate the prompt snapshot fixture this changes.
* test(qa-lab): accept prose pending history
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor: extract reusable AI runtime package
* refactor: complete AI provider relocation
* refactor: keep llm core internal
* refactor(ai): make @openclaw/ai self-contained with host policy ports
Move pure transport helpers (tool projections, strict-schema normalization,
prompt-cache boundary, stream guards, anthropic/openai compat, request
activity) from src into packages/ai; move utf16-slice into
normalization-core. Inject host policy (guarded fetch, redaction,
strict-tool defaults, diagnostics logging) through AiTransportHost with
inert library defaults installed by src/llm/stream.ts. Narrow the public
barrel to instance-scoped createApiRegistry/createLlmRuntime; the
process-default runtime moves behind internal/ and
registerBuiltInApiProviders takes an explicit registry. Delete the
src/llm/api-registry re-export facade.
* fix(ai): teach node, jiti, and vite resolvers the @openclaw/ai and utf16-slice subpaths
The workspace alias tables in root-alias.cjs, plugin-sdk-native-resolver,
sdk-alias, the shared vitest config, and the Control UI vite config only
knew @openclaw/llm-core; Node-side plugin loading resolved @openclaw/ai
through the pnpm symlink to the unbuilt dist (checks-node-compact CI
failures), and the Control UI build broke on the new
normalization-core/utf16-slice subpath.
* chore(ui): drop leftover service-worker debug logging
* build(release): ship @openclaw/ai with its own shrinkwrap and honest dependency set
packages/ai declares only its six real runtime deps (kysely, chalk, json5,
tslog, zod, fs-safe, and proxyline were never imported); orphaned root deps
removed. generate-npm-shrinkwrap now treats publishable packages/* like
publishable plugins so the AI tarball pins its transitive tree even though
workspace deps are omitted from the root shrinkwrap. knip learns the
package entry points; the tsdown dts neverBundle option moves to its
documented deps.dts home; the README documents the no-semver internal/*
contract and host ports.
* docs(ai): add minimal external-consumer example app
examples/ai-chat consumes only the public @openclaw/ai surface (built dist
via the workspace link): isolated runtime, built-in provider registration,
one streamed completion. Supports Anthropic/OpenAI via env keys and a
keyless local Ollama target; live-verified against Ollama.
* docs(ai): document the @openclaw/ai package and workspace shrinkwrap boundary
* chore(check): include examples/ in duplicate-scan targets
* fix: emit normalization package subpaths
* fix: complete AI package boundary artifacts
* fix: align AI package boundary contracts
* fix(ci): stabilize package release contracts
* test: align documentation contract checks
* test: keep cron docs guard aligned
* test: align restored docs contract guards
* test: follow upstream docs contracts
* docs: drop superseded talk wording
Allow busy TUI sessions to forward prompts into the configured queue while
keeping queued-turn admission, cancellation, restart, and transcript ownership
consistent across the TUI, Gateway, and followup queue.
Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca>
The claude-cli (claude-stdio) live-session backend emits native
reasoning on stream-json (`thinking_delta` + a snapshot `thinking`
block), but the CLI parser had no thinking handling at all, so it was
silently dropped on every surface in every /reasoning mode.
- cli-output.ts: parse thinking_delta/snapshot into stream:"thinking"
with replace-style per-content-block-index dedupe
(assembleThinkingTextByIndex), robust to tool-interleaved multi-block
turns (snapshot replaces per-index rather than assuming a streamed
prefix). signature_delta/redacted_thinking stay skipped.
- agent-runner-cli-dispatch.ts: bridge stream:"thinking" into
onReasoningStream via a new createReasoningTextBridge (mirrors the
existing assistant-text bridge), replacing the old
assistant-text-as-reasoning heuristic. Track the final reasoning
snapshot and, when present, prepend a durable
`{text, isReasoning: true}` payload before the answer — mirroring
the embedded-runner durable reasoning payload
(embedded-agent-runner/run/payloads.ts).
- agent-runner-execution.ts: thread isReasoningSnapshot through to
onReasoningStream, still passing requiresReasoningProgressOptIn:true
so CLI reasoning rides the exact same channel gates as embedded
reasoning (Discord/Telegram): /reasoning off -> nothing on any
surface, stream -> window preview only, on -> durable payload too.
The bridge is suppressed under the same conditions as the assistant
bridge (e.g. silentExpected follow-ups), so no durable payload is
synthesized for turns that are expected to stay silent.
Supersedes this branch's prior commit (a straight port of
af7eb48e05's emit-always model, no gating and no durable mode) with
the above gated/durable design per product decision.
Also, in cli-output.ts: reset the per-content-block-index thinking
tracker on every new Anthropic message (message_start, or a fresh
top-level "assistant" snapshot's message.id) instead of leaving it
scoped to the whole CLI turn. Content-block indices restart at 0 per
Anthropic message, so a tool round-trip within one turn (message A:
thinking + tool_use; tool result fed back; message B: a fresh
message_start) was letting message A's stale index-0 thinking text
bleed into message B's index-0 delta.
And in agent-runner-cli-dispatch.ts: attach the durable reasoning
payload whenever any thinking was bridged, not only when the CLI also
produced a visible final answer, so a thinking-only turn (no visible
reply) doesn't silently drop its reasoning.
Round 2: the durable `{isReasoning: true}` payload synthesized above
was also reaching the QUEUED FOLLOW-UP delivery path
(followup-runner.ts -> resolveFollowupDeliveryPayloads) with no gate
at all, so a claude-cli queued follow-up with /reasoning off would
leak internal reasoning to the channel as an ordinary visible message.
resolveFollowupDeliveryPayloads (followup-delivery.ts) now takes a
reasoningPayloadsEnabled flag and drops isReasoning payloads unless
it is true; followup-runner.ts threads opts?.reasoningPayloadsEnabled
through at both call sites, from the same GetReplyOptions the direct
path already reads (dispatch-from-config.ts:2054). This makes the
queued-follow-up path apply the identical isReasoning/
reasoningPayloadsEnabled gate that direct dispatch already enforces
(dispatch-from-config.ts:3347, :3537) — no new mechanism, CLI
follow-ups now go through the exact same gate embedded follow-ups do.
Scope note: routeReply's own unconditional suppression of isReasoning
payloads on the origin-routing branch (route-reply.ts:131,
shouldSuppressReasoningPayload) is pre-existing, shared with the
embedded runner, and unchanged by this commit — that gate governs
whether a kept-and-routed reasoning payload is actually rendered on a
cross-channel queued follow-up, and is out of scope here. This commit
closes the leak (off -> never eligible on any path); it does not
change what happens to an eligible payload once it reaches routeReply.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add session thread management
Squash of codex/thread-management (025aefc3ad1) onto origin/main:
pin/archive/rename sessions via sessions.patch, archived-aware
sessions.list, lifecycle fencing, read-only archived chat, SDK +
Swift protocol support, Control UI session management.
* refactor(ui): minimal session rows with hover-revealed management
Chat picker and sidebar recents share session-row primitives: single-line
rows, relative timestamps, rename/archive/pin revealed on hover or focus,
accent pin badge for pinned rows, and an active-run spinner in the trail
slot. Sidebar floats pinned sessions above recency via the shared
comparator and gains archive/pin actions through the unified sessions-view
patch fallback. Archive eligibility is one shared policy
(canArchiveSessionRow); the sidebar/picker active-run tooltip now uses the
real sessionsView.activeRun locale key.
* fix: align session admission with mailbox-era main
Integration fixes after rebasing onto current main: sessions_list mailbox
test expectations learn the archived/pinned row fields and archived:false
list param; gateway agent admission treats a session as deleted only when
both the requested and canonical alias sets miss it (legacy bare-main
stores and exec-approval followups read under different spellings); cron
persist tests keep a consistent store across claim-guarded persist calls;
the ACP abort hook test asserts abort propagation instead of signal
identity; drop dead lifecycle writes flagged by no-useless-assignment and
fix the promise-executor return in the codex compact test.
* fix(qa): align UI e2e and shard fixtures with redesigned session rows
Sidebar session rows are wrapper divs with an inner link now: update the
navigation browser tests and chat-flow Playwright selectors. Seed a real
per-test session store for the auto-fallback admission guard instead of
depending on leftover host files at /tmp/sessions.json. Teach the
test-projects routing fixture about the suites that newly import the
shared temp-dir helper. Document the Codex thread-format contract for
archivedAt/pinnedAt (flag derived from server-stamped timestamp, epoch ms
here vs Codex epoch seconds) at the type and in the session docs.
* test: route auto-fallback suite through temp-dir helper plans
The auto-fallback suite now imports the shared temp-dir helper for its
seeded session store, so the top-level helper routing fixture must list
it in the auto-reply plan.
* fix(auto-reply): don't block reply completion on transcript mirror
mirrorTranscriptAfterDispatcherDelivery() opens with
`await dispatcher.waitForIdle()` and is awaited from sendFinalPayload,
i.e. before the reply operation completes. When a follow-up is queued
(queueDepth>=2) the dispatcher cannot go idle until this operation
clears, and the operation cannot clear until sendFinalPayload returns —
a completion<->dispatcher-idle deadlock. The reply operation stays
phase=running and surfaces as stalled_agent_run (recovery=none) until
the ~15-minute stuck-session abort.
Run the transcript mirror as fire-and-forget: it is post-delivery
bookkeeping and does not need to gate completion. The operation
completes immediately, the queued follow-up proceeds, the dispatcher
goes idle, and the detached mirror then records the transcript.
Validated live across three backends (LMStudio openai-completions,
claude-cli, DeepSeek cloud): queueDepth 2 and 3, zero stalls.
* test(auto-reply): regression coverage for detached transcript mirror
Two focused tests on the queueDepth>=2 completion<->dispatcher-idle deadlock:
- final reply completes even when dispatcher.waitForIdle() never resolves
(pre-fix this awaited-mirror path deadlocked; the test hangs on the old code).
- a rejecting background mirror does not fail the reply (the detached .catch
contains it instead of surfacing an unhandled rejection).
* fix(auto-reply): settle transcript mirrors after delivery
Co-authored-by: Zaytsev Ivan <ivan@jad.ru>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* [AI] fix(auto-reply): strip stray punctuation before silent-reply token detection
When the model emits .NO_REPLY or *NO_REPLY* instead of bare NO_REPLY,
isSilentReplyText() failed to detect it because the regex requires
the text to consist only of the token with optional whitespace.
Add stripEdgeNonAlnum() that strips leading/trailing non-letter,
non-number characters (Unicode-aware) before the regex test. This
catches stray punctuation while preserving the existing safeguard
against suppressing mixed-content text.
Related to #98166
Co-Authored-By: Claude <noreply@anthropic.com>
* [AI] fix: strip only punct not emoji before silent-reply detection
Change stripEdgeNonAlnum to stripEdgePunct: strip only punctuation (\p{P}) instead of all non-letter/non-number characters. Emoji would be stripped by the former, causing false silent-reply on mixed content.
Related to #98166
Co-Authored-By: Claude <noreply@anthropic.com>
* [AI] fix: preserve exact custom-token match before edge-punct normalization
Try exact regex match first, then fall back to stripEdgePunct.
This preserves exact matching for custom tokens whose first/last
character is punctuation (e.g. *SILENT*, #QUIET#), while still
catching the common case of NO_REPLY wrapped in stray punctuation.
Add regression test for punctuation-edged custom tokens.
ClawSweeper: https://github.com/openclaw/openclaw/pull/98224#issuecomment-4846415801
Related to #98166
Co-Authored-By: Claude <noreply@anthropic.com>
* [AI] fix: handle whitespace-wrapped punctuation in silent-reply fallback
Trim whitespace before stripping edge punctuation in the fallback
path so variants like " .NO_REPLY " and "NO_REPLY. " are also
caught by the silent-reply detector.
Add regression coverage for whitespace-wrapped edge punctuation.
ClawSweeper: https://github.com/openclaw/openclaw/pull/98224#issuecomment-4846415801
Related to #98166
Co-Authored-By: Claude <noreply@anthropic.com>
* test(auto-reply): tighten silent-token boundaries
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Keep CLI session reuse hashes keyed to stable session prompt facts while preserving per-turn room-event message-tool delivery for execution and MCP scoping.
Thread cliSessionBindingFacts from reply assembly through queued/followup CLI runs, make the stable CLI prompt delivery-neutral, and cover production-shaped room-event opts so forced message_tool_only delivery cannot poison binding identity.
Fixes#99372
Release note: Telegram/room-event CLI backends can resume sessions across interactive turns again instead of replaying full history after system-prompt invalidations.
* fix(gateway): include session label in deriveSessionTitle fallback chain
When a session is renamed with /name, the user-provided label is persisted
in sessions.json but was ignored by deriveSessionTitle. This caused the
TUI session picker and other derived-title consumers to show auto-generated
or UUID-based names instead of the user's label.
Add entry.label to the deriveSessionTitle fallback chain after
displayName and subject, before the auto-derived firstUserMessage and
sessionId fallbacks. This aligns with the existing ACP translator
precedence (derivedTitle ?? displayName ?? label ?? key).
Related to #98742
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(gateway): tighten session label precedence coverage
* fix(gateway): prioritize explicit session labels
* fix(commands): preserve named-session suggestions
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Addressed source-reply turns now carry the message-tool-only delivery hint in runtime prompt context, matching the room-event per-turn contract without persisting the hint into transcript rows.
Surface: auto-reply prompt envelopes and agent messaging guidance. Fixes#99371.
Release-note: fixes Telegram group replies under message_tool_only delivery being composed privately instead of being prompted to use message(action=send).
Ambient transcript watermarks now carry the transcript session id, resolve only for the current session entry, and skip stale room-event hooks that no longer match the prepared transcript session.
This protects Telegram group prompt windows after reset by backfilling rows that are no longer present in the new session transcript, while preserving steady-state watermark filtering within one session.
Fixes#99373
Release-note: fixes Telegram group context loss after session reset when ambient transcript watermarks outlived the transcript they referenced.