Skip to content

Workers fork their channel; reflection reads worker transcripts; sandbox traversal fixes - #633

Merged
jamiepine merged 7 commits into
mainfrom
jamiepine/worker-reliability
Aug 10, 2026
Merged

Workers fork their channel; reflection reads worker transcripts; sandbox traversal fixes#633
jamiepine merged 7 commits into
mainfrom
jamiepine/worker-reliability

Conversation

@jamiepine

@jamiepine jamiepine commented Aug 10, 2026

Copy link
Copy Markdown
Member

Phases 7 and 8 of docs/design-docs/worker-reliability.md, plus the sandbox profile fixes that came out of the same investigation.

Workers are forks

Delegation was lossy by construction: workers defaulted to receiving no conversation history, so the channel agent compressed the session into a task paragraph at exactly the moment the details mattered most. WorkerHistoryMode collapses from none | summary | recent(n) | full to:

  • fork (default) — full clone of the channel's history under the worker's own system prompt, the branch semantic. Pre-compacts before the first LLM call via a helper now shared with the branch pre-flight.
  • clean — task only. For fan-out and mechanical tasks; detached workers are clean by definition.

summary was a stub that logged a warning and handed the worker nothing. recent(n) paid for history with no guarantee the load-bearing part was in the window. Both are gone, not deprecated. Stored settings with dead variants fall back to the default via the existing fail-soft parse.

spawn_worker's tool description now reads the conversation's live setting:

let history_mode = self.state.worker_context_settings.read().await.history;
let (history_note, task_description) = match history_mode {
    WorkerHistoryMode::Fork => (
        "The worker forks this conversation's history, so it already knows everything \
         discussed here — describe the task, not the background.",
        ...

so the model stops writing lossy context summaries out of habit when the worker will see everything anyway.

Reflection reads worker transcripts

Skill reflection triggered on worker success but could only see the channel's summary view — a worker could spend eleven minutes discovering a working procedure, report a two-line success, and reflection saw nothing to teach. Now:

  • the reflection signal carries completed worker ids (drained on pass spawn) instead of a bare AtomicBool
  • the persistence prompt lists them and instructs worker_inspect on each, plus failed predecessors from the list view
  • the "worker process chatter" exclusion is scoped to memory extraction so it no longer argues against reading transcripts during reflection
  • worker_inspect's per-result display cap rises to 2000 bytes for reflection passes so error text survives

Sandbox traversal fixes

Three profile gaps that turned a plain git clone into a 30-call ordeal:

  • top-level /etc, /tmp, /var symlinks get file-read-metadata, so resolver config and the CA bundle resolve through their usual paths instead of pushing workers toward GIT_SSL_NO_VERIFY
  • ancestors of allowed paths get metadata access — git stats and mkdirs each leading component, and a denied stat surfaced as EPERM where EEXIST was expected, killing checkout after the full object transfer
  • /private/var/select joins the read-only set, ending the 'Operation not permitted' stderr spam on every shell call

Fork budget

precompact_forked_history re-estimates after every cut instead of trusting one fractional pass, and budgets against the prompt the fork is about to carry rather than a fixed share of the window. Worker forks measure the fully rendered preamble — skills and ambient memory included — plus the task; branches measure their system prompt plus the user prompt. 15% of the window is reserved for the response on top, and the compaction marker counts against the budget so the notice can't push a fork back over the line it was just trimmed to.

The most recent four messages are never dropped. A single oversized tool result can outweigh the whole window, and dropping it would strip the fork of the context it was forked for, so the fork keeps its tail and logs a warning instead of silently returning over budget.

Terminal states

WorkerComplete is the terminal event for both outcomes and fires once per worker — the handler returns early when the id is already gone from worker_handles, so a duplicate event is a no-op. Every completion is now recorded against the reflection signal, which is what makes a failed predecessor's transcript available to the pass a later success triggers. Only success fires the signal (ReflectionSignal::is_set requires turn_work or at least one successful worker), so an unresolved failure can't schedule a reflection pass on its own, and record_worker ignores repeat completions for an id already held.

Collection and scheduling stay separate: mark_reflection_worker always records, and only the success branch calls check_memory_persistence. The signal itself is cleared when the persistence branch actually spawns, so a failed spawn retries on the next check rather than losing the reflection for a cooldown.

Merge with main

Merged current main (autonomy + wakes). One conflict, in WorkerComplete handling: main added wake-event emission per completion, this branch had moved reflection recording out from under the success check. Both kept — wake emission spawns off the event loop first, then reflection records the worker and only success calls check_memory_persistence.

Tests

  • sandbox: new test_sbpl_profile_traversal_rules asserts the symlink rules, per-ancestor metadata rules, and that the data-dir deny still wins ordering
  • prompts: reflection section renders worker ids and worker_inspect instructions when ids are present, stays clean when not
  • settings resolution tests updated for the two-mode enum
  • compactor: budget boundary (exactly at, one token over), oversized-recent-message and prompt-larger-than-window cases, and that a measured preamble changes the outcome for identical history
  • channel: reflection signal records failed workers without firing, carries failed predecessors of a success, and ignores repeat completions

Note

This PR improves worker reliability through three major changes. Workers now fork full channel history by default (instead of receiving only a lossy summary), allowing them to access complete context while remaining clean for fan-out tasks. The reflection system now reads worker transcripts to extract and learn from their actual problem-solving process, not just final results. Sandbox profile fixes eliminate 30+ permission-related syscall failures that plagued git operations by correctly handling symlinks, ancestor path metadata, and common /var directories.

Written by Tembo for commit d859edf. This will update automatically on new commits.

- allow file-read-metadata on the top-level /etc, /tmp, /var symlinks so
  resolver config and the CA bundle resolve through their usual paths
- allow metadata on ancestors of allowed paths; git clone stats and mkdirs
  each leading component and a denied stat surfaces as EPERM where the
  caller expects EEXIST, killing checkout after a full object transfer
- add /private/var/select to the read-only set so every sh invocation
  stops printing 'Operation not permitted' on stderr
…eflection

Workers now fork the channel's conversation history under their own system
prompt, the way branches do — the difference between a worker and a branch
is the tools it gets, not the context it has.

- WorkerHistoryMode collapses to fork (default) | clean. summary was a stub
  that logged a warning and handed the worker nothing; recent(n) paid for
  history without any guarantee the load-bearing part was in the window.
  clean stays for fan-out and mechanical tasks; detached workers are clean
  by definition.
- fork pre-compacts an oversized history before the worker's first LLM call,
  via a helper shared with the branch pre-flight.
- spawn_worker's description now reflects the conversation's live setting,
  so the model stops writing lossy context summaries out of habit.
- the reflection signal carries completed worker ids instead of a bare bool;
  the persistence prompt lists them and instructs worker_inspect on each,
  plus failed predecessors — the lesson lives in what the worker tried, not
  the summary it returned. per-result transcript cap raised to 2000 bytes
  for reflection passes.
- the 'worker process chatter' exclusion is scoped to memory extraction so
  it no longer argues against reading transcripts during reflection.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The PR replaces legacy worker history modes with Fork and Clean, adds prompt-aware forked-history precompaction, tracks reflection worker outcomes, updates memory-persistence prompts, configures transcript limits, and expands macOS sandbox metadata access.

Changes

Worker history and reflection

Layer / File(s) Summary
Worker history contracts and settings
interface/src/api/*, interface/src/components/ConversationSettingsPanel.tsx, src/conversation/settings.rs, src/api/portal.rs, prompts/en/tools/spawn_worker_description.md.j2, src/tools/spawn_worker.rs
Worker history now uses fork and clean in public types, defaults, presets, and worker guidance.
Forked history compaction and worker dispatch
src/agent/compactor.rs, src/agent/branch.rs, src/agent/channel_dispatch.rs
Forked workers receive cloned history that is compacted against prompt and context budgets. Clean workers receive no history.
Reflection worker tracking and persistence
src/agent/channel.rs, src/agent/channel_dispatch.rs, src/prompts/engine.rs, prompts/en/memory_persistence.md.j2, src/tools.rs, src/tools/worker_inspect.rs
Reflection state records worker outcomes, passes them to memory persistence, and supports transcript inspection with a configurable result cap.

macOS sandbox profile updates

Layer / File(s) Summary
macOS sandbox path rules and tests
src/sandbox.rs
macOS profiles allow additional symlink and ancestor metadata access while preserving the data-directory deny rule. Tests cover traversal and rule ordering.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main worker history, reflection transcript, and sandbox traversal changes.
Description check ✅ Passed The description directly explains the worker reliability, reflection, history budgeting, and sandbox changes in the pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jamiepine/worker-reliability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jamiepine
jamiepine marked this pull request as ready for review August 10, 2026 02:50

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/agent/channel.rs (1)

3424-3434: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Include failed worker IDs in reflection input.

Line 3426 records a worker ID only when success is true. A failed worker cannot trigger reflection and is absent when another signal triggers it. Reflection therefore cannot inspect failed predecessor transcripts.

Collect every completed worker ID. If failed completion must not independently trigger reflection, keep that scheduling policy separate from worker-ID collection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent/channel.rs` around lines 3424 - 3434, Update the worker-completion
handling around mark_reflection_worker to record every completed worker ID,
regardless of success, so failed predecessor transcripts are available to
reflection. Keep the existing success-only condition for triggering reflection
scheduling, separating ID collection from the policy that calls
check_memory_persistence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/agent/compactor.rs`:
- Around line 311-320: Update the pre-compaction logic surrounding history.drain
and the compaction marker to repeatedly estimate token usage, removing older
messages until history plus reserved prompt and response capacity fits the
target budget. Recalculate after each removal rather than relying only on the
fraction, and explicitly handle the case where preserved messages alone exceed
the budget without allowing the function to return over budget.

---

Outside diff comments:
In `@src/agent/channel.rs`:
- Around line 3424-3434: Update the worker-completion handling around
mark_reflection_worker to record every completed worker ID, regardless of
success, so failed predecessor transcripts are available to reflection. Keep the
existing success-only condition for triggering reflection scheduling, separating
ID collection from the policy that calls check_memory_persistence.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 04f4b9bf-9019-48a0-bf8e-aa19f8eb0be9

📥 Commits

Reviewing files that changed from the base of the PR and between 04e048e and d859edf.

📒 Files selected for processing (16)
  • interface/src/api/schema.d.ts
  • interface/src/api/types.ts
  • interface/src/components/ConversationSettingsPanel.tsx
  • prompts/en/memory_persistence.md.j2
  • prompts/en/tools/spawn_worker_description.md.j2
  • src/agent/branch.rs
  • src/agent/channel.rs
  • src/agent/channel_dispatch.rs
  • src/agent/compactor.rs
  • src/api/portal.rs
  • src/conversation/settings.rs
  • src/prompts/engine.rs
  • src/sandbox.rs
  • src/tools.rs
  • src/tools/spawn_worker.rs
  • src/tools/worker_inspect.rs

Comment thread src/agent/compactor.rs Outdated
- collect failed worker completions in the reflection signal too — their
  transcripts carry the trials — while keeping success-only as the trigger
  policy. Entries are handed to the pass annotated succeeded/failed.
- precompact_forked_history re-estimates after each drain instead of
  trusting one fractional cut; a few large messages could previously leave
  the fork over budget. Stops at a 4-message floor and leaves the rest to
  overflow recovery. Covered by tests.
Conflict in src/agent/channel.rs (WorkerComplete handling): main added
wake-event emission on completion, this branch moved reflection recording
so every completion is tracked. Kept both — wake emission runs first, then
reflection records the worker and only success fires the persistence check.
Pre-compaction stopped at a raw 70% usage check against the whole context
window, so a fork that fit still had no room for its own system prompt and
response. Budget history against the window minus a 30% reserve, re-estimate
after every cut, and count the compaction marker itself against the budget.

The retention floor is now a named constant and applies to the drain clamp
too (it previously allowed cutting to 2 while the loop guarded at 4). A fork
whose retained tail alone busts the budget — one oversized tool result can —
keeps that tail and logs a warning rather than silently returning over budget.

Reflection worker recording moves onto ReflectionSignal so the lifecycle is
testable: failed workers are collected for their transcripts but don't fire
the signal alone, and repeat completions for a worker are ignored.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/agent/compactor.rs`:
- Around line 333-369: Update precompact_forked_history and every first-call
path that invokes it to account for the tokens required by the subsequently
attached system prompt, task/user prompt, dynamic skills, and ambient memory.
Pass an actual reserved-token value into the precompaction budget calculation,
or enforce equivalent input-size limits that guarantee the existing reserve;
ensure the resulting history plus reserved prompt input stays within the context
window before prompt_once.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f75a9932-cade-47fa-9f6a-a6d4c624d2cd

📥 Commits

Reviewing files that changed from the base of the PR and between 5c96879 and 5d353b9.

📒 Files selected for processing (8)
  • interface/src/api/schema.d.ts
  • src/agent/branch.rs
  • src/agent/channel.rs
  • src/agent/channel_dispatch.rs
  • src/agent/compactor.rs
  • src/conversation/settings.rs
  • src/prompts/engine.rs
  • src/tools.rs
🚧 Files skipped from review as they are similar to previous changes (7)
  • interface/src/api/schema.d.ts
  • src/tools.rs
  • src/conversation/settings.rs
  • src/agent/branch.rs
  • src/agent/channel_dispatch.rs
  • src/agent/channel.rs
  • src/prompts/engine.rs

Comment thread src/agent/compactor.rs
The 30% reserve was a guess made before the caller attached anything.
precompact_forked_history now takes the prompt size the caller is about to
send — worker forks measure the fully rendered preamble (skills and ambient
memory included) plus the task, branches measure their system prompt plus the
user prompt — and reserves 15% of the window for the response on top.

A preamble that outgrows the window collapses the budget to zero; the fork
keeps its retention floor and warns rather than being emptied.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/agent/compactor.rs`:
- Around line 300-304: Update forked_history_budget so response_reserve uses
integer ceiling arithmetic for the 15% FORK_RESPONSE_RESERVE instead of
truncating fractional tokens, ensuring small windows still reserve response
capacity. Add a boundary test using a context window not divisible by 20, such
as 10, and verify the resulting budget reserves the rounded-up token count.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cd1eae27-6d66-4a1b-9189-7448f0d0a609

📥 Commits

Reviewing files that changed from the base of the PR and between 5d353b9 and 31d0a3e.

📒 Files selected for processing (3)
  • src/agent/branch.rs
  • src/agent/channel_dispatch.rs
  • src/agent/compactor.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/agent/branch.rs
  • src/agent/channel_dispatch.rs

Comment thread src/agent/compactor.rs
Comment on lines +300 to +304
/// Tokens available to a forked history, once the prompt the caller is about
/// to attach and room for a response are both reserved.
fn forked_history_budget(context_window: usize, prompt_tokens: usize) -> usize {
let response_reserve = (context_window as f32 * FORK_RESPONSE_RESERVE) as usize;
context_window.saturating_sub(response_reserve.saturating_add(prompt_tokens))

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Round the response reserve up to a whole token.

Line 303 truncates the 15% reserve. For a context_window of 10, it reserves 1 token instead of 2. For a context window of 1, it reserves no response capacity. This permits input that consumes capacity intended for the worker response.

Use integer ceiling arithmetic for the reserve. Add a boundary test for a context window that is not divisible by 20.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent/compactor.rs` around lines 300 - 304, Update forked_history_budget
so response_reserve uses integer ceiling arithmetic for the 15%
FORK_RESPONSE_RESERVE instead of truncating fractional tokens, ensuring small
windows still reserve response capacity. Add a boundary test using a context
window not divisible by 20, such as 10, and verify the resulting budget reserves
the rounded-up token count.

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