Skip to content

fix(device-reconcile): replace uuid[] cast with text[] to avoid PG array parse failure#835

Merged
buremba merged 1 commit into
mainfrom
fix/runs-queue-uuid-array
May 18, 2026
Merged

fix(device-reconcile): replace uuid[] cast with text[] to avoid PG array parse failure#835
buremba merged 1 commit into
mainfrom
fix/runs-queue-uuid-array

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented May 18, 2026

Fixes #781.

Smoking gun

Production app pod logs (image `20260516-235210`, 2026-05-17 00:10:06 UTC):

```
[runs-queue] Run 197444 failed after retries: malformed array literal: "f7623d32-b589-4085-9504-edbf30925961"
[runs-queue] Run 197426 failed after retries: malformed array literal: "f7623d32-b589-4085-9504-edbf30925961"
```

`rg "uuid\[\]" packages db` returns exactly one hit across the entire backend — `packages/server/src/worker-api/device-reconcile.ts:68`, in the `reconcilePin` self-heal UPDATE. The error matches the shape of a bound text parameter (`pgTextArray(matchingDeviceIds)` → `{"f7623d..."}`) being passed through a `::uuid[]` cast that postgres.js's extended-protocol path doesn't re-parse as an array first.

(Note: the issue body pointed at commit 2ffccfb / #814 as a suspect, but that commit landed at 2026-05-17 05:11 UTC — after the crash at 00:10:06 UTC. The bug pre-dates it; `device-reconcile.ts:68` was introduced in #714 / 76fc6e9 on May 11.)

Rationale for the fix shape

Two options the issue suggested:

  1. Wrap as `[uuid]` before insert — already done via `pgTextArray`; the bug isn't a missing wrapper but a postgres.js + `::uuid[]` cast interaction.
  2. Change to scalar `uuid` — wrong: the WHERE clause is genuinely multi-valued (the user's fleet can serve a capability from multiple devices; the pin self-heal needs to check membership against the full fresh-device set).

So the fix instead drops the `uuid[]` cast entirely and switches to `text[]` with `device_worker_id::text` on the left side. UUIDs are canonical lowercase in Postgres, so text equality matches the uuid form 1:1 — no semantic change, zero array-cast surface area.

`SET device_worker_id = ${target}::uuid` and the `IS DISTINCT FROM` clause on line 67 are left as-is — they bind a single uuid (or NULL), which the simple `::uuid` cast handles fine; only the `uuid[]` path was broken.

Test plan

  • `make typecheck` clean
  • Watch `summaries-prod` after deploy: no `[runs-queue] Run ... failed after retries: malformed array literal: ""` entries during a 24h window
  • No regression in device pin reconciliation: existing pinned connections stay pinned, multi-device users still get unpinned, dropped-out pins still get repaired

No regression test added: would require new device_workers + connection + connector_definitions fixtures (none exist for this path) and the vitest suite isn't wired into CI. The fix is single-line and self-contained; pi review is the right gate.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed a database error that could occur during device reconciliation operations, improving system stability and reliability.

Review Change Stack

…ray parse failure

Production app pod logs (image 20260516-235210, 2026-05-17 00:10:06 UTC) repeatedly
surfaced:

  [runs-queue] Run 197444 failed after retries: malformed array literal: "f7623d32-..."
  [runs-queue] Run 197426 failed after retries: malformed array literal: "f7623d32-..."

Smoking gun: `device-reconcile.ts` was the only `::uuid[]` cast site in the entire
server codebase (`rg "uuid\[\]"` returns one hit). The pin-self-heal UPDATE bound a
`pgTextArray(matchingDeviceIds)` literal as a text parameter and then attempted a
`::uuid[]` cast — postgres.js's extended-protocol path doesn't reliably re-parse the
bound text as an array before applying the uuid[] cast, so PG sees the raw element
bytes and fails with "malformed array literal: <uuid>".

Fix: drop the uuid[] cast. Cast `device_worker_id::text` and compare against a
`::text[]` instead. UUIDs are canonical lowercase, so text-form equality matches the
uuid-form 1:1 with no semantic loss. Keeps the multi-device semantics intact (the
column stays scalar uuid; this WHERE clause was always genuinely multi-valued).

Fixes #781.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b857cc99-0aaa-4350-9739-577e6fdbb660

📥 Commits

Reviewing files that changed from the base of the PR and between f597e76 and 38e81ae.

📒 Files selected for processing (1)
  • packages/server/src/worker-api/device-reconcile.ts

📝 Walkthrough

Walkthrough

Device-pin reconciliation SQL WHERE clause updated from uuid[] array comparison to text array comparison to avoid Postgres "malformed array literal" errors when array parameters are bound through the extended protocol.

Changes

Device Reconciliation Array Casting Fix

Layer / File(s) Summary
Device pin reconciliation WHERE clause array comparison fix
packages/server/src/worker-api/device-reconcile.ts
ensureDeviceConnectorWired's reconcilePin UPDATE WHERE condition changed from device_worker_id = ANY($1::uuid[]) to device_worker_id::text = ANY($1::text[]) with comments documenting avoidance of Postgres array literal parsing errors under extended-protocol binding.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A UUID cast in textile guise,
no longer parsed through array's eyes,
The bound array lives, no malformed cries—
Device pins wire true, no more reprise! 🔌✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title is concise, specific, and directly describes the primary change: replacing a uuid[] cast with text[] to fix a Postgres array parse failure.
Description check ✅ Passed Description includes detailed context (smoking gun logs, root cause analysis, rationale for fix shape) but is missing explicit test plan checkboxes from the template.
Linked Issues check ✅ Passed The PR successfully addresses issue #781 by identifying and fixing the exact code location causing the malformed array literal error, using a text-based comparison instead of uuid[] cast.
Out of Scope Changes check ✅ Passed All changes are directly within scope: device-reconcile.ts modifications address the specific issue #781 without introducing unrelated alterations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/runs-queue-uuid-array

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@buremba
Copy link
Copy Markdown
Member Author

buremba commented May 18, 2026

pi review summary (verdict: comment / non-blocking):

  1. Semantics preserved. `matchingDeviceIds` is sourced from `device_workers.id` (Postgres `uuid`), which always renders in canonical lowercase text. `device_worker_id::text` on the connections side renders the same way. text-form equality matches uuid-form 1:1 for every value this path can ever see.

  2. Theoretical gap. `text[]` and `uuid[]` aren't identical: if a future caller ever fed in a non-canonical-but-valid UUID (uppercase, brace-wrapped, hyphenless), the old `::uuid[]` would have canonicalized and matched; the new text form would miss. Not a concern here — all values in `matchingDeviceIds` come from PG, not user input.

  3. Rationale uncertainty (acknowledged). pi couldn't reproduce the postgres.js extended-protocol failure shape my commit message describes. The error text `malformed array literal: ""` matches binding a raw JS array more cleanly than binding a `pgTextArray` literal. I still believe this was the trigger site — it's the only `::uuid[]` cast in the entire server codebase — but the exact failure mechanism may differ from my hypothesis. The fix stands either way: drop the only `uuid[]` cast, dodge the entire surface.

Net: ship.

@buremba buremba merged commit be8166c into main May 18, 2026
18 of 23 checks passed
@buremba buremba deleted the fix/runs-queue-uuid-array branch May 18, 2026 00:08
buremba added a commit that referenced this pull request May 18, 2026
* docs(agents): cross-repo dispatch pattern + e2e-before-merge gate

- Owletto agents work in standalone ~/Code/owletto clone, not packages/owletto submodule (avoids inherited origin → wrong remote).
- Don't pass "REPO: /absolute/path" in dispatch prompts — agents cd out of their isolation worktree.
- Add e2e red→fix→green hard gate before opening bug-fix PRs. Bail if you can't reproduce.

Motivated by the 2026-05-17 triage: both #781 and #782 agents hit the origin misconfig, and all three PRs (#833, #835, owletto#160) shipped without a reproducer.

* docs(agents): clarify Pi reference per coderabbit review
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.

runs-queue: 'malformed array literal' on UUID retry — fix unescaped uuid[] insert

2 participants