π€ feat: RLM Mode β kernel-first exclusive PTC posture with persistent kernel, context isolation, and continual-harness features - #3900
π€ feat: RLM Mode β kernel-first exclusive PTC posture with persistent kernel, context isolation, and continual-harness features#3900ThomasK33 wants to merge 167 commits into
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
π‘ Tip: Enable Workflows to automatically generate PRs for you. |
|
@codex review |
β¦inement journal, RLM mode experiment) Conductor for implementing prime-agent-inspired RLM/continual-harness features behind an opt-in RLM sub-experiment of PTC. Mirrors workflows/track1-implementation.js: per-phase implement -> gate+adversarial-review -> fix rounds -> dogfood.
β¦ code_execution RLM Mode is an opt-in sub-experiment of Programmatic Tool Calling (flat flag, gated on the PTC parent at call sites, nested under the PTC toggle in Settings, mirroring the Memory Hot Set precedent). When enabled with PTC and sandbox context, code_execution runs on the persistent per-workspace kernel mount: the guest vars namespace survives across calls/turns and restarts via snapshots, and the tool description advertises those kernel semantics. MUX_SANDBOX_PERSISTENT_MOUNTS=1 remains a dev/test override with unchanged behavior. With the experiment off (and env unset) behavior is byte-identical to before: fresh runtime per call and today's description. The rlm flag plumbs through the experiments path end to end: ExperimentsSchema (send options) -> aiService.streamMessage -> applyToolPolicyAndExperiments. Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦verified curl recipe) and note r1 landed
Every mutating memory command (create/str_replace/insert/delete/rename) and
every agent_skill_write/agent_skill_delete now appends exactly one
'refinement' durable event to the acting workspace's session journal
(sharedDurableEventJournal), carrying an inverse payload that byte-exactly
restores the prior file state. Prior contents over 4KB are offloaded to the
session blob store (BlobRef), mirroring hook-context. Evidence records
{workspaceId, toolName, toolCallId?, actor?}.
Always-on and purely additive: journaling failures never fail the tool
(log.debug + continue), read-only ops and failed mutations write no rows.
Cross-workspace caveat (v1): memory/skill files are global/project-scoped
while the journal is per-session; rows land in the acting workspace's log.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Standalone, always-on-by-usage gate memoizer: 'fingerprint' hashes HEAD sha + 'git diff HEAD' + sorted untracked-not-ignored files with content hashes; 'record <gate> <pass|fail>' and 'check <gate>' store/look up results in a JSON file inside the worktree-local git dir (git rev-parse --git-path), so records are never committed and never invalidate themselves. wait_pr_ready.sh integration was skipped intentionally: it has no local validation step (it only orchestrates remote Codex/review/CI gates), per the phase brief's conditional. Tests spawn the real script against hermetic temp git repos and cover stability, pass/fail round-trip, tracked-edit / untracked-file / staged-change invalidation, and corrupt-store self-healing. Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦-handle events
Under an RLM persistent mount, nested mux.* results and code_execution
return values whose JSON serialization exceeds 16KB stop entering the
model context: the model-visible record becomes {handle, preview, size}
(plus a follow-up hint for return values) while the full value stays in
the guest at vars.__hN (monotonic per scope via vars.__handleSeq, so it
snapshots/restores with vars), in the content-addressed blob store, and
in one result-handle durable event whose preview mirrors the
model-visible string exactly. Handle bytes retained in vars are capped
with oldest-first eviction (never the newest handle); the blob remains
the durable copy. RLM off / ephemeral runtimes are byte-identical to
today.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦escription) Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦ervice finalizeAgentTaskReport now invokes sandboxHostService.postTaskTerminalEvent (fire-and-forget, gated on no foreground waiters) so spawned-task completions reach the guest host-event queue in production β previously the hook had zero production callsites and mux.events() always drained empty. Regression tests cover both the posted-event and waiter-suppression branches. Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦h lineage listRefinements/rollbackRefinement make the r2 journal actionable: rollbacks apply the recorded inverse (inline or blob-backed) through atomic writes, journal their own refinement row with rollbackOf (so double inversion works), refuse already-rolled-back targets, refuse divergence (later overlapping rows, deleted/recreated files, content drift for rollback rows) unless forced, and confine every touched path to memory scope roots / skill directories with lexical + symlink escape checks that force can never override. Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦orce Signed-off-by: Thomas Kosiewski <tk@coder.com>
Assembled in toolAssembly from the sandbox context inside the PTC branch, so the tool only exists when RLM mode is on (nested under the PTC parent); with the experiment off the toolset β and thus every provider request β stays byte-identical. Force stays CLI-only: divergence overrides are a human call. Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦refinement_rollback Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦ memory to current session - P1: the later-rows divergence check now nets out rollback lineage: rows whose effect was itself rolled back are skipped, and live rollback chains conflict only when their parity re-applies an edit or rewinds past the target β so LIFO multi-edit unrolling works for model tool calls without force, while re-applied edits (rollback-of-rollback) still refuse. - P2: workspace-scope memory confinement resolves strictly to the current session's memory root (<sessionDir>/memory) instead of any session subdir under sessionsDir, closing the cross-workspace write leak. Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦tCode on undefined assignment) Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦action) Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦oundary copies, read-file tracking Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦rendering Signed-off-by: Thomas Kosiewski <tk@coder.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦est metadata, optional chain Signed-off-by: Thomas Kosiewski <tk@coder.com>
β¦tch + staleness guard) Signed-off-by: Thomas Kosiewski <tk@coder.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
validateMutation accepted a create whenever the target path was free, but the real create() also rejects when the scope already holds MEMORY_MAX_FILES_PER_SCOPE files β a create into a full scope staged, rendered approvable, then apply rejected it and consumed the set (Codex round 20). The validator now runs the same listFiles()-based count check with the real error string; listFiles tolerates a missing root by listing empty, so validation still never materializes scope roots.
Family-message rows interpolated the sender's FULL title into the persisted assistant row while the pair/receiver quotas charged only message.trim().length β spawn/retitle impose no title cap, so an attacker-influenced huge title added unbounded uncharged bytes to every send, breaking the 256KiB/1MiB transcript ceilings on both routes (Codex round 20). Both fixes, both routes: the attribution title is capped at TASK_FAMILY_MESSAGE_MAX_TITLE_CHARS (sanity bound), and budgets now charge the COMPLETE rendered payload (label + framing + message) so accounting is exact β the rendered bytes persisted into a transcript can provably never exceed the ceilings. Budget tests updated to the stricter invariant (rendered totals <= ceiling; refusal fires before the raw-chars quotient); smaller sends may still fit residual headroom by design.
|
@codex review Round 20 fixes pushed (head ee8bd16):
|
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee8bd16bfc
βΉοΈ 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".
Codex round 21 (A): round 20 charged the COMPLETE rendered payload row, but every successful send ALSO durably logs a fixed user-role trigger row β the child->parent route via wakeParentWorkspaceWithSyntheticMessage and the sibling route via sendMessageToDescendantAgentTask (which renders label + framing + trigger into one row). Reservations charged only payloadContent.length, so repeated sends still exceeded the documented 256KiB pair / 1MiB receiver ceilings by the accumulated trigger overhead: max-size sends hide it in per-send headroom, but ~2KiB sends admit enough repetitions to persist ~107% of the ceiling. Both routes now build the trigger content BEFORE the reservation (mirroring round 20's payloadContent pattern) and charge payload + trigger rendered lengths. The sibling route charges the RENDERED labeled trigger via a shared renderLabeledTaskMessage helper also used by sendMessageToDescendantAgentTask's persistence, so the charged length can never drift from the delivered bytes. Tests: the round-20 exactness test now sums trigger bytes (observed at the sendMessage boundary) into the airtight ceiling assertion, and a new mid-size test covers the regime where the bug actually manifests. Red-checked via sed toggle: with payload-only charging restored, the mid-size test exceeds the ceiling (max-size passes both ways by design β headroom absorbs its triggers; documented in the test).
Codex round 21 (B): validateMutation's rename branch accepted a
directory source with new_path inside its own subtree
('notes' -> 'notes/archive/notes') β the source exists and the exact
destination doesn't, so the proposal staged and rendered approvable,
then the filesystem rejected moving a dir into itself at apply,
consuming the approved staged set. The real rename handler was worse
than a late clean failure: store.rename mkdirs the destination PARENT
before the rename call, creating 'notes/archive/' INSIDE the source
before the EINVAL β polluting the tree it failed to move.
A shared assertRenameDestinationOutsideDirSource guard now refuses
destinations equal to or descending from a directory source,
path-SEGMENT-aware on the normalized relPaths ('notes-x' does not
match 'notes'); file sources are exempt (a file cannot contain its
destination, and dest===source is already refused by the
destination-exists check). Mirrored verbatim in validateMutation and
the real rename handler per the round-19/20 zero-drift doctrine.
Red-checked: pre-fix the staging test staged the own-subtree rename
(no 'inside itself' error) and the handler test left 'archive/'
pollution inside the source; post-fix both refuse cleanly, the source
keeps exactly its original entries, and the segment-aware sibling
rename ('notes' -> 'notes-x') still succeeds on both paths.
β¦ding Codex round 21 (C): the summary accumulation loop appended each text delta IN FULL before checking BRANCH_SUMMARY_MAX_ACCUMULATED_CHARS β a provider ignoring maxOutputTokens could emit one giant delta, retaining O(delta) host memory, and trimSummaryToBoundary then kept nearly all of it via a late sentence boundary (red-check observed a 160K persisted row against the 32K cap). Each delta is now sliced to the remaining allowance BEFORE appending (cap β accumulated), hard-bounding both the retained buffer and the persisted row regardless of delta sizing. The crossing delta still trips cappedAtLimit, so the existing no-finishReason-await semantics for capped streams are unchanged, and the multi-delta flood cap test keeps passing. Red-checked via toggle: with the old append-then-check restored, the single-giant-delta test persists ~5x the cap; with the slice, the provider-controlled summary portion stays <= the cap and salvages whole sentences.
|
@codex review Round 21 fixes pushed (head 09e23e2):
|
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 09e23e2958
βΉοΈ 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".
There was a problem hiding this comment.
π‘ Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 09e23e2958
Only the user who started this review can view the report in Codex.
βΉοΈ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and youβre opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
Codex R22: xum.file_read resolves normally with {success: false} for
missing/oversized/directory paths β no thrown error β so ToolBridge
records no error, kernel compaction marked the call ok:true, and the
path was advertised in the already-read-files attachment after
compaction, making the agent skip a read it still needs. Compaction now
folds the result's success bit into ok (the compact record drops
result, so the bit must be preserved there), and extractReadFilePaths
also checks retained results on non-compacted classic-PTC records.
Red-checked: both a compacted and a non-compacted success:false read
were tracked pre-fix.
Codex R22: isRlmModeEnabled treated a defined experiments object as fully authoritative, but a renderer with no origin-local override sends a defined-but-EMPTY object (useExperimentOverrideValue sends no explicit values) β the workspace got the persistent RLM kernel (tool assembly backfills via resolveBackendGatedPtcExperiments) while edit-resend summaries, keep-recent stamps, and read-file reinjection silently stayed off. Flags now resolve per-field: explicit booleans win (rlm: false still beats machine overrides), missing fields fall back to isExperimentEnabled β the same semantics tool assembly uses. Red-checked: empty-object case returned false under the old predicate.
Codex round 22 (A): when the target is already streaming, synthetic family-message triggers batch into ONE MessageQueue entry and dequeueNext() joins them with '\n' β one separator per trigger after the first. Round 21 charged payload + rendered trigger but not those joining newlines, so a sender picking payload lengths that consume the chars budget EXACTLY made the eventual durable joined user row exceed the ceilings by the uncharged separators. The trigger charge (shared familyMessageTriggerCharge helper, both routes) now adds one separator length unconditionally as a SAFE UPPER BOUND: unbatched sends over-charge by one byte, which only refuses marginally earlier β never later β and keeps accounting a provable ceiling on durable bytes regardless of batching. Chosen over sealed queue entries, which would change queue semantics for all synthetic sends to fix a one-byte accounting gap. Red-checked via python toggle: with the separator charge removed, the new exactness test (probe-measured framing overhead + equal-length IDs, 32 x 8192-char rendered sends filling the 256KiB pair ceiling to the last byte) exceeds the ceiling by the 31 joined separators; with the charge, delivery refuses one send earlier and worst-case joined bytes stay <= the ceiling.
Codex round 22 (B): the r21 guard compared path segments case-sensitively, so on a case-insensitive filesystem 'Notes' -> 'notes/archive/notes' resolved to the same source directory and bypassed it β reproducing the mkdir pollution in staging AND real execution. Any string comparison also misses in-root symlink aliases of the source on every filesystem. The shared guard keeps the lexical segment comparison as a cheap first layer and adds a physical-identity layer: every EXISTING ancestor of the destination is stat'ed (following symlinks) and the rename refused when the ancestor IS the source directory (same dev+ino). Case variants and symlink aliases both resolve to the source's physical identity regardless of spelling, so one mechanism covers both β no case- sensitivity detection or platform-conditional normalization needed, and no over-refusal of legitimate renames on case-sensitive filesystems (identity either matches or it doesn't). Missing ancestors are skipped (a nonexistent path cannot be the live source dir). Validator and handler stay mirrored through the one shared async guard β both already have store/filesystem access, so their techniques do not diverge. Test limitation (documented in-test): CI filesystems are case- sensitive, so the case-fold bypass cannot be reproduced literally; the tests alias the source via an in-root SYMLINK, which exercises the exact same resolution path the fix checks (an ancestor spelled unlike the source that stats to its identity) and is itself a real bypass of any lexical guard. Red-probed: with the physical-identity layer disabled, both alias tests fail (staging staged; the handler polluted) while the lexical r21 tests stay green; enabled, all four pass.
Codex round 22 (C): offloadValue's JSON.stringify catch returned null,
leaving non-JSON returns entirely inline β bypassing the round-14
handle-offload and retention tiers and then breaking HistoryService's
plain stringify at persistence. Same class as the round-17 console
BigInt fix: serialization failure means the value is unretainable and
unmeasurable (it can hide an arbitrarily large sibling payload), so it
must never stay inline.
The catch now returns the round-4/14 bounded {truncated: true, note}
record with a note saying the value was unserializable and how to
proceed (no raw error text; no preview either β rendering one would
require the very serialization that just failed, and String() can
itself explode on large arrays). Values that serialize to undefined
(bare function/symbol) still stay inline: no bytes reach the
transcript, so there is nothing to bound.
Reachability note (documented in the test): on this runtime's dump
implementation, objects containing BigInts collapse to
'[object Object]' and arrays to a String() join β both flow through
the normal offload tiers β so the vector that actually reaches the
stringify catch is a bare BigInt return, which survives dump as a real
BigInt (r17) and broke persistence pre-fix.
Red-checked via toggle: with the old 'return null' restored, the bare
BigInt return stays inline (no truncated record; persistence stringify
would throw); with the fix, the record is bounded, persistable, and
carries the unserializable note.
|
@codex review Round 22 fixes pushed (head 1e45093):
|
|
Security review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. βΉοΈ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1e4509307c
βΉοΈ 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".
|
|
||
| for (const path of [...incoming, ...existing]) { | ||
| if (typeof path !== "string") continue; | ||
| const trimmed = path.trim(); |
There was a problem hiding this comment.
Preserve whitespace when merging read paths
When a successfully read filename has leading or trailing whitespace, extractReadFilePaths correctly preserves that whitespace, but the next compaction passes it through this merge and silently trims it. The already-read attachment then advertises a different file (and may deduplicate two distinct filenames), causing the agent to believe it read content it never accessed. Keep the original nonempty path unchanged when merging.
Useful? React with πΒ / π.
| // Appended BEFORE the trigger send so the triggered turn's request (which | ||
| // may start streaming in the background immediately, or dispatch later | ||
| // from the queue) always sees the payload in history. | ||
| const appendResult = await this.historyService.appendToHistory(parentWorkspaceId, payloadRow); |
There was a problem hiding this comment.
Serialize each family payload with its notification
When two children send family messages to the same parent concurrently, one sender can append its payload here, the other can append its payload, and only then can the first sender append its fixed trigger. That trigger says its payload is in the βpreceding assistant message,β but the preceding row now belongs to the other child, so the receiving agent can associate the notification with the wrong update; the sibling route has the same split append/send sequence. Keep each payload and trigger ordered as one target-scoped delivery operation.
AGENTS.md reference: AGENTS.md:L150-L150
Useful? React with πΒ / π.
| // promise must not block the fork/edit path behind the deadline. | ||
| const settled = await Promise.race([ | ||
| Promise.all([stream.usage, stream.providerMetadata]), | ||
| new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), 2000)), |
There was a problem hiding this comment.
Keep usage collection inside the summary deadline
When the text stream finishes shortly before the shared branch-summary deadline but stream.usage or stream.providerMetadata settles slowly, this newly started two-second timer is not raced against the existing deadline. The synchronous edit-resend path can therefore block for nearly two seconds beyond BRANCH_SUMMARY_TIMEOUT_MS (and the subsequent awaited recordUsage is also unbounded), despite the constant being documented as a hard wall-clock cap for the user-facing operation. Bound both telemetry waits by the remaining shared deadline.
Useful? React with πΒ / π.
Codex round 23 (security): getSideChannelModelCandidates tried Anthropic Haiku and OpenAI GPT Mini BEFORE the workspace's configured models, so abandoned-branch / edit-resend summaries could ship up to 160,000 chars of user + repo-derived history to a third-party provider even when the workspace deliberately used a local/private route. Candidates now derive STRICTLY from workspace settings: (1) the workspace's current model, (2) known cheap models (the old preferred list) filtered to providers the workspace already uses β a cost fallback with zero new data exposure, (3) the workspace's per-agent models (equally user-consented routes), and nothing else. Cross- provider fallbacks are gone rather than gated behind a consent flow: summaries are best-effort, and the empty-candidates / failed-generation paths already degrade to no summary. No workspace metadata now also means NO candidates (the provider set is unknown), where it previously meant third-party-only candidates. The pinned pricing-identity call (createModelWithPinnedMetadata) and usage-recording semantics are untouched β only the candidate list changes. refineRunner/refineService do NOT share this helper (the /refine pass model comes from resolveDreamModelString's workspace-scoped dream cascade), so this fix is confined to branchSummary.ts. Tests: harness now serves workspace metadata (candidates require it); new provider-confinement suite asserts a workspace on provider X never yields provider-Y candidates, cheap same-provider siblings follow the current model, and the no-metadata path degrades cleanly end-to-end. Red-probed via python toggle: with the old third-party-first builder restored, all three confinement tests fail (Haiku leads for an ollama workspace); with the fix, all 39 suite tests pass.
|
@codex review Round 23 fix pushed (head d640867):
|
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d6408677cb
βΉοΈ 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".
| const workspaceModels = [ | ||
| metadataResult.data.aiSettings?.model, | ||
| ...Object.values(metadataResult.data.aiSettingsByAgent ?? {}).map((settings) => settings.model), |
There was a problem hiding this comment.
Prefer the selected agent's model before legacy settings
When a workspace has both legacy aiSettings and newer per-agent settings, this treats the legacy model as current; when aiSettings is absent, Object.values() instead picks whichever agent was inserted first. Because generation returns after the first successful candidate, fork/edit-resend summaries can be sent through a stale or non-selected provider rather than the model for metadataResult.data.agentId. Fresh evidence beyond the prior provider-confinement finding is that WorkspaceService.updateAgentAISettings updates only aiSettingsByAgent[agentId] and persists the selected agentId, leaving legacy aiSettings unchanged. Resolve the selected agent entry first and retain aiSettings only as a compatibility fallback.
Useful? React with πΒ / π.
| }) | ||
| .strict(); | ||
|
|
||
| export const TaskMessageParentToolResultSchema = z.discriminatedUnion("status", [ |
There was a problem hiding this comment.
Register family-message results for kernel type generation
When RLM family messaging is enabled, both new tools have executable handlers and are therefore bridged by ToolBridge, but neither tool was added to BridgeableToolName or RESULT_SCHEMAS. Consequently generateXumTypes() declares xum.task_message_parent and xum.task_message_sibling as returning unknown, hiding their sent/accepted/queued/error discriminants from the kernel-first model precisely where these tools are intended to be used. Add both result schemas to the bridgeable result registry.
Useful? React with πΒ / π.
| if (workspaceModels.length === 0) { | ||
| return []; |
There was a problem hiding this comment.
Preserve model settings before starting fork summaries
For every normal fork, WorkspaceService.fork() constructs the target metadata without aiSettings, aiSettingsByAgent, or agentId, then immediately starts the background branch summary. The target's first send cannot populate those settings in time because AgentSession.sendMessage() waits for this pending summary before persisting the send options. Therefore workspaceModels is always empty on the fork path and this return makes the advertised abandoned-branch summary silently no-op without attempting any model. Inherit the source workspace's model settings for the fork or pass a source-model snapshot into the background summary.
Useful? React with πΒ / π.
There was a problem hiding this comment.
π‘ Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: d6408677cb
Only the user who started this review can view the report in Codex.
βΉοΈ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and youβre opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
| for (const model of NAME_GEN_PREFERRED_MODELS) { | ||
| if (allowedProviders.has(modelProvider(model))) push(model); |
There was a problem hiding this comment.
Security: Confine summary fallbacks to the selected effective route
When RLM/PTC is enabled, both Coder and direct Anthropic are configured, and the selected summary attempt fails or returns empty, keep the fallback on the selected effective route. Fresh evidence in this fix is that prefix filtering still adds Haiku; routing is per model, so if the selected model is in Coder's catalog but Haiku is not, createModelWithPinnedMetadata falls back to direct Anthropic and sends up to 160K characters of abandoned user/repo history there. Selected-model-first ordering and policy checks mitigate this but do not preserve the route boundary. Compare effective route identities or use only exact configured models.
Useful? React with πΒ / π.
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d6408677cb
βΉοΈ 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".
| attempted.add(edit.toolCallId); | ||
| try { | ||
| await saveStagedRefineSet(sessionDir, { |
There was a problem hiding this comment.
Persist edit completion atomically with the mutation
If the process exits after tool.execute completes but before this progress rewrite finishes, the staged file still lacks the tool-call ID, so the next /refine apply replays the edit; an insert can therefore be duplicated despite the crash-idempotency contract. Fresh evidence in the current fix is that it records attempted only after execution rather than reconciling the already-written refinement row before replay. Persist an execution intent/completion transaction or recover completed IDs from the journal before invoking the tool again.
AGENTS.md reference: AGENTS.md:L108-L110
Useful? React with πΒ / π.
| // Unmeasurable (guest mutated a handle into a cycle) counts as 0; | ||
| // snapshotVars is where cycles crash-fast. | ||
| try { | ||
| bytes = JSON.stringify(vars[k]).length; |
There was a problem hiding this comment.
Measure retained vars in UTF-8 bytes
When handles or mux.load values contain multibyte text, JSON.stringify(...).length counts UTF-16 code units rather than the byte budget advertised by RESULT_HANDLE_VARS_CAP_BYTES. For example, several Japanese-text handles can be counted below 4 MiB while their persisted snapshot exceeds the 8 MiB hard limit, causing persistVars() to fail, dispose the kernel, and discard the current call's state instead of evicting old entries. Use UTF-8 byte lengths consistently for both the new value and retained entries.
Useful? React with πΒ / π.
| const { ref, size } = await this.blobs.put(content); | ||
| // Ownership re-check between put and append (round 11 defense in | ||
| // depth): if this holder was wrongfully displaced, a reclaimer may | ||
| // have deleted the just-put blob β appending would then create a row | ||
| // permanently referencing a missing payload. Abort instead (an | ||
| // unreferenced orphan blob is harmless). | ||
| await this.assertBlobLockOwned(); | ||
| const event = await this.append(buildDraft(ref, size)); |
There was a problem hiding this comment.
Reclaim blobs left behind before journal publication
If the process exits, ownership verification fails, or append() throws after this put(), the content-addressed blob remains on disk without any journal mention. All reclamation passes derive candidates from journal references, so these orphans are never discovered; repeated crashes or publication failures across snapshots, result handles, and refinement inverses can therefore grow a live workspace's session directory outside the configured quotas. Remove the blob on pre-append failures when it is safe, and add a startup mark-and-sweep for crash leftovers.
Useful? React with πΒ / π.
| "<abandoned_branch>", | ||
| transcript, | ||
| "</abandoned_branch>", |
There was a problem hiding this comment.
Keep transcript delimiters out of untrusted branch text
When an abandoned user or assistant message contains </abandoned_branch>, it closes this delimiter and places the remaining attacker-influenced text at the same prompt level as the summarization instructions. Because this side-channel call supplies no higher-priority system instruction, a repo-derived assistant message can direct the summarizer to emit misleading persistent context that survives the rewind as a branch-summary row. Encode or escape delimiter characters in the transcript and put the instruction/data trust boundary in a system message.
Useful? React with πΒ / π.
Parallel tool executions pushed staged edits in completion order, which is nondeterministic; order-dependent sequences (create then str_replace) could stage and apply out of order. Record each tool call's stream emission index and re-sort staged edits into the model's proposal order.
|
@codex review |
|
Security review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. βΉοΈ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 65312fcb30
βΉοΈ 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".
| const size = Buffer.byteLength(serialized, "utf8"); | ||
| if (size <= KERNEL_COMPACT_ARGS_CAP_BYTES) return args; | ||
| return { | ||
| argsPreview: `${serialized.slice(0, KERNEL_COMPACT_ARGS_CAP_BYTES)}β¦[${size} bytes total; truncated]`, |
There was a problem hiding this comment.
Truncate kernel previews by UTF-8 bytes
When guest-supplied arguments contain multibyte text, size is measured in UTF-8 bytes but this preview is sliced by UTF-16 code units, so a nominal 2 KiB preview can retain up to roughly 8 KiB and bypass the documented model-context bound. The same mismatch appears in boundCompactRecordError and the crossing-record path of capKernelConsoleOutput; truncate encoded bytes while preserving valid UTF-8 before appending the marker.
Useful? React with πΒ / π.
| const rollup = await this.sessionUsageService.rollUpUsageIntoParent( | ||
| parentWorkspaceId, | ||
| workspaceId, | ||
| childUsage.byModel, |
There was a problem hiding this comment.
Delay child rollups until deletion is committed
When a non-forced runtime deletion fails after this call, the child remains in config and can be used again, but the parent has already recorded this workspace in rolledUpFrom. Any usage accumulated before a later successful removal is then skipped by rollUpUsageIntoParent, permanently undercounting parent costs; the timing rollup immediately above has the same one-shot behavior. Perform these rollups only after deletion can no longer return early, or make them delta-based so a retained child can be rolled up again.
Useful? React with πΒ / π.
| const seqRaw = vars.__handleSeq; | ||
| // Tolerate a guest-clobbered counter (vars is guest-writable): restart | ||
| // numbering rather than crashing the offload. | ||
| const seq = (typeof seqRaw === "number" && isFinite(seqRaw) ? Math.floor(seqRaw) : 0) + 1; | ||
| vars.__handleSeq = seq; |
There was a problem hiding this comment.
Recover handle counters without reusing existing keys
When guest code clobbers persisted vars.__handleSeq with a non-number, infinity, or a number at the safe-integer limit, this fallback restarts at 1 or produces a value that no longer increments. Existing vars.__h1 or subsequent same-number handles are then overwritten, so previously returned handles silently resolve to unrelated data after the next oversized result; load-retention sequencing repeats the same calculation. Sanitize against Number.isSafeInteger and derive a collision-free sequence from retained handle/load keys instead of restarting blindly.
AGENTS.md reference: AGENTS.md:L108-L110
Useful? React with πΒ / π.
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,
/refinetrajectory 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-verifyon live sessions). With it on,code_executionbecomes 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 β #3865/#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-modeexperiment, nested under PTC; exclusive-only β enabling it forces the kernel-first narrowed toolset (supplement-mode RLM measured ~2x flat cost and was removed)varssurvives calls/turns/restarts via journaled snapshotsshux.*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, andvarsshux.load({path, key}): host-side bulk file ingestion straight intovars(record shows{key, bytes, lines, preview}only)shux.task_spawn+shux.events(): fire-and-forget sub-agents with admission handles, asyncify-safe event drainContinual harness (r2, r6, r11):
refinementdurable event (blob-backed inverses)rollbackOflineage:shux run debug refinementsCLI + RLM-gatedrefinement_rollbacktool/refine: bounded trajectory-distillation pass (dream-agent machinery) applying smallest evidence-backed edits, journaled and reversibleAgent ops (r3, r7, r8, r9):
task_message_parent/task_message_sibling(RLM stamped on task records at spawn; strict same-parent scoping; server-side labels)scripts/gate_fingerprint.shverification-loop memoizerMeasurement (
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
varsadoption 15/16replay-verifyPASS (evidence in the workflow run reports)mux.*alias intactRisks
toolAssembly,code_execution, compaction paths (RLM-gated), task spawn paths (flag stamping).shux.loadmaterializes large files (latent pressure on multi-MB corpora), and one sonnet seed still fragments batching./refineauto-applies edits (no approval UI in v1) β mitigated by journal + rollback + immutable-base guard rails.Pains
mux->shuxrename on main required conflict resolution across the kernel commits (namespace, type-generator identifiers, description text).Generated with
muxβ’ Model:anthropic:claude-fable-5β’ Thinking:xhighβ’ Cost:$763.80