forked from farhoodlabs/paperclip
454edfe81e
## 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>
218 lines
7.1 KiB
TypeScript
218 lines
7.1 KiB
TypeScript
import type { Agent } from "@paperclipai/shared";
|
|
import type { LiveRunForIssue } from "../api/heartbeats";
|
|
import type {
|
|
IssueChatComment,
|
|
IssueChatLinkedRun,
|
|
IssueChatTranscriptEntry,
|
|
} from "../lib/issue-chat-messages";
|
|
import type { IssueTimelineEvent } from "../lib/issue-timeline-events";
|
|
|
|
export const LONG_THREAD_COMMENT_COUNT = 469;
|
|
export const LONG_THREAD_MARKDOWN_COMMENT_COUNT = 150;
|
|
export const LONG_THREAD_EVENT_COUNT = 12;
|
|
export const LONG_THREAD_LINKED_RUN_COUNT = 6;
|
|
|
|
const baseTime = new Date("2026-04-28T14:00:00.000Z").getTime();
|
|
|
|
function atMinute(offset: number) {
|
|
return new Date(baseTime + offset * 60_000);
|
|
}
|
|
|
|
function createAgent(id: string, name: string, icon: string, urlKey: string): Agent {
|
|
const now = new Date("2026-04-28T14:00:00.000Z");
|
|
return {
|
|
id,
|
|
companyId: "company-long-thread",
|
|
name,
|
|
urlKey,
|
|
role: "engineer",
|
|
title: null,
|
|
icon,
|
|
status: "active",
|
|
reportsTo: null,
|
|
capabilities: null,
|
|
adapterType: "codex_local",
|
|
adapterConfig: {},
|
|
runtimeConfig: {},
|
|
budgetMonthlyCents: 0,
|
|
spentMonthlyCents: 0,
|
|
lastHeartbeatAt: null,
|
|
metadata: null,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
pauseReason: null,
|
|
pausedAt: null,
|
|
permissions: { canCreateAgents: false },
|
|
};
|
|
}
|
|
|
|
const primaryAgent = createAgent("agent-perf-codex", "CodexCoder", "code", "codexcoder");
|
|
const reviewerAgent = createAgent("agent-perf-reviewer", "ReviewBot", "sparkles", "reviewbot");
|
|
|
|
export const issueChatLongThreadAgentMap = new Map<string, Agent>([
|
|
[primaryAgent.id, primaryAgent],
|
|
[reviewerAgent.id, reviewerAgent],
|
|
]);
|
|
|
|
function markdownBody(index: number) {
|
|
return [
|
|
`## Baseline note ${index}`,
|
|
"",
|
|
`This assistant update captures a deterministic markdown-heavy row for long-thread rendering. It references [PAP-${2600 + index}](/PAP/issues/PAP-${2600 + index}) and includes enough structure to exercise markdown parsing.`,
|
|
"",
|
|
"- Parsed checklist item one with inline `code`",
|
|
"- Parsed checklist item two with **bold** and _italic_ text",
|
|
"- Parsed checklist item three with a link to [Paperclip](/PAP/dashboard)",
|
|
"",
|
|
"| Metric | Value |",
|
|
"| --- | ---: |",
|
|
`| Fixture row | ${index} |`,
|
|
`| Synthetic tokens | ${1200 + index} |`,
|
|
"",
|
|
"```ts",
|
|
`const fixtureRow${index} = { markdown: true, deterministic: true };`,
|
|
"```",
|
|
].join("\n");
|
|
}
|
|
|
|
function plainUserBody(index: number) {
|
|
return `Board checkpoint ${index}: keep the issue-detail page responsive while the thread is full of historical comments.`;
|
|
}
|
|
|
|
function plainAssistantBody(index: number) {
|
|
return `Processed checkpoint ${index}. The current direct-render path should keep this row mounted with the rest of the thread.`;
|
|
}
|
|
|
|
function createComment(index: number): IssueChatComment {
|
|
const isMarkdown = index < LONG_THREAD_MARKDOWN_COMMENT_COUNT;
|
|
const isAssistant = isMarkdown || index % 4 === 1 || index % 4 === 2;
|
|
const authorAgentId = isAssistant
|
|
? (index % 7 === 0 ? reviewerAgent.id : primaryAgent.id)
|
|
: null;
|
|
|
|
return {
|
|
id: `long-thread-comment-${String(index + 1).padStart(3, "0")}`,
|
|
companyId: "company-long-thread",
|
|
issueId: "issue-long-thread",
|
|
authorType: authorAgentId ? "agent" : "user",
|
|
authorAgentId,
|
|
authorUserId: authorAgentId ? null : "user-board",
|
|
body: isMarkdown
|
|
? markdownBody(index + 1)
|
|
: authorAgentId
|
|
? plainAssistantBody(index + 1)
|
|
: plainUserBody(index + 1),
|
|
presentation: null,
|
|
metadata: null,
|
|
createdAt: atMinute(index),
|
|
updatedAt: atMinute(index),
|
|
};
|
|
}
|
|
|
|
export const issueChatLongThreadComments: IssueChatComment[] = Array.from(
|
|
{ length: LONG_THREAD_COMMENT_COUNT },
|
|
(_, index) => createComment(index),
|
|
);
|
|
|
|
export const issueChatLongThreadMarkdownCommentIds = new Set(
|
|
issueChatLongThreadComments
|
|
.slice(0, LONG_THREAD_MARKDOWN_COMMENT_COUNT)
|
|
.map((comment) => comment.id),
|
|
);
|
|
|
|
export const issueChatLongThreadEvents: IssueTimelineEvent[] = Array.from(
|
|
{ length: LONG_THREAD_EVENT_COUNT },
|
|
(_, index) => ({
|
|
id: `long-thread-event-${index + 1}`,
|
|
createdAt: atMinute(index * 36 + 18),
|
|
actorType: index % 3 === 0 ? "user" : "agent",
|
|
actorId: index % 3 === 0 ? "user-board" : primaryAgent.id,
|
|
statusChange: index % 2 === 0
|
|
? { from: index === 0 ? "todo" : "in_progress", to: "in_progress" }
|
|
: undefined,
|
|
assigneeChange: index % 2 === 1
|
|
? {
|
|
from: { agentId: null, userId: null },
|
|
to: { agentId: index % 4 === 1 ? primaryAgent.id : reviewerAgent.id, userId: null },
|
|
}
|
|
: undefined,
|
|
}),
|
|
);
|
|
|
|
export const issueChatLongThreadLinkedRuns: IssueChatLinkedRun[] = Array.from(
|
|
{ length: LONG_THREAD_LINKED_RUN_COUNT },
|
|
(_, index) => ({
|
|
runId: `long-thread-run-${index + 1}`,
|
|
status: index % 3 === 0 ? "failed" : index % 3 === 1 ? "timed_out" : "succeeded",
|
|
agentId: index % 2 === 0 ? primaryAgent.id : reviewerAgent.id,
|
|
agentName: index % 2 === 0 ? primaryAgent.name : reviewerAgent.name,
|
|
adapterType: "codex_local",
|
|
createdAt: atMinute(index * 72 + 12),
|
|
startedAt: atMinute(index * 72 + 12),
|
|
finishedAt: atMinute(index * 72 + 16),
|
|
hasStoredOutput: true,
|
|
}),
|
|
);
|
|
|
|
export const issueChatLongThreadLiveRuns: LiveRunForIssue[] = [];
|
|
|
|
export const issueChatLongThreadTranscriptsByRunId = new Map<string, readonly IssueChatTranscriptEntry[]>(
|
|
issueChatLongThreadLinkedRuns.map((run, index) => [
|
|
run.runId,
|
|
[
|
|
{
|
|
kind: "thinking",
|
|
ts: atMinute(index * 72 + 13).toISOString(),
|
|
text: `Inspecting long-thread segment ${index + 1}.`,
|
|
},
|
|
{
|
|
kind: "tool_call",
|
|
ts: atMinute(index * 72 + 14).toISOString(),
|
|
name: "read_file",
|
|
toolUseId: `long-thread-tool-${index + 1}`,
|
|
input: { path: "ui/src/components/IssueChatThread.tsx" },
|
|
},
|
|
{
|
|
kind: "tool_result",
|
|
ts: atMinute(index * 72 + 15).toISOString(),
|
|
toolUseId: `long-thread-tool-${index + 1}`,
|
|
content: "Confirmed the direct-render fixture keeps the full message subtree mounted.",
|
|
isError: run.status !== "succeeded",
|
|
},
|
|
{
|
|
kind: "assistant",
|
|
ts: atMinute(index * 72 + 16).toISOString(),
|
|
text: `Run ${index + 1} produced a compact transcript row for adjacent run context.`,
|
|
},
|
|
],
|
|
]),
|
|
);
|
|
|
|
export const issueChatLongThreadFixtureContext = {
|
|
issue: {
|
|
identifier: "PAP-PERF",
|
|
title: "Long-thread rendering baseline fixture",
|
|
status: "in_progress",
|
|
priority: "medium",
|
|
projectName: "Paperclip App",
|
|
},
|
|
documents: [
|
|
"Implementation Plan",
|
|
"Profiler Notes",
|
|
"Release Checklist",
|
|
"QA Readout",
|
|
],
|
|
subIssues: [
|
|
"Phase 1: Add long-thread perf fixture and baseline",
|
|
"Phase 2: Isolate issue-thread row rendering and Markdown work",
|
|
"Phase 3: Apply virtualization and guard scroll behavior",
|
|
"Phase 4: Verify production issue profile improvement",
|
|
],
|
|
sidebarStats: [
|
|
["Comments", String(LONG_THREAD_COMMENT_COUNT)],
|
|
["Markdown bodies", String(LONG_THREAD_MARKDOWN_COMMENT_COUNT)],
|
|
["Timeline events", String(LONG_THREAD_EVENT_COUNT)],
|
|
["Linked runs", String(LONG_THREAD_LINKED_RUN_COUNT)],
|
|
],
|
|
} as const;
|