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
- Delete the cross-boundary edge in the legacy DB before migrating: DELETE FROM dependencies WHERE issue_id = '<id>' AND depends_on_id = '<to>'.
- If the edge is needed, change the ephemeral classification of one endpoint (move the issue into the matching storage class) so both endpoints agree.
- Audit for all such edges pre-migration by comparing each dependency's endpoints' ephemeral flags.
- 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
- Never create dependencies between ephemeral and durable issues; rely on current write-time enforcement.
- When merging databases, keep ephemeral and durable issues in separate dependency graphs.
- Audit edges by comparing endpoint ephemeral flags before any migration.
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
- dependency created_by is empty for %s -> %s
- orphan dependency %s -> %s
- sealed legacy SQLite database does not match source fingerpr
- sealed legacy SQLite WAL does not match source fingerprint
- legacy SQLite source changed while sealing
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/afe50877ddab8e63.
Report an issue: GitHub.