abhigyanpatwari/GitNexus · error · StreamedRelationshipRemovalError
Cannot remove relationship "${relationshipId}": it has alrea
Error message
Cannot remove relationship "${relationshipId}": it has already been streamed to CSV and cannot be recalled. A phase that removes relationships must run before the GraphEmitSink is installed (see the parse-boundary construction in pipeline.ts). What it means
StreamedRelationshipRemovalError from GraphEmitSink (graph-emit-sink.ts:659): once streaming has begun (srcIx.length > 0), relationship rows may already be flushed to per-pair CSVs and cannot be recalled, so removeRelationship() throws instead of pretending to succeed. Phases that remove relationships must run before the sink is armed at the parse boundary (see pipeline.ts). This deliberately diverges from KnowledgeGraph.removeRelationship (which returns false for unknown ids); the only production caller, the COBOL resolver, runs before the sink is armed.
Source
Thrown at gitnexus/src/core/lbug/graph-emit-sink.ts:659
* Deliberately conservative. The dedup Set holds compact keys derived from a
* relationship's endpoints ({@link dedupKey}), and a bare id alone cannot be
* turned back into one — so a streamed edge is not directly identifiable here.
*
* Rather than risk the silent case (returning `false` for an edge that IS on
* disk and cannot be recalled), anything the real graph does not hold is
* treated as possibly-streamed once streaming has begun, and fails loudly. A
* genuinely-absent id therefore throws too, where the object-based graph would
* return `false`; that is acceptable because the only production caller is the
* COBOL resolver, which runs BEFORE the sink is armed and so takes the branch
* below.
*
* NOTE this diverges from {@link KnowledgeGraph.removeRelationship}, which
* returns `false` for an id it does not hold. Pinned by a test so the
* divergence stays deliberate.
*/
removeRelationship(relationshipId: string): boolean {
if (this.real.removeRelationship(relationshipId)) return true;
if (this.srcIx.length > 0) throw new StreamedRelationshipRemovalError(relationshipId);
return false;
}
}
View on GitHub (pinned to aac7515d2a)
Solutions
- Move the phase that removes relationships to before the GraphEmitSink is installed (the parse-boundary construction in pipeline.ts)
- If dedup must happen late, filter/suppress relationship rows before emitting them instead of recalling them after the fact
- If you only needed the boolean semantics of KnowledgeGraph.removeRelationship, perform removals against the real graph before wrapping it in the sink
Example fix
// before: removal after sink is armed const sink = createGraphEmitSink(graph, dir); sink.graph.removeRelationship(relId); // throws // after: remove before installing the sink graph.removeRelationship(relId); const sink = createGraphEmitSink(graph, dir);
Defensive patterns
Strategy: validation
Validate before calling
// Order phases so removals happen while the plain graph is still unwrapped const graph = buildObjectGraph(); resolveCobol(graph); // any removeRelationship callers run HERE graph.removeRelationship?.(staleId); // last-chance removal, still safe const sink = createGraphEmitSink(graph, stagingDir); // sink armed only AFTER all removals await runPipeline(sink);
Try / catch
import { StreamedRelationshipRemovalError } from './core/lbug/graph-emit-sink.js';
try {
graph.removeRelationship(id);
} catch (err) {
if (err instanceof StreamedRelationshipRemovalError) {
// phase-ordering bug: move this caller before sink installation in pipeline.ts; do not catch-and-ignore
}
throw err;
} Prevention
- Treat sink installation as a one-way gate: no graph mutations that remove rows after it
- When adding a resolution/dedup phase, place it before the parse-boundary sink construction in pipeline.ts
- Do not rely on KnowledgeGraph.removeRelationship's false-return semantics once the sink wraps the graph
When it happens
Trigger: Custom pipeline code or a plugin calling graph.removeRelationship(id) after the GraphEmitSink has been installed and streaming started — e.g. adding a late dedup/cleanup phase after the parse boundary, or refactoring the COBOL resolver so it runs after sink installation. Note a genuinely-absent id also throws here (any id is treated as possibly-streamed).
Common situations: Extending the ingestion pipeline with a post-emit normalization pass; reordering phases during a refactor; new code assuming the graph API behaves like the in-memory KnowledgeGraph (false return) instead of the streaming sink.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Streaming PDG manifest collides with a structural node CSV f
- Streaming PDG manifest collides with a structural relationsh
- PdgEmitSink: ${errors.length} streamed CSV writer(s) hit an
- content filter triggered mid-stream. The generated content w
- LLM returned empty streaming response
AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20).
Data as JSON: /api/errors/2f1795a83c685547.
Report an issue: GitHub.