* refactor(agents): persist directive facts at the assistant write boundary
Assistant final text is now stripped of inline delivery directives once, at
the SessionManager append boundary, with the parsed facts persisted as a
typed openclawDelivery field on the assistant message (openclawDeliveryMirror
precedent, inside event_json — no schema change). Live session state is
replaced with the persisted canonical bytes so same-run continuations and
next-turn store rebuilds never diverge (prompt-cache invariant).
Delivery and restart recovery consume the stored facts instead of re-parsing
persisted text; pre-upgrade in-flight messages without facts intentionally get
no text-parse fallback (commented tradeoff at the read site). Directive
parsing is now code-region aware, so markers quoted in inline code or fences
neither parse nor strip — fixes the empty-code-pill display bug.
The undocumented reaction marker DSL ([[react:]], [[react_to_current:]]) is
deleted; structured message-tool reactions remain the canonical path. Stale
assertion-safety baseline entry removed with the parser.
Production net -10, tests net +38. Focused suites: 9,068 passed; pre-fix
regressions fail for the intended reasons. Codex autoreview clean.
* fix(agents): satisfy directive persistence gates
* fix(config): drop unused directive export
* fix(agents): preserve indented directive examples
* fix(state): store in-root agent database registry paths relative to the state dir (schema v9)
Copied state directories retained stale absolute agent database registry rows. The combined gateway store then merged old and new copies, causing every sessions.list request to fail with SessionCanonicalKeyMigrationRequiredError.
Schema v9 stores in-root registry paths relative to the state directory; migration rewrites eligible rows, deletes stale duplicates, and preserves external paths.
* fix(voice-call): describe state-relative registry migration
* fix(state): preserve registry locator traversal
* test(macos): move PortGuardian schema-version boundary to v9
The store opens any state database up to maximumSupportedSchemaVersion,
which the schema v9 bump moved from 8 to 9. Shift the supported/newer
partition in the boundary test accordingly.
* fix(state): re-anchor copied default-layout registry rows instead of deleting them
Resolve the ClawSweeper P1 by preserving genuine external default-layout registrations, re-anchoring copied rows when an in-root counterpart exists, and deleting only dual default-layout conflicts.
The Signal and iMessage channel plugins inferred approval prompts by
regex-sniffing rendered message text (header matching like 'Exec approval
required', /approve command parsing), violating the transport-only channel
doctrine: approval actions must stay typed until channel encoding.
The typed envelope already existed (channelData.execApproval with
approvalId/approvalKind/allowedDecisions) and every payload-level delivery
path consumed it; the regex paths were redundant re-derivation at the raw
send seam plus restart recovery for in-memory iMessage poll state.
- Signal: delete send-level prompt sniffing entirely; all approval sends
already flow through typed structured-payload or native-handler paths.
- iMessage: sendMessageIMessage takes a typed approvalPrompt binding
(id/kind/decisions) from the native approval handler instead of an
approvalKind flag plus text re-parsing.
- iMessage poller: persist pending poll targets in the plugin keyed store
so restart recovery no longer regex-scans chat history; typed recent-chat
discovery for handle-only DM targets stays. Split poll-target ownership
into approval-reaction-poll-targets.ts (max-lines).
- Plugin SDK: remove extractApprovalReactionPromptBinding — beta-only
surface, never in a stable release, so no deprecation window applies;
AGENTS.md now records that rule.
Accepted tradeoff: approval prompts delivered by a pre-upgrade process are
not rediscovered from chat text after restart (<=24h transient state;
persisted reaction bindings and event-driven tapbacks still work).
* feat(ui): surface approvals passively and redesign the approval card
Approvals no longer auto-open the centered modal: the queue is reachable
only through the sidebar attention chip, while the owning session shows
the inline card and other sessions surface via the session-row shield
icon, agent badges, and the chip. Deletes the inline-vs-modal exclusion
machinery (modalApprovalQueue, inlineApprovalId, forceShowAll) and makes
modal dismissal close the view instead of denying the active request.
Card: severity now drives the accent color instead of a table row,
plugin/agent render as header chips, the session key moves behind a
collapsed Details disclosure (modal only), and low-value exec rows
(Resolved/Security/Ask) collapse into the same disclosure. The codex
app-server bridge stops duplicating the session key into description
text; the envelope already carries it.
* fix(ui): gate settings Escape on the approval dialog's recorded open state
ClawSweeper caught that shouldIgnoreSettingsEscape still inferred an open
approval dialog from queue non-emptiness; with passive approvals a pending
queue no longer implies a visible dialog, so settings would swallow Escape.
The exec-approval element now records dialogOpen as a fact and the guard
reads it.
* refactor(line): replace nine-marker prompt DSL with typed rich messages
Delete the LINE plugin's double-bracket marker language (quick_replies,
location, confirm, buttons, media_player, event, agenda, device,
appletv_remote) and its parser. Portable interactions now flow through the
existing presentation-block seam (renderPresentation, matching Discord and
Feishu); LINE-specific cards ride closed channelData.line schemas mapped to
the existing Flex renderers. Prompt section shrinks to four capability
lines and explicitly de-fangs marker text. Removes the stale
assertion-safety baseline entry for the deleted parser.
Production LOC net -69, tests net -433. Suite: 510/510 green.
* fix(line): declare rich message schema dependency
* fix(line): satisfy rich message type checks
* docs(line): mark card fragments as partial
Agent-created agents (durable roster entries with provenance) are now
planned work, so the blanket VISION.md guardrail against agent-hierarchy
frameworks no longer reflects direction. The adjacent guardrail against
heavy duplicative orchestration layers stays.
* refactor(gateway): drop dead session-subscriber registry surface
getForConnection and both registry clear() methods had no production
callers — only server-chat-state.test.ts consumed them (verified by
repo-wide grep incl. plugin-sdk). The recency-sorted connToSessionKeys
index existed solely so getForConnection returned keys in subscription
order, an ordering nothing read, rebuilt via toSorted() on every
subscribe/unsubscribe/settle — including the provisional-replay settle
path.
unsubscribeAll now reads the reverse index directly from
connToSessionRecency's keys (same contents, no ordering), removing the
per-call O(k log k) rebuild and the duplicate map. Tests assert through
get()/getApprovals(), the production read paths, per test-audit norms.
Behavior-neutral: subscription state transitions and event-visible
delivery unchanged; chat-state, broadcast, node-subscription,
instance-runtime, agent-events, session-events suites pass.
* test(gateway): fix ETXTBSY flake in workspace-sync candidate bounding
The test rewrote its mock git script between two createGitTransferList
calls. On Linux CI the second spawn can race a lingering fork of the
first child still holding the script's write fd, failing exec with
ETXTBSY. Write the script once; vary the entry count through a data file
the script reads instead.
* fix(outbound): record queue-write loss and fail every payload on prep abort
Two silent-observer gaps in the outbound queue admission path:
1. Best-effort staging/queue-write failures were swallowed with a bare
'return null'. The send proceeded live-only, so a crash mid-send lost
the message with zero forensic trace of why the write-ahead row was
missing. Every sibling degradation on this boundary logs
(persistQueuedPreSendState, the dispatch-refresh fallback); this one
now warns with channel/target and the error.
2. Batch preparation failure emitted message_sent for only the first
payload while audit emitted per-payload terminals, so plugins with
message_sent hooks saw 1 failure for an N-payload batch. The failure
path now emits one hook failure per logical payload, matching the
audit terminals and the recovery sibling's
queuedTerminalFailureEvents. The unused error.payload pick and its
OutboundPayloadPreparationError import went with it.
Regressions: queue-write warn asserted in the existing best-effort
fallback test; new multi-payload prep-failure test asserts 2 hook
failures. Both fail pre-fix.
* chore(outbound): keep OutboundPayloadPreparationError module-local
Last external consumer went away with the per-payload emit; the
deadcode:exports gate rejects unused exports.
A LINE push made exactly one attempt, so a transient provider or transport
failure dropped the reply even though retrying was safe to do. Retrying alone
would have duplicated a send LINE already accepted, so every push now carries an
X-Line-Retry-Key and reuses it across attempts: LINE answers a replayed key with
409 and the accepted request's sent messages, which resolves to the original
delivery instead of a second message.
Retries follow LINE's documented policy - server errors and transport failures
only, never 2xx, 409 or any 4xx - and run through the shared channel API retry
runner in strict mode. Replies stay single-attempt because LINE offers no retry
key for them.
* fix(ui): keep Control UI device identity working on plain-HTTP origins
@noble/ed25519 defaults its SHA-512 provider to crypto.subtle, which
browsers gate to secure contexts, so device identity silently vanished
on http:// LAN dashboards and connects fell back to shared-credential
auth with no pairing. Wire a lazy pure-JS @noble/hashes fallback for
SHA-512 and the fingerprint SHA-256, and drop the isSecureContext gate
in the connect path. Secure contexts keep the platform digests and pay
no startup bytes: the fallback loads as its own lazy chunk, kept out of
the gateway-runtime startup chunk on purpose.
* test(ui): cover device identity minting and signing without crypto.subtle
New jsdom regression suite fails on pre-fix code (subtle-less crypto stub
with getRandomValues, which real insecure contexts keep). Rewrites the
gateway connect tests that previously asserted the device-less insecure
fallback: an insecure context now attaches a device identity.
* docs(web): plain-HTTP dashboards now pair with a device identity
The signing key never crosses the wire, so HTTP+pairing is strictly
stronger than the old HTTP token-only fallback; HTTPS (Tailscale Serve)
stays the recommendation for transport privacy.
* fix(ui): drop unnecessary boolean literal compare in secure-context timing meta
* test(ui): declare device.id on the connect-frame test shape
* test(ui): split the subtle-less scope-upgrade e2e into the two real invariants
Without crypto.subtle the browser can now sign, so the banner offers the
explicit admin upgrade; manual-only guidance is reserved for browsers that
cannot mint an identity at all (no WebCrypto RNG). Also corrects the
connect-path comment: blocked storage yields an ephemeral identity, only a
failed mint degrades device-less.
* fix(ui): address review findings on the HTTP device-identity path
- Storage-blocked pages keep one stable in-memory identity per page
lifetime instead of minting a fresh unpaired key on every reconnect,
and a write-rejecting store no longer fails the mint (regression tests
bite pre-fix).
- Connect timing now reports the real browser secure-context fact via a
shared browserSecureContext() helper instead of inferring it from
device-identity presence.
- Docs state the accepted trusted-proxy contract: browsers attach a
device identity on every origin, so first connects follow the standard
pairing flow (deviceAutoApprove or a one-time approval); device-less
admission remains only for browsers that cannot mint an identity.
* refactor(ui): trim the connect-path additions under the max-lines cap
* feat(voice-call): add sessionScope "main" for main-session call routing
Inbound and outbound calls can now share the configured agent's main
session instead of a dedicated voice session. The new scope resolves
through the existing explicit-key canonicalization path, honoring core
session.mainKey and global-scope aliasing. The inbound webhook path now
forwards coreSession like every other resolver call site.
* style: format events.test.ts
* test(voice-call): split events.test.ts under the max-lines cap
* fix(cli): fail uninstalled service mutations
Treat Gateway and Node start/restart as failures when no managed service is installed, while preserving absent-service stop as an idempotent success.
* docs(cli): clarify gateway restart recovery