gastownhall/beads · error

dependency %s -> %s crosses ephemeral storage

Error message

dependency %s -> %s crosses ephemeral storage

What it means

A dependency edge connects an ephemeral issue to a non-ephemeral issue (their Ephemeral flags differ). The current storage model separates ephemeral wisps from durable issues, and cross-boundary dependencies are unsupported, so the reader rejects such edges during legacy migration instead of producing an invalid graph.

Source

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

		return err
	}
	if err := checkCurrentVarchars(
		currentVarchar{"dependency issue_id", id, types.MaxFieldLen},
		currentVarchar{"dependency depends_on_id", to, types.MaxFieldLen},
		currentVarchar{"dependency type", typ, currentShortVarcharRunes},
		currentVarchar{"dependency created_by", by, types.MaxFieldLen},
	); err != nil {
		return err
	}
	if by == "" {
		return fmt.Errorf("dependency created_by is empty for %s -> %s", id, to)
	}
	issue := byID[id]
	if issue == nil || byID[to] == nil {
		return fmt.Errorf("orphan dependency %s -> %s", id, to)
	}
	if issue.Ephemeral != byID[to].Ephemeral {
		return fmt.Errorf("dependency %s -> %s crosses ephemeral storage", id, to)
	}
	key := id + "\x00" + to
	if seenDeps[key] {
		return fmt.Errorf("multiple legacy dependencies for %s -> %s", id, to)
	}
	seenDeps[key] = true
	created, e := parseTime(at)
	if e != nil {
		return e
	}
	if created.IsZero() {
		return fmt.Errorf("dependency created_at is zero for %s -> %s", id, to)
	}
	if (metadata.Valid && metadata.String != "") || (thread.Valid && thread.String != "") {
		return fmt.Errorf("dependency %s -> %s uses unsupported metadata or thread ID", id, to)
	}
	if !types.DependencyType(typ).IsValid() {
		return fmt.Errorf("dependency %s -> %s has invalid type", id, to)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Delete the cross-boundary edge in the legacy DB before migrating: DELETE FROM dependencies WHERE issue_id = '<id>' AND depends_on_id = '<to>'.
  2. If the edge is needed, change the ephemeral classification of one endpoint (move the issue into the matching storage class) so both endpoints agree.
  3. Audit for all such edges pre-migration by comparing each dependency's endpoints' ephemeral flags.
  4. Check how the mixed edge was created (old bd version or manual merge) and prevent recurrence with the current write-time enforcement.

Example fix

// before: ephemeral -> durable edge
DELETE FROM dependencies WHERE issue_id = 'bd-abc' AND depends_on_id = 'bd-xyz';
// after: no cross-storage edges remain
Defensive patterns

Strategy: validation

Validate before calling

// verify both endpoints of every edge share the same ephemeral flag
rows, err := legacyDB.Query(`SELECT d.issue_id, d.depends_on_id FROM dependencies d
  JOIN issues a ON a.id = d.issue_id
  JOIN issues b ON b.id = d.depends_on_id
  WHERE a.ephemeral != b.ephemeral`)
// delete or reclassify these edges before migrating

Type guard

func sameStorageClass(a, b *types.Issue) bool {
    return a.Ephemeral == b.Ephemeral
}

Try / catch

if err := migrateLegacy(db); err != nil {
    if strings.Contains(err.Error(), "crosses ephemeral storage") {
        return fmt.Errorf("remove cross-boundary dependency edges and retry: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: appendLegacyDependencyRow loads both endpoint issues and finds issue.Ephemeral != byID[to].Ephemeral for the id -> to pair — i.e. one endpoint lives in ephemeral storage and the other in durable storage.

Common situations: Legacy databases written before ephemeral/durable separation was enforced at write time, allowing mixed edges; manual merging of an ephemeral DB with a durable one; tools that created dependencies across the boundary before the rule existed.

Related errors


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