mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 21:49:18 +01:00
fix(server): wake chat milestones after confirmation verdicts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
20f8c301f5
commit
a406da5bf6
4 files changed
+79
-4
No files matched your search
@@ -1488,3 +1488,11 @@ existing chat acceptance cases, publication policy cases, and server production
|
||||
typecheck passed at their recorded checkpoints. See the detailed milestone audit
|
||||
for retained regression/setup failures, controlled ports and remaining coverage.
|
||||
The initial draft CI was running; no final green-check or readiness claim is made.
|
||||
|
||||
|
||||
Slice 28 now also wakes lifecycle projection from confirmation accept/reject and
|
||||
live/historical comment invalidation of questions and confirmations. Six real
|
||||
producer regressions failed before the added intent signals. All 169 cases across
|
||||
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.
|
||||
@@ -768,3 +768,44 @@ interaction creation/source changes, provider-specific receipts, conversation
|
||||
bindings, configuration and alternate recovery writers. Production registration,
|
||||
removal of only the lifecycle polling lane, complete latest-head checks and final
|
||||
stack/master reconciliation remain required. No merge or full-goal completion.
|
||||
|
||||
|
||||
## Confirmation verdicts and comment invalidation
|
||||
|
||||
The interaction service owns verdict and invalidation writes. Acceptance and
|
||||
rejection now register milestone intent before the caller's optional in-transaction
|
||||
resolution hook. Existing authorization, issue/proposal/interaction lock order,
|
||||
workspace finalization checks and postcommit effects remain unchanged. Only
|
||||
request_confirmation affects the current milestone response-slot policy;
|
||||
checkbox confirmations do not acquire this additional intent.
|
||||
|
||||
Live human-comment invalidation registers intent for question/confirmation rows
|
||||
before changing their state. Historical-comment reconciliation registers one
|
||||
issue-scoped intent per relevant bulk write. A historical checkbox-only group
|
||||
remains outside this notification. No new wrapper, table or periodic retry was
|
||||
introduced. All paths continue using the existing outer-transaction protocol.
|
||||
|
||||
Evidence:
|
||||
|
||||
- `/tmp/sleep28-confirmation-before.log`: real acceptance/rejection both fail to
|
||||
produce the newly eligible failure notice; confirmation skip/withdraw/terminal
|
||||
expiry already pass using the previous checkpoint's signals.
|
||||
- `/tmp/sleep28-confirmation-invalidation-before.log`: six missing-notice
|
||||
regressions fail: acceptance/rejection and question/confirmation supersession
|
||||
by live/historical comments. The invalidation tests verify the actual service
|
||||
expired their original interaction before asserting the missing notice.
|
||||
- `/tmp/sleep28-confirmation-tests.log`: 169 cases pass, comprising all 63 worker
|
||||
cases and 106 existing interaction-service cases, using the same separate
|
||||
self-cleaning PostgreSQL databases as the previous checkpoint.
|
||||
- `/tmp/sleep28-confirmation-typecheck.log`: production server typecheck passes.
|
||||
It excludes test files. `git diff --check` passes.
|
||||
|
||||
The latest fetched master is 3536b3649f98155d92b45cfb5dc3abcc2fea97d6; it has
|
||||
not yet been reconciled into this stack. CI run 40198785531 for parent checkpoint
|
||||
20f8c301f5 was still live with no reported failures at inspection. This does not
|
||||
establish current-head final acceptance.
|
||||
|
||||
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.
|
||||
@@ -633,13 +633,23 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
expect((await publications()).map(row => row.idempotencyKey)).toContain(`run:${targetId}:queued:${f.endpointId}`);
|
||||
await quiet();
|
||||
});
|
||||
it.each(["answer", "skip", "cancel", "withdraw", "expire", "historical answer"] as const)("rechecks an unsent question after %s", async action => {
|
||||
it.each([
|
||||
["question", "answer"], ["question", "skip"], ["question", "cancel"],
|
||||
["question", "withdraw"], ["question", "expire"], ["question", "historical answer"],
|
||||
["confirmation", "accept"], ["confirmation", "reject"], ["confirmation", "skip"],
|
||||
["confirmation", "withdraw"], ["confirmation", "expire"],
|
||||
["question", "comment"], ["question", "historical comment"],
|
||||
["confirmation", "comment"], ["confirmation", "historical comment"],
|
||||
] 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, {
|
||||
const interaction = await interactions.create(issue, kind === "confirmation" ? {
|
||||
kind: "request_confirmation", continuationPolicy: "wake_assignee", sourceRunId: source.id,
|
||||
title: "Confirm", payload: { version: 1, prompt: "Continue?", supersedeOnUserComment: true },
|
||||
} : {
|
||||
kind: "ask_user_questions", continuationPolicy: "wake_assignee", sourceRunId: source.id,
|
||||
title: "Choose", payload: { version: 1, title: "Choose", questions: [{
|
||||
title: "Choose", payload: { version: 1, title: "Choose", supersedeOnUserComment: true, questions: [{
|
||||
id: "choice", prompt: "Continue?", selectionMode: "single", required: true,
|
||||
options: [{ id: "yes", label: "Yes" }],
|
||||
}] },
|
||||
@@ -651,7 +661,18 @@ 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 === "skip") {
|
||||
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"
|
||||
? await interactions.expireRequestConfirmationsSupersededByComment(issue, comment, { userId: f.userId })
|
||||
: await interactions.expireRequestConfirmationsSupersededByHistoricalComments(issue);
|
||||
expect(expired.map(row => row.id)).toContain(interaction.id);
|
||||
} else if (action === "accept") {
|
||||
await interactions.acceptInteraction({ ...issue, projectId: null, goalId: null }, interaction.id, {}, { userId: f.userId });
|
||||
} else if (action === "reject") {
|
||||
await interactions.rejectInteraction(issue, interaction.id, { reason: "No longer needed" }, { userId: f.userId });
|
||||
} else if (action === "skip") {
|
||||
await interactions.skipInteraction(issue, interaction.id, { reason: "No longer needed" }, { userId: f.userId });
|
||||
} else if (action === "cancel") {
|
||||
await interactions.cancelQuestions(issue, interaction.id, { reason: "No longer needed" }, { userId: f.userId });
|
||||
|
||||
@@ -2175,6 +2175,7 @@ export function issueThreadInteractionService(
|
||||
const postCommitActivityPublications: ActivityPublication[] = [];
|
||||
const result = await db.transaction(async (tx) => {
|
||||
await assertInteractionRunWriteAllowed(tx as unknown as Db, args.issue, args.actor);
|
||||
if (args.current.kind === "request_confirmation") await notifyChatRunMilestones(tx, args.current);
|
||||
await args.mutationOptions?.beforeResolveInTransaction?.(tx);
|
||||
// Policy mutations and review transitions use the same issue-row lock,
|
||||
// so the authoritative review policy and requester are stable through
|
||||
@@ -2458,6 +2459,7 @@ export function issueThreadInteractionService(
|
||||
const now = new Date();
|
||||
const updated = await db.transaction(async (tx) => {
|
||||
await assertInteractionRunWriteAllowed(tx as unknown as Db, args.issue, args.actor);
|
||||
if (args.current.kind === "request_confirmation") await notifyChatRunMilestones(tx, args.current);
|
||||
await args.mutationOptions?.beforeResolveInTransaction?.(tx);
|
||||
const issueContext = await tx
|
||||
.select({
|
||||
@@ -4401,6 +4403,7 @@ export function issueThreadInteractionService(
|
||||
const expired: IssueThreadInteraction[] = [];
|
||||
for (const row of superseded) {
|
||||
const updated = await db.transaction(async (tx) => {
|
||||
if (["ask_user_questions", "request_confirmation"].includes(row.kind)) await notifyChatRunMilestones(tx, row);
|
||||
if (row.kind === "connection_intent") {
|
||||
await tx
|
||||
.delete(toolOauthStates)
|
||||
@@ -4537,6 +4540,7 @@ export function issueThreadInteractionService(
|
||||
);
|
||||
if (!sampleQuestionRow) continue;
|
||||
const updatedInteractions = await db.transaction(async (tx) => {
|
||||
await notifyChatRunMilestones(tx, sampleQuestionRow);
|
||||
const updatedRows = await tx
|
||||
.update(issueThreadInteractions)
|
||||
.set({
|
||||
@@ -4575,6 +4579,7 @@ export function issueThreadInteractionService(
|
||||
);
|
||||
if (!sampleConfirmationRow) continue;
|
||||
const updatedInteractions = await db.transaction(async (tx) => {
|
||||
if (commentRows.some(row => row.kind === "request_confirmation")) await notifyChatRunMilestones(tx, sampleConfirmationRow);
|
||||
await notifyDeliveryWork(tx, DELIVERY_QUEUES.toolReviewProjection);
|
||||
const updatedRows = await tx
|
||||
.update(issueThreadInteractions)
|
||||
|
||||
Reference in new issue
Block a user