* fix(plugins): deep-clone registry snapshot values for transactional rollback isolation
Shallow spread / new Map() copies nested objects by reference, so
in-place mutations on PluginRecord fields and Map values inside
arrays leak through rollback() — violating transactional isolation.
Wrap array items, Map values, and object properties in a recursive
deep-clone helper that preserves function references so handlers
and resolvers are not lost. Fixes#106647.
* fix(plugins): replace generic deep-clone with targeted shallow record cloning
Replace the recursive deepCloneRegistryValue with cloneRegistryEntry that
shallow-clones registration records to isolate primitive metadata fields
while preserving opaque plugin-owned instances (providers, services,
channels, harnesses, resolvers) by reference. A generic deep-clone was
too broad: it converted every plugin-owned object into a plain object,
losing prototypes, internal slots, and shared identity.
Add a class-instance regression test proving that providers survive
snapshot/rollback with their prototype chain intact and methods callable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(plugins): add curly braces and fix TS type assertions for CI
- Add curly braces to single-line if statements in cloneRegistryEntry
to satisfy eslint curly rule
- Use `as unknown as ProviderPlugin` double cast for test class instance
- Call test methods on original variable instead of through registry type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: add loader-scenario rollback proof for two sequential plugin transactions (#106647)
Simulate the real loader pattern from loader-runtime-candidate L492-531:
transaction 1 registers a class-backed provider and commits, transaction 2
mutates registry state and rolls back. Prove the first plugin's metadata is
restored and its class-backed provider instance, prototype, and methods
survive the rollback.
* fix(plugins): snapshot active PluginRecord in registration transactions (#106647)
Add activeRecord parameter to createPluginRegistrationTransaction so
the active record's array fields (toolNames, hookNames, providerIds, etc.)
are snapshotted at transaction creation and restored on rollback.
Without this, the loader's recordPluginError path re-pushes the record
with stale id arrays from the failed register() call.
Also replace flat container copies in snapshotPluginRegistry with
cloneRegistryEntry that shallow-clones individual registration records
while preserving opaque plugin-owned objects by reference.
Update all three production callers (loader-runtime-candidate,
loader-channel-runtime, loader-cli-registry) to pass activeRecord.
* fix(plugins): snapshot all mutable PluginRecord metadata in transactions (#106647)
Expand activeRecord snapshot from array-only to full cloneRegistryEntry
so scalars (httpRoutes, hookCount), flags (configSchema, enabled,
memorySlotSelected), and Dates are also restored on rollback. Runtime
objects (configUiHints, configJsonSchema, contracts) stay by reference.
Also update the activeRecord JSDoc to reflect the broader contract.
* fix(plugins): restore exact rollback record shape
Co-authored-by: 詹幸心0668001037 <zhan.xingxin@xydigit.com>
* test(plugins): cover date rollback isolation
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: 詹幸心0668001037 <zhan.xingxin@xydigit.com>
* fix(memory-core): scope dream narrative sessions to their owning agent
Dreaming narratives built unscoped subagent session keys
(`dreaming-narrative-<phase>-<hash>`). Sessions live in per-agent SQLite
stores, so the plugin-runtime ownership pre-check could not resolve a store
and every narrative call failed with "Cannot resolve SQLite session scope
without an agent id" — the whole diary pipeline produced nothing while the
sweep still logged "dreaming promotion complete".
The agent id was available all along: `resolveMemoryDreamingWorkspaces`
returns it per workspace and the hook context carries the triggering agent.
Both were dropped. Session keys are now `agent:<id>:dreaming-narrative-...`,
while runIds stay unscoped so the orphan-transcript scrub marker still
matches. An unexpected narrative failure now writes the same dated fallback
diary entry the other failure branches write, and the promotion summary
warns instead of informing when every workspace failed.
* fix(memory-core): narrow the narrative export surface and type the sweep test double
* test(memory-core): drop the now-unused missing-path helper
* fix(memory-core): scope the narrative run id and keep empty sweeps a no-op
* fix(memory-core): route the ownerless narrative fallback through detached dispatch
* fix(nodes): name node screen captures after the encoding the node returned
screen.snapshot lets the node choose its encoding (macOS defaults to
JPEG) and reports it back in the payload. When the caller supplied
outPath, the tool wrote the bytes to that path verbatim, so a requested
.png happily received JPEG data. Anything dispatching on extension --
viewers, uploads, content-type headers, channel attachment handling --
then sees the wrong type.
Rename a caller-supplied outPath to match the reported format instead of
sniffing bytes, and drop the PNG default baked into
screenSnapshotTempPath. screen_record gets the same treatment; camera
snap/clip and photos.latest already derive their extension from the
payload format and take no outPath.
* fix(nodes): request the snapshot encoding the outPath promises
Correcting the extension after the fact would have written to a path the
workspace guard never saw: outPath is alias-checked on its exact final
segment before the tool runs, so swapping .mov for .mp4 could follow a
symlink out of the workspace.
Ask the node for the encoding the caller's filename already promises
instead, write the guarded path verbatim, and refuse to write bytes that
contradict the name.
* fix(agent): add requireWriteSuccess:true to pendingFinalDelivery updateSessionEntry call
The pendingFinalDelivery write is a critical durable state write where silent
failure would cause data loss. This mirrors the same pattern used in 4 other
sites.
This commit resolves the merge conflict in PR #102128 by moving the change from
the old agent-runner.ts to the new agent-runner-result-complete.ts where the
code now lives after the refactoring.
* fix: update test import path and fix formatting
* fix: remove outdated test — store migrated to SQLite
The test file pending-final-delivery.store-write-failure.test.ts was written
for the legacy file-based store. Since main migrated updateSessionEntry to
SQLite, mocking writeTextAtomic/replaceFileAtomic no longer tests the actual
code path. SQLite writes are inherently ACID — write failures always propagate.
The requireWriteSuccess:true option is retained in agent-runner-result-complete.ts
as documentation of intent for this critical state write.
* fix(auto-reply): verify durable final delivery session
Preserve the session identity and committed pending intent before returning a final reply. Reject deleted or reset SQLite sessions without poisoning replacements, and cover the production reply-runner path. Follow-up to #102128 by @SunnyShu0925.
---------
Co-authored-by: Peter Steinberger <58493+steipete@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
navigate and act results that changed the page document now include the
loaded page's compact snapshot inline (honoring browser.snapshotDefaults
and standard caps/wrapping), removing the follow-up snapshot round-trip.
Snapshot execution moves to browser-tool.snapshot.ts to keep action
executors under the file size cap.
The moonshot provider already declares compat.codeMode: "preferred" for the
same model (kimi-k3), so tools.codeMode: "auto" engaged code mode on the API
surface but silently skipped the Kimi Code subscription surface (kimi/k3,
kimi/k3-256k). Live proof on kimi/k3: "auto" reported codeModeEngaged=false
while true reported codeModeEngaged=true with "code-mode: cataloged 50 tools
behind exec/wait" and 2 bridge tool calls.
K2.7 (kimi-for-coding*) stays unflagged, matching moonshot's kimi-k2.7-code.
* fix(plugins): report missing plugin modules as missing, not boundary escapes
The root-scoped open helper returns a classified failure, but five plugin
loader sites collapsed every failure into "escapes plugin root or fails
alias checks". A plugin artifact that is simply absent — e.g. while
dist/extensions/<id> is being re-emitted by a build — was therefore
logged as a containment violation.
Classify the failure instead: missing (ENOENT/ENOTDIR), unreadable
(coded), or an actual boundary/alias rejection. The containment check is
unchanged; only the reported reason is. Also drops the never-supplied
boundaryLabel/boundaryRootDir parameters on loadChannelPluginModule so
one root carries one label.
* test(infra): rename lint-flagged local helper in boundary failure test
#114819 added importance/triggers to memory_index_chunks with a lazy memory-core
ALTER but did not allowlist their absence, so every existing agent DB fails the
canonical schema check, doctor errors, and the updater rolls back. Observed live
on both production Hetzner hosts (hourly update cycles 10:08/10:18 UTC failed
with 'column definitions differ for memory_index_chunks' and rolled back).
* [AI] fix(tlon): use waitUntilAbort to fix monitor abort-race hang
Replace ad-hoc abort listener with the shared waitUntilAbort
primitive, hoist pollInterval for unified finally cleanup, and
add .unref() per codebase convention.
The original code registered an abort listener inside a Promise
constructor without checking signal.aborted first. Per the
AbortSignal contract, a listener attached after abort has fired
is never called, so the monitor hung without running cleanup.
The fix reuses waitUntilAbort from plugin-sdk/channel-outbound,
which correctly handles already-aborted, normal, and absent
signals. pollInterval is hoisted and cleared in finally as a
single cleanup owner.
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes#114886
* fix(tlon): prove abort-safe Urbit monitor cleanup
---------
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
* feat(memory): add provenance and recall metadata to the memory index
* feat(memory): provenance-gated promotion and capture hygiene
* feat(dreaming): LLM consolidation with deterministic gates, on by default
* feat(active-memory): deterministic recall lane with escalation default
* feat(memory): user model file and standing intents
* docs(memory): document the memory architecture
* fix(memory): live-QA fixes — metadata writers, provenance classes, intent scope, claim accumulation
Restore the already shipped global exec approval notice setting through the same strict validator as its per-agent override.
Reported-by: @glorifiedautocomplete
Original-fix-by: @natedemoss (#57293)