mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 02:32:07 +01:00
feat(ui): show scheduled task waits as Monitoring
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
0748b90062
commit
7ddd8205ad
66 files changed
+537
-123
No files matched your search
@@ -0,0 +1,111 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { expect, it } from "vitest";
|
||||
import request from "supertest";
|
||||
import { agents, heartbeatRuns, issueRecoveryActions, issues } from "@paperclipai/db";
|
||||
import { companySearchQuerySchema, createIssueSchema, PROVIDER_QUOTA_MONITOR_SERVICE_NAME } from "@paperclipai/shared";
|
||||
import { issueDisplayStatusSql } from "../services/issue-display-status.js";
|
||||
import { issueService } from "../services/issues.js";
|
||||
import { companySearchService } from "../services/company-search.js";
|
||||
import { issueRoutes } from "../routes/issues.js";
|
||||
import {
|
||||
describeEmbeddedPostgres, routeApp,
|
||||
seedCompanyWithBoardAccess, useEmbeddedPostgres,
|
||||
} from "./helpers/route-test-harness.js";
|
||||
|
||||
describeEmbeddedPostgres("Monitoring task presentation", () => {
|
||||
const ctx = useEmbeddedPostgres("paperclip-monitoring-status-", {
|
||||
resetEach: async (db) => { await db.execute(sql`SET client_min_messages = warning`); await db.execute(sql`TRUNCATE TABLE companies CASCADE`); },
|
||||
});
|
||||
|
||||
async function fixture() {
|
||||
const company = await seedCompanyWithBoardAccess(ctx.db, "Monitoring");
|
||||
const agentId = randomUUID();
|
||||
await ctx.db.insert(agents).values({ id: agentId, companyId: company.companyId, name: "Monitor owner", role: "engineer", status: "paused", adapterType: "process", adapterConfig: {} });
|
||||
const nextCheckAt = new Date(Date.now() + 60 * 60_000);
|
||||
const issueId = randomUUID();
|
||||
await ctx.db.insert(issues).values({
|
||||
id: issueId, companyId: company.companyId, title: "Check deployment", status: "in_progress",
|
||||
assigneeAgentId: agentId, createdByUserId: company.actor.userId,
|
||||
monitorNextCheckAt: nextCheckAt,
|
||||
executionPolicy: { mode: "normal", stages: [], commentRequired: false, monitor: { nextCheckAt: nextCheckAt.toISOString(), notes: "Check deployment" } },
|
||||
});
|
||||
return { ...company, agentId, issueId };
|
||||
}
|
||||
|
||||
async function displayStatus(id: string) {
|
||||
const [row] = await ctx.db.select({ status: issues.status, displayStatus: issueDisplayStatusSql() }).from(issues).where(eq(issues.id, id));
|
||||
return row;
|
||||
}
|
||||
|
||||
it("projects a scheduled wait consistently across detail, compact rows, summaries, search and counts", async () => {
|
||||
const f = await fixture();
|
||||
const svc = issueService(ctx.db);
|
||||
expect(await svc.getById(f.issueId)).toMatchObject({ status: "in_progress", displayStatus: "monitoring" });
|
||||
expect(await svc.list(f.companyId, { displayStatus: "monitoring", listProjection: "index" })).toEqual([expect.objectContaining({ id: f.issueId, status: "in_progress", displayStatus: "monitoring" })]);
|
||||
expect(await svc.count(f.companyId, { displayStatus: "monitoring" })).toBe(1);
|
||||
const app = routeApp(ctx.db, f.actor, issueRoutes);
|
||||
const summary = await request(app).get(`/api/companies/${f.companyId}/issues/summaries?ids=${f.issueId}`).expect(200);
|
||||
expect(summary.body[0]).toMatchObject({ status: "in_progress", displayStatus: "monitoring" });
|
||||
const result = await companySearchService(ctx.db).search(f.companyId, companySearchQuerySchema.parse({ q: "deployment", displayStatus: "monitoring" }));
|
||||
expect(result.results[0]?.issue).toMatchObject({ id: f.issueId, status: "in_progress", displayStatus: "monitoring" });
|
||||
expect(result.filterOptionCounts.displayStatus?.monitoring).toBe(1);
|
||||
expect(result.filterOptionCounts.status.in_progress).toBe(1);
|
||||
// Existing agent/API lifecycle filters keep their original meaning.
|
||||
expect(await svc.count(f.companyId, { status: "in_progress" })).toBe(1);
|
||||
expect(createIssueSchema.safeParse({ title: "Manual monitoring", status: "monitoring" }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("filters before pagination and keeps In Progress separate from Monitoring", async () => {
|
||||
const f = await fixture();
|
||||
await ctx.db.insert(issues).values({ companyId: f.companyId, title: "Ordinary work", status: "in_progress", assigneeAgentId: f.agentId });
|
||||
const svc = issueService(ctx.db);
|
||||
expect(await svc.list(f.companyId, { displayStatus: "monitoring", limit: 1, offset: 0 })).toEqual([expect.objectContaining({ id: f.issueId })]);
|
||||
expect(await svc.list(f.companyId, { displayStatus: "monitoring", limit: 1, offset: 1 })).toEqual([]);
|
||||
expect(await svc.count(f.companyId, { displayStatus: "in_progress" })).toBe(1);
|
||||
expect(await svc.count(f.companyId, { status: "in_progress" })).toBe(2);
|
||||
});
|
||||
|
||||
it("returns to In Progress during the check and Monitoring after another yield, then Done", async () => {
|
||||
const f = await fixture();
|
||||
const runId = randomUUID();
|
||||
await ctx.db.insert(heartbeatRuns).values({ id: runId, companyId: f.companyId, agentId: f.agentId, issueId: f.issueId, status: "queued" });
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("in_progress");
|
||||
await ctx.db.update(heartbeatRuns).set({ status: "running" }).where(eq(heartbeatRuns.id, runId));
|
||||
await ctx.db.update(issues).set({ executionRunId: runId, checkoutRunId: runId }).where(eq(issues.id, f.issueId));
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("in_progress");
|
||||
await ctx.db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, runId));
|
||||
await ctx.db.update(issues).set({ executionRunId: null, checkoutRunId: null }).where(eq(issues.id, f.issueId));
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("monitoring");
|
||||
await ctx.db.update(issues).set({ status: "done" }).where(eq(issues.id, f.issueId));
|
||||
expect(await displayStatus(f.issueId)).toEqual({ status: "done", displayStatus: "done" });
|
||||
});
|
||||
|
||||
it.each(["blocked", "in_review", "todo", "cancelled"])("preserves %s even with a retained monitor", async (status) => {
|
||||
const f = await fixture();
|
||||
await ctx.db.update(issues).set({ status }).where(eq(issues.id, f.issueId));
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe(status);
|
||||
});
|
||||
|
||||
it("does not label cleared, consumed, orphaned or quota-recovery schedules Monitoring", async () => {
|
||||
const f = await fixture();
|
||||
await ctx.db.update(issues).set({ executionState: { monitor: { status: "cleared" } } }).where(eq(issues.id, f.issueId));
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("in_progress");
|
||||
await ctx.db.update(issues).set({ executionState: null, monitorNextCheckAt: null }).where(eq(issues.id, f.issueId));
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("in_progress");
|
||||
await ctx.db.update(issues).set({ monitorNextCheckAt: new Date(), executionPolicy: { monitor: { serviceName: PROVIDER_QUOTA_MONITOR_SERVICE_NAME } } }).where(eq(issues.id, f.issueId));
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("in_progress");
|
||||
await ctx.db.update(issues).set({ executionPolicy: null }).where(eq(issues.id, f.issueId));
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("in_progress");
|
||||
});
|
||||
|
||||
it("keeps recovery and review holds visible and never writes a new lifecycle state", async () => {
|
||||
const f = await fixture();
|
||||
await ctx.db.insert(issueRecoveryActions).values({ companyId: f.companyId, sourceIssueId: f.issueId, kind: "source", ownerType: "board", cause: "process_lost", fingerprint: randomUUID(), nextAction: "Inspect stopped run" });
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("in_progress");
|
||||
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));
|
||||
expect((await displayStatus(f.issueId)).displayStatus).toBe("in_progress");
|
||||
});
|
||||
});
|
||||
@@ -8379,6 +8379,7 @@ export function issueRoutes(
|
||||
title: row.title,
|
||||
identifier: row.identifier,
|
||||
status: row.status as IssueSummary["status"],
|
||||
displayStatus: row.displayStatus,
|
||||
updatedAt: row.updatedAt,
|
||||
externalConversationState: row.externalConversationState,
|
||||
}));
|
||||
@@ -8619,6 +8620,7 @@ export function issueRoutes(
|
||||
readCondition: await issueReadSqlCondition(db, req.actor),
|
||||
attention: attention === "blocked" ? "blocked" : undefined,
|
||||
status: req.query.status as string | string[] | undefined,
|
||||
displayStatus: req.query.displayStatus as string | string[] | undefined,
|
||||
assigneeAgentId,
|
||||
participantAgentId: uuidFilters.participantAgentId,
|
||||
assigneeUserId,
|
||||
@@ -8869,6 +8871,7 @@ export function issueRoutes(
|
||||
readCondition: await issueReadSqlCondition(db, req.actor),
|
||||
attention: "blocked",
|
||||
status: req.query.status as string | string[] | undefined,
|
||||
displayStatus: req.query.displayStatus as string | string[] | undefined,
|
||||
assigneeAgentId: req.query.assigneeAgentId as string | undefined,
|
||||
participantAgentId: countUuidFilters.participantAgentId,
|
||||
assigneeUserId: req.query.assigneeUserId as string | undefined,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { issueDisplayStatusSql } from "../services/issue-display-status.js";
|
||||
import { Router } from "express";
|
||||
import { and, desc, eq, gte, inArray, isNull, or, sql, type SQLWrapper } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
@@ -347,6 +348,7 @@ export function userProfileRoutes(db: Db) {
|
||||
identifier: issues.identifier,
|
||||
title: issues.title,
|
||||
status: issues.status,
|
||||
displayStatus: issueDisplayStatusSql(),
|
||||
priority: issues.priority,
|
||||
assigneeAgentId: issues.assigneeAgentId,
|
||||
assigneeUserId: issues.assigneeUserId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { issueDisplayStatusSql } from "./issue-display-status.js";
|
||||
import { externalConversationStateSql } from "./slack-conversation-state.js";
|
||||
import { and, desc, eq, gte, inArray, isNotNull, isNull, notInArray, or, sql } from "drizzle-orm";
|
||||
import type { SQL } from "drizzle-orm";
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
COMPANY_ARTIFACTS_MAX_QUERY_LENGTH,
|
||||
ISSUE_PRIORITIES,
|
||||
ISSUE_STATUSES,
|
||||
ISSUE_DISPLAY_STATUSES,
|
||||
SYSTEM_ISSUE_DOCUMENT_KEYS,
|
||||
type CompanyArtifact,
|
||||
type CompanySearchArtifactSummary,
|
||||
@@ -50,6 +52,7 @@ type IssueSearchRow = {
|
||||
title: string;
|
||||
description: string | null;
|
||||
status: string;
|
||||
displayStatus: string;
|
||||
priority: string;
|
||||
assigneeAgentId: string | null;
|
||||
assigneeUserId: string | null;
|
||||
@@ -193,6 +196,7 @@ function emptySearchCounts(): Record<CompanySearchCountType, number> {
|
||||
function emptyFilterOptionCounts(): CompanySearchFilterOptionCounts {
|
||||
return {
|
||||
status: {},
|
||||
displayStatus: {},
|
||||
priority: {},
|
||||
assigneeAgentId: {},
|
||||
assigneeUserId: {},
|
||||
@@ -224,7 +228,7 @@ function updatedWithinStart(value: string | undefined, now = new Date()): Date |
|
||||
}
|
||||
|
||||
function issueOnlyFiltersActive(query: CompanySearchQuery) {
|
||||
return query.status.length > 0
|
||||
return (query.status.length > 0 || query.displayStatus.length > 0)
|
||||
|| query.priority.length > 0
|
||||
|| query.assigneeAgentId !== undefined
|
||||
|| Boolean(query.assigneeUserId)
|
||||
@@ -236,7 +240,7 @@ function issueOnlyFiltersActive(query: CompanySearchQuery) {
|
||||
|
||||
function activeIssueFilters(query: CompanySearchQuery): Array<{ key: CompanySearchIssueFilterKey; values: string[] }> {
|
||||
const filters: Array<{ key: CompanySearchIssueFilterKey; values: string[] }> = [];
|
||||
if (query.status.length > 0) filters.push({ key: "status", values: query.status });
|
||||
if (query.status.length > 0 || query.displayStatus.length > 0) filters.push({ key: "status", values: [...query.status, ...query.displayStatus] });
|
||||
if (query.assigneeAgentId !== undefined) filters.push({ key: "assigneeAgentId", values: [query.assigneeAgentId ?? "null"] });
|
||||
if (query.assigneeUserId) filters.push({ key: "assigneeUserId", values: [query.assigneeUserId] });
|
||||
if (query.projectId) filters.push({ key: "projectId", values: [query.projectId] });
|
||||
@@ -251,6 +255,7 @@ function queryWithoutFilter(query: CompanySearchQuery, key: CompanySearchIssueFi
|
||||
return {
|
||||
...query,
|
||||
status: key === "status" ? [] : query.status,
|
||||
displayStatus: key === "status" ? [] : query.displayStatus,
|
||||
priority: key === "priority" ? [] : query.priority,
|
||||
assigneeAgentId: key === "assigneeAgentId" ? undefined : query.assigneeAgentId,
|
||||
assigneeUserId: key === "assigneeUserId" ? undefined : query.assigneeUserId,
|
||||
@@ -265,6 +270,7 @@ function queryWithoutIssueFilters(query: CompanySearchQuery): CompanySearchQuery
|
||||
return {
|
||||
...query,
|
||||
status: [],
|
||||
displayStatus: [],
|
||||
priority: [],
|
||||
assigneeAgentId: undefined,
|
||||
assigneeUserId: undefined,
|
||||
@@ -277,6 +283,7 @@ function queryWithoutIssueFilters(query: CompanySearchQuery): CompanySearchQuery
|
||||
|
||||
function issueFilterConditions(companyId: string, query: CompanySearchQuery, omit?: CompanySearchIssueFilterKey): SQL[] {
|
||||
const conditions: SQL[] = [];
|
||||
if (omit !== "status" && query.displayStatus.length) conditions.push(inArray(issueDisplayStatusSql(), query.displayStatus));
|
||||
if (omit !== "status" && query.status.length > 0) {
|
||||
conditions.push(query.status.length === 1 ? eq(issues.status, query.status[0]!) : inArray(issues.status, query.status));
|
||||
}
|
||||
@@ -315,6 +322,7 @@ function issueFilterConditions(companyId: string, query: CompanySearchQuery, omi
|
||||
// matched-issues CTE (plain columns, no drizzle table references).
|
||||
function matchedFacetConditions(companyId: string, query: CompanySearchQuery, omit?: CompanySearchIssueFilterKey): SQL[] {
|
||||
const conditions: SQL[] = [];
|
||||
if (omit !== "status" && query.displayStatus.length) conditions.push(sql`m.display_status = ANY(${sqlTextArray(query.displayStatus)})`);
|
||||
if (omit !== "status" && query.status.length > 0) {
|
||||
conditions.push(sql`m.status = ANY(${sqlTextArray(query.status)})`);
|
||||
}
|
||||
@@ -447,6 +455,7 @@ function issueResult(row: IssueSearchRow, prefix: string, normalizedQuery: strin
|
||||
identifier: row.identifier,
|
||||
title: row.title,
|
||||
status: row.status as CompanySearchIssueSummary["status"],
|
||||
displayStatus: row.displayStatus as CompanySearchIssueSummary["displayStatus"],
|
||||
priority: row.priority as CompanySearchIssueSummary["priority"],
|
||||
assigneeAgentId: row.assigneeAgentId,
|
||||
assigneeUserId: row.assigneeUserId,
|
||||
@@ -626,7 +635,7 @@ export function companySearchService(db: Db) {
|
||||
conditions.length > 0 ? sql`WHERE ${sql.join(conditions, sql` AND `)}` : sql``;
|
||||
// Count branches must match the result branch's column list; the
|
||||
// trailing NULLs pad the issue data columns.
|
||||
const countTail = sql`, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::text, NULL::text, NULL::uuid, NULL::text, NULL::uuid, NULL::timestamptz, NULL::timestamptz, NULL::double precision, NULL::text[]`;
|
||||
const countTail = sql`, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::text, NULL::text, NULL::text, NULL::uuid, NULL::text, NULL::uuid, NULL::timestamptz, NULL::timestamptz, NULL::double precision, NULL::text[]`;
|
||||
const branches: SQL[] = [];
|
||||
|
||||
const wantResultRows = scopeIncludesIssues(scope)
|
||||
@@ -652,6 +661,7 @@ export function companySearchService(db: Db) {
|
||||
(SELECT issue_text.description FROM issues issue_text
|
||||
WHERE issue_text.id = m.id AND issue_text.company_id = ${companyId}) AS description,
|
||||
m.status,
|
||||
m.display_status AS "displayStatus",
|
||||
m.priority,
|
||||
m.assignee_agent_id AS "assigneeAgentId",
|
||||
m.assignee_user_id AS "assigneeUserId",
|
||||
@@ -690,6 +700,7 @@ export function companySearchService(db: Db) {
|
||||
GROUP BY 2
|
||||
`;
|
||||
branches.push(facetBranch("facet:status", sql`m.status`, "status"));
|
||||
branches.push(facetBranch("facet:displayStatus", sql`m.display_status`, "status"));
|
||||
branches.push(facetBranch("facet:priority", sql`m.priority`, "priority"));
|
||||
branches.push(facetBranch("facet:assigneeAgentId", sql`m.assignee_agent_id`, "assigneeAgentId", [sql`m.assignee_agent_id IS NOT NULL`]));
|
||||
branches.push(facetBranch("facet:assigneeUserId", sql`m.assignee_user_id`, "assigneeUserId", [sql`m.assignee_user_id IS NOT NULL`]));
|
||||
@@ -757,6 +768,7 @@ export function companySearchService(db: Db) {
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
status: row.status,
|
||||
displayStatus: row.displayStatus,
|
||||
priority: row.priority,
|
||||
assigneeAgentId: row.assigneeAgentId,
|
||||
assigneeUserId: row.assigneeUserId,
|
||||
@@ -780,6 +792,9 @@ export function companySearchService(db: Db) {
|
||||
if (row.kind === "type:issue") aggregates.typeCounts.issue = count;
|
||||
else if (row.kind === "type:comment") aggregates.typeCounts.comment = count;
|
||||
else if (row.kind === "type:document") aggregates.typeCounts.document = count;
|
||||
else if (row.kind === "facet:displayStatus" && row.value && (ISSUE_DISPLAY_STATUSES as readonly string[]).includes(row.value)) {
|
||||
aggregates.filterOptionCounts.displayStatus![row.value as keyof NonNullable<CompanySearchFilterOptionCounts["displayStatus"]>] = count;
|
||||
}
|
||||
else if (row.kind === "facet:status" && row.value && (ISSUE_STATUSES as readonly string[]).includes(row.value)) {
|
||||
aggregates.filterOptionCounts.status[row.value as keyof CompanySearchFilterOptionCounts["status"]] = count;
|
||||
} else if (row.kind === "facet:priority" && row.value && (ISSUE_PRIORITIES as readonly string[]).includes(row.value)) {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { sql, type SQL } from "drizzle-orm";
|
||||
import { issues } from "@paperclipai/db";
|
||||
import { PROVIDER_QUOTA_MONITOR_SERVICE_NAME, type IssueDisplayStatus } from "@paperclipai/shared";
|
||||
|
||||
/** Read-only projection, shared by task reads, search, filters and counts.
|
||||
* The persisted deadline is the scheduler's source of truth. Locks and recovery
|
||||
* take precedence; a retained monitor alone must not disguise active work.
|
||||
*/
|
||||
export function issueDisplayStatusSql(): SQL<IssueDisplayStatus> {
|
||||
// Drizzle removes column qualifiers in a single-table SELECT. Keep these
|
||||
// outer references explicitly qualified so correlated reads cannot bind to
|
||||
// the run/recovery table's own id or company_id instead.
|
||||
const issueId = sql`"issues"."id"`;
|
||||
const companyId = sql`"issues"."company_id"`;
|
||||
return sql<IssueDisplayStatus>`CASE WHEN
|
||||
${issues.status} = 'in_progress'
|
||||
AND ${issues.assigneeAgentId} IS NOT NULL AND ${issues.assigneeUserId} IS NULL
|
||||
AND ${issues.monitorNextCheckAt} IS NOT NULL
|
||||
AND ${issues.executionRunId} IS NULL AND ${issues.checkoutRunId} IS NULL
|
||||
AND ${issues.executionState}->>'currentParticipant' IS NULL
|
||||
AND COALESCE(${issues.executionState}#>>'{monitor,status}', 'scheduled') = 'scheduled'
|
||||
AND ${issues.executionPolicy}->'monitor' IS NOT NULL
|
||||
AND COALESCE(${issues.executionPolicy}#>>'{monitor,serviceName}', '') <> ${PROVIDER_QUOTA_MONITOR_SERVICE_NAME}
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM heartbeat_runs display_run
|
||||
WHERE display_run.company_id = ${companyId}
|
||||
AND display_run.issue_id = ${issueId}
|
||||
AND display_run.status IN ('queued', 'running')
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM issue_recovery_actions display_recovery
|
||||
WHERE display_recovery.company_id = ${companyId}
|
||||
AND display_recovery.source_issue_id = ${issueId}
|
||||
AND display_recovery.status = 'active'
|
||||
)
|
||||
THEN 'monitoring' ELSE ${issues.status} END`;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { issueDisplayStatusSql } from "./issue-display-status.js";
|
||||
import { and, eq, inArray, isNull, type SQL } from "drizzle-orm";
|
||||
import { issues, type Db } from "@paperclipai/db";
|
||||
import { ISSUE_SUMMARIES_MAX_IDS } from "@paperclipai/shared";
|
||||
@@ -18,6 +19,7 @@ export async function listIssueSummaries(
|
||||
title: issues.title,
|
||||
identifier: issues.identifier,
|
||||
status: issues.status,
|
||||
displayStatus: issueDisplayStatusSql(),
|
||||
updatedAt: issues.updatedAt,
|
||||
externalConversationState: externalConversationStateSql(),
|
||||
projectId: issues.projectId,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { IssueDisplayStatus } from "@paperclipai/shared";
|
||||
import { issueDisplayStatusSql } from "./issue-display-status.js";
|
||||
import { notifyStrandedAgentStatus, notifyActiveRunSilence } from "./live-run-recovery-notifications.js";
|
||||
import { finishedIdleWake } from "./idle-work-proof.js";
|
||||
import { notifyDeferredExecutionEligibility } from "./deferred-execution-notifications.js";
|
||||
@@ -1900,6 +1902,7 @@ export function parseStatusFilter(
|
||||
export interface IssueFilters {
|
||||
attention?: "blocked";
|
||||
status?: string | readonly string[];
|
||||
displayStatus?: string | readonly string[];
|
||||
/**
|
||||
* Filter by assignee agent ID.
|
||||
* - `string` (UUID): match issues assigned to that agent.
|
||||
@@ -1951,7 +1954,7 @@ export interface IssueFilters {
|
||||
updatedSince?: string;
|
||||
}
|
||||
|
||||
type IssueRow = typeof issues.$inferSelect & { externalConversationState?: "active" | "waiting" | null };
|
||||
type IssueRow = typeof issues.$inferSelect & { displayStatus?: IssueDisplayStatus; externalConversationState?: "active" | "waiting" | null };
|
||||
type IssueLabelRow = typeof labels.$inferSelect;
|
||||
type IssuePlanDecompositionRow = typeof issuePlanDecompositions.$inferSelect;
|
||||
type IssueActiveRunRow = {
|
||||
@@ -3603,6 +3606,7 @@ function appendBlockerAttentionEdges(
|
||||
}
|
||||
|
||||
type IssueRelationSummaryRow = {
|
||||
displayStatus?: IssueDisplayStatus;
|
||||
relatedId: string;
|
||||
identifier: string | null;
|
||||
title: string;
|
||||
@@ -3620,6 +3624,7 @@ function summarizeIssueRelationRow(
|
||||
identifier: row.identifier,
|
||||
title: row.title,
|
||||
status: row.status as IssueRelationIssueSummary["status"],
|
||||
displayStatus: row.displayStatus,
|
||||
priority: row.priority as IssueRelationIssueSummary["priority"],
|
||||
assigneeAgentId: row.assigneeAgentId,
|
||||
assigneeUserId: row.assigneeUserId,
|
||||
@@ -3735,6 +3740,7 @@ async function terminalExplicitBlockersByRoot(
|
||||
identifier: issues.identifier,
|
||||
title: issues.title,
|
||||
status: issues.status,
|
||||
displayStatus: issueDisplayStatusSql(),
|
||||
priority: issues.priority,
|
||||
assigneeAgentId: issues.assigneeAgentId,
|
||||
assigneeUserId: issues.assigneeUserId,
|
||||
@@ -5039,6 +5045,7 @@ const issueListSelect = {
|
||||
END
|
||||
`,
|
||||
status: issues.status,
|
||||
displayStatus: issueDisplayStatusSql(),
|
||||
statusVersion: issues.statusVersion,
|
||||
lastStatusDecisionId: issues.lastStatusDecisionId,
|
||||
workMode: issues.workMode,
|
||||
@@ -5301,6 +5308,7 @@ async function blockedByMapForIssues(
|
||||
identifier: row.identifier,
|
||||
title: row.title,
|
||||
status: row.status as IssueRelationIssueSummary["status"],
|
||||
displayStatus: row.displayStatus,
|
||||
priority: row.priority as IssueRelationIssueSummary["priority"],
|
||||
assigneeAgentId: row.assigneeAgentId,
|
||||
assigneeUserId: row.assigneeUserId,
|
||||
@@ -6420,6 +6428,8 @@ async function blockedInboxIssueConditions(
|
||||
filters?.lowTrustBoundary,
|
||||
);
|
||||
if (lowTrustCondition) conditions.push(lowTrustCondition);
|
||||
const displayStatuses = parseStatusFilter(filters?.displayStatus);
|
||||
if (displayStatuses.length) conditions.push(inArray(issueDisplayStatusSql(), displayStatuses));
|
||||
const statuses = parseStatusFilter(filters?.status);
|
||||
if (statuses.length > 0) {
|
||||
conditions.push(
|
||||
@@ -6872,7 +6882,7 @@ export function issueService(db: Db) {
|
||||
|
||||
async function getIssueByUuid(id: string) {
|
||||
const row = await retryIdempotentDatabaseOperation(() => db
|
||||
.select({ ...getTableColumns(issues), externalConversationState: externalConversationStateSql() })
|
||||
.select({ ...getTableColumns(issues), displayStatus: issueDisplayStatusSql(), externalConversationState: externalConversationStateSql() })
|
||||
.from(issues)
|
||||
.where(eq(issues.id, id))
|
||||
.then((rows) => rows[0] ?? null));
|
||||
@@ -6883,7 +6893,7 @@ export function issueService(db: Db) {
|
||||
|
||||
async function getIssueByIdentifier(identifier: string) {
|
||||
const row = await retryIdempotentDatabaseOperation(() => db
|
||||
.select({ ...getTableColumns(issues), externalConversationState: externalConversationStateSql() })
|
||||
.select({ ...getTableColumns(issues), displayStatus: issueDisplayStatusSql(), externalConversationState: externalConversationStateSql() })
|
||||
.from(issues)
|
||||
.where(eq(issues.identifier, identifier.toUpperCase()))
|
||||
.then((rows) => rows[0] ?? null));
|
||||
@@ -7482,6 +7492,7 @@ export function issueService(db: Db) {
|
||||
identifier: issues.identifier,
|
||||
title: issues.title,
|
||||
status: issues.status,
|
||||
displayStatus: issueDisplayStatusSql(),
|
||||
priority: issues.priority,
|
||||
assigneeAgentId: issues.assigneeAgentId,
|
||||
assigneeUserId: issues.assigneeUserId,
|
||||
@@ -7502,6 +7513,7 @@ export function issueService(db: Db) {
|
||||
identifier: issues.identifier,
|
||||
title: issues.title,
|
||||
status: issues.status,
|
||||
displayStatus: issueDisplayStatusSql(),
|
||||
priority: issues.priority,
|
||||
assigneeAgentId: issues.assigneeAgentId,
|
||||
assigneeUserId: issues.assigneeUserId,
|
||||
@@ -8281,6 +8293,8 @@ export function issueService(db: Db) {
|
||||
filters?.lowTrustBoundary,
|
||||
);
|
||||
if (lowTrustCondition) conditions.push(lowTrustCondition);
|
||||
const displayStatuses = parseStatusFilter(filters?.displayStatus);
|
||||
if (displayStatuses.length) conditions.push(inArray(issueDisplayStatusSql(), displayStatuses));
|
||||
const statuses = parseStatusFilter(filters?.status);
|
||||
if (statuses.length === 1) {
|
||||
conditions.push(eq(issues.status, statuses[0]));
|
||||
@@ -8574,6 +8588,8 @@ export function issueService(db: Db) {
|
||||
conditions.push(nonIdleSlackIssueCondition());
|
||||
}
|
||||
}
|
||||
const displayStatuses = parseStatusFilter(filters?.displayStatus);
|
||||
if (displayStatuses.length) conditions.push(inArray(issueDisplayStatusSql(), displayStatuses));
|
||||
const statuses = parseStatusFilter(filters?.status);
|
||||
if (statuses.length === 1)
|
||||
conditions.push(eq(issues.status, statuses[0]!));
|
||||
@@ -8895,6 +8911,7 @@ export function issueService(db: Db) {
|
||||
identifier: issues.identifier,
|
||||
title: issues.title,
|
||||
status: issues.status,
|
||||
displayStatus: issueDisplayStatusSql(),
|
||||
priority: issues.priority,
|
||||
assigneeAgentId: issues.assigneeAgentId,
|
||||
assigneeUserId: issues.assigneeUserId,
|
||||
@@ -9522,6 +9539,7 @@ export function issueService(db: Db) {
|
||||
identifier: issues.identifier,
|
||||
title: issues.title,
|
||||
status: issues.status,
|
||||
displayStatus: issueDisplayStatusSql(),
|
||||
priority: issues.priority,
|
||||
assigneeAgentId: issues.assigneeAgentId,
|
||||
assigneeUserId: issues.assigneeUserId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { issueDisplayStatusSql } from "./issue-display-status.js";
|
||||
import { sql, type SQL } from "drizzle-orm";
|
||||
import { COMPANY_SEARCH_MAX_QUERY_LENGTH, COMPANY_SEARCH_MAX_TOKENS } from "@paperclipai/shared";
|
||||
import { visibleIssueCondition } from "./issue-visibility.js";
|
||||
@@ -134,7 +135,7 @@ export function taskSearchCtes(companyId: string, search: TaskSearch, includeCon
|
||||
// The search page fetches descriptions only for its result window.
|
||||
const flags = (fuzzyMatch: SQL) => sql`
|
||||
SELECT issues.id, issues.identifier, issues.title,
|
||||
issues.status, issues.priority, issues.assignee_agent_id, issues.assignee_user_id,
|
||||
issues.status, ${issueDisplayStatusSql()} AS display_status, issues.priority, issues.assignee_agent_id, issues.assignee_user_id,
|
||||
issues.project_id, issues.created_at, issues.updated_at,
|
||||
${identExact} AS ident_exact, ${identStarts} AS ident_starts,
|
||||
${phraseMatch(sql`issues.identifier`)} AS ident_phrase,
|
||||
|
||||
Reference in new issue
Block a user