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
|
For agent-owned tasks whose lifecycle status is `in_progress`, the visible
|
||||||
**In Progress** state requires a task-linked `queued` or `running` run. Stale
|
**In Progress** state requires a task-linked `queued` or `running` run. Stale
|
||||||
execution/checkout locks and scheduled retries alone do not establish activity.
|
execution/checkout locks and scheduled retries alone do not establish activity.
|
||||||
Without an active run, an open recovery action displays **Blocked**, a review
|
Without an active run, an open recovery action displays **Blocked**, a pending review
|
||||||
participant or pending thread interaction displays **In Review**, an ordinary
|
stage or active pending thread interaction displays **In Review**, an ordinary
|
||||||
scheduled monitor displays **Monitoring**, and other work displays **Todo**.
|
scheduled monitor displays **Monitoring**, and other work displays **Todo**.
|
||||||
These projections do not change stored lifecycle status, locks, admission,
|
These projections do not change stored lifecycle status, locks, admission,
|
||||||
continuation authority, or recovery. Human-owned tasks retain their explicit
|
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 { eq, sql } from "drizzle-orm";
|
||||||
import { expect, it } from "vitest";
|
import { expect, it } from "vitest";
|
||||||
import request from "supertest";
|
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 { companySearchQuerySchema, createIssueSchema, defaultStatusCardRefreshPolicy, PROVIDER_QUOTA_MONITOR_SERVICE_NAME, type Issue } from "@paperclipai/shared";
|
||||||
import { issueDisplayStatusSql } from "../services/issue-display-status.js";
|
import { issueDisplayStatusSql } from "../services/issue-display-status.js";
|
||||||
import { issueService } from "../services/issues.js";
|
import { issueService } from "../services/issues.js";
|
||||||
@@ -68,6 +68,28 @@ describeEmbeddedPostgres("Execution-backed task presentation", () => {
|
|||||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("todo");
|
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 () => {
|
it("requires task-scoped active execution and preserves human ownership", async () => {
|
||||||
const f = await fixture();
|
const f = await fixture();
|
||||||
await ctx.db.update(issues).set({ monitorNextCheckAt: null, executionPolicy: null }).where(eq(issues.id, f.issueId));
|
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");
|
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}`);
|
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");
|
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");
|
expect((await displayStatus(f.issueId)).displayStatus).toBe("in_review");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { sql, type SQL } from "drizzle-orm";
|
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 { 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.
|
/** Read-only projection shared by task reads, search, filters and counts.
|
||||||
* Lifecycle status and locks remain admission/recovery inputs. Neither alone
|
* 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.source_issue_id = ${issueId}
|
||||||
AND display_recovery.status IN ('active', 'escalated')
|
AND display_recovery.status IN ('active', 'escalated')
|
||||||
) THEN 'blocked'
|
) THEN 'blocked'
|
||||||
WHEN ${issues.executionState}->>'currentParticipant' IS NOT NULL
|
WHEN (${issues.executionState}->>'status' = 'pending'
|
||||||
|
AND ${issues.executionState}->>'currentParticipant' IS NOT NULL)
|
||||||
OR EXISTS (
|
OR EXISTS (
|
||||||
SELECT 1 FROM issue_thread_interactions display_interaction
|
SELECT 1 FROM ${issueThreadInteractions}
|
||||||
WHERE display_interaction.company_id = ${companyId}
|
WHERE ${issueThreadInteractions.companyId} = ${companyId}
|
||||||
AND display_interaction.issue_id = ${issueId}
|
AND ${issueThreadInteractions.issueId} = ${issueId}
|
||||||
AND display_interaction.status = 'pending'
|
AND ${issueThreadInteractions.status} = 'pending'
|
||||||
|
AND ${activeIssueInteractionCondition()}
|
||||||
) THEN 'in_review'
|
) THEN 'in_review'
|
||||||
WHEN ${issues.monitorNextCheckAt} IS NOT NULL
|
WHEN ${issues.monitorNextCheckAt} IS NOT NULL
|
||||||
AND ${issues.executionRunId} IS NULL AND ${issues.checkoutRunId} IS NULL
|
AND ${issues.executionRunId} IS NULL AND ${issues.checkoutRunId} IS NULL
|
||||||
|
|||||||
Reference in new issue
Block a user