Drop unused agent_messages.read_at column via migration v49 (M1)

The `agent_messages.read_at` column has had no production writer since
v12 moved read tracking into the per-viewer `agent_message_reads`
receipts table. It survived anyway, carrying a NULL through every SELECT
list and begging the next maintainer to accidentally read stale data
from it.

- v49 migration: `ALTER TABLE agent_messages DROP COLUMN read_at`
  (idempotent: skips if absent).
- messages.ts: SELECTs no longer fetch the column; per-viewer `readAt`
  is computed from `agent_message_reads` as before, with `null` for
  call-sites that have no viewer context (getAgentMessage /
  searchMessages / listAgentInboxUnread).
- runtime-bridge.ts / runtime-message-injector.ts: comments referencing
  the dropped column updated to name the read-receipts table instead.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-20 12:20:32 -04:00
1 parent dade5eb333
commit d7e267031f
6 files changed
+90 -36

No files matched your search

+10 -10
View File
@@ -562,16 +562,16 @@ export class RuntimeBridge {
// as plain user turns in its context.
//
// Replay safety lives entirely on the `agent_message_projections`
// ledger. We deliberately do NOT touch `agent_messages.read_at`
// here even though that column exists: "we put the message into
// the agent's transcript" and "the agent has acknowledged the
// message" are two different facts, and welding them onto one
// column made every projected message immediately invisible to
// the agent's `?unreadOnly=true` API call. The agent then read
// its IC prompt ("respond to ALL unread messages"), called the
// API, got an empty list, and posted a status comment instead of
// replying. The explicit POST /messages/{id}/read is now the
// sole signal that flips `read_at`.
// ledger. "We put the message into the agent's transcript" and
// "the agent has acknowledged the message" are two different
// facts, tracked in two different tables: projections are append-
// only on dispatch, while reads are inserted into
// `agent_message_reads` only when the agent POSTs
// /messages/{id}/read. Conflating the two (as an earlier build
// that wrote to `agent_messages.read_at` at projection time did)
// made every projected message immediately invisible to
// `?unreadOnly=true`, so agents posted status comments instead of
// replies. The explicit read endpoint is the sole signal.
const store = this.getOrCreateStore(payload.taskSessionId);
const inboxRecords = this.deps.db.listAgentInboxUnread(payload.companyId, payload.agentId);
if (inboxRecords.length > 0) {
@@ -134,10 +134,10 @@ export class RuntimeMessageInjector {
* the recipient is woken so the next `prepareRun` picks the message
* up through the regular inbox projection.
*
* `read_at` is intentionally NOT touched here: "the worker queued
* this message onto the agent" is not "the agent replied". The
* agent flips `read_at` itself by POSTing /messages/{id}/read once
* it has actually processed the row.
* The read receipt is intentionally NOT written here: "the worker
* queued this message onto the agent" is not "the agent replied".
* The agent inserts into `agent_message_reads` itself by POSTing
* /messages/{id}/read once it has actually processed the row.
*/
handleSteerAck(correlationId: string, accepted: boolean): void {
this.resolvePending(correlationId, accepted, "worker_ack");