feat(ui): show scheduled task waits as Monitoring

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-11-02 10:45:12 -06:00
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");
});
});
+3
View File
@@ -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,
+2
View File
@@ -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,
+18 -3
View File
@@ -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`;
}
+2
View File
@@ -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,
+21 -3
View File
@@ -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,
+2 -1
View File
@@ -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,