feat(graph): two-mode session deletion — the first destructive ledger operation

DELETE /api/chats/[id]?mode=preserve-graph|delete-nodes (mode required):
- preserve-graph: only extraction bookkeeping dies (coverage rows,
  legacy checkpoints) — graph ledgers byte-untouched
- delete-nodes: the chat's ledger traces die. Node survival = surviving
  provenance point / mapping line (notes imports carry empty
  provenance) / surviving event. The creator's aliases + provenance are
  stripped from a surviving node's create line (owner ruling
  2026-09-23: name/statement/createdAt are the shared identity);
  orphan nodes, their edges, events, mappings, positions, embedding
  vectors and cluster members die; cluster labels stay (immutable)

Gates: 404 unknown, 409 turn_in_flight / jobs_running (with jobIds),
503 extraction_winding_down after a 10s abort-and-poll; the purge is
one sync block (ledgers → jobs → chat record last). rewriteLedgers is
atomic (tmp + rename). graphRevision (max mtime over graph data files)
rides the activity feed + the response as the change stamp.

Anti-resurrection: the chat pipeline re-checks chat liveness at each
merge — a run that slips past the wind-down aborts with nothing merged
or checkpointed; its partial spend dies with the chat.
This commit is contained in:
amosblomqvist
2026-09-23 02:41:04 +03:00
parent ff3700a2d0
commit a6338511d3
13 changed files with 856 additions and 5 deletions
+13 -2
View File
@@ -21,6 +21,7 @@
import { extractionStatuses, type ExtractionPhase } from "../agent/subagents/extraction.ts";
import { chatStore } from "../store/chats.ts";
import { graphRevision } from "./graph.ts";
import { listJobs } from "./jobs.ts";
import type { JobRecord } from "../core/contracts.ts";
@@ -45,7 +46,13 @@ export interface ActivityExtraction {
* the blob disappears when the process ends. Cost math is NOT done here:
* raw usage rides on the records and the client folds it with the pure
* helpers in core/prices.ts (transport stays thin). */
export function activityFeed(): { jobs: ActivityJob[]; extractions: ActivityExtraction[] } {
/** The dock's whole world + the graph's version stamp (wave 6 session
* deletion): the payload carries graphRevision (max mtime across the
* graph data files — see server/graph.ts) so any view can detect "the
* graph changed underneath me" (a merge landed, a deletion purged) and
* refetch clusters/layout. Reads never move it; the DELETE response
* returns the same stamp. */
export function activityFeed(): { jobs: ActivityJob[]; extractions: ActivityExtraction[]; graphRevision: number } {
const chats = chatStore.listChats();
const jobs: ActivityJob[] = [];
for (const chat of chats) {
@@ -63,5 +70,9 @@ export function activityFeed(): { jobs: ActivityJob[]; extractions: ActivityExtr
}));
// oldest-first on last activity: the stable blob order (ISO strings sort chronologically)
jobs.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
return { jobs, extractions: extractions.sort((a, b) => a.startedAt - b.startedAt) };
return {
jobs,
extractions: extractions.sort((a, b) => a.startedAt - b.startedAt),
graphRevision: graphRevision(),
};
}