feat(briefs): add deterministic card service

This commit is contained in:
Dotta committed 2026-05-24 02:28:36 +00:00
1 parent 96f0279e08
commit 602902ca3d
11 files changed
+1806

No files matched your search

@@ -0,0 +1,114 @@
CREATE TABLE plugin_briefs_37182b0291.briefs_cards (
id uuid PRIMARY KEY,
company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
user_id text NOT NULL,
slug text NOT NULL,
title text NOT NULL,
grouping_description text NOT NULL,
root_issue_id uuid REFERENCES public.issues(id) ON DELETE SET NULL,
state text NOT NULL CHECK (state IN ('error', 'blocked', 'waiting-user', 'waiting-reviewer', 'live', 'done', 'stale')),
summary_status text NOT NULL CHECK (summary_status IN ('ok', 'pending', 'fallback')),
pinned boolean NOT NULL DEFAULT false,
hidden boolean NOT NULL DEFAULT false,
stale_at timestamptz NOT NULL,
expires_at timestamptz,
latest_snapshot_id uuid,
last_meaningful_event_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (company_id, user_id, slug)
);
CREATE TABLE plugin_briefs_37182b0291.briefs_card_sources (
id uuid PRIMARY KEY,
company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
user_id text NOT NULL,
card_id uuid NOT NULL REFERENCES plugin_briefs_37182b0291.briefs_cards(id) ON DELETE CASCADE,
source_kind text NOT NULL CHECK (source_kind IN ('issue_tree', 'issue', 'comment', 'run', 'document', 'work_product', 'interaction', 'activity_event', 'approval')),
source_id text NOT NULL,
issue_id uuid REFERENCES public.issues(id) ON DELETE SET NULL,
identifier text,
title_line text NOT NULL,
right_tag text NOT NULL,
link_path text NOT NULL,
is_intra_tree_blocked boolean,
event_at timestamptz NOT NULL,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (card_id, source_kind, source_id)
);
CREATE TABLE plugin_briefs_37182b0291.briefs_card_snapshots (
id uuid PRIMARY KEY,
company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
user_id text NOT NULL,
card_id uuid NOT NULL REFERENCES plugin_briefs_37182b0291.briefs_cards(id) ON DELETE CASCADE,
summary_paragraph text,
summary_status text NOT NULL CHECK (summary_status IN ('ok', 'pending', 'fallback')),
summary_model text,
summary_tokens_in integer,
summary_tokens_out integer,
summary_failure_reason text CHECK (summary_failure_reason IS NULL OR summary_failure_reason IN ('model_error', 'truncation_failed', 'budget_capped', 'safety_block')),
task_rows jsonb NOT NULL DEFAULT '[]'::jsonb,
evidence_source_ids jsonb NOT NULL DEFAULT '[]'::jsonb,
generated_by_agent_id uuid,
generated_by_run_id uuid,
deterministic_state_inputs jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE plugin_briefs_37182b0291.briefs_cards
ADD CONSTRAINT briefs_cards_latest_snapshot_fk
FOREIGN KEY (latest_snapshot_id)
REFERENCES plugin_briefs_37182b0291.briefs_card_snapshots(id)
ON DELETE SET NULL;
CREATE TABLE plugin_briefs_37182b0291.briefs_user_preferences (
id uuid PRIMARY KEY,
company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
user_id text NOT NULL,
cadence text NOT NULL DEFAULT 'hourly' CHECK (cadence IN ('manual', 'hourly', 'daily')),
retention_days integer NOT NULL DEFAULT 7 CHECK (retention_days > 0),
done_retention_hours integer NOT NULL DEFAULT 72 CHECK (done_retention_hours > 0),
stale_after_days integer NOT NULL DEFAULT 7 CHECK (stale_after_days > 0),
max_unpinned_cards integer NOT NULL DEFAULT 30 CHECK (max_unpinned_cards > 0),
scope text NOT NULL DEFAULT 'user' CHECK (scope IN ('user')),
settings jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (company_id, user_id)
);
CREATE TABLE plugin_briefs_37182b0291.briefs_cursors (
id uuid PRIMARY KEY,
company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
user_id text NOT NULL,
cursor_key text NOT NULL,
cursor_kind text NOT NULL,
last_seen_at timestamptz,
overlap_window_start_at timestamptz,
dedupe_state jsonb NOT NULL DEFAULT '[]'::jsonb,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (company_id, user_id, cursor_key)
);
CREATE INDEX briefs_cards_company_user_state_idx
ON plugin_briefs_37182b0291.briefs_cards (company_id, user_id, hidden, state, last_meaningful_event_at DESC);
CREATE INDEX briefs_cards_root_issue_idx
ON plugin_briefs_37182b0291.briefs_cards (company_id, root_issue_id);
CREATE INDEX briefs_card_sources_card_event_idx
ON plugin_briefs_37182b0291.briefs_card_sources (card_id, event_at DESC);
CREATE INDEX briefs_card_sources_company_source_idx
ON plugin_briefs_37182b0291.briefs_card_sources (company_id, source_kind, source_id);
CREATE INDEX briefs_card_snapshots_card_created_idx
ON plugin_briefs_37182b0291.briefs_card_snapshots (card_id, created_at DESC);
CREATE INDEX briefs_cursors_company_user_kind_idx
ON plugin_briefs_37182b0291.briefs_cursors (company_id, user_id, cursor_kind);
@@ -0,0 +1,62 @@
{
"name": "@paperclipai/plugin-briefs",
"version": "0.1.0",
"description": "First-party Briefs plugin data model and deterministic briefing card service",
"license": "MIT",
"homepage": "https://github.com/paperclipai/paperclip",
"bugs": {
"url": "https://github.com/paperclipai/paperclip/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/paperclipai/paperclip",
"directory": "packages/plugins/plugin-briefs"
},
"type": "module",
"exports": {
".": "./src/index.ts"
},
"publishConfig": {
"access": "public",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"files": [
"dist",
"migrations"
],
"paperclipPlugin": {
"manifest": "./dist/manifest.js",
"worker": "./dist/worker.js"
},
"keywords": [
"paperclip",
"plugin",
"briefs",
"briefing"
],
"scripts": {
"postinstall": "node ../../../scripts/link-plugin-dev-sdk.mjs",
"prebuild": "pnpm --filter @paperclipai/plugin-sdk ensure-build-deps",
"build": "tsc",
"clean": "rm -rf dist",
"typecheck": "pnpm --filter @paperclipai/plugin-sdk ensure-build-deps && tsc --noEmit",
"test": "vitest run",
"prepack": "rm -f package.dev.json && cp package.json package.dev.json && node ../../../scripts/generate-plugin-package-json.mjs",
"postpack": "if [ -f package.dev.json ]; then mv package.dev.json package.json; fi"
},
"dependencies": {
"@paperclipai/plugin-sdk": "workspace:*"
},
"devDependencies": {
"@types/node": "^24.6.0",
"typescript": "^5.7.3",
"vitest": "^3.0.5"
}
}
@@ -0,0 +1,158 @@
import { z } from "@paperclipai/plugin-sdk";
export const briefCardStateSchema = z.enum([
"error",
"blocked",
"waiting-user",
"waiting-reviewer",
"live",
"done",
"stale",
]);
export const briefSummaryStatusSchema = z.enum(["ok", "pending", "fallback"]);
export const briefSummaryFailureReasonSchema = z.enum([
"model_error",
"truncation_failed",
"budget_capped",
"safety_block",
]);
export const briefSourceKindSchema = z.enum([
"issue_tree",
"issue",
"comment",
"run",
"document",
"work_product",
"interaction",
"activity_event",
"approval",
]);
export const briefTaskRowSchema = z.object({
kind: briefSourceKindSchema.exclude(["issue_tree", "activity_event", "work_product"]),
sourceId: z.string().min(1),
issueId: z.string().min(1).nullable(),
identifier: z.string().min(1).nullable(),
titleLine: z.string().min(1).max(120),
rightTag: z.string().min(1).max(40),
linkPath: z.string().min(1),
isIntraTreeBlocked: z.boolean().nullable(),
eventAt: z.string().min(1),
}).strict();
export const briefCardSourceSchema = z.object({
id: z.string().min(1),
companyId: z.string().min(1),
userId: z.string().min(1),
cardId: z.string().min(1),
sourceKind: briefSourceKindSchema,
sourceId: z.string().min(1),
issueId: z.string().min(1).nullable(),
identifier: z.string().min(1).nullable(),
titleLine: z.string().min(1).max(160),
rightTag: z.string().min(1).max(60),
linkPath: z.string().min(1),
isIntraTreeBlocked: z.boolean().nullable(),
eventAt: z.string().min(1),
metadata: z.record(z.string(), z.unknown()).default({}),
}).strict();
export const briefSnapshotSchema = z.object({
id: z.string().min(1),
companyId: z.string().min(1),
userId: z.string().min(1),
cardId: z.string().min(1),
summaryParagraph: z.string().max(260).nullable(),
summaryStatus: briefSummaryStatusSchema,
summaryModel: z.string().min(1).nullable(),
summaryTokensIn: z.number().int().nonnegative().nullable(),
summaryTokensOut: z.number().int().nonnegative().nullable(),
summaryFailureReason: briefSummaryFailureReasonSchema.nullable(),
taskRows: z.array(briefTaskRowSchema).max(3),
evidenceSourceIds: z.array(z.string().min(1)),
generatedByAgentId: z.string().min(1).nullable(),
generatedByRunId: z.string().min(1).nullable(),
deterministicStateInputs: z.record(z.string(), z.unknown()).default({}),
createdAt: z.string().min(1),
}).strict();
export const briefCardSchema = z.object({
id: z.string().min(1),
companyId: z.string().min(1),
userId: z.string().min(1),
slug: z.string().min(1).max(120),
title: z.string().min(1).max(90),
groupingDescription: z.string().min(1).max(500),
rootIssueId: z.string().min(1).nullable(),
state: briefCardStateSchema,
summaryStatus: briefSummaryStatusSchema,
pinned: z.boolean(),
hidden: z.boolean(),
staleAt: z.string().min(1),
expiresAt: z.string().min(1).nullable(),
latestSnapshotId: z.string().min(1).nullable(),
lastMeaningfulEventAt: z.string().min(1),
snapshot: briefSnapshotSchema,
sources: z.array(briefCardSourceSchema),
moreSourceCount: z.number().int().nonnegative(),
}).strict();
export const briefPreferencesSchema = z.object({
companyId: z.string().min(1),
userId: z.string().min(1),
cadence: z.enum(["manual", "hourly", "daily"]).default("hourly"),
retentionDays: z.number().int().positive().default(7),
doneRetentionHours: z.number().int().positive().default(72),
staleAfterDays: z.number().int().positive().default(7),
maxUnpinnedCards: z.number().int().positive().default(30),
scope: z.literal("user").default("user"),
}).strict();
export const listBriefCardsInputSchema = z.object({
companyId: z.string().min(1),
userId: z.string().min(1),
includeHidden: z.boolean().optional().default(false),
limit: z.number().int().positive().max(100).optional().default(50),
}).strict();
export const pinBriefCardInputSchema = z.object({
companyId: z.string().min(1),
userId: z.string().min(1),
cardId: z.string().min(1),
pinned: z.boolean(),
}).strict();
export const updateBriefPreferencesInputSchema = briefPreferencesSchema.partial({
cadence: true,
retentionDays: true,
doneRetentionHours: true,
staleAfterDays: true,
maxUnpinnedCards: true,
scope: true,
}).required({
companyId: true,
userId: true,
});
export const briefCursorEventSchema = z.object({
id: z.string().min(1),
eventAt: z.string().min(1),
fingerprint: z.string().min(1).optional(),
}).strict();
export type BriefCardState = z.infer<typeof briefCardStateSchema>;
export type BriefSummaryStatus = z.infer<typeof briefSummaryStatusSchema>;
export type BriefSummaryFailureReason = z.infer<typeof briefSummaryFailureReasonSchema>;
export type BriefSourceKind = z.infer<typeof briefSourceKindSchema>;
export type BriefTaskRow = z.infer<typeof briefTaskRowSchema>;
export type BriefCardSource = z.infer<typeof briefCardSourceSchema>;
export type BriefSnapshot = z.infer<typeof briefSnapshotSchema>;
export type BriefCard = z.infer<typeof briefCardSchema>;
export type BriefPreferences = z.infer<typeof briefPreferencesSchema>;
export type ListBriefCardsInput = z.infer<typeof listBriefCardsInputSchema>;
export type PinBriefCardInput = z.infer<typeof pinBriefCardInputSchema>;
export type UpdateBriefPreferencesInput = z.infer<typeof updateBriefPreferencesInputSchema>;
export type BriefCursorEvent = z.infer<typeof briefCursorEventSchema>;
@@ -0,0 +1,766 @@
import { randomUUID } from "node:crypto";
import type {
BriefCard,
BriefCardSource,
BriefCardState,
BriefCursorEvent,
BriefPreferences,
BriefSnapshot,
BriefSummaryFailureReason,
BriefSummaryStatus,
BriefTaskRow,
} from "./contracts.js";
export type BriefIssueStatus =
| "backlog"
| "todo"
| "in_progress"
| "in_review"
| "done"
| "blocked"
| "cancelled";
export type BriefsIssueInput = {
id: string;
companyId: string;
parentId: string | null;
title: string;
identifier: string | null;
status: BriefIssueStatus | string;
priority?: string | null;
assigneeAgentId?: string | null;
assigneeUserId?: string | null;
createdByUserId?: string | null;
updatedByUserId?: string | null;
activeRecoveryAction?: unknown | null;
blockerAttention?: { reason?: string | null } | null;
executionState?: {
currentParticipant?: { type?: string; userId?: string | null; agentId?: string | null } | null;
returnAssignee?: { type?: string; userId?: string | null; agentId?: string | null } | null;
} | null;
createdAt?: Date | string | null;
updatedAt?: Date | string | null;
completedAt?: Date | string | null;
};
export type BriefsRelationInput = {
blockedBy?: Array<{
id: string;
companyId?: string | null;
identifier?: string | null;
title?: string | null;
status?: BriefIssueStatus | string | null;
}>;
};
export type BriefsRunInput = {
id: string;
companyId: string;
issueId: string | null;
status: string;
error?: string | null;
startedAt?: Date | string | null;
finishedAt?: Date | string | null;
createdAt?: Date | string | null;
};
export type BriefsCommentInput = {
id: string;
companyId: string;
issueId: string;
authorUserId?: string | null;
body?: string | null;
createdAt?: Date | string | null;
};
export type BriefsDocumentInput = {
id: string;
companyId: string;
issueId: string;
key: string;
title?: string | null;
createdByUserId?: string | null;
updatedByUserId?: string | null;
updatedAt?: Date | string | null;
};
export type BriefsInteractionInput = {
id: string;
companyId: string;
issueId: string;
kind: "request_confirmation" | "ask_user_questions" | "suggest_tasks" | string;
status: "pending" | "answered" | "accepted" | "rejected" | "cancelled" | string;
targetUserId?: string | null;
createdAt?: Date | string | null;
updatedAt?: Date | string | null;
};
export type BriefsApprovalInput = {
id: string;
companyId: string;
issueId: string;
status: "pending_approval" | "approved" | "rejected" | string;
reviewerUserId?: string | null;
decidedByUserId?: string | null;
createdAt?: Date | string | null;
decidedAt?: Date | string | null;
};
export type BriefsWorkProductInput = {
id: string;
companyId: string;
issueId: string;
title: string;
status?: string | null;
createdAt?: Date | string | null;
updatedAt?: Date | string | null;
};
export type BriefsActivityEventInput = {
id: string;
companyId: string;
issueId?: string | null;
action: string;
createdAt?: Date | string | null;
};
export type BriefsSourceBundle = {
companyId: string;
userId: string;
rootIssueId: string;
issues: BriefsIssueInput[];
relations?: Record<string, BriefsRelationInput>;
activeRuns?: Record<string, BriefsRunInput[]>;
runs?: BriefsRunInput[];
comments?: BriefsCommentInput[];
documents?: BriefsDocumentInput[];
interactions?: BriefsInteractionInput[];
approvals?: BriefsApprovalInput[];
workProducts?: BriefsWorkProductInput[];
activityEvents?: BriefsActivityEventInput[];
relevantAgentIds?: string[];
groupingDescription?: string | null;
title?: string | null;
};
export type DeterministicBriefOptions = {
now?: Date | string;
pinned?: boolean;
hidden?: boolean;
summaryStatus?: BriefSummaryStatus;
summaryParagraph?: string | null;
summaryFailureReason?: BriefSummaryFailureReason | null;
summaryModel?: string | null;
summaryTokensIn?: number | null;
summaryTokensOut?: number | null;
generatedByAgentId?: string | null;
generatedByRunId?: string | null;
preferences?: Partial<BriefPreferences>;
idFactory?: () => string;
};
export type BriefCursorDedupeResult = {
freshEvents: BriefCursorEvent[];
dedupeState: string[];
lastSeenAt: string | null;
};
const DEFAULT_RETENTION_DAYS = 7;
const DEFAULT_DONE_RETENTION_HOURS = 72;
const DEFAULT_STALE_AFTER_DAYS = 7;
const DEFAULT_DISCOVERY_WINDOW_DAYS = 14;
const LIVE_WINDOW_HOURS = 6;
const FAILED_RUN_WINDOW_HOURS = 24;
const MAX_DEDUPE_STATE = 500;
const BLOCKER_ATTENTION_REASONS = new Set([
"owner_paused",
"recovery_required",
"external_wait",
"needs_attention",
"stalled",
]);
function asTime(value: Date | string | null | undefined): number | null {
if (!value) return null;
const time = value instanceof Date ? value.getTime() : new Date(value).getTime();
return Number.isFinite(time) ? time : null;
}
function toIso(value: number | Date | string): string {
const date = typeof value === "number" ? new Date(value) : value instanceof Date ? value : new Date(value);
return date.toISOString();
}
function addMs(time: number, amount: number): number {
return time + amount;
}
function days(value: number): number {
return value * 24 * 60 * 60 * 1000;
}
function hours(value: number): number {
return value * 60 * 60 * 1000;
}
function truncate(value: string, max: number): string {
const compact = value.replace(/\s+/g, " ").trim();
if (compact.length <= max) return compact;
return `${compact.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
}
export function slugifyBriefGrouping(value: string): string {
return value
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.replace(/-+/g, "-")
.slice(0, 96) || "brief";
}
function issueHref(issue: Pick<BriefsIssueInput, "id" | "identifier">): string {
return `/PAP/issues/${issue.identifier ?? issue.id}`;
}
function sourceIssue(bundle: BriefsSourceBundle, issueId: string | null | undefined): BriefsIssueInput | null {
if (!issueId) return null;
return bundle.issues.find((issue) => issue.id === issueId) ?? null;
}
function sourceLink(bundle: BriefsSourceBundle, kind: string, sourceId: string, issueId: string | null, key?: string): string {
const issue = sourceIssue(bundle, issueId);
if (kind === "run") return `/PAP/runs/${sourceId}`;
if (!issue) return `/PAP/issues/${issueId ?? sourceId}`;
if (kind === "comment") return `${issueHref(issue)}#comment-${sourceId}`;
if (kind === "document") return `${issueHref(issue)}#document-${key ?? sourceId}`;
if (kind === "interaction") return `${issueHref(issue)}#interaction-${sourceId}`;
if (kind === "approval") return `/PAP/approvals/${sourceId}`;
if (kind === "work_product") return `${issueHref(issue)}#work-product-${sourceId}`;
return issueHref(issue);
}
function eventTime(...values: Array<Date | string | null | undefined>): number {
return Math.max(0, ...values.map(asTime).filter((time): time is number => time != null));
}
function issueEventTime(issue: BriefsIssueInput): number {
return eventTime(issue.completedAt, issue.updatedAt, issue.createdAt);
}
function runEventTime(run: BriefsRunInput): number {
return eventTime(run.finishedAt, run.startedAt, run.createdAt);
}
function assertCompanyScope(bundle: BriefsSourceBundle): void {
const issueIds = new Set(bundle.issues.map((issue) => issue.id));
const check = (label: string, companyId: string, sourceId: string) => {
if (companyId !== bundle.companyId) {
throw new Error(`Briefs source ${label}:${sourceId} belongs to another company`);
}
};
for (const issue of bundle.issues) check("issue", issue.companyId, issue.id);
for (const run of [...Object.values(bundle.activeRuns ?? {}).flat(), ...(bundle.runs ?? [])]) check("run", run.companyId, run.id);
for (const comment of bundle.comments ?? []) check("comment", comment.companyId, comment.id);
for (const document of bundle.documents ?? []) check("document", document.companyId, document.id);
for (const interaction of bundle.interactions ?? []) check("interaction", interaction.companyId, interaction.id);
for (const approval of bundle.approvals ?? []) check("approval", approval.companyId, approval.id);
for (const workProduct of bundle.workProducts ?? []) check("work_product", workProduct.companyId, workProduct.id);
for (const event of bundle.activityEvents ?? []) check("activity_event", event.companyId, event.id);
for (const [issueId, relation] of Object.entries(bundle.relations ?? {})) {
if (!issueIds.has(issueId)) {
throw new Error(`Briefs relation references issue outside the source bundle: ${issueId}`);
}
for (const blocker of relation.blockedBy ?? []) {
if (blocker.companyId && blocker.companyId !== bundle.companyId) {
throw new Error(`Briefs blocker ${blocker.id} belongs to another company`);
}
}
}
}
function getRootIssue(bundle: BriefsSourceBundle): BriefsIssueInput {
const root = bundle.issues.find((issue) => issue.id === bundle.rootIssueId);
if (!root) throw new Error(`Root issue not found in Briefs source bundle: ${bundle.rootIssueId}`);
return root;
}
function getIssueIds(bundle: BriefsSourceBundle): Set<string> {
return new Set(bundle.issues.map((issue) => issue.id));
}
function activeRuns(bundle: BriefsSourceBundle): BriefsRunInput[] {
return [
...Object.values(bundle.activeRuns ?? {}).flat(),
...(bundle.runs ?? []).filter((run) => ["queued", "running", "in_progress"].includes(run.status)),
];
}
function failedRunsWithinWindow(bundle: BriefsSourceBundle, nowMs: number): Map<string, number> {
const counts = new Map<string, number>();
for (const run of bundle.runs ?? []) {
if (!run.issueId || !["failed", "error"].includes(run.status)) continue;
const at = runEventTime(run);
if (!at || nowMs - at > hours(FAILED_RUN_WINDOW_HOURS)) continue;
counts.set(run.issueId, (counts.get(run.issueId) ?? 0) + 1);
}
return counts;
}
function hasOutOfTreeBlocker(issue: BriefsIssueInput, relation: BriefsRelationInput | undefined, treeIssueIds: Set<string>): boolean {
if (issue.status !== "blocked") return false;
for (const blocker of relation?.blockedBy ?? []) {
if (["done", "cancelled"].includes(String(blocker.status))) continue;
if (!treeIssueIds.has(blocker.id)) return true;
}
return false;
}
function issueHasBlockingAttention(issue: BriefsIssueInput): boolean {
const reason = issue.blockerAttention?.reason;
return Boolean(reason && BLOCKER_ATTENTION_REASONS.has(reason));
}
function participantIsUser(
participant: BriefsIssueInput["executionState"] extends infer T
? T extends { currentParticipant?: infer P } ? P : never
: never,
userId: string,
): boolean {
return Boolean(participant && typeof participant === "object" && "type" in participant && participant.type === "user" && participant.userId === userId);
}
function issueWaitingOnUser(issue: BriefsIssueInput, userId: string): boolean {
return issue.assigneeUserId === userId
|| participantIsUser(issue.executionState?.currentParticipant ?? null, userId)
|| participantIsUser(issue.executionState?.returnAssignee ?? null, userId);
}
function issueHasTypedReviewer(issue: BriefsIssueInput): boolean {
const participant = issue.executionState?.currentParticipant;
return issue.status === "in_review" && Boolean(participant?.type === "user" || participant?.type === "agent");
}
function pendingInteractionForUser(interaction: BriefsInteractionInput, userId: string): boolean {
return interaction.status === "pending" && (!interaction.targetUserId || interaction.targetUserId === userId);
}
function pendingApprovalForUser(approval: BriefsApprovalInput, userId: string): boolean {
return approval.status === "pending_approval" && approval.reviewerUserId === userId;
}
function pendingApprovalForOtherReviewer(approval: BriefsApprovalInput, userId: string): boolean {
return approval.status === "pending_approval" && approval.reviewerUserId !== userId;
}
function lastMeaningfulEventMs(bundle: BriefsSourceBundle): number {
return Math.max(
...bundle.issues.map(issueEventTime),
...Object.values(bundle.activeRuns ?? {}).flat().map(runEventTime),
...(bundle.runs ?? []).map(runEventTime),
...(bundle.comments ?? []).map((comment) => eventTime(comment.createdAt)),
...(bundle.documents ?? []).map((document) => eventTime(document.updatedAt)),
...(bundle.interactions ?? []).map((interaction) => eventTime(interaction.updatedAt, interaction.createdAt)),
...(bundle.approvals ?? []).map((approval) => eventTime(approval.decidedAt, approval.createdAt)),
...(bundle.workProducts ?? []).map((workProduct) => eventTime(workProduct.updatedAt, workProduct.createdAt)),
...(bundle.activityEvents ?? []).map((activity) => eventTime(activity.createdAt)),
);
}
export function resolveBriefCardState(bundle: BriefsSourceBundle, options: { now?: Date | string; preferences?: Partial<BriefPreferences> } = {}): {
state: BriefCardState;
inputs: Record<string, unknown>;
} {
assertCompanyScope(bundle);
const nowMs = asTime(options.now ?? new Date()) ?? Date.now();
const root = getRootIssue(bundle);
const treeIssueIds = getIssueIds(bundle);
const failedRunCounts = failedRunsWithinWindow(bundle, nowMs);
const lastEventMs = lastMeaningfulEventMs(bundle);
const doneRetentionHours = options.preferences?.doneRetentionHours ?? DEFAULT_DONE_RETENTION_HOURS;
const hasRecoveryAction = bundle.issues.some((issue) => Boolean(issue.activeRecoveryAction));
const hasFailedRunLoop = [...failedRunCounts.values()].some((count) => count >= 3);
const hasBlocked = bundle.issues.some((issue) => hasOutOfTreeBlocker(issue, bundle.relations?.[issue.id], treeIssueIds) || issueHasBlockingAttention(issue));
const hasWaitingUser = bundle.issues.some((issue) => issueWaitingOnUser(issue, bundle.userId))
|| (bundle.interactions ?? []).some((interaction) => pendingInteractionForUser(interaction, bundle.userId))
|| (bundle.approvals ?? []).some((approval) => pendingApprovalForUser(approval, bundle.userId));
const hasWaitingReviewer = bundle.issues.some((issue) => issueHasTypedReviewer(issue) && !issueWaitingOnUser(issue, bundle.userId))
|| (bundle.approvals ?? []).some((approval) => pendingApprovalForOtherReviewer(approval, bundle.userId));
const hasLive = (activeRuns(bundle).length > 0 || bundle.issues.some((issue) => issue.status === "in_progress"))
&& lastEventMs > 0
&& nowMs - lastEventMs <= hours(LIVE_WINDOW_HOURS);
const doneAt = asTime(root.completedAt) ?? (root.status === "done" ? issueEventTime(root) : null);
const isRecentlyDone = root.status === "done" && doneAt != null && nowMs - doneAt <= hours(doneRetentionHours);
const inputs = {
hasRecoveryAction,
hasFailedRunLoop,
hasBlocked,
hasWaitingUser,
hasWaitingReviewer,
hasLive,
isRecentlyDone,
lastMeaningfulEventAt: lastEventMs ? toIso(lastEventMs) : null,
};
if (hasRecoveryAction || hasFailedRunLoop) return { state: "error", inputs };
if (hasBlocked) return { state: "blocked", inputs };
if (hasWaitingUser) return { state: "waiting-user", inputs };
if (hasWaitingReviewer) return { state: "waiting-reviewer", inputs };
if (hasLive) return { state: "live", inputs };
if (isRecentlyDone) return { state: "done", inputs };
return { state: "stale", inputs };
}
function sourcePriority(source: BriefCardSource): number {
if (source.rightTag === "blocked") return 0;
if (source.rightTag === "asked you") return 1;
if (source.rightTag === "in_review" || source.rightTag === "approval") return 2;
if (source.rightTag === "running" || source.rightTag === "in_progress") return 3;
if (source.rightTag === "failed") return 4;
return 5;
}
function toTaskRow(source: BriefCardSource): BriefTaskRow | null {
if (!["issue", "run", "comment", "document", "interaction", "approval"].includes(source.sourceKind)) return null;
return {
kind: source.sourceKind as BriefTaskRow["kind"],
sourceId: source.sourceId,
issueId: source.issueId,
identifier: source.identifier,
titleLine: truncate(source.titleLine, 120),
rightTag: source.rightTag,
linkPath: source.linkPath,
isIntraTreeBlocked: source.isIntraTreeBlocked,
eventAt: source.eventAt,
};
}
function issueRightTag(issue: BriefsIssueInput, bundle: BriefsSourceBundle, treeIssueIds: Set<string>): { tag: string; intraTreeBlocked: boolean | null } {
if (issue.status === "blocked") {
const relation = bundle.relations?.[issue.id];
const blockers = relation?.blockedBy ?? [];
const hasOnlyInTreeBlockers = blockers.length > 0 && blockers.every((blocker) => treeIssueIds.has(blocker.id));
return { tag: "blocked", intraTreeBlocked: hasOnlyInTreeBlockers };
}
if (issueWaitingOnUser(issue, bundle.userId)) return { tag: "asked you", intraTreeBlocked: null };
return { tag: String(issue.status), intraTreeBlocked: null };
}
function buildSources(bundle: BriefsSourceBundle, cardId: string, idFactory: () => string): BriefCardSource[] {
const treeIssueIds = getIssueIds(bundle);
const issueSources: BriefCardSource[] = bundle.issues.map((issue) => {
const right = issueRightTag(issue, bundle, treeIssueIds);
return {
id: idFactory(),
companyId: bundle.companyId,
userId: bundle.userId,
cardId,
sourceKind: "issue",
sourceId: issue.id,
issueId: issue.id,
identifier: issue.identifier,
titleLine: truncate(issue.title, 160),
rightTag: right.tag,
linkPath: issueHref(issue),
isIntraTreeBlocked: right.intraTreeBlocked,
eventAt: toIso(issueEventTime(issue) || Date.now()),
metadata: { priority: issue.priority ?? null },
};
});
const runSources = [
...Object.values(bundle.activeRuns ?? {}).flat(),
...(bundle.runs ?? []),
].map((run): BriefCardSource => {
const issue = sourceIssue(bundle, run.issueId);
return {
id: idFactory(),
companyId: bundle.companyId,
userId: bundle.userId,
cardId,
sourceKind: "run",
sourceId: run.id,
issueId: run.issueId,
identifier: issue?.identifier ?? null,
titleLine: truncate(issue ? `Run for ${issue.title}` : "Heartbeat run", 160),
rightTag: ["failed", "error"].includes(run.status) ? "failed" : run.status,
linkPath: sourceLink(bundle, "run", run.id, run.issueId),
isIntraTreeBlocked: null,
eventAt: toIso(runEventTime(run) || Date.now()),
metadata: { error: run.error ?? null },
};
});
const commentSources = (bundle.comments ?? []).map((comment): BriefCardSource => {
const issue = sourceIssue(bundle, comment.issueId);
return {
id: idFactory(),
companyId: bundle.companyId,
userId: bundle.userId,
cardId,
sourceKind: "comment",
sourceId: comment.id,
issueId: comment.issueId,
identifier: issue?.identifier ?? null,
titleLine: truncate(comment.body || `Comment on ${issue?.title ?? "issue"}`, 160),
rightTag: "comment",
linkPath: sourceLink(bundle, "comment", comment.id, comment.issueId),
isIntraTreeBlocked: null,
eventAt: toIso(eventTime(comment.createdAt) || Date.now()),
metadata: { authorUserId: comment.authorUserId ?? null },
};
});
const documentSources = (bundle.documents ?? []).map((document): BriefCardSource => {
const issue = sourceIssue(bundle, document.issueId);
return {
id: idFactory(),
companyId: bundle.companyId,
userId: bundle.userId,
cardId,
sourceKind: "document",
sourceId: document.id,
issueId: document.issueId,
identifier: issue?.identifier ?? null,
titleLine: truncate(document.title ?? document.key, 160),
rightTag: "document",
linkPath: sourceLink(bundle, "document", document.id, document.issueId, document.key),
isIntraTreeBlocked: null,
eventAt: toIso(eventTime(document.updatedAt) || Date.now()),
metadata: { key: document.key },
};
});
const interactionSources = (bundle.interactions ?? []).map((interaction): BriefCardSource => {
const issue = sourceIssue(bundle, interaction.issueId);
return {
id: idFactory(),
companyId: bundle.companyId,
userId: bundle.userId,
cardId,
sourceKind: "interaction",
sourceId: interaction.id,
issueId: interaction.issueId,
identifier: issue?.identifier ?? null,
titleLine: truncate(interaction.kind.replaceAll("_", " "), 160),
rightTag: pendingInteractionForUser(interaction, bundle.userId) ? "asked you" : interaction.status,
linkPath: sourceLink(bundle, "interaction", interaction.id, interaction.issueId),
isIntraTreeBlocked: null,
eventAt: toIso(eventTime(interaction.updatedAt, interaction.createdAt) || Date.now()),
metadata: { kind: interaction.kind, targetUserId: interaction.targetUserId ?? null },
};
});
const approvalSources = (bundle.approvals ?? []).map((approval): BriefCardSource => {
const issue = sourceIssue(bundle, approval.issueId);
return {
id: idFactory(),
companyId: bundle.companyId,
userId: bundle.userId,
cardId,
sourceKind: "approval",
sourceId: approval.id,
issueId: approval.issueId,
identifier: issue?.identifier ?? null,
titleLine: truncate(`Approval for ${issue?.title ?? "issue"}`, 160),
rightTag: approval.status === "pending_approval" ? "approval" : approval.status,
linkPath: sourceLink(bundle, "approval", approval.id, approval.issueId),
isIntraTreeBlocked: null,
eventAt: toIso(eventTime(approval.decidedAt, approval.createdAt) || Date.now()),
metadata: { reviewerUserId: approval.reviewerUserId ?? null },
};
});
const workProductSources = (bundle.workProducts ?? []).map((workProduct): BriefCardSource => {
const issue = sourceIssue(bundle, workProduct.issueId);
return {
id: idFactory(),
companyId: bundle.companyId,
userId: bundle.userId,
cardId,
sourceKind: "work_product",
sourceId: workProduct.id,
issueId: workProduct.issueId,
identifier: issue?.identifier ?? null,
titleLine: truncate(workProduct.title, 160),
rightTag: workProduct.status ?? "work product",
linkPath: sourceLink(bundle, "work_product", workProduct.id, workProduct.issueId),
isIntraTreeBlocked: null,
eventAt: toIso(eventTime(workProduct.updatedAt, workProduct.createdAt) || Date.now()),
metadata: {},
};
});
const activitySources = (bundle.activityEvents ?? []).map((activity): BriefCardSource => ({
id: idFactory(),
companyId: bundle.companyId,
userId: bundle.userId,
cardId,
sourceKind: "activity_event",
sourceId: activity.id,
issueId: activity.issueId ?? null,
identifier: sourceIssue(bundle, activity.issueId)?.identifier ?? null,
titleLine: truncate(activity.action, 160),
rightTag: "activity",
linkPath: sourceLink(bundle, "activity_event", activity.id, activity.issueId ?? null),
isIntraTreeBlocked: null,
eventAt: toIso(eventTime(activity.createdAt) || Date.now()),
metadata: { action: activity.action },
}));
return [
...issueSources,
...runSources,
...commentSources,
...documentSources,
...interactionSources,
...approvalSources,
...workProductSources,
...activitySources,
].sort((a, b) => sourcePriority(a) - sourcePriority(b) || new Date(b.eventAt).getTime() - new Date(a.eventAt).getTime());
}
function fallbackSummary(state: BriefCardState, rows: BriefTaskRow[]): string {
const row = rows[0];
if (!row) return "No current source rows are available for this brief.";
if (state === "blocked") return `${row.identifier ?? "A source issue"} is blocked and needs out-of-tree progress.`;
if (state === "waiting-user") return `${row.identifier ?? "A source item"} is waiting on your response.`;
if (state === "waiting-reviewer") return `${row.identifier ?? "A source item"} is in review with a typed reviewer.`;
if (state === "live") return `${row.identifier ?? "A source issue"} is active with recent work in progress.`;
if (state === "done") return `${row.identifier ?? "The root issue"} was completed recently.`;
if (state === "error") return `${row.identifier ?? "A source issue"} has a recovery or repeated run failure signal.`;
return `${row.identifier ?? "This area"} has no recent meaningful activity.`;
}
export function buildDeterministicBriefCard(bundle: BriefsSourceBundle, options: DeterministicBriefOptions = {}): BriefCard {
assertCompanyScope(bundle);
const root = getRootIssue(bundle);
const idFactory = options.idFactory ?? randomUUID;
const cardId = idFactory();
const snapshotId = idFactory();
const nowMs = asTime(options.now ?? new Date()) ?? Date.now();
const lastEventMs = lastMeaningfulEventMs(bundle) || nowMs;
const stateResult = resolveBriefCardState(bundle, options);
const summaryStatus = options.summaryStatus ?? "fallback";
const groupingDescription = truncate(
bundle.groupingDescription ?? `Issue tree rooted at ${root.identifier ?? root.id}: ${root.title}`,
500,
);
const slug = slugifyBriefGrouping(groupingDescription);
const sources = buildSources(bundle, cardId, idFactory);
const taskRows = sources.map(toTaskRow).filter((row): row is BriefTaskRow => Boolean(row)).slice(0, 3);
const staleAt = addMs(lastEventMs, days(options.preferences?.staleAfterDays ?? DEFAULT_STALE_AFTER_DAYS));
const retentionMs = stateResult.state === "done"
? hours(options.preferences?.doneRetentionHours ?? DEFAULT_DONE_RETENTION_HOURS)
: days(options.preferences?.retentionDays ?? DEFAULT_RETENTION_DAYS);
const pinned = options.pinned ?? false;
const summaryParagraph = summaryStatus === "ok"
? truncate(options.summaryParagraph ?? fallbackSummary(stateResult.state, taskRows), 260)
: null;
const snapshot: BriefSnapshot = {
id: snapshotId,
companyId: bundle.companyId,
userId: bundle.userId,
cardId,
summaryParagraph,
summaryStatus,
summaryModel: options.summaryModel ?? null,
summaryTokensIn: options.summaryTokensIn ?? null,
summaryTokensOut: options.summaryTokensOut ?? null,
summaryFailureReason: summaryStatus === "fallback" ? options.summaryFailureReason ?? "model_error" : null,
taskRows,
evidenceSourceIds: summaryStatus === "ok" ? taskRows.map((row) => row.sourceId) : [],
generatedByAgentId: options.generatedByAgentId ?? null,
generatedByRunId: options.generatedByRunId ?? null,
deterministicStateInputs: stateResult.inputs,
createdAt: toIso(nowMs),
};
return {
id: cardId,
companyId: bundle.companyId,
userId: bundle.userId,
slug,
title: truncate(bundle.title ?? root.title, 90),
groupingDescription,
rootIssueId: root.id,
state: stateResult.state,
summaryStatus,
pinned,
hidden: options.hidden ?? false,
staleAt: toIso(staleAt),
expiresAt: pinned ? null : toIso(addMs(lastEventMs, retentionMs)),
latestSnapshotId: snapshotId,
lastMeaningfulEventAt: toIso(lastEventMs),
snapshot,
sources,
moreSourceCount: Math.max(0, sources.length - taskRows.length),
};
}
export function isBriefTreeRelevantToUser(bundle: BriefsSourceBundle): boolean {
assertCompanyScope(bundle);
return bundle.issues.some((issue) => issue.createdByUserId === bundle.userId || issue.updatedByUserId === bundle.userId || issueWaitingOnUser(issue, bundle.userId))
|| (bundle.comments ?? []).some((comment) => comment.authorUserId === bundle.userId)
|| (bundle.documents ?? []).some((document) => document.createdByUserId === bundle.userId || document.updatedByUserId === bundle.userId)
|| (bundle.interactions ?? []).some((interaction) => interaction.targetUserId === bundle.userId)
|| (bundle.approvals ?? []).some((approval) => approval.reviewerUserId === bundle.userId || approval.decidedByUserId === bundle.userId)
|| bundle.issues.some((issue) => Boolean(issue.assigneeAgentId && bundle.relevantAgentIds?.includes(issue.assigneeAgentId)));
}
export function selectRelevantBriefTrees(input: {
companyId: string;
userId: string;
candidateTrees: BriefsSourceBundle[];
now?: Date | string;
discoveryWindowDays?: number;
}): BriefsSourceBundle[] {
const nowMs = asTime(input.now ?? new Date()) ?? Date.now();
const windowMs = days(input.discoveryWindowDays ?? DEFAULT_DISCOVERY_WINDOW_DAYS);
return input.candidateTrees.filter((tree) => {
if (tree.companyId !== input.companyId || tree.userId !== input.userId) return false;
assertCompanyScope(tree);
return isBriefTreeRelevantToUser(tree) && nowMs - lastMeaningfulEventMs(tree) <= windowMs;
});
}
export function sortBriefCards(cards: BriefCard[]): BriefCard[] {
return [...cards].sort((a, b) => {
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
return new Date(b.lastMeaningfulEventAt).getTime() - new Date(a.lastMeaningfulEventAt).getTime();
});
}
export function filterExpiredBriefCards(cards: BriefCard[], now: Date | string = new Date()): BriefCard[] {
const nowMs = asTime(now) ?? Date.now();
return cards.filter((card) => card.pinned || !card.expiresAt || new Date(card.expiresAt).getTime() > nowMs);
}
export function dedupeBriefCursorEvents(
events: BriefCursorEvent[],
previousDedupeState: readonly string[] = [],
): BriefCursorDedupeResult {
const seen = new Set(previousDedupeState);
const freshEvents: BriefCursorEvent[] = [];
for (const event of events) {
const key = event.fingerprint ?? event.id;
if (seen.has(key)) continue;
seen.add(key);
freshEvents.push(event);
}
const sortedState = [...seen].slice(-MAX_DEDUPE_STATE);
const lastSeen = freshEvents.reduce<number | null>((max, event) => {
const time = asTime(event.eventAt);
return time == null ? max : Math.max(max ?? 0, time);
}, null);
return {
freshEvents,
dedupeState: sortedState,
lastSeenAt: lastSeen == null ? null : toIso(lastSeen),
};
}
@@ -0,0 +1,4 @@
export { default } from "./worker.js";
export { default as manifest } from "./manifest.js";
export * from "./contracts.js";
export * from "./deterministic-card-service.js";
@@ -0,0 +1,41 @@
import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
export const PLUGIN_ID = "paperclipai.plugin-briefs";
const manifest: PaperclipPluginManifestV1 = {
id: PLUGIN_ID,
apiVersion: 1,
version: "0.1.0",
displayName: "Briefs",
description: "Company-scoped briefing cards backed by deterministic Paperclip work-state analysis.",
author: "Paperclip",
categories: ["automation", "ui"],
capabilities: [
"database.namespace.migrate",
"database.namespace.read",
"database.namespace.write",
"companies.read",
"projects.read",
"goals.read",
"agents.read",
"issues.read",
"issue.subtree.read",
"issue.relations.read",
"issue.comments.read",
"issue.documents.read",
"issues.orchestration.read",
"activity.log.write",
"plugin.state.read",
"plugin.state.write"
],
entrypoints: {
worker: "./dist/worker.js"
},
database: {
namespaceSlug: "briefs",
migrationsDir: "migrations",
coreReadTables: ["companies", "issues"]
}
};
export default manifest;
+332
View File
@@ -0,0 +1,332 @@
import { randomUUID } from "node:crypto";
import type { PluginDatabaseClient } from "@paperclipai/plugin-sdk";
import type {
BriefCard,
BriefCardSource,
BriefPreferences,
BriefSnapshot,
} from "./contracts.js";
type BriefCardRow = {
id: string;
company_id: string;
user_id: string;
slug: string;
title: string;
grouping_description: string;
root_issue_id: string | null;
state: BriefCard["state"];
summary_status: BriefCard["summaryStatus"];
pinned: boolean;
hidden: boolean;
stale_at: string;
expires_at: string | null;
latest_snapshot_id: string | null;
last_meaningful_event_at: string;
};
type BriefSnapshotRow = {
id: string;
company_id: string;
user_id: string;
card_id: string;
summary_paragraph: string | null;
summary_status: BriefSnapshot["summaryStatus"];
summary_model: string | null;
summary_tokens_in: number | null;
summary_tokens_out: number | null;
summary_failure_reason: BriefSnapshot["summaryFailureReason"];
task_rows: unknown;
evidence_source_ids: unknown;
generated_by_agent_id: string | null;
generated_by_run_id: string | null;
deterministic_state_inputs: unknown;
created_at: string;
};
type BriefSourceRow = {
id: string;
company_id: string;
user_id: string;
card_id: string;
source_kind: BriefCardSource["sourceKind"];
source_id: string;
issue_id: string | null;
identifier: string | null;
title_line: string;
right_tag: string;
link_path: string;
is_intra_tree_blocked: boolean | null;
event_at: string;
metadata: unknown;
};
function table(namespace: string, name: string): string {
return `${namespace}.${name}`;
}
function toSnapshot(row: BriefSnapshotRow): BriefSnapshot {
return {
id: row.id,
companyId: row.company_id,
userId: row.user_id,
cardId: row.card_id,
summaryParagraph: row.summary_paragraph,
summaryStatus: row.summary_status,
summaryModel: row.summary_model,
summaryTokensIn: row.summary_tokens_in,
summaryTokensOut: row.summary_tokens_out,
summaryFailureReason: row.summary_failure_reason,
taskRows: Array.isArray(row.task_rows) ? row.task_rows as BriefSnapshot["taskRows"] : [],
evidenceSourceIds: Array.isArray(row.evidence_source_ids) ? row.evidence_source_ids as string[] : [],
generatedByAgentId: row.generated_by_agent_id,
generatedByRunId: row.generated_by_run_id,
deterministicStateInputs: row.deterministic_state_inputs && typeof row.deterministic_state_inputs === "object"
? row.deterministic_state_inputs as Record<string, unknown>
: {},
createdAt: row.created_at,
};
}
function toSource(row: BriefSourceRow): BriefCardSource {
return {
id: row.id,
companyId: row.company_id,
userId: row.user_id,
cardId: row.card_id,
sourceKind: row.source_kind,
sourceId: row.source_id,
issueId: row.issue_id,
identifier: row.identifier,
titleLine: row.title_line,
rightTag: row.right_tag,
linkPath: row.link_path,
isIntraTreeBlocked: row.is_intra_tree_blocked,
eventAt: row.event_at,
metadata: row.metadata && typeof row.metadata === "object" ? row.metadata as Record<string, unknown> : {},
};
}
function toCard(row: BriefCardRow, snapshot: BriefSnapshot, sources: BriefCardSource[]): BriefCard {
return {
id: row.id,
companyId: row.company_id,
userId: row.user_id,
slug: row.slug,
title: row.title,
groupingDescription: row.grouping_description,
rootIssueId: row.root_issue_id,
state: row.state,
summaryStatus: row.summary_status,
pinned: row.pinned,
hidden: row.hidden,
staleAt: row.stale_at,
expiresAt: row.expires_at,
latestSnapshotId: row.latest_snapshot_id,
lastMeaningfulEventAt: row.last_meaningful_event_at,
snapshot,
sources,
moreSourceCount: Math.max(0, sources.length - snapshot.taskRows.length),
};
}
export function createBriefsStore(db: PluginDatabaseClient) {
const cardsTable = table(db.namespace, "briefs_cards");
const sourcesTable = table(db.namespace, "briefs_card_sources");
const snapshotsTable = table(db.namespace, "briefs_card_snapshots");
const preferencesTable = table(db.namespace, "briefs_user_preferences");
return {
async listCards(input: { companyId: string; userId: string; includeHidden?: boolean; limit?: number }): Promise<BriefCard[]> {
const cardRows = await db.query<BriefCardRow>(
`SELECT id, company_id, user_id, slug, title, grouping_description, root_issue_id, state, summary_status,
pinned, hidden, stale_at, expires_at, latest_snapshot_id, last_meaningful_event_at
FROM ${cardsTable}
WHERE company_id = $1 AND user_id = $2 AND ($3::boolean OR hidden = false)
ORDER BY pinned DESC, last_meaningful_event_at DESC, id DESC
LIMIT $4`,
[input.companyId, input.userId, Boolean(input.includeHidden), input.limit ?? 50],
);
const cards: BriefCard[] = [];
for (const card of cardRows) {
if (!card.latest_snapshot_id) continue;
const snapshots = await db.query<BriefSnapshotRow>(
`SELECT id, company_id, user_id, card_id, summary_paragraph, summary_status, summary_model,
summary_tokens_in, summary_tokens_out, summary_failure_reason, task_rows, evidence_source_ids,
generated_by_agent_id, generated_by_run_id, deterministic_state_inputs, created_at
FROM ${snapshotsTable}
WHERE company_id = $1 AND user_id = $2 AND card_id = $3 AND id = $4
LIMIT 1`,
[input.companyId, input.userId, card.id, card.latest_snapshot_id],
);
const snapshot = snapshots[0];
if (!snapshot) continue;
const sources = await db.query<BriefSourceRow>(
`SELECT id, company_id, user_id, card_id, source_kind, source_id, issue_id, identifier, title_line,
right_tag, link_path, is_intra_tree_blocked, event_at, metadata
FROM ${sourcesTable}
WHERE company_id = $1 AND user_id = $2 AND card_id = $3
ORDER BY event_at DESC, id DESC`,
[input.companyId, input.userId, card.id],
);
cards.push(toCard(card, toSnapshot(snapshot), sources.map(toSource)));
}
return cards;
},
async saveCard(card: BriefCard): Promise<BriefCard> {
await db.execute(
`INSERT INTO ${cardsTable} (
id, company_id, user_id, slug, title, grouping_description, root_issue_id, state, summary_status,
pinned, hidden, stale_at, expires_at, latest_snapshot_id, last_meaningful_event_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12::timestamptz, $13::timestamptz, NULL, $14::timestamptz)
ON CONFLICT (company_id, user_id, slug) DO UPDATE SET
title = EXCLUDED.title,
grouping_description = EXCLUDED.grouping_description,
root_issue_id = EXCLUDED.root_issue_id,
state = EXCLUDED.state,
summary_status = EXCLUDED.summary_status,
pinned = ${cardsTable}.pinned OR EXCLUDED.pinned,
hidden = EXCLUDED.hidden,
stale_at = EXCLUDED.stale_at,
expires_at = CASE WHEN ${cardsTable}.pinned OR EXCLUDED.pinned THEN NULL ELSE EXCLUDED.expires_at END,
last_meaningful_event_at = EXCLUDED.last_meaningful_event_at,
updated_at = now()`,
[
card.id,
card.companyId,
card.userId,
card.slug,
card.title,
card.groupingDescription,
card.rootIssueId,
card.state,
card.summaryStatus,
card.pinned,
card.hidden,
card.staleAt,
card.expiresAt,
card.lastMeaningfulEventAt,
],
);
const existingRows = await db.query<{ id: string; pinned: boolean }>(
`SELECT id, pinned FROM ${cardsTable} WHERE company_id = $1 AND user_id = $2 AND slug = $3 LIMIT 1`,
[card.companyId, card.userId, card.slug],
);
const persistedCardId = existingRows[0]?.id ?? card.id;
await db.execute(
`DELETE FROM ${sourcesTable} WHERE company_id = $1 AND user_id = $2 AND card_id = $3`,
[card.companyId, card.userId, persistedCardId],
);
for (const source of card.sources) {
await db.execute(
`INSERT INTO ${sourcesTable} (
id, company_id, user_id, card_id, source_kind, source_id, issue_id, identifier, title_line,
right_tag, link_path, is_intra_tree_blocked, event_at, metadata
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::timestamptz, $14::jsonb)`,
[
source.id,
source.companyId,
source.userId,
persistedCardId,
source.sourceKind,
source.sourceId,
source.issueId,
source.identifier,
source.titleLine,
source.rightTag,
source.linkPath,
source.isIntraTreeBlocked,
source.eventAt,
JSON.stringify(source.metadata ?? {}),
],
);
}
await db.execute(
`INSERT INTO ${snapshotsTable} (
id, company_id, user_id, card_id, summary_paragraph, summary_status, summary_model,
summary_tokens_in, summary_tokens_out, summary_failure_reason, task_rows, evidence_source_ids,
generated_by_agent_id, generated_by_run_id, deterministic_state_inputs
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12::jsonb, $13, $14, $15::jsonb)`,
[
card.snapshot.id,
card.companyId,
card.userId,
persistedCardId,
card.snapshot.summaryParagraph,
card.snapshot.summaryStatus,
card.snapshot.summaryModel,
card.snapshot.summaryTokensIn,
card.snapshot.summaryTokensOut,
card.snapshot.summaryFailureReason,
JSON.stringify(card.snapshot.taskRows),
JSON.stringify(card.snapshot.evidenceSourceIds),
card.snapshot.generatedByAgentId,
card.snapshot.generatedByRunId,
JSON.stringify(card.snapshot.deterministicStateInputs),
],
);
await db.execute(
`UPDATE ${cardsTable}
SET latest_snapshot_id = $1, updated_at = now()
WHERE company_id = $2 AND user_id = $3 AND id = $4`,
[card.snapshot.id, card.companyId, card.userId, persistedCardId],
);
return {
...card,
id: persistedCardId,
pinned: existingRows[0]?.pinned ?? card.pinned,
latestSnapshotId: card.snapshot.id,
snapshot: { ...card.snapshot, cardId: persistedCardId },
sources: card.sources.map((source) => ({ ...source, cardId: persistedCardId })),
};
},
async setPinned(input: { companyId: string; userId: string; cardId: string; pinned: boolean }): Promise<void> {
await db.execute(
`UPDATE ${cardsTable}
SET pinned = $1, expires_at = CASE WHEN $1 THEN NULL ELSE expires_at END, updated_at = now()
WHERE company_id = $2 AND user_id = $3 AND id = $4`,
[input.pinned, input.companyId, input.userId, input.cardId],
);
},
async upsertPreferences(preferences: BriefPreferences): Promise<void> {
await db.execute(
`INSERT INTO ${preferencesTable} (
id, company_id, user_id, cadence, retention_days, done_retention_hours, stale_after_days,
max_unpinned_cards, scope
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (company_id, user_id) DO UPDATE SET
cadence = EXCLUDED.cadence,
retention_days = EXCLUDED.retention_days,
done_retention_hours = EXCLUDED.done_retention_hours,
stale_after_days = EXCLUDED.stale_after_days,
max_unpinned_cards = EXCLUDED.max_unpinned_cards,
scope = EXCLUDED.scope,
updated_at = now()`,
[
randomUUID(),
preferences.companyId,
preferences.userId,
preferences.cadence,
preferences.retentionDays,
preferences.doneRetentionHours,
preferences.staleAfterDays,
preferences.maxUnpinnedCards,
preferences.scope,
],
);
},
};
}
@@ -0,0 +1,87 @@
import { definePlugin, runWorker } from "@paperclipai/plugin-sdk";
import {
briefPreferencesSchema,
listBriefCardsInputSchema,
pinBriefCardInputSchema,
updateBriefPreferencesInputSchema,
} from "./contracts.js";
import {
buildDeterministicBriefCard,
type BriefsSourceBundle,
type DeterministicBriefOptions,
} from "./deterministic-card-service.js";
import { createBriefsStore } from "./store.js";
function objectParam(value: unknown, name: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`${name} must be an object`);
}
return value as Record<string, unknown>;
}
const plugin = definePlugin({
async setup(ctx) {
const store = createBriefsStore(ctx.db);
ctx.data.register("cards", async (params) => {
const input = listBriefCardsInputSchema.parse(params);
return {
cards: await store.listCards(input),
};
});
ctx.actions.register("save-deterministic-card", async (params) => {
const input = objectParam(params, "params");
const bundle = objectParam(input.bundle, "bundle") as BriefsSourceBundle;
const options = (input.options && typeof input.options === "object" ? input.options : {}) as DeterministicBriefOptions;
const card = await store.saveCard(buildDeterministicBriefCard(bundle, options));
await ctx.activity.log({
companyId: card.companyId,
message: `Updated briefing card "${card.title}"`,
entityType: "plugin:briefs:card",
entityId: card.id,
metadata: {
userId: card.userId,
state: card.state,
rootIssueId: card.rootIssueId,
summaryStatus: card.summaryStatus,
},
});
return { card };
});
ctx.actions.register("pin-card", async (params) => {
const input = pinBriefCardInputSchema.parse(params);
await store.setPinned(input);
await ctx.activity.log({
companyId: input.companyId,
message: `${input.pinned ? "Pinned" : "Unpinned"} briefing card`,
entityType: "plugin:briefs:card",
entityId: input.cardId,
metadata: { userId: input.userId, pinned: input.pinned },
});
return { ok: true };
});
ctx.actions.register("update-preferences", async (params) => {
const input = updateBriefPreferencesInputSchema.parse(params);
const preferences = briefPreferencesSchema.parse(input);
await store.upsertPreferences(preferences);
await ctx.activity.log({
companyId: preferences.companyId,
message: "Updated briefing preferences",
entityType: "plugin:briefs:preferences",
entityId: preferences.userId,
metadata: preferences,
});
return { preferences };
});
},
async onHealth() {
return { status: "ok", message: "Briefs deterministic card service ready" };
},
});
export default plugin;
runWorker(plugin, import.meta.url);
@@ -0,0 +1,217 @@
import { describe, expect, it } from "vitest";
import {
buildDeterministicBriefCard,
dedupeBriefCursorEvents,
filterExpiredBriefCards,
isBriefTreeRelevantToUser,
resolveBriefCardState,
selectRelevantBriefTrees,
sortBriefCards,
type BriefsIssueInput,
type BriefsSourceBundle,
} from "../src/deterministic-card-service.js";
const companyId = "company-1";
const userId = "user-1";
const now = "2026-05-22T12:00:00.000Z";
function ids() {
let counter = 0;
return () => `00000000-0000-4000-8000-${String(++counter).padStart(12, "0")}`;
}
function issue(overrides: Partial<BriefsIssueInput> = {}): BriefsIssueInput {
return {
id: "issue-root",
companyId,
parentId: null,
title: "Briefs plugin planning",
identifier: "PAP-2381",
status: "in_progress",
priority: "medium",
assigneeAgentId: "agent-1",
assigneeUserId: null,
createdByUserId: userId,
updatedAt: "2026-05-22T11:30:00.000Z",
createdAt: "2026-05-20T11:30:00.000Z",
...overrides,
};
}
function bundle(overrides: Partial<BriefsSourceBundle> = {}): BriefsSourceBundle {
return {
companyId,
userId,
rootIssueId: "issue-root",
issues: [issue()],
relations: {},
activeRuns: {},
comments: [],
documents: [],
interactions: [],
approvals: [],
...overrides,
};
}
describe("deterministic Briefs cards", () => {
it("rejects cross-company source data before producing a card", () => {
expect(() => buildDeterministicBriefCard(bundle({
issues: [issue({ companyId: "company-2" })],
}), { now, idFactory: ids() })).toThrow(/another company/);
});
it("uses user involvement and the discovery window for source selection", () => {
const relevant = bundle({
comments: [{
id: "comment-1",
companyId,
issueId: "issue-root",
authorUserId: userId,
body: "Please continue.",
createdAt: "2026-05-22T10:00:00.000Z",
}],
});
const irrelevant = bundle({
rootIssueId: "other-root",
issues: [issue({
id: "other-root",
identifier: "PAP-10",
createdByUserId: "someone-else",
updatedAt: "2026-05-22T10:00:00.000Z",
})],
});
const stale = bundle({
rootIssueId: "stale-root",
issues: [issue({
id: "stale-root",
createdByUserId: userId,
createdAt: "2026-04-01T09:00:00.000Z",
updatedAt: "2026-04-01T10:00:00.000Z",
})],
});
expect(isBriefTreeRelevantToUser(relevant)).toBe(true);
expect(selectRelevantBriefTrees({
companyId,
userId,
candidateTrees: [relevant, irrelevant, stale],
now,
})).toEqual([relevant]);
});
it("does not promote intra-tree sequencing blockers to card-level blocked", () => {
const child = issue({
id: "issue-child",
parentId: "issue-root",
identifier: "PAP-9961",
title: "Data model",
status: "blocked",
updatedAt: "2026-05-22T11:45:00.000Z",
});
const card = buildDeterministicBriefCard(bundle({
issues: [issue(), child],
relations: {
"issue-child": {
blockedBy: [{ id: "issue-root", companyId, status: "in_progress", identifier: "PAP-2381" }],
},
},
}), { now, idFactory: ids() });
expect(card.state).toBe("live");
expect(card.sources.find((source) => source.sourceId === "issue-child")).toMatchObject({
rightTag: "blocked",
isIntraTreeBlocked: true,
});
});
it("gives out-of-tree blockers precedence over waiting and live signals", () => {
const blocked = issue({
id: "issue-child",
parentId: "issue-root",
status: "blocked",
assigneeUserId: userId,
updatedAt: "2026-05-22T11:45:00.000Z",
});
expect(resolveBriefCardState(bundle({
issues: [issue(), blocked],
relations: {
"issue-child": {
blockedBy: [{ id: "external-blocker", companyId, status: "todo", identifier: "PAP-9999" }],
},
},
activeRuns: {
"issue-root": [{
id: "run-1",
companyId,
issueId: "issue-root",
status: "running",
startedAt: "2026-05-22T11:50:00.000Z",
}],
},
}), { now }).state).toBe("blocked");
});
it("keeps summary failure orthogonal to deterministic state", () => {
const card = buildDeterministicBriefCard(bundle(), {
now,
summaryStatus: "fallback",
summaryFailureReason: "budget_capped",
idFactory: ids(),
});
expect(card.state).toBe("live");
expect(card.summaryStatus).toBe("fallback");
expect(card.snapshot.summaryFailureReason).toBe("budget_capped");
});
it("keeps pinned cards stable while unpinned cards expire predictably", () => {
const unpinned = buildDeterministicBriefCard(bundle(), {
now,
idFactory: ids(),
preferences: { retentionDays: 1 },
});
const pinned = buildDeterministicBriefCard(bundle(), {
now,
pinned: true,
idFactory: ids(),
preferences: { retentionDays: 1 },
});
expect(unpinned.expiresAt).toBe("2026-05-23T11:30:00.000Z");
expect(pinned.expiresAt).toBeNull();
expect(filterExpiredBriefCards([unpinned, pinned], "2026-05-24T00:00:00.000Z")).toEqual([pinned]);
});
it("sorts pinned cards first and then by recent meaningful activity", () => {
const olderPinned = buildDeterministicBriefCard(bundle({
issues: [issue({ id: "old", updatedAt: "2026-05-22T09:00:00.000Z" })],
rootIssueId: "old",
}), { now, pinned: true, idFactory: ids() });
const newerUnpinned = buildDeterministicBriefCard(bundle({
issues: [issue({ id: "new", updatedAt: "2026-05-22T11:00:00.000Z" })],
rootIssueId: "new",
}), { now, idFactory: ids() });
expect(sortBriefCards([newerUnpinned, olderPinned]).map((card) => card.id)).toEqual([
olderPinned.id,
newerUnpinned.id,
]);
});
it("deduplicates cursor overlap events by fingerprint", () => {
expect(dedupeBriefCursorEvents([
{ id: "a", fingerprint: "issue-1:update", eventAt: "2026-05-22T10:00:00.000Z" },
{ id: "b", fingerprint: "issue-2:update", eventAt: "2026-05-22T11:00:00.000Z" },
{ id: "c", fingerprint: "issue-2:update", eventAt: "2026-05-22T11:05:00.000Z" },
], ["issue-1:update"])).toEqual({
freshEvents: [
{ id: "b", fingerprint: "issue-2:update", eventAt: "2026-05-22T11:00:00.000Z" },
],
dedupeState: ["issue-1:update", "issue-2:update"],
lastSeenAt: "2026-05-22T11:00:00.000Z",
});
});
});
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022", "DOM"],
"strict": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["dist", "node_modules"]
}
@@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["tests/**/*.spec.ts"],
environment: "node",
},
});