* feat(tui): add local embedded TUI mode with terminal/chat aliases
Adds a gateway-free local TUI path so users can run openclaw in their
terminal without needing a running gateway process.
- TuiBackend interface abstraction (tui-backend.ts) with EmbeddedTuiBackend
implementation that drives the agent loop in-process
- openclaw tui --local flag for local embedded mode
- openclaw terminal / openclaw chat aliases that imply --local
- /auth slash command with codex CLI delegation to avoid prolite plan issue
- Default model display fallback on startup
- Local-aware status text and log suppression
- Concise auth error hints, raw HTML 403 suppression
- Onboarding hatch flow launches local TUI (no gateway required)
- Commander alias bug fix in run-main.ts (.aliases() check)
- All new and updated tests passing (145/145)
* TUI: fix alias detection, cross-platform codex lookup, and history byte-budget safeguards
* TUI: remove RuntimeEnv type annotation to fix CI oxlint error
* TUI: filter gateway-dependent tools and auto-approve plugin hooks in embedded mode
* TUI: suppress console noise and add embedded mode system prompt note
* TUI: reduce embedded-mode tool filtering from 15 to 7, add local session tools
* TUI: fix remaining PR review comments
* TUI: address latest review feedback and CI drift
* Core: align prompt helper with latest base
* Core: match prompt helper formatting with base
* Core: restore prompt helper from latest base
* fix(tui): preserve local auth fallback in source checkouts
* fix(tts): guard telephony provider invocation
* fix(tui): support Windows codex auth shim
* fix(tui): harden local auth flow
* fix: preserve embedded tool-first run events
* fix(tui): keep embedded plugin approvals gated
* fix(tui): restore embedded attempt import
* fix(tui): resolve sessions in embedded stub
* fix: add embedded TUI changelog entry (#66767) (thanks @fuller-stack-dev)
* fix: pass setup TUI local mode through relaunch (#66767) (thanks @fuller-stack-dev)
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Addresses codex P1 review on PR #69940: the previous guard rejected
targets that simply omitted accountId, but message-tool fills accountId
from the agent's bound account at exec time (message-tool.ts:730-733),
so account-bound cron jobs legitimately start with target.accountId
undefined. Rejecting that case lost skipMessagingToolDelivery, causing
dispatchCronDelivery to double-send.
Now we only reject when the tool explicitly names a *different*
accountId — which is the real CWE-284 spoof vector. Omission matches.
Tests updated accordingly:
- matcher unit test: flipped "omit accountId" case from false to true;
"accountIds differ" case preserved as the real spoof guard
- integration tests: one legitimate-default case (rewrite happens),
one explicit-mismatch case (rewrite suppressed)
658 cron tests pass.
When a cron job sends via the generic `message` tool, the delivery trace
previously recorded `messageToolSentTo[i].channel = "message"` even
though the send was resolved to a specific channel (e.g. telegram). This
made `jq` diffing intended-vs-actual awkward for the happy path.
Fix:
- `normalizeMessagingToolTarget` now rewrites `channel: "message"`
to the resolved channel when `matchesMessagingToolDeliveryTarget`
confirms the tool send matches the resolved cron delivery target.
Genuinely unmatched generic sends keep the literal "message" so
audits can still flag them.
- `matchesMessagingToolDeliveryTarget` now requires strict accountId
equality whenever the resolved delivery carries an `accountId`. An
omitted `target.accountId` previously short-circuited the guard and
was treated as a wildcard, letting a generic send spoof attribution to
any bot identity in the cron delivery trace (CWE-284). This was
flagged by Aisle on #69771.
Tests:
- Unit: `matchesMessagingToolDeliveryTarget` rejects omitted-accountId
against account-tied delivery; still matches same-accountId.
- Integration: cron run trace rewrites generic "message" to the
resolved channel, preserves accountId on both sides, and leaves the
literal "message" provider in place when the tool send omits
accountId against an account-tied delivery.
* fix(memory/dreaming): surface blocked status in memory status when heartbeat disabled for main
Replace the hand-rolled heartbeat-rules logic in resolveDreamingBlockedReason
with the shared resolveHeartbeatSummaryForAgent helper, promoted from core to
the plugin-sdk via infra-runtime. Collapses the two disabled-reason branches
into a single message that points at a new Troubleshooting section in the
dreaming docs, so the silent-failure mode described in openclaw/openclaw#69843
becomes legible without the extension re-encoding heartbeat-enablement rules.
Refs openclaw/openclaw#69843, openclaw/openclaw#46046.
* refactor(memory/dreaming): share resolveDreamingBlockedReason across cli and /dreaming surfaces
- Move resolveDreamingBlockedReason from cli.runtime.ts into dreaming.ts as an exported helper and pin its heartbeat check to DEFAULT_AGENT_ID (now exported from plugin-sdk/routing) so the status-line check agrees with the cron's hardcoded sessionTarget even when the configured default agent is not main.
- Render the blocked reason from formatStatus in dreaming-command.ts directly under the enabled line, so /dreaming status, /dreaming on, /dreaming off, and bare /dreaming all flag that the cron is blocked instead of implying dreaming is healthy.
- Tighten the blocked-reason text to lead with user impact ('dreaming is enabled but will not run because heartbeat is disabled for main'), so operators immediately understand the config is toggled on but nothing is actually running.
- Tighten the dreaming Troubleshooting copy to name main explicitly and mention both surfaces.
- Add tests locking the new behavior across cli.test.ts (default-agent=ops still reports blocked for main) and dreaming-command.test.ts (/dreaming status ordering, /dreaming on surfacing, healthy-heartbeat omission).
Refs openclaw/openclaw#69843, openclaw/openclaw#46046.
* fix(memory/dreaming): check heartbeat for the resolved default agent, not the literal 'main'
sessionTarget: 'main' is a cron session-type enum variant meaning 'the default agent's main session', not an agent id (see src/cron/service/jobs.ts). buildManagedDreamingCronJob does not set agentId, and cron runtime resolves the missing agentId through resolveDefaultAgentId(cfg) before enqueuing or waking. The previous pin to DEFAULT_AGENT_ID could produce a false 'blocked' reading when a configured default agent is not 'main' and its heartbeat is fine, and could miss a real block when the default agent is not 'main' and that agent's heartbeat is actually off.
Switch resolveDreamingBlockedReason to resolveDefaultAgentId(cfg) and interpolate the resolved agent id into the message so the blocked line names the agent whose heartbeat is the blocker. Introduce a narrow local CRON_SESSION_TARGET_MAIN constant for the cron session-type enum variant (used by the sessionTarget type and value) so the remaining 'main' literal is semantically distinct from any agent id. Revert the DEFAULT_AGENT_ID export addition on plugin-sdk/routing; memory-core no longer needs it. Update the Troubleshooting doc wording and the cli test that was locking the wrong behaviour.
Refs openclaw/openclaw#69843, openclaw/openclaw#46046.
* fix(memory/dreaming): align blocked check with server-cron wake's defaults-only heartbeat
resolveDreamingBlockedReason was using resolveHeartbeatSummaryForAgent, which merges agents.defaults.heartbeat with agents.list[].heartbeat. The managed dreaming cron leaves job.agentId and job.sessionKey unset, so server-cron's wake wrapper cannot look up a per-agent entry and calls runHeartbeatOnce with agents.defaults.heartbeat only. Using the summary helper would disagree with the actual wake when the default agent overrides heartbeat.every differently from the defaults (either direction — false blocked when the override would run, or false healthy when defaults block).
Mirror the wake path explicitly: rule-1 enablement via isHeartbeatEnabledForAgent against the default agent, rule-3 interval via resolveHeartbeatIntervalMs with defaults-only heartbeat config. Comment points at server-cron so a future cleanup of that latent override-propagation gap sees the coupling.
Refs openclaw/openclaw#69843.
Fix Slack thread bootstrap replaying the bot's own prior turns into new sessions and duplicating the thread-starter prompt block.
Narrows first-turn context seeding to exclude only the current Slack bot's own starter/history entries, so self-authored turns no longer pollute new session prompts while preserving human and third-party bot context
Removes the redundant plain-text starter prelude in runPreparedReply() that doubled thread-starter content when no ThreadHistoryBody was present
Fixes concurrent manager creation races that caused SafeOpenErrors during session export.
Deduplicates in-flight manager creation so only one full QMD manager arms per agent/config at a time, eliminating the concurrent exportSessions() collisions that triggered path changed during write errors
Resolves and snapshots runtime inputs before cache reuse, replacing stale managers atomically when workspace/config changes, and aborting queued export work promptly on close()
Greptile/Codex review follow-ups on #69817:
- Narrow skipA2AFlow from target-only detection to a combined check that
the caller is the parent of the target (new
isRequesterParentOfBackgroundAcpSession helper). Under
tools.sessions.visibility=all a non-parent sender can see the same
oneshot ACP session; the previous guard would have suppressed their
only follow-up delivery path. With requester ownership required, those
senders continue through the normal A2A flow.
- When the A2A flow is skipped, return delivery.status="skipped" instead
of "pending" so the parent LLM does not wait for a second result that
will never arrive.
- Add unit tests for resolveAcpSessionInteractionMode and
isRequesterParentOfBackgroundAcpSession covering both the new
ownership gate and the existing target-type branches.