Submatter/apps/worker/src/db/operator-notes.ts
Randall Stillwell 86155455a7 feat(worker): close Epic 7 — draft PR + operator notes + doc-writer
- Draft PR via forge (apps/worker/src/git/pr.ts) push branch + createPullRequest draft with echodo-convoy marker, expose prUrl in Mission Control (convoy-service, convoys router, detail panel)
- Operator notes mid-build: convoy_operator_messages (0016), tRPC post/list, worker poll between tool rounds (queued) and before generateText (direct), detail panel thread for in_progress
- Doc-writer: harness doc-writer examples (docs/examples/research-pipeline.md), SOP template (docs/templates/doc-sop.md), role-doc-writer wiring

Convoy: .convoys/worker-closeout-docwriter

Co-authored-by: orchestrator
2026-08-25 16:56:02 -05:00

68 lines
2 KiB
TypeScript

import { and, asc, eq, isNull } from "drizzle-orm";
import type { db as dbInstance } from "@tasks/database";
import { convoyOperatorMessages } from "@tasks/database/schema";
type Db = typeof dbInstance;
export type OperatorMessageRow = typeof convoyOperatorMessages.$inferSelect;
/**
* Poll undelivered operator messages for a convoy.
* Keep polling fallback simple — no Redis pubsub, just a DB query.
*/
export async function pollOperatorNotes(
db: Db,
workspaceId: string,
convoyId: string,
briefId?: string | null,
): Promise<OperatorMessageRow[]> {
const conditions = [
eq(convoyOperatorMessages.workspaceId, workspaceId),
eq(convoyOperatorMessages.convoyId, convoyId),
isNull(convoyOperatorMessages.deliveredAt),
];
// Optional brief scoping — include global convoy notes plus brief-specific ones
// When briefId is provided we return rows where brief_id is null OR matches the brief.
// Keeping the simple fallback: just filter by workspace+convoy when briefId not set.
// When briefId is set we still return all for convoy so direct messages are not missed.
void briefId;
const rows = await db
.select()
.from(convoyOperatorMessages)
.where(and(...conditions))
.orderBy(asc(convoyOperatorMessages.createdAt));
return rows;
}
export async function markOperatorNotesDelivered(
db: Db,
ids: string[],
): Promise<void> {
if (ids.length === 0) return;
for (const id of ids) {
await db
.update(convoyOperatorMessages)
.set({ deliveredAt: new Date() })
.where(eq(convoyOperatorMessages.id, id));
}
}
/** Convenience: poll and mark delivered in one step */
export async function consumeOperatorNotes(
db: Db,
workspaceId: string,
convoyId: string,
briefId?: string | null,
): Promise<OperatorMessageRow[]> {
const rows = await pollOperatorNotes(db, workspaceId, convoyId, briefId);
if (rows.length === 0) return rows;
await markOperatorNotesDelivered(
db,
rows.map((r) => r.id),
);
return rows;
}