Architecture review

whatsapp-llm-bot

2026-06-16 16:49 UTC. No CONTEXT.md or docs/adr entries found; vocabulary inferred from docs and modules.

solid box = module
dashed line = seam
->red arrow = leakage
dark box = deep module

Deepen the ACP client request channel

Strong ports & adapters

Files

harnesses/acp-runner.js harnesses/acp-filesystem-capability.js harnesses/acp-extension-router.js harnesses/protected-paths.js harnesses/sandbox-approval*.js tests/acp-harness.test.js

Before

startAcpRun
createAcpClientRequestHandler
permission
elicitation
terminal
filesystem
protected paths
sandbox checks
apply_patch snapshots
config options

Request policy leaks across the run execution module.

After

AcpClientRequestChannel

one interface: handle provider requests for a run

permission
elicitation
terminal
filesystem
protected paths
sandbox checks
startAcpRun keeps session execution and snapshot timing.

Problem

ACP run execution is shallow around provider requests; one file owns several policies that change for different reasons.

Solution

Move request handling into one deep module whose implementation owns permission, elicitation, terminal, filesystem, and approval policy.

Wins

  • locality: approval bugs concentrate
  • leverage: one request interface
  • tests hit provider requests
  • runner interface shrinks

Deepen WhatsApp runtime presentation

Strong in-process

Files

whatsapp/outbound/send-content.js whatsapp/tool-presenter.js whatsapp/tool-flow-presenter.js whatsapp/tool-presentation-model.js tests/sendBlocks.test.js tests/acp-*-presentation-vertical.test.js

Before

flowchart TD E[OutboundEvent] --> S[send-content.js] S --> R[runtime status maps] S --> C[compact tool state] S --> F[file-change rendering] S --> H[edit handles] S --> B[sendBlocks] R -.leak.-> H C -.leak.-> B F -.leak.-> B classDef leak stroke:#dc2626,stroke-width:2px,color:#dc2626; class R,C,F leak

After

sendEvent

WhatsAppRuntimePresenter

pinned status
compact activity
file changes
sendBlocks + edit handles

Problem

Runtime event presentation is deep behavior hidden in a broad WhatsApp sending module, so unrelated changes share state and tests.

Solution

Put runtime event formatting and state into one deep module; keep message delivery and edit handles as separate implementation.

Wins

  • locality: presentation state
  • leverage: one event interface
  • tests stop importing delivery
  • file-change rules concentrate

Collapse turn-start and follow-up orchestration

Worth exploring in-process

Files

conversation/create-conversation-runner.js conversation/turn-routing.js harnesses/run-coordinator.js conversation/build-harness-turn-input.js tests/conversation-runner-prompt-formatting.test.js tests/harness-run-coordinator.test.js

Before

decide route
pending follow-up branch
normal harness branch
slash clear branch
runStartedLlmMessage

Same run-start invariants appear in multiple branches.

After

small interface

ConversationTurnOrchestrator

route, persist, buffer, inject, start, replay

Problem

The runner has deep helper modules, but the orchestration interface is still spread across route branches and live-input special paths.

Solution

Consolidate run-start and follow-up policy behind one module that returns the next action for the runner to execute.

Wins

  • locality: run invariants
  • leverage: route branches shrink
  • tests hit turn interface
  • less branch duplication

Extract the HTTP transport turn ledger

Speculative local-substitutable

Files

http-api-transport.js docs/api-transport.md tests/http-api-transport.test.js index.js

Before

flowchart LR H[HTTP routes] --> T[turn maps] H --> E[event array] H --> S[SSE clients] H --> N[payload normalizers] T -.leak.-> E E -.leak.-> S classDef leak stroke:#dc2626,stroke-width:2px,color:#dc2626; class T,E,S leak

After

HTTP request router

HttpTransportTurnLedger

idempotency, status, active turns, event cursor, text accumulation

SSE subscription adapter

Problem

The HTTP transport is currently acceptable, but state policy and wire handling are one module, limiting locality if media or durable events arrive.

Solution

Extract the turn ledger only when another adapter or durable implementation justifies the seam.

Wins

  • locality: request id policy
  • leverage: durable adapter later
  • tests hit ledger interface
  • HTTP module shrinks

Top recommendation

Deepen the ACP client request channel

It has the clearest leverage: one provider request interface can absorb permission, elicitation, terminal, filesystem, protected-path, and sandbox policy while preserving the existing ACP run interface and tests.