Skip to content

Apply Claude turn settings without replacing live sessions - #1236

Merged
ymichael merged 5 commits into
get-bb:mainfrom
sholub-dev:per-turn-permission-escalation
Aug 11, 2026
Merged

ymichael merged 5 commits into
get-bb:mainfrom
sholub-dev:per-turn-permission-escalation

Conversation

@sholub-dev

@sholub-dev sholub-dev commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #1235.

Claude background commands, workflows, and agents all live inside one Agent SDK session. bb previously treated execution-option drift as construction-time configuration, dispatched thread/resume, and replaced that session. Alternating user/system turns therefore interrupted background work, and several other setting changes had the same destructive behavior.

This PR makes reconfiguration provider-specific. Claude settings that can safely change in place now travel with each turn; only actual session-construction changes rebuild the provider session.

What changes

  • Permission escalation is live per input. A steer adopts its new escalation only after the input is accepted, and prompt/subagent/tool tracking preserves the originating turn's policy for background work.
  • Claude background subagents inherit approval provenance from their parent Agent/legacy Task tool chain, including SDK callbacks that omit agent metadata. Permission-request hooks provide an additional tool-level provenance boundary.
  • Approval-policy ownership is explicit on provider adapters. Claude filters allow/ask/deny using the originating work before forwarding an approval, so the runtime no longer reclassifies that request against newer thread settings. Codex, ACP, and Pi retain runtime-owned approval filtering.
  • Model changes use the SDK's live setModel control.
  • Memory, workflow, and reasoning changes use applyFlagSettings, including transitions to and from max reasoning.
  • Claude's native Agent/legacy Task and Workflow toggles are enforced live. They no longer depend on stale construction-time disallowedTools, and disabling them rejects new launches without replacing the session or killing existing work.
  • Unsupported Claude serviceTier: "fast" is normalized to "default" before validation, comparison, persistence, and provider command construction, so it cannot cause a rebuild or reach the Claude bridge.
  • Provider adapters now classify setting changes as unchanged, live, or session-scoped. Codex, ACP, and Pi retain their existing session-rebuild behavior.

The permission path remains conservative: readonly enforcement resolves escalation at tool-call time, unsandboxed retries are automatically denied on escalation-denied turns, and that denial runs before cached session grants so an approval from an earlier user turn cannot leak into later system work.

The following remain session-scoped and still rebuild when changed:

  • Claude Plan mode override
  • Permission mode, workspace scope, or approval reviewer
  • Mock Claude CLI traffic configuration

HOST_DAEMON_PROTOCOL_VERSION is bumped to 99 because the server no longer sends construction-time Claude disallowedTools values for workflow/subagent settings. Version 98 is already used by qualified upstream PR lookup on the updated base.

Validation

  • Standalone QA started a real Claude background process, then changed memory, workflow, and subagent settings while switching Haiku/low to Opus 1M/max. The same bridge PID, Claude CLI PID, provider thread ID, and background process survived; the next turn completed with the new model.
  • A second real Claude run used serviceTier: "fast" on both start and agent-origin steer. It retained one provider thread, preserved the background child's original ask policy after the steer changed the current thread to deny, surfaced the dangerous write as a pending approval, and completed it after allow_once.
  • Affected package typechecks pass for @bb/agent-runtime, @bb/server, @bb/host-daemon, and @bb/host-daemon-contract.
  • Focused execution-option, runtime lifecycle, interactive-request, and Claude bridge coverage: 115 tests passed.
  • Full server suite: 1,395 tests passed.
  • Host-daemon contract suite: 49 tests passed.
  • Full agent-runtime suite: 871/872 passed locally; the only failure is the unchanged timing-sensitive bounds provider stderr while data arrives without a newline lifecycle test, which is outside this diff.

@sholub-dev
sholub-dev force-pushed the per-turn-permission-escalation branch from 4aa085b to 1c28080 Compare August 9, 2026 18:39
@SawyerHood

Copy link
Copy Markdown
Collaborator

🚨 SLOP COP 🚨 · review

I am SlopCop. I am reviewing this pull request for security, code quality, architecture, performance, and end-to-end behavior.

return;
}

threadSession.permissionEscalation = params.permissionEscalation;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚨 slopcop/review — A shared session value applies the newest turn policy to all background work.

Background tasks outlive their start turn. A later turn overwrites this shared value. An older system task can then read ask from a user turn and enter the approval path. The reverse change can also deny older user work. Please attach the policy to the work that created each tool request. Add overlap tests for both policy directions.

@sholub-dev sholub-dev Aug 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The SDK gives canUseTool no link to the originating turn or task, so per-work policy isn't possible at this layer. The old behavior rebuilt the session on every escalation flip and killed background work — the bug this PR fixes. Both crossovers fail safe: ask only happens while a user turn is active, deny auto-denies.

return;
}

threadSession.permissionEscalation = params.permissionEscalation;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚨 slopcop/review — A failed steer leaves the new permission policy active.

This assignment occurs before pushInput confirms the steer. The error path returns an error without restoring the old value. A rejected ask steer can therefore loosen the policy for existing deny work. Update the value after acceptance, or restore it in the error path. Add a failed-steer test.

@sholub-dev sholub-dev Aug 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d4bf20e: escalation is set only after pushInput succeeds. Added a failed-steer test.

@SawyerHood SawyerHood left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚨 SLOP COP 🚨 · review

ELI5: This change keeps old Claude jobs alive. But it gives all jobs one permission switch. It is like one light switch for every room.

I found two defects. I recommend fixes before merge.

  1. High — Background work crosses the per-turn permission boundary.

    Background tasks outlive their start turn. Each new turn overwrites one session-wide permissionEscalation value. An older system task can later read ask from a user turn and enter the approval path. The reverse change can deny older user work. Attach the policy to the work that created each tool request. Add overlap tests for both policy directions.

  2. Medium — A failed steer keeps its permission change.

    The bridge changes the value before pushInput succeeds. The error path does not restore the old value. A rejected ask steer can loosen existing deny work. Update the value after acceptance, or restore it after failure.

Architecture and performance

The adapter capability is an acceptable abstraction. The two Claude permission checks serve different SDK enforcement points. I found no useful refactor requirement. The change removes session replacement and adds constant-time state access.

Validation

I verified the exact head SHA and reviewed all 14 changed files. Three newer base commits do not touch these files. git diff --check passed. All reported GitHub CI checks passed. The change does not require a host-daemon protocol increase.

The pull request comes from an untrusted fork, and this backend change has no browser route. I did not start a server or execute the pull-request code locally.

This is a comment-only review. I did not approve the pull request or request changes.

@sholub-dev
sholub-dev force-pushed the per-turn-permission-escalation branch from 9269d62 to d4bf20e Compare August 10, 2026 16:18
@ymichael
ymichael force-pushed the per-turn-permission-escalation branch from d4bf20e to 3b81ef3 Compare August 10, 2026 23:48
@ymichael ymichael changed the title Apply permission escalation per turn in the Claude adapter Apply Claude turn settings without replacing live sessions Aug 10, 2026
@ymichael ymichael self-assigned this Aug 10, 2026
@ymichael
ymichael force-pushed the per-turn-permission-escalation branch from 3b81ef3 to f79aeb7 Compare August 11, 2026 02:30
@ymichael ymichael reopened this Aug 11, 2026
sholub-dev and others added 5 commits August 11, 2026 09:49
Escalation follows the turn initiator, but it was pinned into Claude session
construction, so every user/system turn alternation replaced the session and
killed the background tasks inside it. Turn commands now carry escalation, the
bridge adopts it in place, and the runtime records escalation-only changes
without a session-replacing reconfigure.
ymichael added a commit to sholub-dev/bb that referenced this pull request Aug 11, 2026
## Summary

- resolve PRs through the configured upstream remote and branch when a
BB managed local branch has a different name
- preserve distinct no-PR and unavailable GitHub CLI outcomes
- add real-Git regression coverage for fork remotes, renamed branches,
no-PR, authentication, and missing CLI cases
- bump the host-daemon protocol to 97 so enrolled hosts receive the
corrected lookup behavior

## Validation

- pnpm exec turbo run test --filter=@bb/host-workspace --force
- pnpm exec turbo run test --filter=@bb/host-daemon-contract --force
- pnpm exec turbo run typecheck --filter=@bb/host-workspace
- pnpm exec turbo run typecheck --filter=@bb/host-daemon-contract
--filter=@bb/host-daemon
- live read-only lookup against env_yikdjjfryt resolved get-bb#1236
@ymichael
ymichael force-pushed the per-turn-permission-escalation branch from 551feeb to daae198 Compare August 11, 2026 16:51
@ymichael
ymichael merged commit a8f7030 into get-bb:main Aug 11, 2026
9 checks passed
SawyerHood pushed a commit that referenced this pull request Aug 11, 2026
…thread (#1321)

# Problem

Every thread on the host freezes at "waiting" and only a full app
restart clears it. This has now happened **four times on bb-app
0.36.0**, each time triggered by a single event the server could never
store.

The daemon holds **one in-memory event queue for the whole host** and
reposts it as a single batch. When the head of that queue is an event
the server deterministically refuses, the batch can never succeed — so
every other thread's `turn/started`, `item/*` and `turn/completed`
events pile up behind it and never reach the database. The UI reads the
database, so every thread looks stuck.

### From the logs

`~/.bb/logs/server.3.log` — the first rejection, then the same one
repeating verbatim:

```json
{"level":40,"time":1786360836499,"eventType":"provider/unhandled","scopeKind":"turn",
 "threadId":"thr_fpx3vkax5h","turnId":"auto-compact-1",
 "errorMessage":"Cannot append provider/unhandled for turn auto-compact-1 before turn/started is stored",
 "errorName":"MissingStoredTurnStartedError","msg":"Rejected daemon event before turn/started"}
{"level":40,"time":1786360836616,"eventType":"provider/unhandled","scopeKind":"turn",
 "threadId":"thr_fpx3vkax5h","turnId":"auto-compact-1", ... }
```

Every occurrence, grouped by the turn that poisoned the queue:

| Thread | Turn | Rejections | Window |
|---|---|---:|---|
| `thr_dwmzmanhn5` | `auto-compact-2` | 1911 | 08-07 14:57:19 → 15:27:06
(29.8 min) |
| `thr_fpx3vkax5h` | `auto-compact-1` | 505 | 08-10 13:20:36 → 13:25:57
(5.3 min) |
| `thr_sdc5dy277m` | `auto-compact-3` | 171 | 08-10 13:48:47 → 13:52:43
(3.9 min) |
| `thr_qifimqh4a6` | `auto-compact-1` | 260 | 08-10 15:08:46 → 15:14:00
(5.2 min) |

Every window ends at a restart, never at a recovery.

During the 13:20 window the server logged **no thread activity
whatsoever** — only the rejections:

```
1  [plugin:connect] rpc listAccountServers failed: not_paired
5  Skipping malformed prompt history row
1  [plugin:agent-limits] disposed        <- the restart
```

The 15:08 occurrence is visible directly in the database. Rows inserted
per minute across all threads, spanning that window:

```
15:03 |  90 events | 4 threads
15:04 |  91        | 1
15:05 |  66        | 1
15:06 |  22        | 1
15:07 |  32        | 1
15:08 |  35        | 2     <- poison event lands at 15:08:46
15:09 |   0        | 0
15:10 |   0        | 0
15:11 |   1        | 1
15:12 |   0        | 0
15:13 |   4        | 2
15:14 |  29        | 4     <- restart at 15:14:00
15:15 |  48        | 3
```

Five minutes in which the whole machine persisted essentially nothing,
then instant recovery on restart. Those events are gone: the queue is
in-memory, so the restart that clears the wedge also discards everything
held behind it, leaving a hole in each affected thread's transcript.

# Root cause

1. **A provider-minted turn id is trusted.**
`createUnhandledProviderEvent` falls back to reading `turnId` out of the
raw provider event when the caller does not supply one:

   ```ts
   const turnId = args.turnId ?? getTurnIdFromRawEvent(args.rawEvent);
   ```

Codex labels its automatic-compaction traffic `auto-compact-N`. The
string `auto-compact` appears nowhere in bb's source — it is entirely
provider-minted, and every `provider/unhandled` event on all four
affected threads carries `providerId: "codex"`. bb never opened that
turn, so it never emitted a `turn/started` for it. Critically, every
caller supplies `turnId` from bb's own turn registry and omits it *only
when bb has no active turn* — precisely the case where a scraped id is
guaranteed wrong.

2. **The server hard-rejects the orphan.**
`resolveDaemonTurnStartDisposition` finds no stored `turn/started`; the
escape hatch `ORPHAN_DROPPABLE_TURN_EVENT_TYPES` held only the two
usage-snapshot types, so it throws `MissingStoredTurnStartedError`.

3. **The whole batch dies with it.** `/session/events` appends every
event in one `immediate` transaction, so the throw rolls all of them
back and returns `409 invalid_request`.

4. **The daemon reposts it forever.** The drain loop takes the entire
queue as one batch and splices only on success:

   ```ts
   try { response = await options.postEvents(batch) }
   catch (error) {
logger.error(..., "Failed to post daemon events; will retry on the next
flush");
     return;                       // queue untouched
   }
   queue.splice(0, batch.length);  // only reached on success
   ```

The daemon already knows this class of error is permanent —
`defaultRetryableForStatus(409)` is `false`, and
`ServerResponseError.retryable` carries that verdict — but nothing
consults it.

# Fix

**1. `apps/host-daemon/src/event-sink.ts` — never repost a batch the
server permanently refused.** On a non-retryable `invalid_request`, the
sink bisects the batch, drops the events that are undeliverable by
construction, and delivers the rest. Since the server appends in one
transaction and rolls back entirely on refusal, nothing was committed
and re-posting the halves cannot duplicate. Isolating k bad events costs
O(k log n) posts.

The `invalid_request` code check is what keeps this narrow:
`/session/events` also fails non-retryably with `401 unauthorized` and
`401 inactive_session`, and those say nothing about the events
themselves. Those must stay queued for the session the daemon is about
to reopen, not be discarded one at a time — there is a regression test
for exactly this.

**2. `packages/agent-runtime/src/shared/provider-unhandled-event.ts` —
stop trusting provider turn ids.** Only a turn id the caller vouched for
scopes the event; the raw-event fallback is gone.

**3. `packages/db/src/data/events.ts` — `provider/unhandled` becomes
orphan-droppable.** A backstop, in the spirit of the existing comment
about fork usage snapshots. An unhandled passthrough event is diagnostic
only: losing one is a non-event, failing the batch it rode in with is
not. Turn-content events still require a stored `turn/started`, so
genuine ordering bugs are still caught.

**4. The queue-backup tripwire logs at `warn`, not `debug`.** It never
once fired in any of the four incidents, so there was no signal short of
noticing the UI had stopped moving.

Fix 1 is the load-bearing one. Fixes 2 and 3 close this particular
trigger; only fix 1 stops the *next* unknown orphan event from wedging
the host.

## Note on ordering of fixes 1 and 3

Fix 3 alone repairs already-enrolled daemons: an old daemon talking to a
new server stops receiving 409s, so the wedge cannot recur even before
it updates. Fix 1 is what makes the daemon resilient to the next unknown
case.

# Protocol version

Bumped `HOST_DAEMON_PROTOCOL_VERSION` 99 → 100, matching the convention
used by #1224, #1208, #1232, #1314 and #1236 for daemon-behaviour
changes. Nothing in the wire *schema* changed, and both directions are
compatible (old daemon + new server is in fact the repair path above) —
the bump is here to push fix 1 out to enrolled machines rather than
leave them on a build that wedges. Happy to drop it if you would rather
not force an update cycle for this.

# Tests

Written as reproductions first, and confirmed failing against the base
commit before the fix:

| Test | Package | Reproduces |
|---|---|---|
| `ignores a provider-supplied turn id the caller did not vouch for` |
`@bb/agent-runtime` | root cause — `auto-compact-1` scraped from raw
params |
| `drops orphan provider/unhandled events instead of failing the batch`
| `@bb/db` | the batch-wide rollback |
| `drops a permanently rejected event instead of retrying it forever` |
`@bb/host-daemon` | the infinite repost |
| `delivers events queued behind a permanently rejected event` |
`@bb/host-daemon` | **the wedge itself** — healthy traffic from other
threads gets through |
| `accepts a batch carrying a provider/unhandled event for a turn bb
never started` | `@bb/server` | end-to-end at the
`/internal/session/events` route that produced the 409 |

Plus guards against over-correcting:

- `keeps events queued when the session, not the batch, is rejected` — a
401 must not bisect the queue away.
- `keeps retrying a batch that fails for a retryable reason` — 5xx
behaviour unchanged.

One existing expectation changed: `codex/adapter.test.ts >
translateEvent unknown codex notifications fall back to
provider/unhandled` now expects thread scope. That path handles
notifications which *failed* schema parsing, so nothing there vouches
for the turn id; Codex notifications bb does parse still carry turn
scope. Comment in the test explains it.

## Verification

Rebased onto `d07c1ce28` and re-verified there. `pnpm exec turbo run
test` on `@bb/db`, `@bb/agent-runtime`, `@bb/host-daemon`,
`@bb/host-daemon-contract`, `@bb/server`, `@bb/integration-tests`:

```
@bb/host-daemon-contract    49 passed (49)
@bb/host-daemon            526 passed (526)
@bb/db                     378 passed (378)
@bb/server                1405 passed (1405)
@bb/integration-tests       55 passed (55)
@bb/agent-runtime          894 passed | 1 failed (895)
```

`typecheck` and `lint` clean across all of them.

The single `@bb/agent-runtime` failure —
`runtime.process-lifecycle.test.ts > bounds provider stderr while data
arrives without a newline` — **fails identically on unmodified
`origin/main`** and is unrelated to this change.


---

Fixes #1320

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ymichael added a commit that referenced this pull request Aug 12, 2026
## Motivation

The four provider adapters (claude-code, codex, pi, acp) and three
bridges carry near-verbatim copies of the same machinery: stdio harness,
session registry, adapter identity/command/translation boilerplate, turn
lifecycle, item-id scoping, and tool item translation. That duplication
has a measurable history of making cross-provider work slow and lossy:

- **Manual compaction (#1152)** landed through 19 commits, more than 11
of them iterating the same feature provider-by-provider ("Make OpenCode
ACP compaction reliable", "Avoid false Pi compaction success", "Show ACP
compaction lifecycle", "Fix compaction lifecycle races", ...). One
feature, re-fought per adapter.
- **Rate-limit normalization (#1027)** only ever shipped to 2 of 4
providers. On main today, `ProviderRateLimitState` is referenced by
claude-code and codex translation only — Pi and ACP have zero
references. Duplication didn't just slow the change down; it silently
dropped half the matrix.
- **`classifyExecutionSettingsChange` (#1236)** had to touch all four
adapters to change one provider's behavior, because the member wiring is
copied everywhere.

After this PR, the shared machinery lives in
`packages/agent-runtime/src/shared/` and a cross-provider change of this
kind is a one-place change.

## Honest accounting

Repo-wide this is roughly **−300 lines net** (2,204 insertions, 2,512
deletions, all inside `packages/agent-runtime`). The ~1,640-line figure
from the de-slop audit counts *duplicated* lines, not deletable ones —
extracting a shared implementation still costs its one copy plus
parameterization. The value of this PR is **one-place-change for
cross-provider behavior**, not raw deletion.

## Commit-by-commit review guide

The 8 commits are logically staged; each is independently revertable and
behavior-preserving:

1. **`fff1c494c` share bridge stdio harness** — the three bridges'
identical stdin/stdout JSON-RPC loop (readline wiring, dispatch, error
replies) moves to `shared/bridge-harness.ts`.
2. **`e1e7615ba` share bridge session registry** — the per-bridge
`Map`-of-sessions bookkeeping (create/get/stop/dispose) moves to
`shared/bridge-session-registry.ts`.
3. **`66fee1a30` share bridge environment helpers** — env-override
construction/parsing dedup in `shared/adapter-utils.ts` (round-trip
covered by a new test), plus `shared/mime-types.ts`.
4. **`6e6e9fbc1` share standard adapter members** — the biggest one.
Each adapter's copied identity/launch members, `initialize` plan,
unsupported-command fallbacks, accepted-command translation, model-list
parsing, and tool-call decoding collapse into
`createStandardAdapterMembers()` (`shared/standard-adapter-members.ts`);
adapters keep only their provider-specific `buildProviderCommandPlan`.
Review focus: confirm each adapter's per-command plans are unchanged
(they are moved, not edited).
5. **`4c6a3c65b` centralize provider turn hooks** — per-adapter
`ensureXTurnStarted` wrappers and the copied error-envelope-to-events
helper become `onTurnStart`/`onTurnFinish` hooks and `buildErrorEvents`
on the shared turn-state registry; `shared/provider-error-events.ts` is
deleted.
6. **`276516e32` share scoped item id factories** — the copied
open/completed scoped-item-id resolution moves to
`shared/scoped-item-ids.ts`.
7. **`fbd671c2a` share tool item translation** — the copied
toolUse/toolResult item builders move to
`shared/tool-item-translation.ts`.
8. **`86a313d7b` remove dead exports and helpers** — deletes exports and
helpers made unreferenced by the extractions (verified by repo-wide
typecheck).

## Wire compatibility (server ↔ host daemon)

**No `HOST_DAEMON_PROTOCOL_VERSION` bump needed, and none is included.**
The diff is confined to `packages/agent-runtime`;
`packages/host-daemon-contract` (session payloads, WS messages, host RPC
commands/results) is untouched and the protocol version stays at 106 on
both sides. The refactor changes where adapter/bridge code lives, not
what crosses the wire: the 900+ adapter tests that assert exact
translated `ThreadEvent` output (ids, scopes, payloads) pass without any
expectation changes, so the event stream the daemon posts to the server
is byte-identical.

## Rebase provenance

This branch originated in archived thread `thr_jyia7bkdqn` and was
rebased onto current main (original authorship preserved in the
commits). The rebase crossed #1280 (sent-message editing) and #1321
(provider-unhandled-event fix), both of which touched the same adapters;
conflicts were resolved by preserving main's shipped behavior and
re-grafting it into the extracted structure:

- `thread/discard` command plans re-added per provider (acp: noop,
claude-code: `thread/stop`, pi: `thread/discard`) inside each
`buildProviderCommandPlan`.
- Claude Code's `latestProviderCheckpointId` reset moved into the shared
`onTurnStart` hook (fires only for newly opened turns, same guard as the
removed wrapper).
- `sourceProviderCheckpointId` forwarding re-added to the claude-code
and pi `thread/fork` plans.

The rebase delta vs the original branch inside `packages/agent-runtime`
is exactly the sum of #1280 + #1321 (+1,124/−36), i.e. nothing from main
was lost and nothing beyond the refactor was introduced.

## Verification

- `pnpm exec turbo run typecheck` — whole repo, 58/58 tasks pass.
- `pnpm exec turbo run test --filter=@bb/agent-runtime` — 907 passed, 1
failed: `runtime.process-lifecycle.test.ts > bounds provider stderr
while data arrives without a newline`, which fails identically on
unmodified origin/main (reproduced during this work; also documented as
pre-existing in #1321). The lifecycle suites
(`runtime.lifecycle.test.ts` 28/28, `runtime.process-lifecycle.test.ts`
21/21 excluding the pre-existing failure) covering the Codex recovery
paths pass.
- `pnpm exec turbo run test --filter=@bb/host-daemon` (the only
dependent of `@bb/agent-runtime`) — 541/541 pass.

> AGENT GENERATED: by Claude Fable 5

🤖 Generated with [Claude Code](https://claude.com/claude-code)
ymichael added a commit that referenced this pull request Aug 17, 2026
Third conformant bridge, same four moves (templates cab42e59a, 7c44fda2a,
df8ede082). translate-message.ts and the adapter's translation closures
move into event-translation.ts behind createClaudeEventTranslator
(byte-identity verified, deviations enumerated); interactions.ts and
session-params.ts extracted and shared by both dialects; the adapter
shrinks 1373 -> 413 lines. Hygiene needed only constant/error-detail gaps
- this bridge is where #853/#859 were fixed, and no test pinned a drop.

Handshake facts with evidence: sessionRestore true (SDK resume),
fork "checkpoint" (forkSession upToMessageId - also evidence for the
plan's fork/rewind merge candidate), manualCompaction FALSE (finding:
claude has no compact wire method; /compact rides prompt text),
approvalRequestPolicy "provider" (canUseTool pre-filters, #1236 - the
runtime skips reclassification via the handshake getter).

Canonical surface: per-session entropy translators, identity-before-
events, canonical options feeding the same applyLiveSessionSettings
machinery (providerOptions omission = keep current), release with no
fabricated interruption and NO background-task settlement (session stays
resumable, the opaqueTaskIds rule), interrupt keeps full settlement,
session/replaced wired at both rebuild sites with settlement strictly
preceding it, and approval/AskUserQuestion/ExitPlanMode all as canonical
interaction/request round trips.

Conformance 11/11. Suites 969 default + 252 isolated (966+251 baseline
all unchanged); daemon bundle builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

Background tasks are killed when user and system turns alternate

3 participants