Files
AgentCompany/src/main/database/migrations/v46-v46.ts
T
ZacharyZhang-NY cb0ac485da Close inbox projection under one transaction
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.
2026-04-19 15:46:12 -04:00

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");
},
},
];
}