Adds a subagent config block with two independent switches:
{
"subagent": {
"compress": false, // children take the primary path: compartments, historian, full guidance, mural, caveman, Rust pass inputs
"memory_write": false // children may call ctx_memory / ctx_note
}
}
Both default to today's behaviour, so an unchanged config changes nothing.
The reason it is two switches and not one: a child session carries one stored fact (session_meta.is_subagent), and that fact answers two unrelated questions. One is what the session is shown: compartments, the historian, guidance, mural, caveman, the Rust pass inputs, the system-prompt identity, the todo snapshot. The other is what it may do: write the shared memory store. Wanting the first without the second is the case that motivated the issue.
How it is implemented
session_meta.is_subagent stays exactly what it is today, the record that this session has a parent, so the dashboard, storedSubagentMode, the RPC handlers and the OC2 parent fallback keep working on the truth.
plugin/subagent-policy.ts holds the resolved policy (set once per plugin instance from the resolved config, next to setKeepSubagents) and the two derived answers.
getOrCreateSessionMeta and readSessionMeta pass their result through effectiveSubagent. That is where the shown gates are decided: 25 non-test call sites read .isSubagent off those two functions, covering compartment injection, guidance, historian eligibility, fullFeatureMode, mural, caveman, the todo snapshot and the flag handed to the Rust module. None of them needed a change.
Every gate that writes, delivers or reports reads the stored fact instead, through storedSubagentMode / resolveStoredSubagentMode:
the memory tools in subagent-tool-policy, so compress never hands a child ctx_memory;
channel-2 delivery, so a compressed child whose run ended cannot have a follow-up turn started for it;
/ctx-wrapup, so a compressed child stays ineligible;
note-nudge accumulation, so it does not collect state it can never deliver;
/ctx-status, so the status surfaces agree with the RPC handlers.
resolveStoredSubagentMode creates the session row and re-reads the raw column. That also closes the first-pass window in hideSubagentTools: on a fresh child the row is written during the same pass, and the projection getOrCreateSessionMeta returns still says primary.
subagent.* is user-tier only: stripUnsafeProjectConfigFields drops it from project config with a warning, the way debug_rpc and pi.subagent_extensions are dropped. A project value would also leak process-wide, because the policy lives in the module.
OpenCode 2 boots through registerContext, which now installs the policy right after its enabled gate. Before this, the only caller of setSubagentPolicy was the OpenCode 1 server, so both switches were dead on OpenCode 2, the harness where this config matters most.
Pi: the call is removed rather than left as a no-op. Both child guards (pi-plugin/src/index.ts:1170-1175 for a spawned child, :1182-1187 for an in-process child) return before startPiMagicContextRuntime, so a Pi child never registers the extension and the value cannot reach it. A Pi child has no Magic Context tools today.
Harness coverage (required)
OpenCode 1: covered. The shown gates share getOrCreateSessionMeta, and the write gate is guardSubagentTools with the same policy check.
OpenCode 2: covered. registerContext installs the policy, pinned by v2/hooks/subagent-policy-wiring.test.ts (it fails 3/3 if that call is removed), and the tool-map hiding plus /ctx-status read the stored fact.
Pi: not applicable, with the reason. The two child guards above return before the runtime starts, so no child registers the extension and no Magic Context tool is reachable.
OMP: same path as Pi, same not-applicable reason.
Rust module: covered by input. rust-mode-transform.ts passes is_subagent: sessionMeta.isSubagent into the module. Two production sites read it inside Rust, crates/mc-module/src/lib.rs:10736 (the historian does not fire for a child) and :10886 (no emergency refold for a child), and both match what compress promises: the historian gate follows the switch.
Known limitation
The policy is process-global, like keep_subagents. OpenCode Desktop runs one plugin instance per open project in the same process, so the last enabled project to boot owns it, and changing the block needs a restart. v2/hooks/subagent-policy-wiring.test.ts asserts that ordering, so it stays visible instead of surprising someone later. Making it per-instance means threading the policy through every gate, which is a repository-wide change and not part of this PR.
Verification
bun run typecheck in packages/plugin and in packages/pi-plugin: both exit 0.
14 affected suites: 236 pass, 1 fail. The failure is dream-timer.test.ts's "newest matching root wins past the first page", which takes about 20s against a 5s timeout on this Windows machine and fails identically on the previous head f9ced64c6.
channel2-subagent.test.ts run on its own: 1 pass, 1 fail. Its source-text guard slices with "\n}\n" while this working tree is CRLF, so it captures the whole file and trips on an unrelated isSubagent. It also fails identically on f9ced64c6. It is not part of the 14-suite run above.
biome over the changed files: 0 errors. The one remaining warning (rpc-handlers.ts:7) predates this branch.
New coverage: OC2 boot wiring (3 tests, verified to fail when the setSubagentPolicy call is removed), stored-fact gates ignoring compress, first-pass tool visibility on a fresh child with compress: true, and project-tier stripping of subagent.*.
Note on CI: ci.yml runs on pushes to master / main / train/**, on the nightly schedule and on manual dispatch, not on pull requests. The PR-time workflows are design-gate, harness-coverage, review-findings, the Linux smoke run and Socket. So the full unit and e2e matrix cannot run on this branch until it is on a train branch; the numbers above are from running the suites locally.
Not in this PR
dream, which the first commits carried and this one removes. No harness acted on it: OpenCode 1 hardcodes the filter (dreamer/retrospective-raw-provider.ts:142), OpenCode 2 lost children to rootSessionActivity's WHERE parent_id IS NULL, and Pi has no filter at all. The comment above the OpenCode 1 filter also records why children were excluded in the first place: a child has no user messages, agent-written task prompts trip the frustration regex, and delegation-heavy periods drown the scan budget at 30:1. What counts as a child's retrospective input is a semantics question for #832, and a boolean cannot answer it.
A per-agent allow list (subagent_agents), mentioned in the issue. It needs the agent identity at getOrCreateSessionMeta time, which would mean a host-DB read on a hot path; better as a follow-up with its own design.
Surfacing the switches in the dashboard.
Confidence Score: 5/5
The reviewed changes appear safe to merge; the previous memory-tool blocker is fixed.
Summary
Adds independent, user-only switches for child-session compression and memory writes.
Users can choose which context features child sessions receive.
Child sessions can use memory tools when users allow them.
Child-only actions keep using the stored parent flag.
The retrospective provider explains why it uses root-session activity.