mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 06:04:11 +01:00
fix(server): wake milestones when interactions replace older cards
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a406da5bf6
commit
0214d06a64
4 files changed
+53
-5
No files matched your search
@@ -1496,3 +1496,11 @@ producer regressions failed before the added intent signals. All 169 cases acros
|
||||
the worker and interaction-service suites pass, as does production server
|
||||
typechecking. The draft remains incomplete: other producers and production
|
||||
registration/polling removal are still pending. See the milestone audit for logs.
|
||||
|
||||
|
||||
Slice 28 creation/sibling replacement now registers intent before the first
|
||||
interaction write. Both question and confirmation replacement failed before the
|
||||
fix; all 184 milestone/integration/unit interaction cases and production server
|
||||
typechecking pass. Remaining terminal-interaction writers are document-target
|
||||
expiry, deleted-addressee cancellation and startup supersession; other producer
|
||||
audits and production migration remain pending.
|
||||
@@ -809,3 +809,34 @@ Remaining interaction producers include document-target invalidation, deleted
|
||||
addressees, startup supersession and creation/replacement. Provider receipt,
|
||||
conversation/configuration and alternate recovery audits also remain. Production
|
||||
worker registration and lifecycle polling removal are still intentionally pending.
|
||||
|
||||
|
||||
## Creation and sibling replacement
|
||||
|
||||
Creating a question or confirmation can expire a prior card from the same agent,
|
||||
release the prior run's provider response slot, and cancel its unsent prompt.
|
||||
The creation transaction now registers a single issue-scoped milestone intent
|
||||
before inserting the new row. This precedes both sibling invalidation and prompt
|
||||
publication changes. Existing idempotent reuse and validation return/throw before
|
||||
this boundary; unrelated interaction kinds remain outside it. Provider policy
|
||||
and sibling-supersession semantics are unchanged.
|
||||
|
||||
The replacement regressions create two runs before worker startup, suppress the
|
||||
older failed run with its pending card, let the worker become quiet, then create
|
||||
the replacement through the actual service. They verify that the old card is
|
||||
expired and its failure notice appears without another run transition or poll.
|
||||
|
||||
- `/tmp/sleep28-interaction-replacement-before.log`: question and confirmation
|
||||
replacement both fail the missing-notice assertion before the creation signal.
|
||||
- `/tmp/sleep28-interaction-replacement-tests.log`: all 184 cases pass across
|
||||
the 65 milestone worker, 106 interaction-service integration and 13 interaction
|
||||
service unit cases. PostgreSQL suites use separate isolated databases.
|
||||
- `/tmp/sleep28-interaction-replacement-typecheck.log`: production server
|
||||
typecheck passes; test files are excluded. `git diff --check` passes.
|
||||
|
||||
The remaining direct terminal-interaction writers are document-target expiry
|
||||
(the resolution-time helper and explicit document-change method), deletion of
|
||||
an addressed agent, and startup supersession. Document-change callers are in
|
||||
`routes/issues.ts`; the owning state mutation is in the interaction service.
|
||||
Audit those before removing the lifecycle poll. This checkpoint still does not
|
||||
register the worker in production or establish final CI/review acceptance.
|
||||
@@ -640,20 +640,23 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
["confirmation", "withdraw"], ["confirmation", "expire"],
|
||||
["question", "comment"], ["question", "historical comment"],
|
||||
["confirmation", "comment"], ["confirmation", "historical comment"],
|
||||
["question", "replacement"], ["confirmation", "replacement"],
|
||||
] as const)("rechecks an unsent %s after %s", async (kind, action) => {
|
||||
const source = await run(f, { status: "failed", errorCode: "test_run_failed" });
|
||||
const interactions = issueThreadInteractionService(db);
|
||||
const issue = { id: f.issueId, companyId: f.companyId, status: "in_progress" };
|
||||
const interaction = await interactions.create(issue, kind === "confirmation" ? {
|
||||
kind: "request_confirmation", continuationPolicy: "wake_assignee", sourceRunId: source.id,
|
||||
const createInteraction = (sourceRunId: string) => interactions.create(issue, kind === "confirmation" ? {
|
||||
kind: "request_confirmation", continuationPolicy: "wake_assignee", sourceRunId,
|
||||
title: "Confirm", payload: { version: 1, prompt: "Continue?", supersedeOnUserComment: true },
|
||||
} : {
|
||||
kind: "ask_user_questions", continuationPolicy: "wake_assignee", sourceRunId: source.id,
|
||||
kind: "ask_user_questions", continuationPolicy: "wake_assignee", sourceRunId,
|
||||
title: "Choose", payload: { version: 1, title: "Choose", supersedeOnUserComment: true, questions: [{
|
||||
id: "choice", prompt: "Continue?", selectionMode: "single", required: true,
|
||||
options: [{ id: "yes", label: "Yes" }],
|
||||
}] },
|
||||
}, { agentId: f.agentId, runId: source.id });
|
||||
}, { agentId: f.agentId, runId: sourceRunId });
|
||||
const interaction = await createInteraction(source.id);
|
||||
const replacement = action === "replacement" ? await run() : null;
|
||||
if (action === "historical answer") {
|
||||
await interactions.dismissInteraction(issue, interaction.id, { userId: f.userId });
|
||||
await db.update(issues).set({ status: "done" }).where(eq(issues.id, f.issueId));
|
||||
@@ -661,7 +664,10 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
await start(); await flush();
|
||||
expect((await publications()).some(row => row.idempotencyKey.startsWith(`run:${source.id}:`))).toBe(false);
|
||||
await quiet();
|
||||
if (action === "comment" || action === "historical comment") {
|
||||
if (action === "replacement") {
|
||||
await createInteraction(replacement!.id);
|
||||
expect((await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, interaction.id)))[0].status).toBe("expired");
|
||||
} else if (action === "comment" || action === "historical comment") {
|
||||
const [comment] = await db.insert(issueComments).values({ companyId: f.companyId, issueId: f.issueId,
|
||||
authorType: "user", authorUserId: f.userId, origin: "comment", body: "Ignore the earlier request" }).returning();
|
||||
const expired = action === "comment"
|
||||
|
||||
@@ -3697,6 +3697,9 @@ export function issueThreadInteractionService(
|
||||
eq(chatVoiceSessions.callerAuthority, "guest_intake"))).limit(1);
|
||||
if (guest) throw unprocessable("Ask this unverified caller a clarification in a task comment; submit_request will deliver their spoken follow-up. Protected human-input questions require authenticated access.", {code: "voice_guest_use_task_comment"});
|
||||
}
|
||||
if (data.kind === "ask_user_questions" || data.kind === "request_confirmation") {
|
||||
await notifyChatRunMilestones(tx, { companyId: issue.companyId, issueId: issue.id });
|
||||
}
|
||||
const [row] = await tx
|
||||
.insert(issueThreadInteractions)
|
||||
.values({
|
||||
|
||||
Reference in new issue
Block a user