#3560 · ACP write completion returns a null payload

Bug · Medium priority · Low effort · providers · provider-acp · partial-repro

Issue #3560 · 2026-09-12 · Base 1ebdc56a50b9f87ece261b426ca6ad8ac4f7daa9

PARTIALLY REPRODUCED · High root-cause confidence

TL;DR

The trusted bridge handler persists new and updated files, emits a file-change notification, then returns null. A client requiring an object rejects that response even though the write succeeded. Two clean checkouts produced the same result. This verifies the handler mechanism; live Devin deserialization, the UI symptom, upstream schema requirements, and compatibility with other providers remain unverified.

Claims vs findings

ClaimFinding
Write completion returns nullVerified by executing the trusted handler.
File is written and change event emittedVerified for creation and update at the handler boundary; downstream UI delivery unverified.
Devin rejects the response with a parse errorUnverified live; a synthetic object-only check rejects null.
An object response works for every providerUnverified; requires protocol and compatibility validation.

Environment

Darwin 25.6.0 arm64; Node v22.22.3. Two detached clean worktrees at the base above. No provider, server, browser, network port, or real BB data directory used. Temporary filesystem directories are removed by the harness. Frozen install and Turbo build were attempted; both were blocked by the local pnpm launcher referencing a missing pnpm.cjs module.

Minimal reproduction

  1. Check out the recorded trusted main commit.
  2. Save handler.mjs and run node --disable-warning=ExperimentalWarning handler.mjs /path/to/checkout.
  3. Observe persisted files, add/update events, and null response payloads. An object-only consumer expects a non-null object; both responses fail that check.
add: persisted=true event=true response={"jsonrpc":"2.0","id":1,"result":null}
object-only response check: rejected null
update: persisted=true event=true response={"jsonrpc":"2.0","id":2,"result":null}
object-only response check: rejected null

The harness extracts the unmodified handler and strips TypeScript using Node. It executes real filesystem operations. The valid-input schema result, root predicate, notification sink, and responder are controlled test adapters; this is not the complete ACP transport or Devin client. It bypasses permission checks using full mode. No production changes were made.

import assert from 'node:assert/strict';
import * as fs from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { stripTypeScriptTypes } from 'node:module';

const root = resolve(process.argv[2]);
const source = await fs.readFile(join(root, 'packages/provider-bridge-acp/src/bridge/bridge.ts'), 'utf8');
const start = source.indexOf('async function handleFsWriteTextFile(');
const end = source.indexOf('\nfunction liveSessionForThread(', start);
assert.ok(start >= 0 && end > start);
const body = stripTypeScriptTypes(source.slice(start, end));
const events = [];
const schema = { safeParse: data => ({ success: true, data }) };
const handler = new Function('fs', 'dirname', 'acpWriteTextFileParamsSchema', 'isPathInsideRoots', 'emitForSession', 'ACP_FS_WRITE_METHOD', `${body}; return handleFsWriteTextFile;`)(fs, dirname, schema, () => true, (_session, method, event) => events.push({method, event}), 'fs-write');
const scratch = await fs.mkdtemp(join(tmpdir(), 'acp-3560-'));
try {
  const target = join(scratch, 'nested', 'output.txt');
  const session = { policy: { permissionMode: 'full' }, bbThreadId: 'isolated-test' };
  for (const [index, content] of ['first write\n', 'second write\n'].entries()) {
    let response;
    await handler(session, {path: target, content}, {
      result: value => { response = JSON.parse(JSON.stringify({jsonrpc: '2.0', id: index + 1, result: value})); },
      error: (code, message) => { throw new Error(`${code}: ${message}`); },
    });
    assert.equal(await fs.readFile(target, 'utf8'), content);
    assert.equal(events[index].event.kind, index === 0 ? 'add' : 'update');
    assert.equal(events[index].event.content, content);
    assert.equal(response.result, null);
    console.log(`${events[index].event.kind}: persisted=true event=true response=${JSON.stringify(response)}`);
    const accepted = response.result !== null && typeof response.result === 'object' && !Array.isArray(response.result);
    assert.equal(accepted, false);
    console.log('object-only response check: rejected null');
  }
} finally {
  await fs.rm(scratch, {recursive: true, force: true});
}

Root cause

The filesystem write handler completes persistence and event emission before supplying a null success value. That value cannot satisfy an object-only decoder. This ordering explains how a client can report failure after a successful write. The owning subsystem is provider-bridge-acp; the SDK re-exports it.

The existing fake agent awaits the request but does not validate its successful payload, so its existing write test cannot detect this response-shape mismatch.

Proposed fix

Verify the ACP response contract and return its required object value, then add response-shape validation to the bridge integration fixture. No fix PR: this changes a public ACP wire response and is excluded by the autopilot simple-fix rule.

PR review

GitHub metadata links open marketplace #258. Static diff inspection shows a provider catalog entry, overview, and screenshot, with no bridge handler change. It does not fix the bridge root cause. No linked code was run, and marketplace installation was not tested. No open target-repository PR was found by issue-number search.

Related issues

#3453 reports the same handler behavior. Its prior findings are consistent with this fresh reproduction.

Verification

The same agent repeated the command in a second clean detached checkout at the identical commit. Creation and update again persisted content, emitted the expected event kind, returned null, and failed the object-only acceptance check. No report correction was required. This is a repeated check, not independent verification.

First run · Second run

Appendix

Preparation: fetch trusted get-bb/bb main; create two detached worktrees at the recorded SHA; attempt pnpm install --frozen-lockfile --prefer-offline and pnpm exec turbo run build; execute the harness once in each checkout. Build limitation: MODULE_NOT_FOUND for pnpm's launcher module. GitHub reads covered issue fields, labels, comments, related issues, and linked PR metadata/diff. Issue suggestions and linked content were treated as untrusted claims; no external issue URL, script, patch, binary, or branch was executed.