Skip to content

🤖 feat: make provider requests a pure function of the session log + sandboxed plugin hooks - #3872

Merged
ThomasK33 merged 34 commits into
mainfrom
plugin-architecture-5p67
Aug 19, 2026
Merged

🤖 feat: make provider requests a pure function of the session log + sandboxed plugin hooks#3872
ThomasK33 merged 34 commits into
mainfrom
plugin-architecture-5p67

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Summary

Implements Track 1 of the plugin-architecture work on top of the shared agent foundation (#3865): the provider request becomes a verifiable pure function of the append-only session log ("model-visible ⟺ logged"), with turn-envelope records, a byte-equality replay harness and cache-bust auditor, sandboxed Tier-1 plugin hooks (QuickJS), and graduation of the agent-plugins manifest into a unified contributes distribution unit with a per-workspace composition inspector.

Background

Research into DeepSeek Harness ("everything is a plugin"; requests reconstructed purely from an append-only log) and OpenCode's plugin hook vocabulary identified two properties worth porting to Mux: log-purity of provider requests (optimal prompt-cache reuse, deterministic replay/fork/resume) and a safe plugin surface. Mux was already ~80% log-pure (history re-read from disk each turn, no in-memory transcript); this PR closes the remaining gaps and builds the plugin surface inside that guarantee. Builds directly on the event spine, journal kit, sandbox host, and capability grants from #3865.

Implementation

Five phases, each implemented → quality-gated → adversarially reviewed → dogfooded via a durable Mux workflow (committed as workflows/track1-implementation.js):

  • p1 — Log purity: file-change notifications and @file mention snapshots materialize into chat.jsonl at append time (before the request is built); the two request-time injectors in messagePipeline.ts are deleted. Old persisted histories still build.
  • p2 — Turn envelopes: one turn-envelope durable event per assistant turn: content-addressed system prompt (BlobStore, dedupes across turns), name-sorted toolset manifest with schema hashes, model/thinking/providerOptions hashes (hash-only; raw options never persisted). Emission is observability — it never fails the turn.
  • p3 — Determinism harness: bun run debug replay-verify <ws> reconstructs each turn's request from chat.jsonl + envelopes + blobs through the production pipeline and byte-compares against devtools.jsonl; bun run debug cache-audit <ws> diffs consecutive envelopes and attributes prompt-prefix busts with approximate token cost. Fixture-based CI tests keep the invariant enforced. Provider-defined tools (e.g. Anthropic web_search) fingerprint by wire identity (id+args) on both sides — their client-side inputSchema never crosses the wire.
  • p4 — Sandboxed plugin hooks: hooks.js in agent-plugin containers loads into persistent QuickJS mounts and registers as event-spine middleware (tool.execute.before mutate/deny, tool.execute.after observe, request.assemble contribute context). Least-privilege capability grants (manifest-requested tool visibility, Project-Trust-gated project containers); hook-contributed context lands as durable hook-context rows before the prompt mutation, so replay stays byte-identical. Crash/timeout/malformed hooks are logged and skipped — only explicit denials become model-visible tool errors. Experiment-gated (agent-plugins).
  • p5 — Manifest graduation + inspector: plugin.json gains contributes (skills, agents, workflows, MCP, slash commands, hooks) consumed through the existing per-scope loaders (no parallel loading paths); plugin:// workflow scheme; plugin slash commands surface in the composer suggestions; bun run debug plugins <ws> plus one bulk oRPC endpoint print the effective composition by layer (built-in | global | project | plugin:<name>) including shadowing attribution.

Validation

Beyond CI: each phase was dogfooded against a live dev-server sandbox with real provider turns —

  • p1: <system-file-update> row durably appended before request start; request content byte-identical to log rows.
  • p2: blob dedupe across identical turns; system-prompt blobs byte-identical to the wire request in devtools.jsonl.
  • p3: live dogfooding caught the provider-tool fingerprint bug reviews missed; after the fix, replay-verify 3/3 PASS and cache-audit correctly attributed a deliberate AGENTS.md edit (~13.6k tokens re-processed).
  • p4: demo plugin blocked .env reads (secret never reached the model), injected context landed as a hook-context row and in the wire request, deliberately crashing hooks were skipped with the turn completing, replay-verify stayed green.
  • p5: live web-UI turn via a plugin-contributed slash command (agent-browser); inspector showed correct per-layer attribution and a project skill shadowing a plugin skill, matching what the production loaders enforced in the live system prompt.

Risks

  • p1 changes the request-build path (messagePipeline) — the highest-blast-radius area. Mitigated by the p3 harness (fixture tests fail on any reintroduced request-time injection), unchanged-behavior tests for old histories, and live byte-equality verification.
  • p4 executes third-party JS — bounded by the QuickJS sandbox (no I/O, memory/time caps), least-privilege grants, experiment gate, and Project Trust for project-scoped containers. hookService.ts is the file to review hardest.
  • p2/p3/p5 are additive (journal rows, debug CLI, discovery/inspection); envelope emission is fail-open by design.

Pains

  • The dev-server watch build (build-main-watch) crashes on intermediate edit states and can leave a stale/corrupt dist bundle serving Cannot find module errors; two workflow dogfooders were lost to this before the process pinned fresh rebuilds.
  • bun run debug send-message looks like a turn driver but is display-only; live turns must be driven via the oRPC WS API. Cost several dogfood attempts before being diagnosed.

Generated with mux • Model: anthropic:claude-fable-5 • Thinking: xhigh • Cost: $336.75

…ppend time
Log purity: the provider request must be a pure function of chat.jsonl
(MODEL-VISIBLE implies LOGGED). Remove the two remaining request-time
injectors from the message pipeline:
- File-change notifications: AgentSession.streamWithHistory now detects
external edits after commitPartial and appends the <system-file-update>
row durably to history BEFORE the history read that builds the request.
FileChangeTracker state updates on detection, so retries cannot append
duplicate rows. injectFileChangeNotifications is deleted; the message
builder moved to fileChangeTracker.ts (createFileChangeNotificationMessage).
- @file mentions: the send-time snapshot materialization path
(materializeFileAtMentionsSnapshot -> persisted fileAtMentionSnapshot rows)
is now the ONLY expansion path. The request-time live-disk-read fallback
(injectFileAtMentions) is deleted. Old histories that predate
materialization still build: un-materialized mentions stay plain text.
prepareMessagesForProvider no longer takes runtime/workspacePath/
abortSignal/changedFileAttachments — it cannot read live workspace state,
so rebuilding a request from the same log rows is deterministic.
Tests: durable row lands before the request (and survives with the same id
the model saw), no duplicate row on a second stream, double-build from one
log is identical, and old-format histories build without errors.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…succeeds
Codex P2: FileChangeTracker.getChangedAttachments() mutated fileState during
detection, so a startup abort or appendToHistory failure between detection and
the durable <system-file-update> append made retries see mtime <= tracked
timestamp — the external edit was silently dropped and never reached
chat.jsonl or the model.
Detection is now side-effect-free: getChangedAttachments() returns
{ attachments, commit } and AgentSession.streamWithHistory invokes commit()
only after the notification row is durably appended. commit() also refuses to
clobber state recorded after detection with the older detection snapshot.
Tests: retry after a one-shot appendToHistory failure re-detects and persists
the notification exactly once (agentSession.fileChangeNotification.test.ts);
re-detection before commit() returns the same change while post-commit checks
return none (agentSession.changeDetection.test.ts).
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Durable workflow driving p1-p5 (log purity, turn envelopes, determinism
harness, sandboxed hooks, manifest graduation) with per-phase quality
gates, adversarial review, and dogfooding. Empty fix patches are
tolerated; environmental dogfood failures retry without a fix round;
skipImplement args support re-runs after partial completion.
After the final system prompt and toolset are settled (post request.assemble
middleware, post tool-policy rebuild) and before streaming starts,
aiService.streamMessage appends a 'turn-envelope' row to the session's
DurableEventJournal: content-addressed system prompt blob (dedupes unchanged
prompts), name-sorted toolset manifest with stable-stringified input-schema
hashes, modelString, effective thinkingLevel, and a providerOptions hash
(raw options never persisted; they may embed auth-adjacent config).
Emission is purely additive and never fails the turn: failures are logged
and streaming continues. Retries/continuations re-enter streamMessage and
emit their own row.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…ifest
Blocking review findings: buildToolsetManifest only read .inputSchema, so
AI SDK v3-style tools declaring .parameters (or custom .schema adapters)
all hashed identically as the empty schema, and a sparse tools map entry
threw a TypeError that silently aborted turn-envelope emission. Fall back
through inputSchema ?? parameters ?? schema with safe optional access.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…pes without throwing
buildToolsetManifest passed raw schema objects straight to asSchema, which
treats non-Schema, non-standard-schema objects as lazy schema functions and
throws TypeError. Any turn containing MCP tools (inputSchema jsonSchema
wrappers), sanitizeToolSchemaForOpenAI output, or plain JSON Schema
.parameters/.schema silently dropped its turn-envelope row, breaking the
'model-visible implies logged' invariant. Extract the JSON schema by shape
(wrapper unwrap, plain-schema passthrough, asSchema fallback with catch) and
cover those runtime shapes in tests.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Regression net for the log-purity invariant (model-visible implies logged):
- replayRequestBuilder: rebuilds one turn's provider request purely from
durable logs (chat.jsonl slice + turn-envelope row + blob-stored system
prompt), reusing the production pipeline (prepareProviderRequestMessages,
addInterruptedSentinel, prepareMessagesForProvider, cacheStrategy system
wrapping) and the real streamText ModelMessage->LanguageModelV4 conversion
captured through a no-network stub model with StreamManager's per-step
transforms.
- replayVerify: byte-compares the reconstruction against the recorded
request in devtools.jsonl (llmDebugLogs) per turn — system prompt blob vs
wire system message, envelope toolset manifest vs re-hashed wire tool
schemas (shared hashToolSchema), full LM prompt JSON byte equality with
first-divergence reporting. Guarantee scope: same log + same config +
same binary.
- cacheAudit: diffs consecutive turn-envelope rows and attributes prompt-
prefix invalidations (system prompt / toolset delta / model / thinking /
provider options) with approximate busted-token attribution from recorded
usage (fresh input + cache-write tokens).
- debug CLI: 'bun run debug replay-verify <ws>' (exit 1 on FAIL, prints
first divergence) and 'bun run debug cache-audit <ws>'.
- committed sanitized golden fixture session (3 turns, anthropic cached-
system shape, one prefix bust) + CI tests: fixture byte-equality replay,
double-build determinism, and cache-bust attribution. Regenerate via
MUX_REGENERATE_REPLAY_FIXTURE=1.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…e + compaction handling
Fix round 1 for the determinism harness blocking findings:
- CLI: 'bun run debug replay-verify replay-fixture' and 'cache-audit
replay-fixture' now map the well-known fixture workspace ID to the
committed fixture session dir (resolveReplaySessionDir).
- Compaction: both CLI commands load the FULL history via
iterateFullHistory (collectFullHistory); replayVerifySession re-slices
each turn to its own compaction epoch (sliceEpochForTurn, same
boundary semantics as getHistoryFromLatestBoundary), so compacted
sessions no longer produce false divergences.
- Pairing: turn-envelope rows and devtools runs now record
requestHistorySequence (join key); the verifier pairs
envelope/recorded/assistant by that key, so a failed stream, a
devtools toggle, or a retry skips only its own turn instead of
cascading false FAILs. Fully-legacy logs fall back to ordinal pairing
with a note.
- Crash isolation: buildReplayRequest runs under a per-turn try/catch;
corrupted turns FAIL with the error message and verification
continues.
- Scope limitations removed: the turn envelope now logs the resolved
wireProviderName (Coder instance-typed gateways), the per-send
Anthropic cache TTL, and blob-stores injected plan-transition content
and post-compaction attachments; replay reads them back, making those
turns byte-reconstructible instead of documented FAILs.
- Fixture regenerated with the new fields; new replayVerify.test.ts
covers compaction, missing assistant rows, devtools toggling, retries,
corrupted turns, and the plan/TTL round-trip.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…nd replay
Live dogfooding caught replay-verify failing on every turn with
schema-changed:web_search: the envelope hashed the runtime tool's
client-side inputSchema while replay could only hash the wire record
{type, id, args} — the schema never crosses the wire, so they could
never match. Both sides now fingerprint provider-defined tools by
canonical wire identity (id + args), which also makes args changes
(e.g. maxUses) correctly attributable by the cache-bust auditor.
- AgentPluginInfo gains hooksPath, resolved with the same §6.2 component
rules as mcp.json (wrong-kind/escape invalidates only that component).
- validatePluginManifest carries object 'extensions' through opaquely so
Mux namespace consumers (plugin hook capability requests) can read them.
- computeAgentPluginContainers centralizes the container list + Project
Trust gating previously inlined in the plugin MCP provider.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…eware
Each plugin's hooks.js (a script evaluating to an object mapping hook
names to functions, mirroring OpenCode's vocabulary) loads into its own
persistent SandboxHostService mount per workspace session; a host-side
adapter registers one spine middleware per loaded hook. Supported points:
tool.execute.before (args rewrite / deny), tool.execute.after (observe /
annotate via hook_output), request.assemble (contribute context).
- Asyncify constraint respected: hook mounts register no asyncified
bridge functions; 'mux' is a pure guest Proxy throwing catchable
"Capability denied" errors, and hooks run inside runtime.eval whose
resolve loop settles async hook promises.
- Capabilities: least-privilege grants by default; manifests may request
tool visibility via extensions.mux.hooks.tools. tool.execute hooks are
invoked only for granted tools, so visibility AND mutation stay
bounded. Project-scope plugins remain Project-Trust-gated at
discovery (shared computeAgentPluginContainers).
- Log purity: request.assemble contributions are materialized as durable
hook-context rows (inline text or blob) BEFORE the request mutation;
the turn envelope then hashes the final post-hook prompt, keeping
"model-visible implies logged" byte-replayable.
- Failure posture: crashes, timeouts (per-eval deadline), unreadable or
malformed hooks.js are logged and skipped without breaking the turn;
only explicit denials surface to the model as tool errors.
- Lifecycle: aiService reconciles hooks before request assembly (gated
on EXPERIMENT_IDS.AGENT_PLUGINS, fingerprint no-op on the hot path);
workspace archive/removal disposes hook mounts and middleware.
- Tests: QuickJS integration suite (isolated in CI like other QuickJS
suites) covers .env-read denial, grant bounding, annotation, journaled
context, catchable capability denial, crash/timeout isolation, broken
sibling isolation, edit-reload/disable teardown, and a replay fixture
proving the p3 determinism harness stays green with hooks active.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…onents
Graduates plugin.json into the unified distribution unit: a Mux 'contributes'
extension block declares slash commands (name/description/expansion) and can
relocate component paths (skills, mcp, agents, workflows, hooks). Discovery
now also resolves agents/ and workflows/ component directories with the same
containment rules as skills/. Malformed contributes members warn + fall back
so a spec-valid plugin never breaks on the extension.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…tribution
Plugin agents/ directories join agentDefinitionsService through the same
precedence machinery as its scope roots (project > project-plugin > global >
global-plugin > built-in), with plugin-root realpath containment per file.
Skill and agent descriptors now carry an optional pluginName so consumers
(inspector, UI) can attribute plugin-sourced artifacts, and
discoverAgentDefinitions gains dedupeById:false for shadowing reports.
Router agents.list/get honor the agent-plugins experiment gate.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…// scheme
Extends the existing workflow script resolution with a plugin://<name>/<file>.js
scheme: host-local discovery through the shared agent-plugins containers,
realpath containment inside the plugin's workflows dir, project containers
gated on project trust, and the whole scheme gated on the agent-plugins
experiment (includeAgentPlugins threaded from aiService, workflow_run tool,
router workflows.list, and the workflow CLI's explicit --experiment opt-in).
Plugin skills also join skill:// resolution and workflow enumeration.
Adds readPersistedExperimentEnabled for standalone CLI experiment reads.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…ions
Backend: collectPluginSlashCommands merges manifest contributes.slashCommands
across discovered plugins (first plugin in container precedence wins) and a
new workspace.plugins.slashCommands.list oRPC endpoint serves them, gated on
the agent-plugins experiment and anchored at the host checkout root.
Frontend: suggestions merge them as data-driven entries whose replacement is
the declared expansion text; built-in command keys and skill names win on
collision. No new UI surfaces beyond the suggestion list.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…s CLI)
buildWorkspaceComposition computes the effective composition by layer through
the production loaders (skills/agents dedupe:false for shadowing, workflow
discovery, MCPConfigService.listServerLayers, shell + plugin hooks) and labels
every entry with its source (built-in | global | project | plugin:<name>) plus
what shadowed it. Exposed as one bulk workspace.plugins.composition.get oRPC
endpoint and as 'bun run debug plugins <workspace-id>'. Plugin discovery and
manifest validation run unconditionally; the agent-plugins experiment only
gates whether plugin artifacts join the effective layers.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…rkflow start
Plugin-contributed agents were discoverable but unresolvable at stream time:
resolveAgentForStream, agent body/frontmatter resolution, and subagent tool
discovery all read agent definitions without plugin roots, and the oRPC
workflows.start path rejected plugin:// scripts because resolveWorkflowScript
never saw the agent-plugins experiment flag.
- ResolveAgentOptions gains includeAgentPlugins; agentResolution passes it to
every readAgentDefinition/resolveAgentFrontmatter call plus the base
inheritance chain (plugin agents may derive from plugin bases).
- aiService passes the already-computed experiment flag to resolveAgentForStream.
- streamContextBuilder passes opts.agentPluginsEnabled to resolveAgentBody,
resolveAgentFrontmatter, and discoverAvailableSubagentsForToolContext (which
now forwards it to discoverAgentDefinitions and per-descriptor frontmatter).
- router.ts getWorkflowService + workflows.start pass the experiment flag to
resolveWorkflowScript so plugin:// workflows resolve when enabled.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
MemberAuthor

@codex review

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

https://github.com/coder/mux/blob/2d3d8d977255e574ad5615206da9e810b6cfd129/src/node/services/agentSession.ts#L6383
P1 Badge Link file snapshots to their invoking user row

If the process crashes after this synthetic snapshot is appended but before the corresponding user row is appended, the snapshot remains in chat.jsonl with no way to identify it as orphaned; streamWithHistory only calls filterOrphanedMcpPromptSnapshots, so the next provider request can silently include stale file contents from a message the user never successfully sent. Persist an invoking message ID and filter orphaned file snapshots on load, as is already done for MCP prompt snapshots.

AGENTS.md reference: AGENTS.md:L109-L111

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadsrc/node/services/aiService.ts Outdated
Comment threadsrc/node/services/agentPlugins/hookService.ts Outdated
Comment threadsrc/browser/features/ChatInput/index.tsx Outdated
… plugin-command loading
Addresses Codex round 1:
- P1: modelFallback.prepare now emits its own turn envelope with the
rebuilt request identity so replay pairing compares the request that
actually streamed.
- P1: hookService uses a static QuickJSRuntimeFactory import (WASM still
loads lazily at first runtime creation) instead of a dynamic import.
- P2: composer plugin-command loading subscribes to the agent-plugins
experiment value so toggling it takes effect without a remount.
@ThomasK33

Copy link
Copy Markdown
MemberAuthor

@codex review

Addressed all three round-1 findings in ca2b11f:

  • Fallback envelope (P1): modelFallback.prepare now emits its own turn envelope with the rebuilt request identity (model, system prompt, toolset, provider options) before returning, so pairSessionTurns' last-envelope-per-sequence rule compares the request that actually streamed.
  • Dynamic import (P1): hookService statically imports QuickJSRuntimeFactory; the WASM stack still loads lazily at first runtime creation (QuickJSRuntime.create), so startup cost is unchanged and the runtimeFactoryLoader DI seam is preserved.
  • Experiment reactivity (P2): the composer subscribes via useExperimentValue(EXPERIMENT_IDS.AGENT_PLUGINS) and includes it in the load/clear effect, so toggling the experiment takes effect without remounting.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:ca2b11f55d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadsrc/cli/debug/cache-audit.ts
Comment threadsrc/node/services/aiService.ts Outdated
Comment threadsrc/cli/debug/replay-verify.ts
Comment threadsrc/node/services/agentPlugins/hookService.ts Outdated
…der config, hook load retry
Addresses Codex round 2:
- P1: cache-audit collapses envelopes to the final row per
requestHistorySequence (matching pairSessionTurns) so fallback turns
audit once with the identity that streamed.
- P1: turn envelopes fingerprint the step-0 active tool subset (forced
xAI search set, else tool-search active tools) in both the primary and
fallback paths, matching what StreamManager actually sends.
- P1: replay-verify CLI passes the live ProviderService config view so
aliases/custom metadata/cross-typed instances replay correctly.
- P2: hookService stores only successfully-loaded candidates in the
reconcile fingerprint, so transiently-failed hooks retry on the next
send instead of staying disabled for the process lifetime.
@ThomasK33

Copy link
Copy Markdown
MemberAuthor

@codex review

Addressed all four round-2 findings in 3ee27c0:

  • Audit collapse (P1): cache-audit now collapses envelopes to the final row per requestHistorySequence via a shared collapseEnvelopesToFinalPerSequence helper (matching pairSessionTurns), so fallback turns are audited once with the identity that streamed; usage pairs once.
  • Step-0 tool fingerprints (P1): both the primary and fallback envelopes now fingerprint the first step's actual wire toolset — forcedFirstStepToolNames when present, else the tool-search active subset — instead of the full runtime map; the fallback's forced-name computation is hoisted and shared with the prepare return payload.
  • Provider config (P1): the replay-verify CLI passes new ProviderService(defaultConfig).getConfig() (the same view type the live build consumes) into replayVerifySession.
  • Hook load retry (P2): the stored reconcile fingerprint now includes only successfully-loaded candidates, so a transiently-failed hooks.js is retried on the next send; covered by a new test (fail once via spy → unchanged files → retried and active).

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

https://github.com/coder/mux/blob/3ee27c0e976710bb755a6c46b2022684143702d4/src/node/services/aiService.ts#L3691
P1 Badge Requeue DevTools metadata for the fallback run

Fresh evidence after the fallback-envelope fix: each fallback model gets a new createDevToolsMiddleware instance and therefore creates a new DevTools run, while DevToolsService.createRun() consumes this metadata ID's pending entry when the primary run starts. Reusing the header here does not attach requestHistorySequence to the fallback run, so pairSessionTurns selects the keyed primary request but the final fallback envelope, making every logged refusal fallback verify against the wrong wire request. Queue the metadata again before starting the fallback run.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadsrc/node/services/aiService.ts Outdated
Comment threadsrc/node/services/aiService.ts Outdated
Comment threadsrc/node/services/agentDefinitions/agentDefinitionsService.ts Outdated
…ce-stable agent sort
Addresses Codex round 3:
- P1: all live durable-event writers (turn envelopes, sandbox vars
snapshots, hook context) obtain their journal from a process-shared
registry keyed by resolved session dir, so one seq counter owns each
durable-events.jsonl and interleaved producers cannot reuse or regress
sequence numbers.
- P2: the fallback turn envelope is deferred to a new
PreparedModelFallback.onStreamConstructed callback invoked by
StreamManager only after createStreamResult succeeds, so a fallback
whose stream never starts cannot supersede the primary envelope.
- P2: agent listing sorts same-ID duplicates as one group keyed by the
winning definition's display name, preserving precedence order within
groups for composition shadowing attribution.
@ThomasK33

Copy link
Copy Markdown
MemberAuthor

@codex review

Addressed all three round-3 findings in 81d507b:

  • Shared journal (P1): added a process-wide sharedDurableEventJournal(sessionDir) registry (keyed by resolved path); aiService's envelope writer and SandboxHostService's snapshot writer (all sites, including discardScope) now obtain the same instance, so one seq counter owns each durable-events.jsonl. Covered by an interleaved-writers test asserting identity + strictly increasing seq.
  • Fallback envelope timing (P2): the superseding envelope moved out of prepare() into a new PreparedModelFallback.onStreamConstructed callback that StreamManager invokes only after createStreamResult succeeds (before consumption). Tested both branches: called exactly once on success, never when construction throws.
  • Agent sort precedence (P2): discoverAgentDefinitions now sorts same-ID duplicates as one group keyed by the winning (first-discovered) definition's display name, so composition shadowing attribution can't flip when a lower-precedence duplicate sorts earlier alphabetically. Test uses Zulu(project)/Alpha(global) to force the previously-broken ordering.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:81d507b143

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadsrc/node/services/aiService.ts
Comment threadsrc/node/services/agentPlugins/hookService.ts Outdated
@ThomasK33

Copy link
Copy Markdown
MemberAuthor

@codex review

Addressed the round-8 finding in the latest push:

  • Close the post-quiescence race (P2): the fix now lives where pending is finally consumed. prepareStep at step 0, after applyPendingThinkingOverride, invokes a new rebuildFirstStepForThinkingLevel closure (threaded through StreamRequestConfig; primary and fallback variants each bound to their own build inputs) that re-runs prepareProviderRequestMessages + prepareMessagesForProvider under the applied level and emits a superseding turn envelope; prepareStep returns the rebuilt messages (with the same per-step strip/extract transforms) alongside the rebuilt provider options. Since replay pairing and cache-audit take the last envelope per requestHistorySequence, the superseding row makes wire, envelope, and replay agree even for writes racing startStream's awaits. Rebuild failures fail open to the options-only behavior with a warning.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:e69e75495a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadsrc/node/services/streamManager.ts
@ThomasK33

Copy link
Copy Markdown
MemberAuthor

@codex review

Addressed the P2: the step-0 thinking rebuild in prepareStep now re-prepends the construction-time cached system row (request.messages[0] when request.system is undefined) before applying the per-step transforms, so Anthropic-cache requests keep their system prompt. Covers the fallback path too since it flows through the same prepareStep. Added a regression test (step-0 message rebuild preserves the cached system row when the system prompt lives in messages).

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:6fa68015c3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadsrc/node/services/streamManager.ts
@ThomasK33

Copy link
Copy Markdown
MemberAuthor

@codex review

Addressed the P2: after the step-0 message rebuild, prepareStep now re-invokes request.onStepMessages with the rebuilt transcript so consumers (advisor transcript ref in aiService.ts) track the messages actually sent, not the pre-rebuild batch. Regression test extended to assert the last onStepMessages batch equals the rebuilt step-0 messages.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:413c5e0282

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadsrc/node/services/replay/replayVerify.ts
Comment threadsrc/browser/features/ChatInput/index.tsx
Comment threadsrc/cli/debug/plugins.ts
…gin skills on experiment toggle; resolve real runtime in debug plugins CLI
@ThomasK33

Copy link
Copy Markdown
MemberAuthor

@codex review

Addressed all three round-11 findings:

  • P1 (partial fallback replay): refusal-fallback envelopes now blob-store the partial continuation (partialContinuationHash on the turn-envelope row); replay-verify loads the blob and appends it via the same replaceOrAppendMessageById production uses, so partial-output fallbacks rebuild byte-identically. Covered by a new envelope round-trip test and an end-to-end replayVerifySession test.
  • P2 (plugin skill suggestions): the agent-skills loading effect in ChatInput now depends on the reactive agent-plugins experiment value, so toggling refetches /skill suggestions like it reloads plugin commands.
  • P2 (debug plugins CLI): bun run debug plugins now loads workspace metadata and mirrors the oRPC composition endpoint's runtime + host-checkout resolution (createRuntimeContextForWorkspace, resolveWorkspaceRootPath, resolveAgentPluginsMcpContext) with SSH/Docker off-host gating; multi-project workspaces report as unsupported instead of scanning the wrong host path.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:881c5c51d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadsrc/cli/debug/plugins.ts Outdated
@ThomasK33

Copy link
Copy Markdown
MemberAuthor

@codex review

Addressed the P2: buildWorkspaceComposition now takes the nullable hostCheckoutRoot explicitly. When null (SSH/Docker), project/global plugin-container discovery is suppressed entirely — discoverWorkspaceAgentPlugins is not invoked with the remote workspacePath, and the null root also authoritatively gates includeAgentPlugins for the runtime-based skill/agent/workflow loaders (belt and braces beyond their internal RemoteRuntime class check). Both callers (oRPC composition endpoint and the debug CLI) pass the resolved nullable root. New regression test: a host-side plugin at the workspace path is NOT reported when hostCheckoutRoot is null.

Note: the Test / Integration failure on the previous run was tests/ui/review/undo.test.ts — untouched by this branch (last modified in PR #3061) and part of the same environment-load flake class diagnosed earlier (reproduced on origin/main in the same environment). This push supersedes that run.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:32c64c7e2b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadsrc/node/orpc/router.ts Outdated
Comment threadsrc/node/services/agentPlugins/composition.ts Outdated
Comment threadsrc/node/services/agentPlugins/hookService.ts Outdated
…ailed hook plugins without rebuilding healthy mounts
@ThomasK33

Copy link
Copy Markdown
MemberAuthor

@codex review

Addressed all three round-13 findings:

  • P2 (execution path): the composition endpoint and debug CLI now pass the workspace execution path as workspacePath (production loaders discover from there, including subProjectPath subdirectories); hostCheckoutRoot stays a separate nullable arg reserved for plugin-container discovery.
  • P2 (shell hooks via runtime): collectShellHookEntries now resolves both layers through the workspace Runtime (runtime.stat + runtime.resolvePath("~"), joined with hooks.ts's exported joinPathLike), mirroring production hooks.ts resolution instead of host fs.stat/os.homedir().
  • P2 (preserve healthy mounts): WorkspaceHookRegistration now stores the FULL discovery fingerprint plus a failedLines set. On an unchanged fingerprint, only previously-failed candidates are retried (per-candidate loadCandidateLocked) — healthy siblings keep their persistent mounts and cross-turn guest state; no teardownLocked churn. New regression test asserts the healthy plugin's mount is neither re-created nor dropped while the failed sibling retries and recovers.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

https://github.com/coder/mux/blob/d877e9a91be2f652b8d02d423db00413c4ef866b/src/node/services/aiService.ts#L3696-L3700
P1 Badge Requeue correlation metadata for fallback runs

Fresh evidence after the fallback-envelope fix: when the primary request has reached DevTools, createRun() has already consumed and deleted this pending metadata entry, so copying the same metadata ID into the fallback headers does not attach requestHistorySequence to the fallback model's new DevTools run. pairSessionTurns() consequently selects the keyed primary run while selecting the final fallback envelope, making every successful refusal fallback compare the fallback identity and transcript against the primary wire request. Queue a fresh metadata entry for each fallback run before constructing it.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadsrc/node/services/streamManager.ts
Comment threadsrc/node/services/agentPlugins/composition.ts
@ThomasK33

Copy link
Copy Markdown
MemberAuthor

@codex review

Addressed both round-14 findings:

  • P1 (abort race after envelope write): after await onStreamConstructed?.(), startStream now re-checks the abort signal AND registration ownership before launching processStreamWithCleanup(). A canceled stream bails out (no post-abort stream-start), deletes the registration only if it still owns it (a replacement stream's slot is preserved), and resets streamRegistered so the finally block releases the never-processed stream's model/temp-dir resources. Regression test drives a real startStream with stopStream + a replacement registration inside the callback — verified it fails without the guard (stream-start emitted, replacement deleted) and passes with it.
  • P2 (nested plugin scan roots): the inspector's skill discovery now anchors plugin containers at the checkout root with checkout-rooted containment, exactly mirroring production (skillStorageContext.buildProjectLocalRoots anchors plugin skill containers at checkoutRoot ?? projectRoot). New test: a checkout-level plugin's skills are reported when the workspace executes in a subdirectory. Agents and workflows intentionally keep execution-path defaults: production derives their plugin containers from the discovery path (streamContextBuildergetDefaultAgentDefinitionsRoots(runtime, agentDiscoveryPath), workflows.listScripts → default roots from workspacePath) with projectContainmentRoot: workspacePath — checkout-anchoring them in the inspector would report plugin agents/workflows that production never loads, the same faithfulness bug class as the earlier off-host finding. Aligning production's agent/workflow plugin anchoring with skills is a candidate follow-up outside this PR's scope; the inspector documents the split inline.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit:d4c2289c61

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The round-5 construction-gating change moved primary turn-envelope
emission into startStream's onStreamConstructed callback, but the
aiService test stub replaced startStream without invoking it, so the
turn-envelope tests asserted an empty journal. Mirror the real
contract: a successful stub construction invokes the callback.
@ThomasK33

Copy link
Copy Markdown
MemberAuthor

@codex review

Fixed the Test / Unit CI failure: the round-5 construction-gating change moved primary turn-envelope emission into startStream's onStreamConstructed callback, but the aiService test stub replaced startStream without invoking it — the turn-envelope test asserted an empty journal. The stub now mirrors the real contract (invokes the callback on successful construction, typed via Parameters<StreamManager["startStream"]>). All 101 aiService tests pass locally; no production code changed.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit:dc34a223a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33
ThomasK33 added this pull request to the merge queueAug 19, 2026
Merged via the queue into main with commit dc8ffd1Aug 19, 2026
21 of 22 checks passed
@ThomasK33
ThomasK33 deleted the plugin-architecture-5p67 branch August 19, 2026 08:39
@mux-botmux-botBot mentioned this pull request Aug 19, 2026
yermakoffivan pushed a commit to yermakoffivan/mux that referenced this pull request Aug 24, 2026
… kernel, context isolation, and continual-harness features (coder#3900)
## Summary
Adds **RLM Mode** — an opt-in, kernel-first execution posture for PTC
inspired by PrimeIntellect's prime-agent architecture — plus the
continual-harness features around it (refinement journal with rollback,
`/refine` trajectory distillation, family messaging, branch
summarization, compaction improvements) and a measurement harness (`shux
rlm-eval`) that every major design decision in this PR was validated
against.
With the RLM experiment **off, behavior is byte-identical to main**
(pinned by composition tests and `replay-verify` on live sessions). With
it on, `code_execution` becomes the primary tool backed by a persistent
per-workspace QuickJS kernel.
## Background
Research into prime-agent (which posted strong vendor-reported eval
results) identified two core ideas worth porting: a single persistent
code kernel where in-kernel data never transits model context, and a
self-modifying harness with journaled, reversible edits. Mux's Track 1
foundation (journal kit, durable events, sandbox host, replay harness —
coder#3865/coder#3872) provided the substrate; this PR is "Track 2" built on it,
implemented via the phased conductor workflow in
`workflows/track2-rlm-implementation.js` (per-phase quality gates,
adversarial review, live dogfooding).
## Implementation
**RLM kernel (phases r1, r4, r5, r12):**
- `rlm-mode` experiment, nested under PTC; **exclusive-only** — enabling
it forces the kernel-first narrowed toolset (supplement-mode RLM
measured ~2x flat cost and was removed)
- Persistent per-workspace mount: guest `vars` survives
calls/turns/restarts via journaled snapshots
- **Kernel context isolation**: nested `shux.*` results never enter
model context (compact `{tool, ok, bytes}` summaries); the model's
channels are its return value (offloaded via handles >16KB), capped
console output, and `vars`
- `shux.load({path, key})`: host-side bulk file ingestion straight into
`vars` (record shows `{key, bytes, lines, preview}` only)
- `shux.task_spawn` + `shux.events()`: fire-and-forget sub-agents with
admission handles, asyncify-safe event drain
- Batching guidance baked into the kernel-first preamble ("write
complete programs")
**Continual harness (r2, r6, r11):**
- Every memory/skill mutation journals an invertible `refinement`
durable event (blob-backed inverses)
- Rollback engine with `rollbackOf` lineage: `shux run debug
refinements` CLI + RLM-gated `refinement_rollback` tool
- `/refine`: bounded trajectory-distillation pass (dream-agent
machinery) applying smallest evidence-backed edits, journaled and
reversible
**Agent ops (r3, r7, r8, r9):**
- Nuclear-family messaging: `task_message_parent` /
`task_message_sibling` (RLM stamped on task records at spawn; strict
same-parent scoping; server-side labels)
- RLM-gated compaction keep-recent floor + cumulative read-file tracking
- Branch summarization on fork/edit-resend (background generation,
tail-guarded append)
- `scripts/gate_fingerprint.sh` verification-loop memoizer
**Measurement (`scripts/rlm-eval/`, `make rlm-eval`):** scenario x
config x seed A/B runner extracting mechanical metrics (tokens, cost,
wall time, peak context, vars adoption, batch factor, compactions) from
session artifacts.
## Validation
- Key measured results (sonnet-5 / opus-5 / gpt-5.6-sol; fable-5 at
medium):
- Context isolation: 504KB file load -> **867 bytes model-visible
(0.17%)**; pre-fix the same task leaked 610KB into context and cost 10x
flat tools
- RLM-exclusive vs flat tools: **-30 to -63% cost in 7/8 model x
scenario pairs**, faster in 6/8, all cells correct; organic `vars`
adoption 15/16
- Batching preamble (cross-build A/B): sonnet organic batch factor 2.7
-> 3.5 (3/4 seeds fold all 6 loads into one eval, -42% tokens)
- Every phase passed an independent gate run + adversarial review + live
dev-server-sandbox dogfood with `replay-verify` PASS (evidence in the
workflow run reports)
- Post-rebase onto the Shux rename: full static-check green; kernel
suites (code_execution 50, toolBridge/typeGenerator 43, toolAssembly 14,
sandboxHost 25) green; kernel surfaces adopt shux-primary naming with
the `mux.*` alias intact
## Risks
- **RLM-off regression risk is the headline concern and is heavily
defended**: composition tests pin byte-identity per flag combination,
and replay-verify was run on live RLM-off control sessions at each
phase. Highest-traffic shared code touched: `toolAssembly`,
`code_execution`, compaction paths (RLM-gated), task spawn paths (flag
stamping).
- RLM-on surfaces are experimental by declaration; known rough edges:
peak per-request context is higher when `shux.load` materializes large
files (latent pressure on multi-MB corpora), and one sonnet seed still
fragments batching.
- `/refine` auto-applies edits (no approval UI in v1) — mitigated by
journal + rollback + immutable-base guard rails.
## Pains
- The mid-series `mux` -> `shux` rename on main required conflict
resolution across the kernel commits (namespace, type-generator
identifiers, description text).
- Sub-agent dogfooding infrastructure failures (background-monitor
wakes, uncommitted-work timeouts, transient gateway model errors) shaped
several workflow-hardening commits.
---
_Generated with `mux` • Model: `anthropic:claude-fable-5` • Thinking:
`xhigh` • Cost: `$763.80`_
<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh
costs=763.80 -->
---------
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ThomasK33