feat(broker): record daemon idempotency fields on message_queue
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

Additive plumbing for v0.9.0 daemon spec §4.2/§4.4. Adds two nullable
columns to mesh.message_queue — client_message_id (caller-supplied) and
request_fingerprint (canonical sha256 of the send shape) — and threads
them through the broker:

  - handleSend reads them off the wire envelope when present
  - queueMessage persists them on the row
  - drainForMember projects them onto the push so receiving daemons
    can dedupe their local inbox by client_message_id

Columns stay nullable so legacy traffic (launch CLI, dashboard chat)
continues to flow uninterrupted. Sprint 7 (broker hardening) will add
the partial unique index and the client_message_dedupe atomic-accept
table once we're ready to enforce dedupe broker-side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-03 20:05:36 +01:00
parent abaa4bcf87
commit bf22afb0ed
5 changed files with 62 additions and 4 deletions

View File

@@ -564,6 +564,8 @@ async function maybePushQueuedMessages(
nonce: m.nonce,
ciphertext: m.ciphertext,
createdAt: m.createdAt.toISOString(),
...(m.clientMessageId ? { client_message_id: m.clientMessageId } : {}),
...(m.requestFingerprint ? { request_fingerprint: m.requestFingerprint } : {}),
};
sendToPeer(presenceId, push);
metrics.messagesRoutedTotal.inc({ priority: m.priority });
@@ -1968,6 +1970,12 @@ async function handleSend(
}
}
// v0.9.0 daemon clients attach a stable idempotency id and the canonical
// request fingerprint per spec §4.2/§4.4. Forward both verbatim; legacy
// callers omit them and the columns are nullable.
const clientMessageId = (msg as { client_message_id?: string }).client_message_id;
const requestFingerprint = (msg as { request_fingerprint?: string }).request_fingerprint;
const messageId = await queueMessage({
meshId: conn.meshId,
senderMemberId: conn.memberId,
@@ -1976,6 +1984,8 @@ async function handleSend(
priority: msg.priority,
nonce: msg.nonce,
ciphertext: msg.ciphertext,
clientMessageId: clientMessageId && clientMessageId.length > 0 ? clientMessageId : undefined,
requestFingerprint: requestFingerprint && requestFingerprint.length > 0 ? requestFingerprint : undefined,
});
// Topic-tagged messages (targetSpec starts with `#<topicId>`) get