mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-20 04:20:38 +09:00
Add recovery handoff system notices (#5289)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - Agent runs can end productively while the source issue still lacks a durable final disposition. > - That leaves the control plane unsure whether to resume, escalate, or close the work. > - Issue comments also need a presentation contract so system-authored recovery notices can render as first-class thread messages without overloading normal comments. > - This pull request adds successful-run handoff recovery, comment presentation metadata, and system notice rendering. > - The benefit is stricter task liveness with clearer operator-facing recovery state. ## What Changed - Added successful-run handoff decisions, wake payloads, escalation behavior, and recovery tests. - Added issue comment presentation metadata with migration `0078_white_darwin.sql` and shared/server/company portability support. - Rendered recovery/system notices in issue chat with dedicated UI components, fixtures, tests, and storybook/lab coverage. - Included the current recovery model-profile hint patch so automatic recovery follow-ups use the cheap profile. ## Verification - `pnpm install --frozen-lockfile` - `pnpm exec vitest run server/src/services/recovery/successful-run-handoff.test.ts ui/src/components/SystemNotice.test.tsx ui/src/lib/system-notice-comment.test.ts ui/src/components/IssueChatThreadSystemNotice.test.tsx` ## Risks - Migration-bearing PR: merge this before any other branch that might later add a migration. - The branch touches both recovery services and issue-thread rendering, so review should pay attention to recovery wake idempotency and comment metadata compatibility. ## Model Used - OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with shell/git/GitHub CLI tool use. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
50db8c01d2
commit
454edfe81e
70 changed files with 21919 additions and 125 deletions
|
|
@ -28,6 +28,9 @@ import {
|
|||
projects,
|
||||
} from "@paperclipai/db";
|
||||
import type {
|
||||
IssueCommentAuthorType,
|
||||
IssueCommentMetadata,
|
||||
IssueCommentPresentation,
|
||||
IssueBlockerAttention,
|
||||
IssueProductivityReview,
|
||||
IssueProductivityReviewTrigger,
|
||||
|
|
@ -37,6 +40,9 @@ import {
|
|||
clampIssueRequestDepth,
|
||||
extractAgentMentionIds,
|
||||
extractProjectMentionIds,
|
||||
issueCommentAuthorTypeSchema,
|
||||
issueCommentMetadataSchema,
|
||||
issueCommentPresentationSchema,
|
||||
isUuidLike,
|
||||
normalizeIssueIdentifier as normalizeIssueReferenceIdentifier,
|
||||
} from "@paperclipai/shared";
|
||||
|
|
@ -1679,10 +1685,47 @@ export function issueService(db: Db) {
|
|||
return enriched;
|
||||
}
|
||||
|
||||
function redactIssueComment<T extends { body: string }>(comment: T, censorUsernameInLogs: boolean): T {
|
||||
function deriveIssueCommentAuthorType(comment: {
|
||||
authorType?: string | null;
|
||||
authorAgentId?: string | null;
|
||||
authorUserId?: string | null;
|
||||
}): IssueCommentAuthorType {
|
||||
const explicit = issueCommentAuthorTypeSchema.safeParse(comment.authorType);
|
||||
if (explicit.success) return explicit.data;
|
||||
if (comment.authorAgentId) return "agent";
|
||||
if (comment.authorUserId) return "user";
|
||||
return "system";
|
||||
}
|
||||
|
||||
function assertIssueCommentAuthorTypeAllowed(
|
||||
actor: { agentId?: string | null; userId?: string | null },
|
||||
authorType: IssueCommentAuthorType,
|
||||
) {
|
||||
if (actor.agentId && authorType !== "agent") {
|
||||
throw unprocessable("Comment authorType must match authenticated actor");
|
||||
}
|
||||
if (actor.userId && authorType !== "user") {
|
||||
throw unprocessable("Comment authorType must match authenticated actor");
|
||||
}
|
||||
if (!actor.agentId && !actor.userId && authorType !== "system") {
|
||||
throw unprocessable("System comments cannot use user or agent authorType without an author id");
|
||||
}
|
||||
}
|
||||
|
||||
function redactIssueComment<T extends { body: string; authorType?: string | null; authorAgentId?: string | null; authorUserId?: string | null; presentation?: unknown; metadata?: unknown }>(
|
||||
comment: T,
|
||||
censorUsernameInLogs: boolean,
|
||||
): T & {
|
||||
authorType: IssueCommentAuthorType;
|
||||
presentation: IssueCommentPresentation | null;
|
||||
metadata: IssueCommentMetadata | null;
|
||||
} {
|
||||
return {
|
||||
...comment,
|
||||
authorType: deriveIssueCommentAuthorType(comment),
|
||||
body: redactCurrentUserText(comment.body, { enabled: censorUsernameInLogs }),
|
||||
presentation: issueCommentPresentationSchema.nullable().catch(null).parse(comment.presentation ?? null),
|
||||
metadata: issueCommentMetadataSchema.nullable().catch(null).parse(comment.metadata ?? null),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -3743,6 +3786,12 @@ export function issueService(db: Db) {
|
|||
issueId: string,
|
||||
body: string,
|
||||
actor: { agentId?: string; userId?: string; runId?: string | null },
|
||||
options?: {
|
||||
authorType?: IssueCommentAuthorType | null;
|
||||
presentation?: IssueCommentPresentation | null;
|
||||
metadata?: IssueCommentMetadata | null;
|
||||
createdAt?: Date | string | null;
|
||||
},
|
||||
) => {
|
||||
const issue = await db
|
||||
.select({ companyId: issues.companyId })
|
||||
|
|
@ -3756,6 +3805,13 @@ export function issueService(db: Db) {
|
|||
enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs,
|
||||
};
|
||||
const redactedBody = redactCurrentUserText(body, currentUserRedactionOptions);
|
||||
const authorType = issueCommentAuthorTypeSchema.parse(
|
||||
options?.authorType ?? (actor.agentId ? "agent" : actor.userId ? "user" : "system"),
|
||||
);
|
||||
assertIssueCommentAuthorTypeAllowed(actor, authorType);
|
||||
const presentation = issueCommentPresentationSchema.nullable().parse(options?.presentation ?? null);
|
||||
const metadata = issueCommentMetadataSchema.nullable().parse(options?.metadata ?? null);
|
||||
const createdAt = options?.createdAt ? new Date(options.createdAt) : null;
|
||||
const [comment] = await db
|
||||
.insert(issueComments)
|
||||
.values({
|
||||
|
|
@ -3763,8 +3819,12 @@ export function issueService(db: Db) {
|
|||
issueId,
|
||||
authorAgentId: actor.agentId ?? null,
|
||||
authorUserId: actor.userId ?? null,
|
||||
authorType,
|
||||
createdByRunId: actor.runId ?? null,
|
||||
body: redactedBody,
|
||||
presentation,
|
||||
metadata,
|
||||
...(createdAt && !Number.isNaN(createdAt.getTime()) ? { createdAt } : {}),
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue