Skip to content

feat(config): let subagent sessions opt into compression, memory tools and the dreamer - #833

Open
Neko-Yukari wants to merge 7 commits into
cortexkit:masterfrom
Neko-Yukari:subagent-policy
Open

Neko-Yukari wants to merge 7 commits into
cortexkit:masterfrom
Neko-Yukari:subagent-policy

Conversation

@Neko-Yukari

@Neko-Yukari Neko-Yukari commented Nov 1, 2026 •

Copy link
Copy Markdown

Refs #832 (draft: waiting for design-approved)

What this does

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 Subagent sessions should be able to use compartments, memory tools and the dreamer, with three separate switches #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.

RetriggerView in GreptileConfidence 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.

Reviews (4) · Last reviewed commit: "fix(subagent): let memory_write reach th..." · Reviewed by Greptile

…d the dreamer

A child session gets one stored fact, session_meta.is_subagent, and that
single flag decides three unrelated things: whether the session takes the
reduced path, whether ctx_memory and ctx_note are visible, and whether the
dreamer retrospective may read it. A long-running child (an explorer walking
a repository, a reviewer reading fifteen files) wants the first and not the
other two.

Keep the stored flag as the parent-link record and derive the three answers
from a new subagent config block:

- compress: getOrCreateSessionMeta presents a child as primary, so every
  reduced-path gate (compartments, historian, full guidance, the flag handed
  to the Rust module) follows without touching any of the 25 call sites.
- memory_write: decided from the stored column through storedSubagentMode,
  because the tools must stay hidden when compression is on.
- dream: the retrospective session query drops its subagent filter.

Defaults reproduce today's behaviour exactly.
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Harness coverage advisory: these conventional twins were not touched:

  • packages/plugin/src/config/project-security.test.ts → packages/pi-plugin/src/config/project-security.test.ts
  • packages/plugin/src/index.ts → packages/pi-plugin/src/index.ts
  • packages/plugin/src/v2/retrospective-raw-provider.ts → packages/pi-plugin/src/dreamer/retrospective-raw-provider-pi.ts

Cover every shipped harness or explain why the surface does not exist there. See CONTRIBUTING.md. This is advisory only; no draft state is changed.

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

Waiting for design-approved on #832.

This PR needs a linked issue with the design-approved label before it can be reviewed or merged. Link the approved issue with Approved issue: #<issue> (or Refs #<issue>) in the description. A maintainer will apply the design-approved label on the issue, or trivial on the pull request when there is genuinely no design to agree. The issue stays open until the change ships; maintainers take care of it then.

@Neko-Yukari
Neko-Yukari marked this pull request as ready for review November 2, 2026 00:16

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 9 files

Reply to a comment to ask cubic a question or push back. It learns from your replies.

View guided diff | Re-trigger cubic

Comment thread packages/pi-plugin/src/index.ts Resolved
Comment thread packages/plugin/src/index.ts Resolved
Comment thread packages/plugin/src/index.ts Resolved
81004 added 2 commits November 2, 2026 09:32
…/report gates on the stored fact

- OC2 boot installs the policy: setSubagentPolicy's only caller was the OC1 lane,
  so compress/memory_write were dead on OpenCode 2.
- drop subagent.dream. No harness ever acted on it (OC1 hardcodes the filter,
  OC2 loses children in rootSessionActivity, Pi has no filter at all); the input
  semantics belong to cortexkit#832.
- strip subagent.* from project config: it is user-tier, and a project value
  would also leak process-wide through the policy.
- the memory-tool gates, channel-2 delivery, /ctx-wrapup, note-nudge accumulation
  and /ctx-status now read the stored is_subagent, so subagent.compress cannot
  hand a child a memory write, a follow-up turn after the run ended, or a wrapup.
  resolveStoredSubagentMode() creates the row and re-reads the raw column, which
  also closes the first-pass window in hideSubagentTools.
- Pi: drop the no-op setSubagentPolicy call; child processes never register the
  extension, so the value could not reach them.
Covers the lane that had no caller: setup installs the project's switches, a
disabled project leaves another project's policy alone, and the documented
process-wide limitation (last enabled boot wins) is asserted rather than implied.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 17 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

81004 added 3 commits November 2, 2026 16:43
Converting it to 'not a subagent' would let /ctx-wrapup write into a child
session when the row cannot be read; the pre-existing gate let the
getOrCreateSessionMeta error escape, and that is the behaviour to keep.
The replay removed the primary-only tools whenever the stored flag said child,
so a storage-busy turn served a reduced tool map even with subagent.memory_write
on. Read the same policy the managed pass reads (in memory, still no write).
OC1's task tool turns experimental.primary_tools into child-session permission
denies, and the list carried ctx_memory/ctx_note unconditionally, so the host
refused the call before this package's own gate was consulted. With
subagent.memory_write on, the two entries are no longer added; user-supplied
entries stay.

This branch has not been deployed

No deployments
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.

2 participants