Remix.run Logo
▲ badlogic an hour ago

You can already (sort of, kind of) do that via a task (please excuse the agent slop, it's midnight and it's been a long day):

``` const SyncToPostgres = defineTask<{ entryId: string }, { phase: "send" }, void>({ kind: "app.sync-postgres", version: 1, initial: () => ({ phase: "send" }), phases: { send: async (task, runtime, context) => { const entry = await runtime.read(/* the entry */); await postgres.upsert("messages", { id: task.input.entryId, ...entry }); // idempotent by id await runtime.commit(() => ({ status: "terminal", outcome: { status: "completed" } }), context); }, }, });

   await root.commit(async (tx) => {
      const id = await tx.entry(AssistantEntry, answer);
      await tx.createTask(SyncToPostgres, { entryId: id }, { ownership: { kind: "conversation" },
 background: true });
   }, context);
```

The commit on the root conversation picks out the last agent answer id from the transcript, and durably schedules a task that then syncs it to postgres. inside the task, you fetch the answer by id and send it over to postgres indempotently.

What's missing here is sugar, basically a hook that runs inside each commit so the outbox write is atomic with the state change, with ordered delivery, and possibly a durable change feed with cursors.

Thanks for the input!