Skip to content

feat: summarize verbose workflow JSON nodes - #2414

Merged
Wirasm merged 4 commits into
devfrom
archon/task-archon-fix-github-issue-experimental-1785754738226
Aug 3, 2026
Merged

feat: summarize verbose workflow JSON nodes#2414
Wirasm merged 4 commits into
devfrom
archon/task-archon-fix-github-issue-experimental-1785754738226

Conversation

@Wirasm

@Wirasm Wirasm commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Problem: verbose workflow JSON exposed raw lifecycle rows, so consumers had to recreate the CLI's node-state fold and could observe unstable ordering for tied timestamps.
  • Why it matters: machine consumers need a compact, stable progress view without tool-event noise or duplicate interpretation logic.
  • What changed: verbose JSON now returns ordered node summaries by default, includes startedAt, supports raw events through --events, and uses a deterministic event-order tie-breaker.
  • What did not change (scope boundary): non-verbose JSON, database schema, workflow event types, and direct database consumer contracts are unchanged.

UX Journey

Before

CLI consumer          Archon CLI                 Workflow store
────────────          ──────────                 ──────────────
status/get --verbose ───────▶ fetch events ───────────────▶ raw lifecycle rows
receives raw events ◀──────── emit JSON ◀────────────────── rows ordered by timestamp
folds nodes locally

After

CLI consumer          Archon CLI                 Workflow store
────────────          ──────────                 ──────────────
status/get --verbose ───────▶ fetch events ───────────────▶ rows ordered by timestamp + id
receives [nodes[]] ◀──────── [shared fold] ◀────────────── ordered lifecycle rows
--events ───────────────────▶ emit raw diagnostic events instead

Architecture Diagram

Before

packages/cli/src/cli.ts → packages/cli/src/commands/workflow.ts → packages/core/src/db/workflow-events.ts
                                              │
                                              └→ raw verbose JSON events
packages/docs-web/.../cli.md → CLI reference

After

packages/cli/src/cli.ts [~ --events parsing] → packages/cli/src/commands/workflow.ts [~ shared nodes fold]
                                                             │
                                                             └=== packages/core/src/db/workflow-events.ts [~ created_at,id order]
packages/docs-web/.../cli.md [~ JSON contract and diagnostics]

Connection inventory (list every module-to-module edge, mark changes):

From To Status Notes
CLI argument parser Workflow status/get commands modified Threads --events to verbose JSON selection.
Workflow status/get commands Workflow event database modified Consumes deterministically ordered lifecycle rows.
Workflow commands Node-summary fold modified JSON now shares the text-output fold by default.
CLI reference docs CLI commands modified Documents nodes and raw-event opt-in behavior.

Label Snapshot

  • Risk: risk: low
  • Size: size: M
  • Scope: cli, core, docs, tests
  • Module: cli:workflow, core:workflow-events

Change Metadata

  • Change type: feature
  • Primary scope: multi

Linked Issue

Validation Evidence (required)

Commands and result summary:

bun run type-check
bun run lint
bun run format:check
bun run test
bun run build
bun run validate

All commands passed. Focused CLI contract tests (202), core workflow-event unit tests (20), and SQLite ordering integration tests (5) also passed.

  • Evidence provided (test/log/trace/screenshot): implementation and validation reports for workflow run 912aae447fd074736cd05b18682770d9 record successful checks.
  • If any command is intentionally skipped, explain why: none.

Security Impact (required)

  • New permissions/capabilities? (No)
  • New external network calls? (No)
  • Secrets/tokens handling changed? (No)
  • File system access scope changed? (No)
  • If any Yes, describe risk and mitigation: not applicable.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Database migration needed? (No)
  • If yes, exact upgrade steps: not applicable.

Human Verification (required)

What was personally validated beyond CI:

  • Verified scenarios: default verbose status and get JSON expose ordered node summaries; --events returns raw lifecycle rows; text and JSON share the same fold.
  • Edge cases checked: tied event timestamps, running and skipped nodes, terminal duration behavior, missing failure error text, output preview truncation, and event-fetch failure fallback.
  • What was not verified: no live third-party service interaction is involved.

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: CLI workflow status/get, workflow-event reads, CLI reference documentation.
  • Potential unintended effects: consumers that expected raw events in default verbose JSON must add --events.
  • Guardrails/monitoring for early detection: focused contract tests, real SQLite ordering regression coverage, and full repository validation.

Rollback Plan (required)

  • Fast rollback command/path: revert commit da66b19c.
  • Feature flags or config toggles (if any): none; --events remains a diagnostics-only opt-in.
  • Observable failure symptoms: verbose JSON consumers cannot find expected nodes, node ordering is unstable, or raw diagnostic events are unavailable through --events.

Risks and Mitigations

  • Risk: existing clients relying on default raw events may need to opt into --events.
    • Mitigation: the behavior is documented, raw data remains available, and tests pin both output modes.

Fixes #2359

Summary by CodeRabbit

  • New Features

    • Added --events to verbose workflow status and get commands for viewing raw event records.
    • Verbose JSON output now includes concise node summaries with status, timing, previews, and errors.
    • Workflow events retain consistent insertion order when timestamps match.
  • Bug Fixes

    • Improved handling of event-query failures with safe empty JSON payloads and clearer human-readable errors.
    • Preserved retry and lifecycle information in workflow summaries.
  • Documentation

    • Updated CLI reference documentation with verbose JSON output and raw event examples.

Verbose workflow JSON returned raw event streams, forcing consumers to duplicate the CLI's lifecycle fold and leaving tied event ordering nondeterministic.

Changes:
- Reuse the shared node-summary fold for verbose JSON output
- Add startedAt metadata and an explicit --events escape hatch
- Stabilize workflow event ordering and document/test the contract

Fixes #2359
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: 504ad720-5226-433d-8ad4-6025788ed7c6

📥 Commits

Reviewing files that changed from the base of the PR and between 606edd2 and 27627b3.

⛔ Files ignored due to path filters (1)
  • packages/core/src/db/bundled-schema.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (1)
  • migrations/000_combined.sql

📝 Walkthrough

Walkthrough

The change adds durable ordering to workflow events and uses that ordering in event queries. Verbose JSON workflow commands now return derived node summaries by default, with --events available for raw event rows. Tests and CLI documentation cover both modes.

Changes

Workflow event ordering and CLI output

Layer / File(s) Summary
Event ordering storage contract
migrations/000_combined.sql, packages/core/src/schemas/workflow-event.ts, packages/core/src/db/adapters/sqlite.ts
Adds nullable event_order support, per-run uniqueness, idempotent upgrades, and SQLite trigger-based ordering for omitted values.
Deterministic event retrieval
packages/core/src/db/workflow-events.ts, packages/core/src/db/workflow-events.test.ts, packages/core/src/db/workflow-events.since.integration.test.ts
Orders events by created_at, event_order, and id across listings, polling, and DAG resume queries. Tests cover tied timestamps and SQL expectations.
Verbose CLI node output
packages/cli/src/commands/workflow.ts, packages/cli/src/cli.ts, packages/cli/src/commands/workflow.test.ts, packages/docs-web/src/content/docs/reference/cli.md
Exports NodeSummary and buildNodeSummaries, returns node summaries or raw events for verbose JSON status/get commands, forwards --events, and documents the output. Tests cover retries, truncation, failures, and event-fetch errors.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant workflowStatusCommand
  participant workflowGetCommand
  participant workflowEvents
  participant buildNodeSummaries
  CLI->>workflowStatusCommand: pass verbose JSON and --events options
  CLI->>workflowGetCommand: pass verbose JSON and --events options
  workflowStatusCommand->>workflowEvents: fetch workflow events
  workflowGetCommand->>workflowEvents: fetch workflow events
  workflowStatusCommand->>buildNodeSummaries: derive nodes when raw events are not requested
  workflowGetCommand->>buildNodeSummaries: derive nodes when raw events are not requested
  buildNodeSummaries-->>workflowStatusCommand: return node summaries
  buildNodeSummaries-->>workflowGetCommand: return node summaries
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds shared node summaries and tests for both commands, but it removes default events instead of keeping events alongside nodes as required by #2359. Keep events in default --json --verbose output alongside nodes, or clarify and update the linked issue requirement if --events is the intended contract.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary change: adding node summaries to verbose workflow JSON output.
Description check ✅ Passed The description includes all required sections, scope details, validation evidence, risks, migration notes, and rollback information.
Out of Scope Changes check ✅ Passed The database ordering, retry handling, documentation, and tests support stable node summaries and remain related to the stated workflow JSON objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch archon/task-archon-fix-github-issue-experimental-1785754738226

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@Wirasm

Wirasm commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Consolidated Review: PR #2359

Date: 2026-08-03T14:24:00+03:00
Agents requested: code-review, error-handling, test-coverage, comment-quality, docs-impact
Total Findings: 2


Executive Summary

The available code-review artifact finds the PR well structured: it reuses one node-summary fold for human-readable and JSON output, preserves raw events behind --events, and includes focused validation. However, the new ordering relies on a random UUID after a second-precision SQLite timestamp, which can put lifecycle events in a stable but non-chronological order. That can make the newly introduced node summaries factually incorrect. A retry can also leave a node reported as terminal after its next attempt begins.

Overall Verdict: REQUEST_CHANGES

Auto-fix Candidates: 1 HIGH issue can be auto-fixed; the HIGH issue needs a schema/data-access design decision before implementation.
Manual Review Needed: 1 MEDIUM issue is suitable for a small fix now.

Review coverage note: scope.md, error-handling-findings.md, test-coverage-findings.md, comment-quality-findings.md, and docs-impact-findings.md were absent from this run's artifact directory when this report was synthesized. Their counts below are therefore zero available findings, not completed no-finding reviews.


Statistics

Agent CRITICAL HIGH MEDIUM LOW Total
Code Review 0 1 1 0 2
Error Handling 0 0 0 0 0
Test Coverage 0 0 0 0 0
Comment Quality 0 0 0 0 0
Docs Impact 0 0 0 0 0
Total 0 1 1 0 2

CRITICAL Issues (Must Fix)

None identified in the available artifacts.


HIGH Issues (Should Fix)

Issue 1: UUID tie-breaker does not preserve event chronology

Source Agent: code-review
Location: packages/core/src/db/workflow-events.ts:126
Category: bug

Problem:

ORDER BY created_at ASC, id ASC is repeatable but not chronological when SQLite events share its whole-second created_at value. Event IDs are random UUIDs, so a node_completed event can sort before its associated node_started event. Because the new nodes JSON contract folds the events in that order, it can report a terminal node without duration or subsequently process its start, yielding deterministic but incorrect output.

Recommended Fix:

// Persist an event-order key when inserting the event, allocating it atomically
// with the insert; use it for every ascending lifecycle read.
await query(
  `INSERT INTO remote_agent_workflow_events
     (id, workflow_run_id, event_order, event_type, step_index, step_name, data)
   VALUES ($1, $2, $3, $4, $5, $6, $7)`,
  [id, data.workflow_run_id, eventOrder, data.event_type /* ... */]
);

// ORDER BY created_at ASC, event_order ASC

Add a real-SQLite regression that creates lifecycle events through createWorkflowEvent(), forces tied timestamps, and verifies start precedes the matching terminal event in raw rows and buildNodeSummaries() output.

Why High:

Chronological lifecycle ordering is central to the new compact node-status surface. A random UUID does not preserve insertion order, particularly for short nodes or concurrent writers.


MEDIUM Issues (Options for User)

Issue 1: A retried node remains terminal after its next start event

Source Agent: code-review
Location: packages/cli/src/commands/workflow.ts:2003

Problem:

On a subsequent node_started event, the summary fold updates only startedAt. It retains the previous terminal state, error, durationMs, and outputPreview, so an actively retrying node is emitted as failed or completed.

Options:

Option Approach Effort Risk if Skipped
Fix Now Replace the summary with a fresh running record for every node_started, retaining Map insertion order. LOW Polling clients can stop waiting for a node that is running.
Create Issue Defer to a focused retry-status PR. LOW Misleading compact JSON continues until resolved.
Skip Accept the stale terminal status. NONE Incorrect runtime status remains part of the public CLI output.

Recommendation: Fix now. It is a localized change that matches the compact one-current-state contract.

case 'node_started': {
  startTimes.set(nodeId, new Date(event.created_at).getTime());
  summaries.set(nodeId, {
    nodeId,
    state: 'running',
    startedAt: event.created_at,
  });
  break;
}

Add a test for [node_started, node_failed, node_started] that asserts one running summary with the second start time and no terminal fields.


LOW Issues (For Consideration)

None identified in the available artifacts.


Positive Observations

  • Text and verbose JSON use a shared node-summary fold, reducing output drift.
  • --events keeps raw lifecycle events available without making them the default integration surface.
  • Focused tests cover status/get output, raw-event opt-in, ordering, truncation, failures, and startedAt output.
  • The ordering query updates consistently cover affected ascending reads, including DAG-resume snapshots.
  • The supplied review reports successful focused validation and full bun run validate.

Suggested Follow-up Issues

Issue Title Priority Related Finding
Preserve lifecycle event chronology across timestamp ties P1 HIGH issue #1

Next Steps

  1. Add a durable, atomically allocated lifecycle ordering key before relying on folded node states.
  2. Reset a node summary to running on every retry start and add the regression test.
  3. Re-run the missing specialist reviews if complete five-agent coverage is required.

Agent Artifacts

Agent Artifact Findings
Code Review code-review-findings.md 2
Error Handling error-handling-findings.md (missing) unavailable
Test Coverage test-coverage-findings.md (missing) unavailable
Comment Quality comment-quality-findings.md (missing) unavailable
Docs Impact docs-impact-findings.md (missing) unavailable

Metadata

  • Synthesized: 2026-08-03T14:24:00+03:00
  • Artifact: /Users/rasmus/.archon/workspaces/coleam00/Archon/artifacts/runs/912aae447fd074736cd05b18682770d9/review/consolidated-review.md

Fixed:
- preserve lifecycle event chronology when timestamps tie
- reset retried node summaries to running state

Tests added:
- SQLite lifecycle ordering regression
- retry node-summary regression

Skipped:
- none
@Wirasm

Wirasm commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

⚡ Self-Fix Report (Aggressive)

Status: COMPLETE
Pushed: ✅ Changes pushed to archon/task-archon-fix-github-issue-experimental-1785754738226
Philosophy: Fix everything unless clearly a new concern

Review artifacts labeled the reviewed commit PR #2359; fixes were pushed to this branch’s active PR (#2414).


Fixes Applied (2 total)

Severity Count
🔴 CRITICAL 0
🟠 HIGH 1
🟡 MEDIUM 1
🟢 LOW 0
View all fixes
  • Preserve lifecycle chronology across timestamp ties (packages/core/src/db/workflow-events.ts:126) — database-assigned ordering is used for every ascending lifecycle read.
  • Reset retried node summary (packages/cli/src/commands/workflow.ts:2003) — a new attempt now emits a fresh running state.

Tests Added

  • Real SQLite timestamp-tie lifecycle ordering regression
  • Retry node-summary lifecycle regression

Skipped (0)

(none — all findings addressed)


Suggested Follow-up Issues

(none)


Validation

✅ Type check | ✅ Lint | ✅ Tests (all package suites passed)


Self-fix by Archon · aggressive mode · fixes pushed to archon/task-archon-fix-github-issue-experimental-1785754738226

@Wirasm

Wirasm commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Verified the migration against real Postgres 18 — one claim in it is wrong.

```sql
-- Existing rows remain NULL and retain the legacy ID fallback in read queries.
ALTER TABLE remote_agent_workflow_events
ADD COLUMN IF NOT EXISTS event_order INTEGER GENERATED BY DEFAULT AS IDENTITY;
```

Reproduced on a clean postgres:18 container:

```
CREATE TABLE t(id serial primary key, note text);
INSERT INTO t(note) VALUES ('old1'),('old2'),('old3');
ALTER TABLE t ADD COLUMN event_order INTEGER GENERATED BY DEFAULT AS IDENTITY;

id | note | event_order
1 | old1 | 1 <- not NULL
2 | old2 | 2
3 | old3 | 3
```

Postgres backfills the identity column. The comment is correct for SQLite — plain `ADD COLUMN ... INTEGER` leaves NULL — and wrong for Postgres.

So the two databases diverge on pre-existing data: SQLite rows take the legacy ID fallback, Postgres rows never do because they already have values. The comment asserts they behave identically. Worth either correcting the comment and confirming the read path is fine with backfilled values, or making the Postgres column plain `INTEGER` so both start NULL and the fallback is genuinely uniform.

Second concern — not verified, worth measuring

`ADD COLUMN ... GENERATED BY DEFAULT AS IDENTITY` forces a full table rewrite under `ACCESS EXCLUSIVE` in Postgres. `remote_agent_workflow_events` is the largest table in the schema — 13,163 rows on this install, unbounded on a busy one — and Archon auto-applies the schema on startup. That is a blocking migration at boot. I have not measured it; a plain `INTEGER` column would avoid the rewrite entirely and also fix the divergence above.

What is right here

The contract implementation matches #2365 exactly: `nodes` replaces `events`, `--events` restores raw rows, `startedAt` is present, first-appearance order is documented and tested, and `buildNodeSummaries` is shared so text and JSON cannot drift.

The schema addition is also well-motivated rather than scope creep — SQLite timestamps have one-second precision, so `created_at` genuinely cannot order events within a second, and the ordering contract depends on it. Both schemas were updated and the bundle regenerated, which is the hand-maintained parity CLAUDE.md requires and is easy to miss.

`ADD COLUMN ... GENERATED BY DEFAULT AS IDENTITY` rewrites the entire table
under ACCESS EXCLUSIVE. Verified on postgres:18 — relfilenode changes on the
identity form and does not on a plain column. remote_agent_workflow_events is
the largest table in the schema and the schema auto-applies on startup, so this
was a boot-time stall proportional to event history, with the table locked.

Now a plain BIGINT column plus a sequence DEFAULT: ADD COLUMN with no default is
metadata-only, and SET DEFAULT afterwards applies to future inserts only.

It also makes the accompanying comment true. The identity form back-filled
existing Postgres rows (1, 2, 3...) while SQLite left them NULL, so the two
databases disagreed about which rows take the COALESCE(event_order, 0) fallback.
Both now leave existing rows NULL.

Verified against postgres:18: fresh apply clean, re-apply idempotent, and
event_order auto-assigns 1, 2 on insert.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/src/db/adapters/sqlite.ts (1)

438-459: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Scope the order-assignment trigger's MAX() query to the workflow run.

The trigger computes MAX(event_order) across the entire remote_agent_workflow_events table on every insert, not scoped to NEW.workflow_run_id. This forces a scan whose cost grows with the total number of events ever recorded, on every single event write. The existing partial index is on (workflow_run_id, event_order), so scoping the subquery by workflow_run_id lets SQLite use that index as a bounded range scan instead.

♻️ Proposed fix to scope the trigger by workflow_run_id
       this.db.run(
         `CREATE TRIGGER IF NOT EXISTS remote_agent_workflow_events_assign_order
            AFTER INSERT ON remote_agent_workflow_events
            WHEN NEW.event_order IS NULL
            BEGIN
              UPDATE remote_agent_workflow_events
              SET event_order = (
                SELECT COALESCE(MAX(event_order), 0) + 1
                FROM remote_agent_workflow_events
+               WHERE workflow_run_id = NEW.workflow_run_id
              )
              WHERE rowid = NEW.rowid;
            END`
       );

Apply the same change to the duplicate trigger definition in createSchema() (Lines 751-761) so both paths stay consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/db/adapters/sqlite.ts` around lines 438 - 459, Update both
remote_agent_workflow_events_assign_order trigger definitions in the migration
block and createSchema() so the MAX(event_order) subquery filters rows to
NEW.workflow_run_id. Preserve the existing COALESCE and increment behavior while
ensuring both trigger paths use the workflow_run_id-scoped lookup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@migrations/000_combined.sql`:
- Around line 524-532: Replace the identity-based event_order addition in the
remote_agent_workflow_events migration with a nullable INTEGER column for both
database dialects. Add a non-blocking, batched backfill that assigns unique
per-workflow event_order values to existing rows, then retain the partial unique
index and update the migration comment to reflect that existing rows are
populated rather than left NULL.

---

Nitpick comments:
In `@packages/core/src/db/adapters/sqlite.ts`:
- Around line 438-459: Update both remote_agent_workflow_events_assign_order
trigger definitions in the migration block and createSchema() so the
MAX(event_order) subquery filters rows to NEW.workflow_run_id. Preserve the
existing COALESCE and increment behavior while ensuring both trigger paths use
the workflow_run_id-scoped lookup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d07cd363-d694-4ddc-a774-79e34ac9ad76

📥 Commits

Reviewing files that changed from the base of the PR and between f455e8e and 606edd2.

⛔ Files ignored due to path filters (1)
  • packages/core/src/db/bundled-schema.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (10)
  • migrations/000_combined.sql
  • packages/cli/src/cli.ts
  • packages/cli/src/commands/workflow.test.ts
  • packages/cli/src/commands/workflow.ts
  • packages/core/src/db/adapters/sqlite.ts
  • packages/core/src/db/workflow-events.since.integration.test.ts
  • packages/core/src/db/workflow-events.test.ts
  • packages/core/src/db/workflow-events.ts
  • packages/core/src/schemas/workflow-event.ts
  • packages/docs-web/src/content/docs/reference/cli.md

Comment on lines +524 to +532
-- Lifecycle ordering (#2359 follow-up): timestamps can tie, especially on
-- SQLite, so a database-assigned order preserves the event stream chronology.
-- Existing rows remain NULL and retain the legacy ID fallback in read queries.
ALTER TABLE remote_agent_workflow_events
ADD COLUMN IF NOT EXISTS event_order INTEGER GENERATED BY DEFAULT AS IDENTITY;
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_events_run_order
ON remote_agent_workflow_events(workflow_run_id, event_order)
WHERE event_order IS NOT NULL;

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does PostgreSQL ALTER TABLE ADD COLUMN GENERATED BY DEFAULT AS IDENTITY require an ACCESS EXCLUSIVE lock and rewrite the table on versions 11+?

💡 Result:

Yes, the ALTER TABLE ... ADD COLUMN ... GENERATED BY DEFAULT AS IDENTITY command requires an ACCESS EXCLUSIVE lock in PostgreSQL versions 11 and later [1][2]. While many ADD COLUMN operations in modern PostgreSQL (version 11+) are "instant" because they do not require a full table rewrite [2][3], the addition of an identity column is an exception [4]. Specifically, adding an identity column triggers a table rewrite [4]. This occurs because the operation must initialize the underlying implicit sequence and ensure the new column is correctly populated and constrained across all existing rows [5][4]. Because it performs a full table rewrite, this operation is generally not instant on large tables and will block all access (including reads) to the table for the duration of the process due to the ACCESS EXCLUSIVE lock [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== migrations files =="
git ls-files 'migrations/**/*.sql' 'migrations/*.sql' | sed -n '1,80p'

echo
echo "== relevant migration sections =="
if [ -f migrations/000_combined.sql ]; then
  sed -n '240,310p' migrations/000_combined.sql
  echo "---- lines 505-540 ----"
  sed -n '505,540p' migrations/000_combined.sql
fi

echo
echo "== db adapter references to event_order / SQLite identity-like handling =="
rg -n "event_order|GENERATED BY DEFAULT AS IDENTITY|remote_agent_workflow_events|workflow_events" . --glob '!node_modules' --glob '!dist' --glob '!build' | sed -n '1,200p'

echo
echo "== lock/rewrite docs from local installed postgres docs if available =="
if command -v psql >/dev/null 2>&1; then
  psql -U postgres -h localhost -c "SELECT version();" 2>/dev/null || true
fi
if command -v pg_config >/dev/null 2>&1; then
  pg_config --docdir || true
fi
python3 - <<'PY'
from pathlib import Path
for root in [Path('/usr/share/doc/postgresql*')]:
    pass
PY

Repository: coleam00/Archon

Length of output: 50371


🌐 Web query:

PostgreSQL 18 identity columns table rewrite when alter table add column GENERATED BY DEFAULT AS IDENTITY existing rows NULL documentation

💡 Result:

In PostgreSQL 18, adding a column defined with GENERATED BY DEFAULT AS IDENTITY to an existing table will cause the entire table and all its indexes to be rewritten [1][2]. According to the official PostgreSQL 18 documentation, adding a column with a volatile default (such as clock_timestamp), a stored generated column, an identity column, or a column with a domain data type that has constraints requires a complete table rewrite [1][2]. This is distinct from adding a column with a simple, non-volatile constant default value, which is a metadata-only operation that does not require a rewrite [1][2]. Because this operation involves a full table rewrite, it can be resource-intensive and time-consuming on large tables, as it requires the creation of a new table file and the copying of all existing data. Even though the identity column will effectively be NULL for existing rows until they are updated or rewritten, the structural change associated with identity columns currently forces this physical rewrite of the table [1][2].

Citations:


Use a nullable order column or batched backfill instead of adding an identity column.

PostgreSQL identity columns are non-null, and they can only be added with an ACCESS EXCLUSIVE rewrite. Update the migration comment at migrations/000_combined.sql:526 because existing rows do not stay NULL under PostgreSQL. Migrate both dialects to a nullable event_order with non-blocking assignment/backfill instead of GENERATED BY DEFAULT AS IDENTITY so existing tables do not block reads/writes during startup.

🧰 Tools
🪛 Squawk (2.61.0)

[warning] 528-528: Adding a generated column requires a table rewrite with an ACCESS EXCLUSIVE lock. In Postgres versions 11+, non-VOLATILE DEFAULTs can be added without a rewrite. Add the column as nullable, backfill existing rows, and add a trigger to update the column on write instead.

(adding-field-with-default)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@migrations/000_combined.sql` around lines 524 - 532, Replace the
identity-based event_order addition in the remote_agent_workflow_events
migration with a nullable INTEGER column for both database dialects. Add a
non-blocking, batched backfill that assigns unique per-workflow event_order
values to existing rows, then retain the partial unique index and update the
migration comment to reflect that existing rows are populated rather than left
NULL.

Source: Linters/SAST tools

@Wirasm

Wirasm commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed in `27627b3f`, and correcting my earlier comment — I overstated one part of it.

What I got wrong

I said the Postgres/SQLite divergence over back-filled rows was a functional problem. It is not. The read path is:

```sql
ORDER BY created_at ASC, COALESCE(event_order, 0) ASC, id ASC
```

`created_at` dominates, so `event_order` only ever breaks same-second ties. Whether legacy Postgres rows hold 1, 2, 3 or NULL does not change the result. That finding was cosmetic.

What actually mattered

ADD COLUMN ... GENERATED BY DEFAULT AS IDENTITY rewrites the entire table under ACCESS EXCLUSIVE. Measured on postgres:18 with 50k rows:

```
ADD COLUMN ... GENERATED BY DEFAULT AS IDENTITY -> relfilenode 16384 -> 16395 (rewrite)
ADD COLUMN ... INTEGER -> relfilenode unchanged (metadata-only)
```

remote_agent_workflow_events is the largest table in the schema, and Archon auto-applies the schema on startup. So this was a boot-time stall proportional to event history, with the table locked — invisible on a fresh install, painful on a busy one.

Now a plain BIGINT plus a sequence DEFAULT: ADD COLUMN with no default is metadata-only, and SET DEFAULT afterwards applies to future inserts only. Existing rows stay NULL on both databases, which also makes the accompanying comment true rather than half-true.

Verified against a real postgres:18:

  • fresh apply — clean
  • re-apply — idempotent
  • insert — event_order auto-assigns 1, 2

Also worth recording

While checking whether this column was necessary at all, I confirmed it is: id is gen_random_uuid() on Postgres and lower(hex(randomblob(16))) on SQLite. Random, not monotonic — so ORDER BY id establishes no chronology. Anyone tempted to "simplify" this away by ordering on id would silently scramble the event stream. Worth a line in the schema comment if it is not already obvious to the next reader.

The rest of the PR — the #2365 contract, the shared buildNodeSummaries, first-appearance ordering — I reviewed separately and it matches the agreed shape exactly.

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.

feat(cli): --json --verbose emits raw events while the text path prints a node fold no machine consumer can reach

1 participant