Skip to content

πŸ€– feat: RLM Mode β€” kernel-first exclusive PTC posture with persistent kernel, context isolation, and continual-harness features - #3900

Open
ThomasK33 wants to merge 167 commits into
mainfrom
research-qr9r
Open

πŸ€– feat: RLM Mode β€” kernel-first exclusive PTC posture with persistent kernel, context isolation, and continual-harness features#3900
ThomasK33 wants to merge 167 commits into
mainfrom
research-qr9r

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

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 β€” #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-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

@mintlify

mintlify Bot commented Aug 20, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Mux 🟒 Ready View Preview Aug 20, 2026, 5:42 PM

πŸ’‘ Tip: Enable Workflows to automatically generate PRs for you.

@ThomasK33

Copy link
Copy Markdown
Member Author

@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>
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>
…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>
…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.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Round 20 fixes pushed (head ee8bd16):

  • agent_skill_write.ts:143 β€” the staging validator now runs the write path's own lexical resolver (resolveSkillFilePath, export-only change) against a synthetic root: normalization first, then the traversal check and the SKILL.md/frontmatter decision on the normalized path. nested/../../escape.md refuses at staging; docs/../SKILL.md gets frontmatter-validated at staging.
  • memoryConsolidation.ts:167 β€” MemoryService.validateMutation extended to delete (target must exist) and rename (same-scope, contained destination, existing source, free destination) with the real handlers' checks and error strings.
  • memoryService.ts:1025 β€” the create branch mirrors the real create()'s listFiles()-based scope file-count cap with the identical error string; validation still never materializes scope roots.
  • taskService.ts:7659 β€” both routes now cap the attribution title (TASK_FAMILY_MESSAGE_MAX_TITLE_CHARS = 256) AND charge the complete rendered payload length against the pair/receiver budgets, so persisted rendered bytes provably never exceed the documented 256KiB/1MiB ceilings.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: 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".

Comment thread src/node/services/taskService.ts Outdated
Comment thread src/node/services/memoryService.ts
Comment thread src/node/services/branchSummary.ts Outdated
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.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Round 21 fixes pushed (head 09e23e2):

  • taskService.ts:7539 β€” both family-message routes now build the trigger content before reservation and charge payload + rendered trigger lengths (the sibling route's labeled trigger renders through a shared renderLabeledTaskMessage helper also used by persistence, so charging cannot drift). Exactness tests count trigger bytes; a new mid-size test covers the regime where trigger overhead actually breaks the ceiling.
  • memoryService.ts:1082 β€” directory-source renames whose destination equals or descends into the source are rejected by a shared segment-aware guard, mirrored in both validateMutation and the real handler (the handler pre-flight also prevents the stray mkdir that polluted the source before the filesystem EINVAL).
  • branchSummary.ts:320 β€” each summary delta is sliced to the remaining allowance before appending, hard-bounding retained memory and the persisted row even when a provider ignores maxOutputTokens and emits one giant delta (pre-fix red-check observed a 160,050-char row vs the 32,000 cap).

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: 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".

Comment thread src/node/services/taskService.ts Outdated
Comment thread src/node/services/memoryService.ts
Comment thread src/node/services/tools/code_execution.ts Outdated
Comment thread src/common/utils/messages/extractReadFiles.ts
Comment thread src/node/services/branchSummary.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot 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 Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: 09e23e2958

View security finding report

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.

Comment thread src/node/services/branchSummary.ts Outdated
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.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Round 22 fixes pushed (head 1e45093):

  • taskService.ts:7553 β€” both routes charge one queue-join separator per send unconditionally (safe upper bound: unbatched sends refuse marginally earlier, never later); worst-case joined durable bytes stay ≀ the ceilings, verified by an exactness test that fills the pair ceiling to the last byte.
  • memoryService.ts:168 β€” the own-subtree guard gained a physical-identity layer: every existing destination ancestor is stat'ed and refused when it is the source dir (same dev+ino), covering case-insensitive spellings and in-root symlink aliases through one mechanism with no platform detection; validator and handler stay mirrored.
  • code_execution.ts:160 β€” unserializable kernel returns (bare BigInt) are replaced with the bounded {truncated, note} record instead of staying inline, protecting handle offloading, the retention cap, and HistoryService persistence.
  • extractReadFiles.ts:39 β€” nested reads that resolve with {success: false} (missing/oversized/directory) are excluded from tracked paths: compaction folds the result's success bit into the record's ok, and non-compacted classic-PTC records are checked directly.
  • branchSummary.ts:86 β€” isRlmModeEnabled resolves flags per-field with the same backend-override fallback as tool assembly (resolveBackendGatedPtcExperiments semantics): explicit booleans win, missing fields consult isExperimentEnabled, so backend-only-enabled RLM workspaces get summaries/keep-recent/read-reinjection consistent with their kernel.

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: 1e4509307c

View security finding report

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: 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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Round 23 fix pushed (head d640867):

  • branchSummary.ts:190 β€” side-channel summary model candidates are now derived strictly from workspace settings: the workspace's current model first, the cheap fallbacks (Haiku/GPT Mini) only when their provider is one the workspace already uses, then the workspace's per-agent models β€” cross-provider fallbacks dropped entirely. A workspace with no metadata now yields no candidates (previously the worst case: third-party-only), degrading to no summary through the existing best-effort paths. Pinned pricing identity and usage recording unchanged. Verified /refine is unaffected (it resolves via the workspace-scoped dream-agent cascade, not this helper).

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: 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".

Comment on lines +208 to +210
const workspaceModels = [
metadataResult.data.aiSettings?.model,
...Object.values(metadataResult.data.aiSettingsByAgent ?? {}).map((settings) => settings.model),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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", [

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 πŸ‘Β / πŸ‘Ž.

Comment on lines +212 to +213
if (workspaceModels.length === 0) {
return [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 πŸ‘Β / πŸ‘Ž.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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 Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: d6408677cb

View security finding report

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.

Comment on lines +225 to +226
for (const model of NAME_GEN_PREFERRED_MODELS) {
if (allowedProviders.has(modelProvider(model))) push(model);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 πŸ‘Β / πŸ‘Ž.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: 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".

Comment on lines +472 to +474
attempted.add(edit.toolCallId);
try {
await saveStagedRefineSet(sessionDir, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 πŸ‘Β / πŸ‘Ž.

Comment on lines +262 to +269
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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 πŸ‘Β / πŸ‘Ž.

Comment on lines +174 to +176
"<abandoned_branch>",
transcript,
"</abandoned_branch>",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: 65312fcb30

View security finding report

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: 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]`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 πŸ‘Β / πŸ‘Ž.

Comment on lines +5013 to +5016
const rollup = await this.sessionUsageService.rollUpUsageIntoParent(
parentWorkspaceId,
workspaceId,
childUsage.byModel,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 πŸ‘Β / πŸ‘Ž.

Comment on lines +421 to +425
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 πŸ‘Β / πŸ‘Ž.

Sign up for free to 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