gastownhall/beads · error

issue %s waiters: invalid JSON

Error message

issue %s waiters: invalid JSON

What it means

applyMetadataAndWaiters validates the legacy issue's waiters column: it must be valid JSON (this error), free of unpaired surrogates, and decodable by decodeWaiters. A non-empty waiters value that json.Valid rejects aborts migration for that issue with 'issue <id> waiters: invalid JSON'.

Source

Thrown at internal/migration/legacysqlite/reader.go:522

// applyMetadataAndWaiters validates the legacy metadata and waiters JSON blobs
// (well-formed and free of unpaired surrogates) and assigns them to issue.
// metadata is stored verbatim unless it is the empty object; waiters is decoded.
func (x legacyExtras) applyMetadataAndWaiters(issue *types.Issue) error {
	if x.metadata.Valid && x.metadata.String != "" && !json.Valid([]byte(x.metadata.String)) {
		return fmt.Errorf("legacy SQLite issue %s has invalid metadata JSON", issue.ID)
	}
	if x.metadata.Valid && x.metadata.String != "" {
		if err := checkJSONSurrogates(x.metadata.String); err != nil {
			return fmt.Errorf("legacy SQLite issue %s metadata: %w", issue.ID, err)
		}
	}
	if x.metadata.Valid && x.metadata.String != "" && x.metadata.String != "{}" {
		issue.Metadata = []byte(x.metadata.String)
	}
	if x.waiters.Valid && x.waiters.String != "" {
		if !json.Valid([]byte(x.waiters.String)) {
			return fmt.Errorf("issue %s waiters: invalid JSON", issue.ID)
		}
		if err := checkJSONSurrogates(x.waiters.String); err != nil {
			return fmt.Errorf("issue %s waiters: %w", issue.ID, err)
		}
		waiters, err := decodeWaiters(x.waiters.String)
		if err != nil {
			return fmt.Errorf("issue %s waiters: %w", issue.ID, err)
		}
		issue.Waiters = waiters
	}
	return nil
}

// applyCanonicalTimestamps normalizes the required created_at/updated_at values
// to the canonical current-schema representation.
func (x legacyExtras) applyCanonicalTimestamps(issue *types.Issue) error {
	var err error
	if issue.CreatedAt, err = canonicalCurrentDatetime(issue.CreatedAt); err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Rewrite the waiters column for the reported issue ID as valid JSON in the expected waiters shape (or NULL), then re-run migration.
  2. Check the version that wrote the legacy DB and convert the old waiters serialization to the expected JSON format first.
  3. Clear the corrupted waiters value (NULL) if the waiter relationships can be recreated after migration.

Example fix

-- before
UPDATE issues SET waiters='bd-1,bd-2' WHERE id='bd-8';
-- after
UPDATE issues SET waiters='["bd-1","bd-2"]' WHERE id='bd-8';
Defensive patterns

Strategy: validation

Validate before calling

rows, _ := db.Query(`SELECT id, waiters FROM issues WHERE waiters IS NOT NULL AND waiters != ''`)
for rows.Next() {
  var id, w string
  rows.Scan(&id, &w)
  if !json.Valid([]byte(w)) { /* convert or NULL before migrate */ }
}

Try / catch

if !json.Valid([]byte(waiters)) {
  // convert old serialization to JSON or NULL the column before migrating
}

Prevention

When it happens

Trigger: Migrating a legacy DB where an issues.waiters column contains non-JSON text (e.g. a comma-separated ID list, truncated array, or empty braces variant) instead of a valid JSON document.

Common situations: Old beads versions that stored waiters in a different (non-JSON) serialization; truncated writes; manual edits pasting plain-text waiter lists; data munging by external scripts.

Understand the failure class

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/68d812e1060e2d15. Report an issue: GitHub.