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.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 15:46:12 -04:00
1 parent 213837af1d
commit cb0ac485da
5 files changed
+133 -26

No files matched your search

+15
View File
@@ -23,6 +23,7 @@ import { buildKnowledgeMethods } from "./database/knowledge";
import { buildWakeupMethods } from "./database/wakeup";
import { buildBackupMethods } from "./database/backup";
import { buildSessionEntryMethods } from "./database/session-entries";
import { buildInboxProjectionMethods } from "./database/inbox-projections";
import { buildCredentialMethods } from "./database/credentials";
import { buildModelCapabilityMethods } from "./database/model-capabilities-crud";
import { MODEL_CAPABILITY_SEED } from "./model-capability-seed";
@@ -69,6 +70,7 @@ export class AppDatabaseBase {
public readonly wakeup: ReturnType<typeof buildWakeupMethods>;
public readonly backupMod: ReturnType<typeof buildBackupMethods>;
public readonly sessionEntries: ReturnType<typeof buildSessionEntryMethods>;
public readonly inboxProjections: ReturnType<typeof buildInboxProjectionMethods>;
public readonly credentialsTable: ReturnType<typeof buildCredentialMethods>;
public readonly modelCapabilities: ReturnType<typeof buildModelCapabilityMethods>;
@@ -143,6 +145,7 @@ export class AppDatabaseBase {
this.knowledgeMod = buildKnowledgeMethods(getDb, ra);
this.wakeup = buildWakeupMethods(getDb, ra, tx);
this.sessionEntries = buildSessionEntryMethods(getDb, ra, tx);
this.inboxProjections = buildInboxProjectionMethods(getDb);
this.credentialsTable = buildCredentialMethods(getDb, ra, tx);
this.modelCapabilities = buildModelCapabilityMethods(getDb, ra, tx);
this.backupMod = buildBackupMethods({
@@ -443,6 +446,18 @@ export class AppDatabaseBase {
}
}
/**
* Run `operation` inside a `BEGIN IMMEDIATE` transaction. Public
* surface for callers outside the database module that need to bind
* multiple writes into one atomic unit — the inbox projection closure
* is the canonical example, where the append to the session shadow,
* the thread event, and the delivered-mark all have to commit
* together or roll back together.
*/
withTransaction<T>(operation: () => T): T {
return this.transaction(operation);
}
// ─── Methods used by subclass constructor callbacks ────────────────────────
addActivity(input: { companyId: string; actor: string; action: string; entityType: string; entityId: string; detail: string }) { this.activityMod.addActivity(input); }