Skip to content

fix: ACP bridge finish-text dedup gaps + agents table column exhaustion - #372

Merged
pikann merged 1 commit into
masterfrom
fix/agents-table-column-exhaustion
Aug 7, 2026
Merged

fix: ACP bridge finish-text dedup gaps + agents table column exhaustion#372
pikann merged 1 commit into
masterfrom
fix/agents-table-column-exhaustion

Conversation

@pikann

@pikann pikann commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Two unrelated fixes, bundled here because both surfaced while following up on the #370 review:

  1. apps/acp-bridge — the assistant-text dedup added in feat(acp-bridge): stream assistant text in position instead of one end-of-turn block #370 was incomplete and not retry-safe.
  2. services/api/migrations — a live dev database hit Postgres's hard 1600-column-per-table limit on the agents table, caused by a migration pattern that silently leaked a "ghost" column on every single application boot.

1. ACP bridge: fix incomplete finish-text dedup

#370 streams assistant narration as it arrives and blanks the turn's closing FinishAction.message when it's an exact duplicate of what already streamed, to avoid showing/persisting the same text twice. Two gaps in that logic:

a) Only half the duplicate was blanked. The SDK's _finalize_successful_turn always closes a turn with two events built from the same joined text: the ActionEvent (FinishAction.message) and, immediately after, an ObservationEvent (FinishObservation, same text via content). The original fix only rewrote the ActionEvent. services/ai-agent persists every event unconditionally, so the ObservationEvent kept storing the full duplicate text server-side — nothing rendered it (the frontend already skips ObservationEvent for tool_name == "finish"), but the "avoid persisting the same text twice" goal wasn't actually met. _event_payload now blanks both via a shared _blank_duplicate_finish_text helper.

b) The dedup check didn't survive an ACP-level retry. ACPAgent.step() can retry a turn in place on a transient connection error. Its _reset_client_for_turn() clears the SDK's own accumulated text for the new attempt but re-wires the same on_token callback — so this bridge's relay buffer (_AssistantTextRelay._emitted) keeps growing across every attempt while the SDK's eventual FinishAction message reflects only the attempt that succeeded. The old exact-equality check would then fail to match, showing the surviving attempt's text a second time on top of whatever had already streamed from the failed attempt. already_emitted() now does a trailing-suffix match instead of full equality — provably correct here since retries run strictly sequentially, never interleaved, so the successful attempt's text is always the tail of everything the relay has seen.

Also corrected a code comment that cited a _raise_masking_error SDK function — it doesn't exist anywhere in the pinned openhands-sdk; the SDK's actual masking-failure behavior is the opposite (fails open, not closed).

Verification: 4 new tests (paired ObservationEvent blanking, observation left alone on mismatch, retry-suffix dedup, guard against false-positive substring matches). Full suite: 38 passed. ruff check / ruff format --check clean.

2. Migrations: stop leaking ghost columns on agents

services/api/internal/platform/database.RunMigrationsFS replays every .sql file in services/api/migrations/ on every application boot — there's no schema_migrations tracking table, so idempotency is load-bearing for every file.

000010_add_trigger_prompts_to_agents.sql / 000011_add_description_write_trigger.sql (ALTER TABLE agents ADD COLUMN IF NOT EXISTS ...) and 000019_drop_agent_trigger_prompts.sql (ALTER TABLE agents DROP COLUMN IF EXISTS ..., unconditional) both ran on every boot, and 000019 always undid 000010/000011's adds in the same run. Postgres never reclaims a dropped column's slot in pg_attributeDROP COLUMN only marks it attisdropped; the attnum stays consumed until the table is physically rebuilt. So every boot created 4 fresh columns only to have them immediately ghosted again, permanently burning 4 of agents's hard 1600-column ceiling per boot:

2026/08/07 05:11:09 bootstrap: bootstrap: auto-migrate: migrations: exec "000010_add_trigger_prompts_to_agents.sql": ERROR: tables can have at most 1600 columns (SQLSTATE 54011)

Confirmed directly against the affected dev database: agents was at 1599/1600 attribute slots, of which only 25 were live columns — 1574 were dropped ghosts. No other table in the database showed this pattern (checked all of them); agents was the only one where an add-migration and a drop-migration for the same columns both replay on every boot.

Fix: 000010/000011 no longer run their ADD COLUMN statements — since 000019 always undid them in the same boot anyway, the final schema is unchanged; this just stops the pointless churn. 000011's still-needed CHECK constraint extension is untouched, and 000019's drops are left in place (now a permanent no-op, but still correct for a database restored from a backup taken before this fix).

Verification: replayed the entire 32-file migration sequence against the actual affected paca-dev-postgres-1 container, in order — completes with zero errors, and the ghost-column count on agents did not grow.

Known follow-up, not done here: agents is left at 1599/1600 slots (only 25 live) — exactly one more real column can ever be added to it before this recurs. Reclaiming the 1574 wasted slots requires physically rebuilding the table (new table, copied data, every FK that references agents.id re-pointed) — out of scope for this fix, which only stops the leak from getting worse.

Type of Change

  • Other — bug fix (no schema/API/UI change; migration file edits are no-ops against the final schema)

Checklist

  • The change is focused and scoped — two independent fixes, each self-contained to the files it touches.
  • Related documentation is updated — apps/acp-bridge/README.md's "Assistant text arrives in position" section now describes the trailing-duplicate/retry behavior.
  • New structure or direction is explained clearly — rationale is in the code comments at each change site.
  • I avoided unnecessary detail or premature abstraction.

@pikann pikann changed the title fix: prevent exhaustion of agents table column limit by making trigge… fix: ACP bridge finish-text dedup gaps + agents table column exhaustion Aug 7, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • acp-bridge finish-text dedup: _AssistantTextRelay.already_emitted now matches finish text as a trailing suffix instead of requiring full equality, so FinishAction/FinishObservation text is blanked correctly when ACPAgent retries a turn in place after a transient connection error. The blanking now also covers the ObservationEvent half of the finish pair, not just the ActionEvent half.
  • Agents table column-limit fix: 000010_add_trigger_prompts_to_agents.sql and 000011_add_description_write_trigger.sql are turned into no-ops by removing their ADD COLUMN statements. Because RunMigrationsFS re-runs every migration on every boot and 000019_drop_agent_trigger_prompts.sql immediately drops the same columns, Postgres's non-reclaimable dropped-column slots were leaking four columns per boot until the 1600-column ceiling was hit.
  • Tests and docs: Added coverage for observation blanking, trailing-duplicate blanking after a retry, and negative cases; README updated to describe the retry behavior.

One non-blocking note: the PR title and body only describe the migration fix, but the runner.py changes are a second, independent behavioral fix. Worth calling out in the merge commit or a quick update to the PR description so the changelog captures both.

Pullfrog  | View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

@pikann
pikann merged commit 24f3b53 into master Aug 7, 2026
8 checks passed
@pikann
pikann deleted the fix/agents-table-column-exhaustion branch August 7, 2026 06:17
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