#124844 made resolvePluginMetadataSnapshot synthesize workspacePluginRootPresent
itself with a process-memoized fs.existsSync probe whenever a caller did not
supply it. That fact is owned by the prepared-model-runtime lease, which resolves
it only for real agent runs (resolveWorkspacePluginRootPresence) and passes it
explicitly, so the probe silently turned an opt-in fast path into the default for
unrelated control-plane callers.
Gateway startup config validation is one of them. It never asserts the fact, so it
began projecting the published lifecycle graph instead of loading a fresh one, and
the projection derives configFingerprint from that graph rather than a real load.
Startup convergence rewrites the persisted plugin index between the two reads that
form the migration checkpoint identity, so the pre-convergence read projected while
the post-convergence read loaded. The two pluginMigrationFingerprint values differed
and the gateway refused readiness with "OpenClaw plugin migration inputs changed
during startup convergence", failing every config-patch restart.
Deleting the probe restores one owner for the fact and a net-negative production
diff. The agent-side hunks of #124844 are untouched: they still pass the fact
explicitly from the lease.
Root cause: admission fact manufactured outside its lifecycle owner.
Owner boundary: src/agents/prepared-model-runtime-lease.ts owns workspace
plugin-root presence; src/plugins/plugin-metadata-snapshot.ts only consumes it.
Production LOC: -18.
Fixes red main ci-gate: QA Smoke memory-dreaming-sweep, matrix-restart-resume,
matrix-post-restart-room-continue.
Recover owner-qualified rows from the retired top-level session store into each configured agent database without guessing ambiguous ownership. Relocate stale transcript paths, tolerate historical Unicode IDs when an explicit transcript exists, and avoid double-counting physical SQLite databases in migration totals.
Amp-Thread-ID: https://ampcode.com/threads/T-01a00a6a-b64e-74a5-8b15-2d3b966a468d
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Josh Lehman <josh@martian.engineering>
* fix(gateway): stop a plugin dangerous flag from revoking desktop computer.act
`resolveNodeCommandAllowlistInternal` subtracted every plugin-declared
dangerous command from the composed allowlist, including core's own
`PLATFORM_DEFAULTS` entries and including the pairing allowlist, where core's
dangerous defaults are exempted. `computer.act` is both a desktop platform
default (grant = node-local enablement + pairing approval) and a command that
`registerComputerUseProvider` marks dangerous. Since `cua-computer` became
enabled-by-default on darwin, every macOS-hosted Gateway stripped `computer.act`
from both allowlists, so `normalizeDeclaredNodeCommands` dropped it from the
node's declaration with no pairing upgrade, no prompt, and no record. The
`computer` capability survived because caps were never allowlist-filtered, and
the `computerUse` descriptor was then dropped for the missing command.
Scope the plugin-dangerous subtraction to commands outside the platform-default
base: the flag still keeps a plugin's own surface behind an explicit allow and
still forces a registered invoke policy, but it no longer revokes a command core
declares itself.
Also make cap-without-command unrepresentable. `retainFulfilledNodeCapabilities`
drops a capability when policy withheld commands from its family and admitted
none, and the reconciler records the withheld commands so a refused declaration
is never silent.
* test(gateway): type the computer-use fixture against its contract
Adds an additive agent_provenance table (shared state DB, schema v8) owned
by src/state/agent-provenance.ts. createAgent() records operator/agent
provenance after commit, the system-agent create-agent operation passes its
own id as creator, and Claw installs record created-via claw at their roster
commit point. Agent deletion removes the agent's own row inside the deletion
journal transaction; children keep dangling creator ids as historical fact.
openclaw agents list gains --tree (provenance hierarchy) and JSON provenance
fields.
* fix(sessions): migrate historical transcript directives
* fix(infra): complete directive migration on pre-archives-schema databases
session_transcript_archives is a lazy additive v17 surface; valid databases
may omit it. The archives phase now treats the missing table as empty and
completes the cursor instead of warning-skipping the whole database forever.
Verified against a real long-lived agent DB: previously stuck cursor
completes with zero warnings in ~100ms.
* chore(ci): allow directive migration SQLite access
* fix(tui): report the first last-session write failure
rememberCurrentSessionKey swallowed writeTuiLastSessionKey failures
with catch(() => undefined). A corrupt or locked state DB silently
disabled session restore: the operator only discovers it on next
launch, landing in the wrong session with no clue why.
Root cause: fire-and-forget with no recorded failure. The write wrapper
moved to its owner (tui-last-session.ts) as
createRememberSessionKeyWriter, which surfaces the first failure via
the chat log ('session memory write failed: ...') and stays silent on
repeats — one line per TUI run, not one per session switch. Empty and
'unknown' keys still skip without touching the writer.
Regression: colocated tests prove first-failure-once reporting and the
skip path — fail pre-fix (factory did not exist; failure was
swallowed at the call site).
* chore(tui): make the last-session write dependency explicit
deadcode:exports rejects writeTuiLastSessionKey as unused when only the
factory default referenced it; tui.ts now passes it explicitly.
sandboxListCommand and fetchAndFilterContainers swallowed backend and
registry probe failures with catch(() => []). A broken Docker daemon or
corrupt registry rendered as 'No sandbox runtimes found.' — and in
recreate, filtered an entire fleet down to nothing — indistinguishable
from a clean empty state. --json emitted a success envelope for a
failed probe.
Root cause: failure collapsed into the empty success shape at the
consumer. The listers now propagate; the sandbox CLI runner already
owns the error path (message + exit 1), and the new --json failure
contract (6c66f48a7c) emits the JSON error envelope.
Regression: replaced the test that enshrined the masking ('handle
errors gracefully' asserting the empty-state message) with the
propagation contract — fails pre-fix.
openSelector's onSelect fired the async handler with void and only
closed the overlay after a successful await. A rejecting handler (e.g.
setAgent -> setSession against a failing gateway) left the selector
stranded open with an unhandled rejection: the TUI froze on the picker
with no visible cause — a silent dead-end.
Root cause: failure path missing from the overlay lifecycle. The
handler now catches, surfaces the cause via chatLog, and always closes
the overlay.
Regression: /agent selection with a rejecting setSession asserts the
overlay closes and the cause reaches the chat log — fails pre-fix.
* fix(skills): expand explicit references on agent turns
Route generic Gateway, CLI, webhook, and local agent turns through the same explicit skill-reference renderer as channel auto-replies. Keep original transcript text, preserve unknown slash behavior, and fail visibly for allowlist-hidden skills.
Maintainer review: scoped Option 1 — generic agent turns expand both $skill-name and leading /skill-name args through shared skill rendering; they do not run the channel command dispatcher, and all other slash commands retain their existing behavior.
* fix(skills): bound explicit reference prompts
* fix(skills): prefer allowed reference collisions
* fix(skills): preserve command invocation boundaries
* fix(skills): reject hidden channel slash commands
* perf(skills): skip literal dollar discovery
* fix: stop service child trees before timeout completion
* fix: preserve service child root results during cleanup
* test: isolate relay selection from linux spawn wrapping
* refactor: keep service anchor payload internal
* test: validate lifecycle probe pid output
* fix: close service child adversarial races
* fix: preserve service startup failures
* fix(process): flush service output before root result
* fix(process): bound cleanup on held output
* chore(process): satisfy promise executor lint
* fix(process): finish cleanup after lineage loss
* fix(process): retain relay output before subscription
* fix(ci): register service child runtime entries
* fix(ci): expose relay safety invariants
* fix(process): preserve service cancellation grace
* test(process): prove service grace on macOS
* fix(delivery): unify terminal settlement ownership
Treat identityless adapter returns as potentially visible across channel, queue, and cron paths. Let recovery own terminal completion so ambiguity persists as notice debt instead of being double-settled or silently suppressed.
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
* refactor(delivery): narrow terminal internals
Remove now-unused internal exports after terminal-settlement ownership was consolidated.
* test(tts): preserve message runtime exports
Import and spread the actual message runtime so the focused mock retains every runtime binding while overriding only the durable send core.
---------
Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
When a caller passed an explicit maxBytes with optimizeImages on (the
default), the source-read bound was inflated to max(maxBytes, 100MB
document cap). A channel with a 5MB attachment cap therefore let a
hostile or oversized URL buffer up to 100MB before the final size check
— 20x resource amplification, and the inflation applied before content
classification, so it was not limited to images.
The inflation exists so oversized-but-compressible originals can be
fetched and compressed under the delivery cap. Size that headroom off
the thing that justifies it: 4x the 6MB image cap (24MB), enough for
large phone photos, instead of the document cap.
Accepted tradeoff (named at the constant): originals above 24MB that
would have compressed under the cap now fail; the fetch error names the
bound. Callers without an explicit cap keep per-kind defaults.
Live proof on a real localhost HTTP server (no mocked fetch): an 8.1MB
PNG under a 5MB cap compresses to 3.75MB and delivers; a 30MB-declared
image is rejected at the Content-Length precheck in 7ms with 'content
length 31457280 exceeds maxBytes 25165824'.
Regression: headroom-bound test fails pre-fix (30MB passes the old
100MB bound); companion test proves compression headroom still works.
Worktree sessions now derive their worktree/branch name from the same
concise generated title the sidebar shows (bounded 8s wait, raw-prompt
slug then crustacean fallback), persisted once as displayName so the
chat-send pass never duplicates the model call. Dashboard title
scheduling moves from the dispatch chain's finally (after the whole
first turn) back to dispatch start, so long or interrupted first turns
no longer leave sessions silently untitled (regression from #122471).
slugifyWorktreeTitle truncates at word boundaries; dead
generatedDisplayName plumbing removed.
* 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