Skip to content

Add bb thread import for existing ACP sessions - #1047

Open
mgpai22 wants to merge 14 commits into
get-bb:mainfrom
mgpai22:mgpai22/acp-session-import
Open

Add bb thread import for existing ACP sessions#1047
mgpai22 wants to merge 14 commits into
get-bb:mainfrom
mgpai22:mgpai22/acp-session-import

Conversation

@mgpai22

@mgpai22 mgpai22 commented Aug 6, 2026

Copy link
Copy Markdown

Implements #1028.

Adds bb thread import, which adopts an existing external agent session as a bb thread: the replayed history lands in the timeline, and the thread continues that same provider session.

bb thread import --project proj_x --provider acp-omp \
  --provider-session 019fd3b5-baf1-7000-b953-df71341a47d3 \
  --cwd /path/to/repo

How it works

The ACP bridge already calls session/load when resuming a bb-created thread, and drops the replayed session/update stream behind the session.loading gate. That is correct for resume, because bb already holds that history.

Import reuses the same call with a caller-supplied session id, and forwards the replay instead of dropping it. Replayed events are marked historical: they persist as timeline events but skip turn lifecycle effects, so the thread lands idle without running a turn. Live turns after that behave normally.

Also included:

  • supportsSessionImport, derived from agentCapabilities.loadSession at initialize, with a live per-agent probe on top of the static ACP-family default. Providers without it are refused.
  • cwd validation: the asserted session cwd must match the project source or an existing project workspace.
  • A duplicate-binding guard: two threads cannot bind the same provider session (409, plus a bridge-side check).
  • Contract, route, SDK verb, CLI command, and the bb-cli skill doc.

HOST_DAEMON_PROTOCOL_VERSION goes to 75. thread.start gained the sessionImport descriptor and provider.list_models gained the optional supportsSessionImport result field; an old daemon would drop the descriptor and silently start a fresh, history-less session.

Verification

Tests cover the bridge (replay persisted, unsupported agent refused, load failure surfaced), adapter translation, and the route (cwd mismatch, unsupported provider) against real in-memory sqlite.

Live run against omp 17.2.9: a 2,404-message session imported as 1,994 persisted events (993 completed items, including tool calls and reasoning), thread idle, no model calls. Bogus session id and cwd mismatch both refuse with a clear error.

Known limits

  • Replayed history lands as one synthetic historical turn. ACP replay carries no turn boundaries, so adjacent assistant messages merge unless a user message or tool call separates them.
  • The session cwd is caller-asserted. session/load takes cwd as input and it cannot be probed from the agent, so validation is against the project's own paths.
  • No UI surface. The capability is exposed in the providers API for gating.
  • Fork of an imported thread is unchanged, so still unsupported for ACP.

Two commits touch test infrastructure unrelated to the feature: a @bb/server vitest timeout raise, and hermeticity fixes in install-machine-script (a globally installed bb-app on PATH leaks into the test that asserts its absence) and internal-skill-trees (hardcoded 0644 vs a 0002 umask). Happy to split those out.

Copilot AI lite review requested due to automatic review settings August 6, 2026 03:12

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

mgpai22 added 13 commits August 6, 2026 03:15
Adds supportsSessionImport to provider capabilities (true for ACP
providers, whose session/load maps to import; verified live at session
open), the /threads/import request contract, the thread.start
sessionImport wire descriptor, and the historical marker on turn framing
events so replayed history persists without lifecycle side effects.
Bumps HOST_DAEMON_PROTOCOL_VERSION to 72 for the thread.start payload
change.
POST /threads/import validates the imported session's cwd against the
project source (or an existing project workspace) and creates a thread
carrying a sessionImport descriptor through provisioning to thread.start.
The runtime maps it to a new thread/import adapter command; the ACP
bridge opens the session via session/load (no fresh-session fallback,
clear errors when the agent lacks loadSession or the load fails) and
forwards the replayed history as historical updates instead of dropping
them behind the loading gate. Historical turn framing settles the thread
idle with no live turn state or server lifecycle effects, and replayed
user_message_chunk updates become userMessage items.
bb thread import --project --provider --provider-session creates a
thread bound to an existing external ACP session, with optional --host,
--cwd, --title, --permission-mode, and --visibility. The SDK exposes
threads.import and the bb-cli skill documents the workflow.
The fake ACP agent can now fail session/load and replay a scripted
history during it. Bridge tests prove the replay is forwarded as
historical updates, that imports refuse agents without session/load, and
that load failures surface a clear error instead of a fresh session.
Adapter tests cover the historical replay translation; route tests cover
the sessionImport thread.start dispatch, cwd mismatch refusal, and the
capability gate.
The skill-tree route test hardcoded mode 0o644 for a file written with
default permissions, which only holds under a 0022 umask; write it
explicitly instead. The install-machine-script fallback test assumed
no bb-app binary is reachable on PATH, which a machine with a real
global bb-app install breaks; give the script a hermetic PATH that
strips any entry resolving bb-app while still exposing node.
…agents

Refuse importing a provider session another live thread already binds
(server-side 409 via a new findLiveThreadIdByProviderThreadId reverse
lookup, plus a bridge-side guard so the process-local routing map is
never silently overwritten). Derive supportsSessionImport from the
agent's live `initialize` handshake instead of trusting the static
ACP-family constant, so an agent without session/load is refused before
an environment is provisioned and a doomed thread.start is dispatched;
this needed a new provider.list_models result field, bumping
HOST_DAEMON_PROTOCOL_VERSION.

Also close two historical-replay gaps: exclude historical turn/completed
rows from start-activation staleness so a replayed frame can never strand
a thread in "starting", and close the synthetic historical turn with a
cancelled turn/completed when session/load fails after a partial replay
so it doesn't stay open forever.
The in-CLI guide (bb-guide-threads.md) documented Forking but never
gained an Importing section when the import verb shipped, so agents
reading it in-app never learn the command exists. Add it alongside the
other flags in the same style, and regenerate the derived template and
plugin-sdk bundle outputs.
The suite mixes fast unit tests with fixtures that shell out for real
git clone / npm install / esbuild work (plugin install, bb-app
artifact builds). Under full-parallel turbo runs, those fixtures can
exceed vitest's default 5s budget even though nothing is hung, which
was tripping a rotating, unrelated subset of tests on every full gate
run. Sibling packages already raise this for the same reason
(plugin-registry, host-daemon, app); do the same here.
…nd lossy replay

Move the duplicate-binding check before session/load so a rejected import
never forwards history for a provider session it won't end up owning, and
close any historical turn that still slips through a later race instead of
leaving it open forever. Probe supportsSessionImport for ACP agents whose
model list comes from a CLI command too, since they previously never reached
the session-discovery path that populates the capability cache. Replace
dropped non-text replayed user message chunks with a placeholder instead of
losing them, since a later resume drops replay entirely. Require an explicit
--cwd for `bb thread import`: bb has no way to read the external session's
real working directory back from it, so a silently-defaulted cwd made the
mismatch refusal unreachable for the common case. Add a runtime-level test
covering the historical-replay bypass that skips turn/background/idle/goal
state machines for imported history.
findLiveThreadIdByProviderThreadId ordered by events.sequence, a per-thread
counter, so across multiple matching threads it picked whichever had logged
the most events rather than the one most recently touched. Order by
createdAt instead. The lookup also had no supporting index and scanned the
full events table on every thread import; add a partial index on
provider_thread_id.
…import

A stranded replayed user message survived a torn-down historical turn
(e.g. thread/stop racing ahead of the trailing turn/completed) and leaked
into the next live turn as a phantom userMessage that was never actually
sent; clear the accumulator alongside the other per-turn state.

Concurrent thread/import requests for the same provider session both
passed the pre-load unbound check (neither was bound yet) and both
forwarded replayed history before the loser was rejected. Reserve the
provider session id right after the pre-load check, before session/load
goes out, so a losing concurrent import fails before it can dispatch
session/load at all.

The session/load capability cache expired independently of, and earlier
than, the model-discovery cache it's learned alongside, silently dropping
supportsSessionImport from model/list in the gap. Re-stamp both caches
together. Also add negative caching and a bounded timeout to the
CLI-agent capability probe so a broken or slow agent isn't re-spawned on
every model/list call and can't push an otherwise-fast CLI catalog reply
toward the server's command timeout.

The capability probe resolved its launch spec from the static known-agent
table only, so a custom ACP agent shadowing a built-in provider id was
probed against the wrong binary, and a purely custom agent skipped the
probe entirely and fell back to the static ACP-family allow. Share the
same custom-agent-aware resolution thread.start uses.

Fix the @bb/server-contract allowlist claiming importThreadRequestSchema.cwd
is optional when the schema requires it, and cover the previously-untested
workspace and cross-project branches of the import cwd check.
@SawyerHood

Copy link
Copy Markdown
Collaborator

TY for the pr @mgpai22 will take a look tomorrow

@mgpai22
mgpai22 force-pushed the mgpai22/acp-session-import branch from 1fa96a2 to d624d6d Compare August 6, 2026 03:34
@mgpai22 mgpai22 changed the title feat: acp session import Add bb thread import for existing ACP sessions Aug 6, 2026
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.

3 participants