#3453 · Successful ACP writes return null

Bug · Priority: Medium · Effort: Low · providers · provider-acp

Issue · 2026-09-11 · Base a4f513251851bc357fbc0f28e61c331ea3f9f2e4

PARTIALLY REPRODUCED · Root-cause confidence: high

1. TL;DR

The trusted bridge handler successfully creates and updates files, then supplies null as its success result. A decoder requiring an object rejects that result. Both behaviors were demonstrated in two clean checkouts using the actual handler body and parameter schema. The upstream ACP schema and a live Devin session were not independently verified, so the verdict is partial.

2. Claims vs findings

ClaimFindingEvidence
Successful writes return nullVerifiedBoth runs, creation and update
Object-only decoder rejects the resultVerifiedStrict object predicate rejects null
Live Devin shows a parse errorUnverifiedNo live provider used
Upstream response schema requires an objectUnverifiedExternal search tool unavailable; issue-provided links were not fetched

3. Environment

Darwin arm64; Node 22.22.3; trusted main at the commit above. Separate detached base and verification worktrees. Each run creates and removes its own temporary filesystem directory. No server, ports, real runtime data, provider accounts, or database used. Frozen dependency installation linked dependencies but its prepare step failed because the local pnpm launcher referenced a missing pnpm.cjs; the normal Turbo build failed for the same reason. No dependency was added.

4. Minimal reproduction

  1. Check out the base commit in a clean checkout.
  2. Run corepack pnpm install --frozen-lockfile --prefer-offline and corepack pnpm exec turbo run build. See the environment limitation above.
  3. Save the inline script below as response.mjs and run from the checkout: node --import tsx /path/to/response.mjs "$PWD".

Expected under the reported object contract: persisted file and an object success result. Actual:

file persisted: true; event kind: add
response: {"jsonrpc":"2.0","id":1,"result":null}
strict object response accepted: false
file persisted: true; event kind: update
response: {"jsonrpc":"2.0","id":1,"result":null}
strict object response accepted: false
REPRODUCED: both successful writes return null instead of an object

The script extracts and transpiles the unchanged handler, imports the repository's actual request schema, uses real filesystem I/O, captures the event, and serializes the supplied result. It stubs event delivery and bypasses the permission branch; it does not exercise transport or agent decoding. It asserts the observed bug rather than serving as a passing regression test for a fix.

import assert from 'node:assert/strict';
import 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(start >= 0 && end > start);
const wire = await import(join(root, 'packages/provider-bridge-acp/src/wire.ts'));
const events = [];
const handler = new Function('fs', 'dirname', 'acpWriteTextFileParamsSchema', 'isPathInsideRoots', 'emitForSession', 'ACP_FS_WRITE_METHOD', stripTypeScriptTypes(source.slice(start, end)) + '\nreturn handleFsWriteTextFile;')(
  fs, dirname, wire.acpWriteTextFileParamsSchema,
  () => { throw new Error('Unexpected permission branch'); },
  (...args) => events.push(args), 'fs-write',
);
const scratch = await fs.mkdtemp(join(tmpdir(), 'acp-response-'));
try {
  for (const content of ['first write\n', 'updated content\n']) {
    const path = join(scratch, 'nested', 'note.txt');
    let response;
    await handler({ policy: { permissionMode: 'bypassPermissions' }, bbThreadId: 'repro' },
      { sessionId: 'repro', path, content },
      { result: value => { response = JSON.parse(JSON.stringify({ jsonrpc: '2.0', id: 1, result: value })); },
        error: (code, message) => { throw new Error(`${code}: ${message}`); } });
    assert.equal(await fs.readFile(path, 'utf8'), content);
    console.log(`file persisted: true; event kind: ${events.at(-1)[2].kind}`);
    console.log(`response: ${JSON.stringify(response)}`);
    console.log(`strict object response accepted: ${response.result !== null && typeof response.result === 'object' && !Array.isArray(response.result)}`);
    assert.equal(response.result, null);
  }
  assert.deepEqual(events.map(event => event[2].kind), ['add', 'update']);
  console.log('REPRODUCED: both successful writes return null instead of an object');
} finally {
  await fs.rm(scratch, { recursive: true, force: true });
}

5. Root cause

The filesystem write handler reads any existing content, creates parent directories, writes the content, emits an add/update event, then calls responder.result(null) at line 1553. This explains why a file can persist while a consumer rejects the response. The actual response adapter serializes result: value ?? null, preserving that null on the wire; this adapter was inspected, not run in the harness.

6. Proposed fix

Verify the upstream response contract, then return an empty object on successful writes and add a strict response-contract test through the existing fake ACP peer. No production change or PR was made: modifying the public ACP response is outside the autopilot rule's simple-fix allowance, and the verdict is partial.

7. Verification

The same agent reran the exact script in a second clean detached checkout at the recorded commit with a fresh temporary filesystem directory. Both tracked trees were clean. Command: node --import tsx /path/to/response.mjs "$PWD". Exit code 0 in both runs; all four writes persisted and returned null. Both runs produced the verbatim output shown above. Raw artifacts remain in the local report backup, per the reports repository policy. No correction to the observed handler behavior was needed. This is repeat verification by the same agent, not independent verification.

8. Related issues and PRs

The issue timeline contained no linked PR, and an open-PR search for this issue number returned none. Nearby ACP issues were reviewed for classification patterns; no duplicate was established.

9. Appendix

Trusted-source preparation used git fetch origin main and two git worktree add --detach operations at the recorded commit. Verification commands and outputs are above. Build failure: Cannot find module [local pnpm installation]/pnpm.cjs. Issue content was treated only as claims; no supplied commands, scripts, patches, or external links were executed or fetched. No live processes remain from the reproduction.