fix(core): sync review round 1: lists, supersede, dedupe conflicts, retries

- dedupe files conflicts for disagreeing fields instead of dropping them
- merged-away records never file removals
- matched work committed before apply so retries reuse it (no draft leaks)
- stale pending events superseded by newer values
- multi-source engagement removal only detaches the source
- list records applied with history-derived ownership
- gone targets resolve as ignored; counts only for committed records

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-30 20:43:08 +01:00
1 parent 0604c323c3
commit a003c3eb7d
8 files changed
+675 -196

No files matched your search

+3 -3
View File
@@ -5,7 +5,7 @@ import type { ConnectionStatus, Database, RunMode } from "@sttracker/db/schema";
import type { PluginContext, SourcePlugin, SourceRecord } from "@sttracker/plugin-sdk/types";
import { NotFoundError, ValidationError } from "../errors.ts";
import type { Actor } from "../history.ts";
import { applyRecord, emptyCounts, markRemoved, type RunCounts, type SyncState } from "./apply.ts";
import { applyRecord, committed, emptyCounts, markRemoved, type RunCounts, type SyncState } from "./apply.ts";
import { ingestListens } from "./listens.ts";
/** A `source_connections` row with JSON parsed. Credentials stay encrypted; the caller's `makeContext` decrypts. */
@@ -92,7 +92,7 @@ export async function runSync(deps: SyncDeps, plugin: SourcePlugin, connectionId
.execute();
const actor: Actor = `plugin:${conn.pluginId}:${conn.id}`;
const st: SyncState = { db, userId: conn.userId, connectionId: conn.id, runId, actor, counts: emptyCounts(), log: [] };
const st: SyncState = { db, userId: conn.userId, connectionId: conn.id, pluginId: conn.pluginId, runId, actor, counts: emptyCounts(), log: [] };
const seen = new Set<string>();
const errors: string[] = [];
let pullError: string | null = null;
@@ -140,7 +140,7 @@ export async function runSync(deps: SyncDeps, plugin: SourcePlugin, connectionId
.execute();
for (const r of live) {
if (seen.has(r.key)) continue;
await db.k.transaction().execute((tx) => markRemoved(tx, st, r));
await committed(st, (local) => db.k.transaction().execute((tx) => markRemoved(tx, local, r)));
}
} catch (e) {
errors.push(`removal: ${message(e)}`);