The old `prepareRun` projection drifted: append-to-shadow, thread-event, and `markAgentInboxDelivered` ran as three independent writes, so a crash between the append and the delivered mark replayed the same inbox row on the next run and double-populated the transcript. v46 installs `agent_message_projections` keyed by (message_id, session_id) as the idempotent gate. The projection closure now wraps the ledger claim, the shadow append, the thread-event write, and the delivered-mark inside one `BEGIN IMMEDIATE` transaction (exposed via `AppDatabase.withTransaction`). `claimProjection` returns false on a replay, so the whole closure short-circuits — no double transcript, no double thread event.
45 lines
1.8 KiB
TypeScript
45 lines
1.8 KiB
TypeScript
import log from "electron-log/main.js";
|
|
import type { Migration, MigrationDeps } from "./types";
|
|
|
|
export function createMigrationsV46V46(deps: MigrationDeps): Migration[] {
|
|
const { db } = deps;
|
|
return [
|
|
{
|
|
// Ledger for `prepareRun` inbox → session-shadow projection.
|
|
//
|
|
// Before v46 the projection sequence — append to the session
|
|
// shadow, write the thread event, mark the inbox row delivered —
|
|
// ran as three independent SQL writes. A crash or worker restart
|
|
// between the append and the delivered mark caused the next run
|
|
// to replay the same inbox row, duplicating transcript turns and
|
|
// thread events.
|
|
//
|
|
// The ledger is keyed by the natural identity of a projection
|
|
// attempt: the combination of the inbox message and the session
|
|
// shadow it lands on. `INSERT OR IGNORE` against that key is the
|
|
// idempotent gate — the first insert wins, a replay silently
|
|
// skips. Callers wrap the ledger insert, the shadow append, the
|
|
// thread-event write, and the delivered-mark in a single
|
|
// `BEGIN IMMEDIATE` transaction so the four writes commit
|
|
// together or roll back together.
|
|
version: 46,
|
|
name: "agent_message_projections_ledger",
|
|
fn: () => {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS agent_message_projections (
|
|
message_id TEXT NOT NULL,
|
|
session_id TEXT NOT NULL,
|
|
projected_at TEXT NOT NULL,
|
|
PRIMARY KEY (message_id, session_id)
|
|
)
|
|
`);
|
|
db.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_agent_message_projections_session
|
|
ON agent_message_projections(session_id, projected_at)
|
|
`);
|
|
log.info("[migration v46] agent_message_projections ledger installed");
|
|
},
|
|
},
|
|
];
|
|
}
|