mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 21:49:18 +01:00
fix(server): wake chat milestones from recovered prompt receipts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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({
|
||||
|
||||
Reference in new issue
Block a user