Skip to content

WIP: superseded ACP producer correlation candidate (#805 → #806) - #1183

Draft
sethkarten wants to merge 21 commits into
mainfrom
v080/acp-806-reviewed
Draft

WIP: superseded ACP producer correlation candidate (#805 → #806)#1183
sethkarten wants to merge 21 commits into
mainfrom
v080/acp-806-reviewed

Conversation

@sethkarten

@sethkarten sethkarten commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Historical P2 candidate — superseded

This draft is frozen at 4673116c6debf5fe75650784cdefe9178078fc5b for audit history. It is not the current P2 stack child and must not be merged.

Current exact stack:

  1. P1: test(acp): preserve resident recovery on final core #1236 at c3ab0a13ff164598ac42c3cca2bc426cc5cccd68
  2. P2: feat(acp): preserve causal prompt correlation #1239 at 6dd9e86b75a18a13a7139631a679fb25593645d1, tree 149ac0ccee3df28688bbd7d3d15ab7dbec3bd0c0

No live or paid evaluation was run.

sethkarten and others added 21 commits August 10, 2026 14:31
Two reviewers independently caught that `clientOwned: appMode !== "acp"` made
every ACP session resident, including `--no-session`.

A no-session run has no session file, so nothing can later reattach to it.
Marking it resident means dispose() sends detach rather than completing the
session, and the worker survives with no way to reclaim it -- a leak per ACP
invocation. Interactive mode already passes `clientOwned: parsed.noSession` for
this reason.

ACP now requests resident only when it has a session to reattach to, which is the
path this branch exists to enable; `--no-session` keeps the previous
client-owned lifetime.
`launchEnv` was gated on `clientOwned`, so making ACP resident would have started
its worker without the caller's environment.

That gate was harmless while every ACP session was client-owned. It is not
harmless now: the daemon still launches the worker for a resident session, and an
embedder passes what the worker needs through that environment. The verifiers ACP
harness supplies the model endpoint, its bearer token, and proxy settings exactly
this way, so a resident worker would come up unable to reach the model and every
rollout would fail with a connection error that looks like a provider outage.

`collectDaemonLaunchEnv()` already strips the `PRIME_AGENT_INTERNAL_` role
variables, which is the reason the gate existed, so forwarding it unconditionally
is safe for both lifecycles.

Found by tracing a live verifiers E2E failure rather than by review: the released
0.7.0 that CI installs still uses client-owned ACP, so this would only have bitten
after this branch shipped.
The supervisor discarded launchEnv unless the session had an owner client:

  const launchEnv =
      ownerClientId || existing?.descriptor.ownerClientId
          ? (command.launchEnv ?? existing?.launchEnv)
          : undefined;

A resident session has no owner client, so a resident worker launched without the
caller's environment. Making the client always send launchEnv (earlier on this
branch) was therefore not enough: the two halves disagreed and the worker still
came up with no model endpoint, no bearer token, and no proxy settings, which
surfaces as a provider connection error rather than a configuration fault.

Ownership governs worker LIFETIME, not whether the launch environment is honored,
so launchEnv is now kept whenever the command supplies it, retaining the fallback
to the existing worker's env on recovery.

The added test asserts the resident worker PROCESS actually observes the env --
an extension writes the received value to disk -- rather than checking the field
was passed along.

Note: daemon-supervisor-process.test.ts already fails 8/14 locally on macOS
(these spawn real supervisors over unix sockets under /var/folders); the same
baseline failures occur without this change, so CI on Linux is the signal here.
The roster fix added coverage for a snapshot failure during session/new, but not
for one at emission time. Those are separate call sites, and only the first was
guarded: re-adding a swallowing `.catch(() => undefined)` on the emission read
still passed the suite.

That is the failure mode the metadata exists to prevent. Degrading to an empty
roster reports `outstandingSubagents: 0` while children are still running, so a
consumer scores a turn that has not finished -- wrong, and silently so.

The new test drives the emission read to fail and asserts the prompt rejects
rather than answering a clean end_turn with fabricated quiescence. Re-adding the
swallow now fails it.
Comment on lines +497 to 498
if (abort.signal.aborted) return { stopReason: "cancelled" satisfies AcpStopReason };
await connection.promptAndWait(text, images.length > 0 ? { images } : undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium acp/acp-mode.ts:497

Two cancellation branches in session/prompt return { stopReason: "cancelled" } without awaiting entry.producer.drain(), unlike every other cancellation branch. If session/cancel arrives while getMessages() or getInitialSnapshot() is pending and subscription events have already queued producer notifications, the prompt response is sent before those updates are delivered, so they appear after the terminal prompt response and break the producer's update-before-response ordering. The missing await entry.producer.drain() calls are on the early return after connection.getMessages() and on the final pre-terminal return after the live snapshot read. Add await entry.producer.drain() before both returns.

				if (abort.signal.aborted) {
+					await entry.producer.drain();
 					return { stopReason: "cancelled" satisfies AcpStopReason };
 				}
 				await connection.promptAndWait(text, images.length > 0 ? { images } : undefined);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/acp/acp-mode.ts around lines 497-498:

Two cancellation branches in `session/prompt` return `{ stopReason: "cancelled" }` without awaiting `entry.producer.drain()`, unlike every other cancellation branch. If `session/cancel` arrives while `getMessages()` or `getInitialSnapshot()` is pending and subscription events have already queued producer notifications, the prompt response is sent before those updates are delivered, so they appear after the terminal prompt response and break the producer's update-before-response ordering. The missing `await entry.producer.drain()` calls are on the early return after `connection.getMessages()` and on the final pre-terminal return after the live snapshot read. Add `await entry.producer.drain()` before both returns.

}
const previousDescriptor = worker.descriptor;
worker.descriptor = { ...previousDescriptor, ownerClientId: undefined };
worker.descriptor = { ...previousDescriptor, ownerClientId: undefined, launchEnv: undefined };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High daemon/daemon-supervisor.ts:2092

promoteOwnedWorker clears launchEnv entirely when promoting a client-owned worker to resident, so a crash/restart after promotion loses the owner's persisted allowlisted environment settings (e.g. PATH, HOME, PI_PACKAGE_DIR, PI_OFFLINE). The recovered worker then launches with only the supervisor's ambient environment, which can select the wrong CLI or fail to relaunch. In launchWorker, resident recovery reads existing.descriptor.launchEnv, which was set to undefined during promotion. Consider retaining the persisted allowlisted subset via filterPersistedDaemonLaunchEnv(worker.launchEnv) instead of discarding all launch settings.

-		worker.descriptor = { ...previousDescriptor, ownerClientId: undefined, launchEnv: undefined };
+		worker.descriptor = { ...previousDescriptor, ownerClientId: undefined, launchEnv: filterPersistedDaemonLaunchEnv(worker.launchEnv) };
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/daemon/daemon-supervisor.ts around line 2092:

`promoteOwnedWorker` clears `launchEnv` entirely when promoting a client-owned worker to resident, so a crash/restart after promotion loses the owner's persisted allowlisted environment settings (e.g. `PATH`, `HOME`, `PI_PACKAGE_DIR`, `PI_OFFLINE`). The recovered worker then launches with only the supervisor's ambient environment, which can select the wrong CLI or fail to relaunch. In `launchWorker`, resident recovery reads `existing.descriptor.launchEnv`, which was set to `undefined` during promotion. Consider retaining the persisted allowlisted subset via `filterPersistedDaemonLaunchEnv(worker.launchEnv)` instead of discarding all launch settings.

@sethkarten

Copy link
Copy Markdown
Contributor Author

v0.8 stack checkpoint — 2026-08-11 15:50 UTC

This draft PR remains the GitHub ACP/Evaluation stack surface and will be kept current.

  • Parent Core candidate: 41ef592f8474a1d67b25ee6e495df24be3ab804e (feat(coding-agent): finalize v0.8 Core lifecycle and project MCP hardening #1224), exact CI green.
  • P1 test/docs/CI reconstruction on Core 41ef is staged with zero production-source diff. Biome, typecheck, protocol/recovery tests, and the package-CWD kernel route pass 5 files / 15 tests.
  • Evidence receipt SHA-256: fccce01339b91f207db6956f073f7334ba800911fdf5d70f5978f1ed4c6d30e8.
  • Next: explicit independent approval, append-only P1 commit, manual P2 lifecycle reconciliation, full review, then non-force advancement of this PR branch.
  • Paid/live EmulatorBench evaluation remains held pending immutable artifact/model/task/seed/pricing/cap/approval evidence.

This is a progress checkpoint, not a readiness claim.

@sethkarten sethkarten changed the title WIP: ACP producer correlation reviewed candidate (#805 → #806) feat(acp): preserve causal prompt correlation Aug 11, 2026
@sethkarten
sethkarten changed the base branch from main to v080/acp-p1-final-core August 11, 2026 17:08
@sethkarten sethkarten changed the title feat(acp): preserve causal prompt correlation WIP: superseded ACP producer correlation candidate (#805 → #806) Aug 11, 2026
@sethkarten
sethkarten changed the base branch from v080/acp-p1-final-core to main August 11, 2026 17:08
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