7f893ac4ec
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Reliable execution depends on heartbeat routing, issue lifecycle semantics, telemetry, and a fast enough local verification loop to keep regressions visible > - The remaining commits on this branch were mostly server/runtime correctness fixes plus test and documentation follow-ups in that area > - Those changes are logically separate from the UI-focused issue-detail and workspace/navigation branches even when they touch overlapping issue APIs > - This pull request groups the execution reliability, heartbeat, telemetry, and tooling changes into one standalone branch > - The benefit is a focused review of the control-plane correctness work, including the follow-up fix that restored the implicit comment-reopen helpers after branch splitting ## What Changed - Hardened issue/heartbeat execution behavior, including self-review stage skipping, deferred mention wakes during active execution, stranded execution recovery, active-run scoping, assignee resolution, and blocked-to-todo wake resumption - Reduced noisy polling/logging overhead by trimming issue run payloads, compacting persisted run logs, silencing high-volume request logs, and capping heartbeat-run queries in dashboard/inbox surfaces - Expanded telemetry and status semantics with adapter/model fields on task completion plus clearer status guidance in docs/onboarding material - Updated test infrastructure and verification defaults with faster route-test module isolation, cheaper default `pnpm test`, e2e isolation from local state, and repo verification follow-ups - Included docs/release housekeeping from the branch and added a small follow-up commit restoring the implicit comment-reopen helpers that were dropped during branch reconstruction ## Verification - `pnpm vitest run server/src/__tests__/issue-comment-reopen-routes.test.ts server/src/__tests__/issue-telemetry-routes.test.ts` - `pnpm vitest run server/src/__tests__/http-log-policy.test.ts server/src/__tests__/heartbeat-run-log.test.ts server/src/__tests__/health.test.ts` - `server/src/__tests__/activity-service.test.ts`, `server/src/__tests__/heartbeat-comment-wake-batching.test.ts`, and `server/src/__tests__/heartbeat-process-recovery.test.ts` were attempted on this host but the embedded Postgres harness reported init-script/data-dir problems and skipped or failed to start, so they are noted as environment-limited ## Risks - Medium: this branch changes core issue/heartbeat routing and reopen/wakeup behavior, so regressions would affect agent execution flow rather than isolated UI polish - Because it also updates verification infrastructure, reviewers should pay attention to whether the new tests are asserting the right failure modes and not just reshaping harness behavior ## Model Used - OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact deployed model ID is not exposed in this environment), reasoning enabled, tool use and local code execution enabled ## 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) - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] 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>
210 lines
7.9 KiB
TypeScript
210 lines
7.9 KiB
TypeScript
import { z } from "zod";
|
|
import {
|
|
ISSUE_EXECUTION_DECISION_OUTCOMES,
|
|
ISSUE_EXECUTION_POLICY_MODES,
|
|
ISSUE_EXECUTION_STAGE_TYPES,
|
|
ISSUE_EXECUTION_STATE_STATUSES,
|
|
ISSUE_PRIORITIES,
|
|
ISSUE_STATUSES,
|
|
} from "../constants.js";
|
|
|
|
export const ISSUE_EXECUTION_WORKSPACE_PREFERENCES = [
|
|
"inherit",
|
|
"shared_workspace",
|
|
"isolated_workspace",
|
|
"operator_branch",
|
|
"reuse_existing",
|
|
"agent_default",
|
|
] as const;
|
|
|
|
const executionWorkspaceStrategySchema = z
|
|
.object({
|
|
type: z.enum(["project_primary", "git_worktree", "adapter_managed", "cloud_sandbox"]).optional(),
|
|
baseRef: z.string().optional().nullable(),
|
|
branchTemplate: z.string().optional().nullable(),
|
|
worktreeParentDir: z.string().optional().nullable(),
|
|
provisionCommand: z.string().optional().nullable(),
|
|
teardownCommand: z.string().optional().nullable(),
|
|
})
|
|
.strict();
|
|
|
|
export const issueExecutionWorkspaceSettingsSchema = z
|
|
.object({
|
|
mode: z.enum(ISSUE_EXECUTION_WORKSPACE_PREFERENCES).optional(),
|
|
workspaceStrategy: executionWorkspaceStrategySchema.optional().nullable(),
|
|
workspaceRuntime: z.record(z.unknown()).optional().nullable(),
|
|
})
|
|
.strict();
|
|
|
|
export const issueAssigneeAdapterOverridesSchema = z
|
|
.object({
|
|
adapterConfig: z.record(z.unknown()).optional(),
|
|
useProjectWorkspace: z.boolean().optional(),
|
|
})
|
|
.strict();
|
|
|
|
const issueExecutionStagePrincipalBaseSchema = z.object({
|
|
type: z.enum(["agent", "user"]),
|
|
agentId: z.string().uuid().optional().nullable(),
|
|
userId: z.string().optional().nullable(),
|
|
});
|
|
|
|
export const issueExecutionStagePrincipalSchema = issueExecutionStagePrincipalBaseSchema
|
|
.superRefine((value, ctx) => {
|
|
if (value.type === "agent") {
|
|
if (!value.agentId) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Agent participants require agentId", path: ["agentId"] });
|
|
}
|
|
if (value.userId) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Agent participants cannot set userId", path: ["userId"] });
|
|
}
|
|
return;
|
|
}
|
|
if (!value.userId) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "User participants require userId", path: ["userId"] });
|
|
}
|
|
if (value.agentId) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "User participants cannot set agentId", path: ["agentId"] });
|
|
}
|
|
});
|
|
|
|
export const issueExecutionStageParticipantSchema = issueExecutionStagePrincipalBaseSchema.extend({
|
|
id: z.string().uuid().optional(),
|
|
}).superRefine((value, ctx) => {
|
|
if (value.type === "agent") {
|
|
if (!value.agentId) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Agent participants require agentId", path: ["agentId"] });
|
|
}
|
|
if (value.userId) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Agent participants cannot set userId", path: ["userId"] });
|
|
}
|
|
return;
|
|
}
|
|
if (!value.userId) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "User participants require userId", path: ["userId"] });
|
|
}
|
|
if (value.agentId) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "User participants cannot set agentId", path: ["agentId"] });
|
|
}
|
|
});
|
|
|
|
export const issueExecutionStageSchema = z.object({
|
|
id: z.string().uuid().optional(),
|
|
type: z.enum(ISSUE_EXECUTION_STAGE_TYPES),
|
|
approvalsNeeded: z.literal(1).optional().default(1),
|
|
participants: z.array(issueExecutionStageParticipantSchema).default([]),
|
|
});
|
|
|
|
export const issueExecutionPolicySchema = z.object({
|
|
mode: z.enum(ISSUE_EXECUTION_POLICY_MODES).optional().default("normal"),
|
|
commentRequired: z.boolean().optional().default(true),
|
|
stages: z.array(issueExecutionStageSchema).default([]),
|
|
});
|
|
|
|
export const issueExecutionStateSchema = z.object({
|
|
status: z.enum(ISSUE_EXECUTION_STATE_STATUSES),
|
|
currentStageId: z.string().uuid().nullable(),
|
|
currentStageIndex: z.number().int().nonnegative().nullable(),
|
|
currentStageType: z.enum(ISSUE_EXECUTION_STAGE_TYPES).nullable(),
|
|
currentParticipant: issueExecutionStagePrincipalSchema.nullable(),
|
|
returnAssignee: issueExecutionStagePrincipalSchema.nullable(),
|
|
completedStageIds: z.array(z.string().uuid()).default([]),
|
|
lastDecisionId: z.string().uuid().nullable(),
|
|
lastDecisionOutcome: z.enum(ISSUE_EXECUTION_DECISION_OUTCOMES).nullable(),
|
|
});
|
|
|
|
export const createIssueSchema = z.object({
|
|
projectId: z.string().uuid().optional().nullable(),
|
|
projectWorkspaceId: z.string().uuid().optional().nullable(),
|
|
goalId: z.string().uuid().optional().nullable(),
|
|
parentId: z.string().uuid().optional().nullable(),
|
|
blockedByIssueIds: z.array(z.string().uuid()).optional(),
|
|
inheritExecutionWorkspaceFromIssueId: z.string().uuid().optional().nullable(),
|
|
title: z.string().min(1),
|
|
description: z.string().optional().nullable(),
|
|
status: z.enum(ISSUE_STATUSES).optional().default("backlog"),
|
|
priority: z.enum(ISSUE_PRIORITIES).optional().default("medium"),
|
|
assigneeAgentId: z.string().uuid().optional().nullable(),
|
|
assigneeUserId: z.string().optional().nullable(),
|
|
requestDepth: z.number().int().nonnegative().optional().default(0),
|
|
billingCode: z.string().optional().nullable(),
|
|
assigneeAdapterOverrides: issueAssigneeAdapterOverridesSchema.optional().nullable(),
|
|
executionPolicy: issueExecutionPolicySchema.optional().nullable(),
|
|
executionWorkspaceId: z.string().uuid().optional().nullable(),
|
|
executionWorkspacePreference: z.enum(ISSUE_EXECUTION_WORKSPACE_PREFERENCES).optional().nullable(),
|
|
executionWorkspaceSettings: issueExecutionWorkspaceSettingsSchema.optional().nullable(),
|
|
labelIds: z.array(z.string().uuid()).optional(),
|
|
});
|
|
|
|
export type CreateIssue = z.infer<typeof createIssueSchema>;
|
|
|
|
export const createIssueLabelSchema = z.object({
|
|
name: z.string().trim().min(1).max(48),
|
|
color: z.string().regex(/^#(?:[0-9a-fA-F]{6})$/, "Color must be a 6-digit hex value"),
|
|
});
|
|
|
|
export type CreateIssueLabel = z.infer<typeof createIssueLabelSchema>;
|
|
|
|
export const updateIssueSchema = createIssueSchema.partial().extend({
|
|
assigneeAgentId: z.string().trim().min(1).optional().nullable(),
|
|
comment: z.string().min(1).optional(),
|
|
reopen: z.boolean().optional(),
|
|
interrupt: z.boolean().optional(),
|
|
hiddenAt: z.string().datetime().nullable().optional(),
|
|
});
|
|
|
|
export type UpdateIssue = z.infer<typeof updateIssueSchema>;
|
|
export type IssueExecutionWorkspaceSettings = z.infer<typeof issueExecutionWorkspaceSettingsSchema>;
|
|
|
|
export const checkoutIssueSchema = z.object({
|
|
agentId: z.string().uuid(),
|
|
expectedStatuses: z.array(z.enum(ISSUE_STATUSES)).nonempty(),
|
|
});
|
|
|
|
export type CheckoutIssue = z.infer<typeof checkoutIssueSchema>;
|
|
|
|
export const addIssueCommentSchema = z.object({
|
|
body: z.string().min(1),
|
|
reopen: z.boolean().optional(),
|
|
interrupt: z.boolean().optional(),
|
|
});
|
|
|
|
export type AddIssueComment = z.infer<typeof addIssueCommentSchema>;
|
|
|
|
export const linkIssueApprovalSchema = z.object({
|
|
approvalId: z.string().uuid(),
|
|
});
|
|
|
|
export type LinkIssueApproval = z.infer<typeof linkIssueApprovalSchema>;
|
|
|
|
export const createIssueAttachmentMetadataSchema = z.object({
|
|
issueCommentId: z.string().uuid().optional().nullable(),
|
|
});
|
|
|
|
export type CreateIssueAttachmentMetadata = z.infer<typeof createIssueAttachmentMetadataSchema>;
|
|
|
|
export const ISSUE_DOCUMENT_FORMATS = ["markdown"] as const;
|
|
|
|
export const issueDocumentFormatSchema = z.enum(ISSUE_DOCUMENT_FORMATS);
|
|
|
|
export const issueDocumentKeySchema = z
|
|
.string()
|
|
.trim()
|
|
.min(1)
|
|
.max(64)
|
|
.regex(/^[a-z0-9][a-z0-9_-]*$/, "Document key must be lowercase letters, numbers, _ or -");
|
|
|
|
export const upsertIssueDocumentSchema = z.object({
|
|
title: z.string().trim().max(200).nullable().optional(),
|
|
format: issueDocumentFormatSchema,
|
|
body: z.string().max(524288),
|
|
changeSummary: z.string().trim().max(500).nullable().optional(),
|
|
baseRevisionId: z.string().uuid().nullable().optional(),
|
|
});
|
|
|
|
export const restoreIssueDocumentRevisionSchema = z.object({});
|
|
|
|
export type IssueDocumentFormat = z.infer<typeof issueDocumentFormatSchema>;
|
|
export type UpsertIssueDocument = z.infer<typeof upsertIssueDocumentSchema>;
|
|
export type RestoreIssueDocumentRevision = z.infer<typeof restoreIssueDocumentRevisionSchema>;
|