mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 07:10:34 +01:00
fix: exclude historical input and returned reviews from task waits
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
5ea1b1455a
commit
45c4ababc5
3 files changed
+35
-10
No files matched your search
@@ -140,8 +140,8 @@ terminal, and provider-quota recovery states keep their own presentation.
|
||||
For agent-owned tasks whose lifecycle status is `in_progress`, the visible
|
||||
**In Progress** state requires a task-linked `queued` or `running` run. Stale
|
||||
execution/checkout locks and scheduled retries alone do not establish activity.
|
||||
Without an active run, an open recovery action displays **Blocked**, a review
|
||||
participant or pending thread interaction displays **In Review**, an ordinary
|
||||
Without an active run, an open recovery action displays **Blocked**, a pending review
|
||||
stage or active pending thread interaction displays **In Review**, an ordinary
|
||||
scheduled monitor displays **Monitoring**, and other work displays **Todo**.
|
||||
These projections do not change stored lifecycle status, locks, admission,
|
||||
continuation authority, or recovery. Human-owned tasks retain their explicit
|
||||
|
||||
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { expect, it } from "vitest";
|
||||
import request from "supertest";
|
||||
import { agents, heartbeatRuns, issueRecoveryActions, issueRelations, issueThreadInteractions, issues, statusCards } from "@paperclipai/db";
|
||||
import { agents, heartbeatRuns, issueComments, issueRecoveryActions, issueRelations, issueThreadInteractions, issues, statusCards } from "@paperclipai/db";
|
||||
import { companySearchQuerySchema, createIssueSchema, defaultStatusCardRefreshPolicy, PROVIDER_QUOTA_MONITOR_SERVICE_NAME, type Issue } from "@paperclipai/shared";
|
||||
import { issueDisplayStatusSql } from "../services/issue-display-status.js";
|
||||
import { issueService } from "../services/issues.js";
|
||||
@@ -68,6 +68,28 @@ describeEmbeddedPostgres("Execution-backed task presentation", () => {
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("todo");
|
||||
});
|
||||
|
||||
it.each(["dismissed", "historical"])("does not turn a monitor into review for a %s request", async (kind) => {
|
||||
const f = await fixture();
|
||||
const createdAt = new Date(Date.now() - 60_000);
|
||||
await ctx.db.insert(issueThreadInteractions).values({ companyId: f.companyId, issueId: f.issueId,
|
||||
kind: kind === "dismissed" ? "request_confirmation" : "ask_user_questions", status: "pending", createdAt,
|
||||
dismissedAt: kind === "dismissed" ? new Date() : null, effectiveResolverPolicySource: "requested",
|
||||
payload: { version: 1, prompt: "Earlier request" } });
|
||||
if (kind === "historical") await ctx.db.insert(issueComments).values({ companyId: f.companyId, issueId: f.issueId,
|
||||
authorUserId: f.actor.userId, body: "Continue with the current instructions", createdAt: new Date() });
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("monitoring");
|
||||
expect(await issueService(ctx.db).count(f.companyId, { displayStatus: "in_review" })).toBe(0);
|
||||
});
|
||||
|
||||
it("does not treat a retained reviewer as pending after changes are requested", async () => {
|
||||
const f = await fixture();
|
||||
await ctx.db.update(issues).set({ executionState: { status: "changes_requested", currentStageId: "review",
|
||||
currentStageType: "review", currentParticipant: { type: "user", userId: "reviewer" }, lastDecisionOutcome: "changes_requested" } }).where(eq(issues.id, f.issueId));
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("monitoring");
|
||||
await ctx.db.update(issues).set({ monitorNextCheckAt: null }).where(eq(issues.id, f.issueId));
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("todo");
|
||||
});
|
||||
|
||||
it("requires task-scoped active execution and preserves human ownership", async () => {
|
||||
const f = await fixture();
|
||||
await ctx.db.update(issues).set({ monitorNextCheckAt: null, executionPolicy: null }).where(eq(issues.id, f.issueId));
|
||||
@@ -163,7 +185,7 @@ describeEmbeddedPostgres("Execution-backed task presentation", () => {
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("blocked");
|
||||
await ctx.db.execute(sql`UPDATE issue_recovery_actions SET status = 'resolved' WHERE source_issue_id = ${f.issueId}`);
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("monitoring");
|
||||
await ctx.db.update(issues).set({ executionState: { currentParticipant: { type: "user", userId: "reviewer" } } }).where(eq(issues.id, f.issueId));
|
||||
await ctx.db.update(issues).set({ executionState: { status: "pending", currentStageId: "review", currentStageType: "review", currentParticipant: { type: "user", userId: "reviewer" } } }).where(eq(issues.id, f.issueId));
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("in_review");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { sql, type SQL } from "drizzle-orm";
|
||||
import { issues } from "@paperclipai/db";
|
||||
import { issues, issueThreadInteractions } from "@paperclipai/db";
|
||||
import { PROVIDER_QUOTA_MONITOR_SERVICE_NAME, type IssueDisplayStatus } from "@paperclipai/shared";
|
||||
import { activeIssueInteractionCondition } from "./issue-question-context.js";
|
||||
|
||||
/** Read-only projection shared by task reads, search, filters and counts.
|
||||
* Lifecycle status and locks remain admission/recovery inputs. Neither alone
|
||||
@@ -28,12 +29,14 @@ export function issueDisplayStatusSql(): SQL<IssueDisplayStatus> {
|
||||
AND display_recovery.source_issue_id = ${issueId}
|
||||
AND display_recovery.status IN ('active', 'escalated')
|
||||
) THEN 'blocked'
|
||||
WHEN ${issues.executionState}->>'currentParticipant' IS NOT NULL
|
||||
WHEN (${issues.executionState}->>'status' = 'pending'
|
||||
AND ${issues.executionState}->>'currentParticipant' IS NOT NULL)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM issue_thread_interactions display_interaction
|
||||
WHERE display_interaction.company_id = ${companyId}
|
||||
AND display_interaction.issue_id = ${issueId}
|
||||
AND display_interaction.status = 'pending'
|
||||
SELECT 1 FROM ${issueThreadInteractions}
|
||||
WHERE ${issueThreadInteractions.companyId} = ${companyId}
|
||||
AND ${issueThreadInteractions.issueId} = ${issueId}
|
||||
AND ${issueThreadInteractions.status} = 'pending'
|
||||
AND ${activeIssueInteractionCondition()}
|
||||
) THEN 'in_review'
|
||||
WHEN ${issues.monitorNextCheckAt} IS NOT NULL
|
||||
AND ${issues.executionRunId} IS NULL AND ${issues.checkoutRunId} IS NULL
|
||||
|
||||
Reference in new issue
Block a user