fix(server): wake chat milestones from recovered prompt receipts

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-25 09:35:51 -05:00
1 parent 41cc717572
commit 36cf01b4b5
4 files changed
+71 -2

No files matched your search

@@ -1536,3 +1536,11 @@ native review acceptance cases. Server production types pass. The slice audit
retains the invalid fixture and missed test-file selection. Polling removal,
remaining producers, fresh CI/review, older findings and the full inventory remain
unfinished; no stack merge or whole-goal completion is claimed.
Slice 28 also covers interactive callback recovery of an unknown prompt receipt.
Its existing Slack acceptance test failed before the answer-write barrier without
the receipt signal, then five Slack/Discord/Telegram cases and server production
types passed after the one-line integration. File transport and ordinary tool
receipt ordering were audited without adding unnecessary notifications. Remaining
configuration/recovery producers and production wiring still gate polling removal.
@@ -994,3 +994,53 @@ above or chat-run-publications since the preceding 3536b3649f inspection. Full
stack reconciliation remains outstanding. The lifecycle projector remains
unregistered and its periodic production scan remains until the rest of the
publication, configuration, and recovery producer audit is complete.
## Interactive callback receipt recovery
The shared interactive callback can recover a question/confirmation prompt from
`delivery_unknown` to `published` before attempting to resolve the interaction.
That receipt alone can authorize existing continuation lineage, even when the
later resolution does not write. It now supplies the issue scope to the existing
publication notification in that receipt transaction. A published row receiving
only its missing provider message ID keeps its existing delivery wake without
adding a milestone wake: prompt lineage reads publication state, not that ID.
No provider protocol, action authorization, or dispatch policy moved into wiring.
The existing full Slack closed-choice test now observes the scoped settled signal
at its pre-resolution barrier. This prevents the later answer transaction from
masking a missing receipt wake. `/tmp/sleep28-callback-receipt-before.log` fails
there before the change; `/tmp/sleep28-callback-receipt-tests.log` passes all five
selected Slack/Discord/Telegram callback, modal, and continuation cases. The test
still verifies actual publication receipt, answer, continuation, deduplication,
unauthorized actions and acknowledged callback behavior. Provider transports are
controlled test ports, not live-provider qualification. Server production types
pass in `/tmp/sleep28-callback-receipt-typecheck.log`; diff whitespace checks pass.
Additional producer dispositions inspected at this checkpoint:
- Discord and Telegram large-publication preparation explicitly reject cards and
interactionId. They preserve the root publication identity while splitting text
or attachments; they do not confirm an interactive prompt receipt.
- Slack file-receipt admission rejects cards/interactionId. Teams file-transfer
authority also rejects those plus progress/control/wake publications. Their
file delivery settlement is not an interactive prompt lineage producer.
- Authored-final suppression tests durable publication/comment identity, not the
transport state. File receipt outcomes do not remove that identity.
- Ordinary provider question/confirmation winner-action writes run in the same
resolution transaction as the already-covered interaction signal. Winner
actions restrict eligible destinations; they do not create a separate callback
polling obligation. Alternate recovery writers still require review.
- Tool-action continuation admission selects already-terminal requests joined to
existing delivery receipts before calling heartbeat.wakeup. Delivery's later
deliveredAt write is not read by milestone lineage. Normal admission therefore
observes the earlier durable request/receipt and supplies its existing wake.
Do not add an unconditional signal to every tool-request metadata write merely
because the lineage query joins that table.
- Gateway connection-authorization creation/reopening inserts a pending review
without superseding another or changing sourceRunId. It restricts eligibility;
its later ordinary resolution is covered by interaction-service signals.
These dispositions do not finish the entire publication/configuration/recovery
audit. In particular, endpoint and conversation reactivation, policy changes and
alternate recovery receipts still need complete real-producer coverage before
production registration and removal of the lifecycle scan.
@@ -41213,13 +41213,18 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
await service.shutdown();
});
it("publishes a closed-choice question, settles its Slack card, and delivers its exact continuation response", async () => {
it("publishes a closed-choice question, settles its Slack card, and delivers its exact continuation response", async ({ onTestFinished }) => {
let receiptWakeObserved = false;
const fixture = await seedCompany();
const continuationRunId = randomUUID();
const resolveNativeQuestion = vi.fn(async () => "not_native" as const);
const { callbacks, endpoint, runtime, service } =
await configuredSlackEndpoint(fixture, {
resolveNativeQuestion,
questionResolutionPersistBarrier: async () => {
// Receipt commit must wake projection before the separate answer write.
expect(receiptWakeObserved).toBe(true);
},
wakeup: async (agentId, options) => {
if (options.contextSnapshot?.source !== "issue.interaction.respond") {
return { accepted: true };
@@ -41542,6 +41547,12 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
.set({ state: "delivery_unknown", providerMessageId: null })
.where(eq(chatPublications.id, publication.id));
const unsubscribeReceipt = subscribeDatabaseWork(db, "chat-run-milestones", (event, hints) => {
if (event === "settled" && hints?.has(JSON.stringify({ companyId: fixture.companyId, issueId: conversation.issueId }))) {
receiptWakeObserved = true;
}
});
onTestFinished(unsubscribeReceipt);
const providerAcknowledgement = callbacks.onAction(actionEvent());
await expect(
Promise.race([
@@ -781,7 +781,7 @@ export function createChatInteractionCallbacks(db: Db, options: InteractionCallb
.then((rows) => rows[0] ?? null);
if (occupied && occupied.publicationId !== current.id) return null;
const reconciledAt = new Date();
await notifyChatPublicationWork(tx);
await notifyChatPublicationWork(tx, current.state === "delivery_unknown" ? current : undefined);
const [reconciled] = await tx
.update(chatPublications)
.set({