2026-02-18 14:23:16 -06:00
import { spawn , type ChildProcess } from "node:child_process" ;
2026-03-18 14:21:50 -05:00
import { constants as fsConstants , promises as fs , type Dirent } from "node:fs" ;
2026-02-18 14:23:16 -06:00
import path from "node:path" ;
2026-03-18 14:21:50 -05:00
import type {
AdapterSkillEntry ,
AdapterSkillSnapshot ,
} from "./types.js" ;
2026-02-18 14:23:16 -06:00
export interface RunProcessResult {
exitCode : number | null ;
signal : string | null ;
timedOut : boolean ;
stdout : string ;
stderr : string ;
2026-03-19 11:20:36 -05:00
pid : number | null ;
startedAt : string | null ;
2026-02-18 14:23:16 -06:00
}
2026-04-20 10:38:57 -05:00
export interface TerminalResultCleanupOptions {
hasTerminalResult : ( output : { stdout : string ; stderr : string } ) = > boolean ;
graceMs? : number ;
}
2026-02-18 14:23:16 -06:00
interface RunningProcess {
child : ChildProcess ;
graceSec : number ;
2026-04-10 22:26:21 -05:00
processGroupId : number | null ;
2026-02-18 14:23:16 -06:00
}
2026-03-09 21:52:06 +09:00
interface SpawnTarget {
command : string ;
args : string [ ] ;
}
2026-03-05 12:39:37 -03:00
type ChildProcessWithEvents = ChildProcess & {
on ( event : "error" , listener : ( err : Error ) = > void ) : ChildProcess ;
2026-04-20 10:38:57 -05:00
on (
event : "exit" ,
listener : ( code : number | null , signal : NodeJS.Signals | null ) = > void ,
) : ChildProcess ;
2026-03-05 12:39:37 -03:00
on (
event : "close" ,
listener : ( code : number | null , signal : NodeJS.Signals | null ) = > void ,
) : ChildProcess ;
} ;
2026-04-10 22:26:21 -05:00
function resolveProcessGroupId ( child : ChildProcess ) {
if ( process . platform === "win32" ) return null ;
return typeof child . pid === "number" && child . pid > 0 ? child.pid : null ;
}
function signalRunningProcess (
running : Pick < RunningProcess , "child" | "processGroupId" > ,
signal : NodeJS.Signals ,
) {
if ( process . platform !== "win32" && running . processGroupId && running . processGroupId > 0 ) {
try {
process . kill ( - running . processGroupId , signal ) ;
return ;
} catch {
// Fall back to the direct child signal if group signaling fails.
}
}
if ( ! running . child . killed ) {
running . child . kill ( signal ) ;
}
}
2026-02-18 14:23:16 -06:00
export const runningProcesses = new Map < string , RunningProcess > ( ) ;
export const MAX_CAPTURE_BYTES = 4 * 1024 * 1024 ;
export const MAX_EXCERPT_BYTES = 32 * 1024 ;
2026-04-20 10:38:57 -05:00
const TERMINAL_RESULT_SCAN_OVERLAP_CHARS = 64 * 1024 ;
2026-02-18 14:23:16 -06:00
const SENSITIVE_ENV_KEY = /(key|token|secret|password|passwd|authorization|cookie)/i ;
2026-03-12 15:44:44 -05:00
const PAPERCLIP_SKILL_ROOT_RELATIVE_CANDIDATES = [
"../../skills" ,
"../../../../../skills" ,
] ;
[codex] Add run liveness continuations (#4083)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Heartbeat runs are the control-plane record of each agent execution
window.
> - Long-running local agents can exhaust context or stop while still
holding useful next-step state.
> - Operators need that stop reason, next action, and continuation path
to be durable and visible.
> - This pull request adds run liveness metadata, continuation
summaries, and UI surfaces for issue run ledgers.
> - The benefit is that interrupted or long-running work can resume with
clearer context instead of losing the agent's last useful handoff.
## What Changed
- Added heartbeat-run liveness fields, continuation attempt tracking,
and an idempotent `0058` migration.
- Added server services and tests for run liveness, continuation
summaries, stop metadata, and activity backfill.
- Wired local and HTTP adapters to surface continuation/liveness context
through shared adapter utilities.
- Added shared constants, validators, and heartbeat types for liveness
continuation state.
- Added issue-detail UI surfaces for continuation handoffs and the run
ledger, with component tests.
- Updated agent runtime docs, heartbeat protocol docs, prompt guidance,
onboarding assets, and skills instructions to explain continuation
behavior.
- Addressed Greptile feedback by scoping document evidence by run,
excluding system continuation-summary documents from liveness evidence,
importing shared liveness types, surfacing hidden ledger run counts,
documenting bounded retry behavior, and moving run-ledger liveness
backfill off the request path.
## Verification
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/run-continuations.test.ts
server/src/__tests__/run-liveness.test.ts
server/src/__tests__/activity-service.test.ts
server/src/__tests__/documents-service.test.ts
server/src/__tests__/issue-continuation-summary.test.ts
server/src/services/heartbeat-stop-metadata.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/components/IssueContinuationHandoff.test.tsx
ui/src/components/IssueDocumentsSection.test.tsx`
- `pnpm --filter @paperclipai/db build`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/run-continuations.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts -t "treats a
plan document update"`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts -t "activity
service|treats a plan document update"`
- Remote PR checks on head `e53b1a1d`: `verify`, `e2e`, `policy`, and
Snyk all passed.
- Confirmed `public-gh/master` is an ancestor of this branch after
fetching `public-gh master`.
- Confirmed `pnpm-lock.yaml` is not included in the branch diff.
- Confirmed migration `0058_wealthy_starbolt.sql` is ordered after
`0057` and uses `IF NOT EXISTS` guards for repeat application.
- Greptile inline review threads are resolved.
## Risks
- Medium risk: this touches heartbeat execution, liveness recovery,
activity rendering, issue routes, shared contracts, docs, and UI.
- Migration risk is mitigated by additive columns/indexes and idempotent
guards.
- Run-ledger liveness backfill is now asynchronous, so the first ledger
response can briefly show historical missing liveness until the
background backfill completes.
- UI screenshot coverage is not included in this packaging pass;
validation is currently through focused component tests.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4, local tool-use coding agent with terminal, git,
GitHub connector, GitHub CLI, and Paperclip API access.
## 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
Screenshot note: no before/after screenshots were captured in this PR
packaging pass; the UI changes are covered by focused component tests
listed above.
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:01:49 -05:00
export const DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE = [
"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work." ,
"" ,
"Execution contract:" ,
"- Start actionable work in this heartbeat; do not stop at a plan unless the issue asks for planning." ,
"- Leave durable progress in comments, documents, or work products with a clear next action." ,
"- Use child issues for parallel or long delegated work instead of polling agents, sessions, or processes." ,
"- If blocked, mark the issue blocked and name the unblock owner and action." ,
"- Respect budget, pause/cancel, approval gates, and company boundaries." ,
] . join ( "\n" ) ;
2026-03-12 15:44:44 -05:00
export interface PaperclipSkillEntry {
2026-03-16 18:27:20 -05:00
key : string ;
runtimeName : string ;
2026-03-12 15:44:44 -05:00
source : string ;
2026-03-15 07:05:01 -05:00
required? : boolean ;
requiredReason? : string | null ;
2026-03-12 15:44:44 -05:00
}
2026-02-18 14:23:16 -06:00
2026-03-18 14:21:50 -05:00
export interface InstalledSkillTarget {
targetPath : string | null ;
kind : "symlink" | "directory" | "file" ;
}
interface PersistentSkillSnapshotOptions {
adapterType : string ;
availableEntries : PaperclipSkillEntry [ ] ;
desiredSkills : string [ ] ;
installed : Map < string , InstalledSkillTarget > ;
skillsHome : string ;
locationLabel? : string | null ;
installedDetail? : string | null ;
missingDetail : string ;
externalConflictDetail : string ;
externalDetail : string ;
warnings? : string [ ] ;
}
2026-03-12 15:57:37 -05:00
function normalizePathSlashes ( value : string ) : string {
return value . replaceAll ( "\\" , "/" ) ;
}
function isMaintainerOnlySkillTarget ( candidate : string ) : boolean {
return normalizePathSlashes ( candidate ) . includes ( "/.agents/skills/" ) ;
}
2026-03-18 14:21:50 -05:00
function skillLocationLabel ( value : string | null | undefined ) : string | null {
if ( typeof value !== "string" ) return null ;
const trimmed = value . trim ( ) ;
return trimmed . length > 0 ? trimmed : null ;
}
function buildManagedSkillOrigin ( entry : { required? : boolean } ) : Pick <
AdapterSkillEntry ,
"origin" | "originLabel" | "readOnly"
> {
if ( entry . required ) {
return {
origin : "paperclip_required" ,
originLabel : "Required by Paperclip" ,
readOnly : false ,
} ;
}
return {
origin : "company_managed" ,
originLabel : "Managed by Paperclip" ,
readOnly : false ,
} ;
}
function resolveInstalledEntryTarget (
skillsHome : string ,
entryName : string ,
dirent : Dirent ,
linkedPath : string | null ,
) : InstalledSkillTarget {
const fullPath = path . join ( skillsHome , entryName ) ;
if ( dirent . isSymbolicLink ( ) ) {
return {
targetPath : linkedPath ? path . resolve ( path . dirname ( fullPath ) , linkedPath ) : null ,
kind : "symlink" ,
} ;
}
if ( dirent . isDirectory ( ) ) {
return { targetPath : fullPath , kind : "directory" } ;
}
return { targetPath : fullPath , kind : "file" } ;
}
2026-02-18 14:23:16 -06:00
export function parseObject ( value : unknown ) : Record < string , unknown > {
if ( typeof value !== "object" || value === null || Array . isArray ( value ) ) {
return { } ;
}
return value as Record < string , unknown > ;
}
export function asString ( value : unknown , fallback : string ) : string {
return typeof value === "string" && value . length > 0 ? value : fallback ;
}
export function asNumber ( value : unknown , fallback : number ) : number {
return typeof value === "number" && Number . isFinite ( value ) ? value : fallback ;
}
export function asBoolean ( value : unknown , fallback : boolean ) : boolean {
return typeof value === "boolean" ? value : fallback ;
}
export function asStringArray ( value : unknown ) : string [ ] {
return Array . isArray ( value ) ? value . filter ( ( item ) : item is string = > typeof item === "string" ) : [ ] ;
}
export function parseJson ( value : string ) : Record < string , unknown > | null {
try {
return JSON . parse ( value ) as Record < string , unknown > ;
} catch {
return null ;
}
}
export function appendWithCap ( prev : string , chunk : string , cap = MAX_CAPTURE_BYTES ) {
const combined = prev + chunk ;
return combined . length > cap ? combined . slice ( combined . length - cap ) : combined ;
}
[codex] Improve agent runtime recovery and governance (#4086)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The heartbeat runtime, agent import path, and agent configuration
defaults determine whether work is dispatched safely and predictably.
> - Several accumulated fixes all touched agent execution recovery, wake
routing, import behavior, and runtime concurrency defaults.
> - Those changes need to land together so the heartbeat service and
agent creation defaults stay internally consistent.
> - This pull request groups the runtime/governance changes from the
split branch into one standalone branch.
> - The benefit is safer recovery for stranded runs, bounded high-volume
reads, imported-agent approval correctness, skill-template support, and
a clearer default concurrency policy.
## What Changed
- Fixed stranded continuation recovery so successful automatic retries
are requeued instead of incorrectly blocking the issue.
- Bounded high-volume issue/log reads across issue, heartbeat, agent,
project, and workspace paths.
- Fixed imported-agent approval and instruction-path permission
handling.
- Quarantined seeded worktree execution state during worktree
provisioning.
- Queued approval follow-up wakes and hardened SQL_ASCII heartbeat
output handling.
- Added reusable agent instruction templates for hiring flows.
- Set the default max concurrent agent runs to five and updated related
UI/tests/docs.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run server/src/__tests__/company-portability.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/heartbeat-comment-wake-batching.test.ts
server/src/__tests__/heartbeat-list.test.ts
server/src/__tests__/issues-service.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
packages/adapter-utils/src/server-utils.test.ts
ui/src/lib/new-agent-runtime-config.test.ts`
- Split integration check: merged this branch first, followed by the
other [PAP-1614](/PAP/issues/PAP-1614) branches, with no merge
conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Medium risk: touches heartbeat recovery, queueing, and issue list
bounds in central runtime paths.
- Imported-agent and concurrency default behavior changes may affect
existing automation that assumes one-at-a-time default runs.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## 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>
2026-04-20 06:19:48 -05:00
export function appendWithByteCap ( prev : string , chunk : string , cap = MAX_CAPTURE_BYTES ) {
const combined = prev + chunk ;
const bytes = Buffer . byteLength ( combined , "utf8" ) ;
if ( bytes <= cap ) return combined ;
const buffer = Buffer . from ( combined , "utf8" ) ;
let start = Math . max ( 0 , bytes - cap ) ;
while ( start < buffer . length && ( buffer [ start ] ! & 0xc0 ) === 0x80 ) start += 1 ;
return buffer . subarray ( start ) . toString ( "utf8" ) ;
}
function resumeReadable ( readable : { resume : ( ) = > unknown ; destroyed? : boolean } | null | undefined ) {
if ( ! readable || readable . destroyed ) return ;
readable . resume ( ) ;
}
2026-02-18 14:23:16 -06:00
export function resolvePathValue ( obj : Record < string , unknown > , dottedPath : string ) {
const parts = dottedPath . split ( "." ) ;
let cursor : unknown = obj ;
for ( const part of parts ) {
if ( typeof cursor !== "object" || cursor === null || Array . isArray ( cursor ) ) {
return "" ;
}
cursor = ( cursor as Record < string , unknown > ) [ part ] ;
}
if ( cursor === null || cursor === undefined ) return "" ;
if ( typeof cursor === "string" ) return cursor ;
if ( typeof cursor === "number" || typeof cursor === "boolean" ) return String ( cursor ) ;
try {
return JSON . stringify ( cursor ) ;
} catch {
return "" ;
}
}
export function renderTemplate ( template : string , data : Record < string , unknown > ) {
return template . replace ( /{{\s*([a-zA-Z0-9_.-]+)\s*}}/g , ( _ , path ) = > resolvePathValue ( data , path ) ) ;
}
2026-03-13 08:49:11 -05:00
export function joinPromptSections (
sections : Array < string | null | undefined > ,
separator = "\n\n" ,
) {
return sections
. map ( ( value ) = > ( typeof value === "string" ? value . trim ( ) : "" ) )
. filter ( Boolean )
. join ( separator ) ;
}
2026-03-28 09:55:41 -05:00
type PaperclipWakeIssue = {
id : string | null ;
identifier : string | null ;
title : string | null ;
status : string | null ;
priority : string | null ;
} ;
2026-04-08 08:05:35 -05:00
type PaperclipWakeExecutionPrincipal = {
type : "agent" | "user" | null ;
agentId : string | null ;
userId : string | null ;
} ;
type PaperclipWakeExecutionStage = {
wakeRole : "reviewer" | "approver" | "executor" | null ;
stageId : string | null ;
stageType : string | null ;
currentParticipant : PaperclipWakeExecutionPrincipal | null ;
returnAssignee : PaperclipWakeExecutionPrincipal | null ;
lastDecisionOutcome : string | null ;
allowedActions : string [ ] ;
} ;
2026-03-28 09:55:41 -05:00
type PaperclipWakeComment = {
id : string | null ;
issueId : string | null ;
body : string ;
bodyTruncated : boolean ;
createdAt : string | null ;
authorType : string | null ;
authorId : string | null ;
} ;
[codex] Add run liveness continuations (#4083)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Heartbeat runs are the control-plane record of each agent execution
window.
> - Long-running local agents can exhaust context or stop while still
holding useful next-step state.
> - Operators need that stop reason, next action, and continuation path
to be durable and visible.
> - This pull request adds run liveness metadata, continuation
summaries, and UI surfaces for issue run ledgers.
> - The benefit is that interrupted or long-running work can resume with
clearer context instead of losing the agent's last useful handoff.
## What Changed
- Added heartbeat-run liveness fields, continuation attempt tracking,
and an idempotent `0058` migration.
- Added server services and tests for run liveness, continuation
summaries, stop metadata, and activity backfill.
- Wired local and HTTP adapters to surface continuation/liveness context
through shared adapter utilities.
- Added shared constants, validators, and heartbeat types for liveness
continuation state.
- Added issue-detail UI surfaces for continuation handoffs and the run
ledger, with component tests.
- Updated agent runtime docs, heartbeat protocol docs, prompt guidance,
onboarding assets, and skills instructions to explain continuation
behavior.
- Addressed Greptile feedback by scoping document evidence by run,
excluding system continuation-summary documents from liveness evidence,
importing shared liveness types, surfacing hidden ledger run counts,
documenting bounded retry behavior, and moving run-ledger liveness
backfill off the request path.
## Verification
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/run-continuations.test.ts
server/src/__tests__/run-liveness.test.ts
server/src/__tests__/activity-service.test.ts
server/src/__tests__/documents-service.test.ts
server/src/__tests__/issue-continuation-summary.test.ts
server/src/services/heartbeat-stop-metadata.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/components/IssueContinuationHandoff.test.tsx
ui/src/components/IssueDocumentsSection.test.tsx`
- `pnpm --filter @paperclipai/db build`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/run-continuations.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts -t "treats a
plan document update"`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts -t "activity
service|treats a plan document update"`
- Remote PR checks on head `e53b1a1d`: `verify`, `e2e`, `policy`, and
Snyk all passed.
- Confirmed `public-gh/master` is an ancestor of this branch after
fetching `public-gh master`.
- Confirmed `pnpm-lock.yaml` is not included in the branch diff.
- Confirmed migration `0058_wealthy_starbolt.sql` is ordered after
`0057` and uses `IF NOT EXISTS` guards for repeat application.
- Greptile inline review threads are resolved.
## Risks
- Medium risk: this touches heartbeat execution, liveness recovery,
activity rendering, issue routes, shared contracts, docs, and UI.
- Migration risk is mitigated by additive columns/indexes and idempotent
guards.
- Run-ledger liveness backfill is now asynchronous, so the first ledger
response can briefly show historical missing liveness until the
background backfill completes.
- UI screenshot coverage is not included in this packaging pass;
validation is currently through focused component tests.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4, local tool-use coding agent with terminal, git,
GitHub connector, GitHub CLI, and Paperclip API access.
## 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
Screenshot note: no before/after screenshots were captured in this PR
packaging pass; the UI changes are covered by focused component tests
listed above.
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:01:49 -05:00
type PaperclipWakeContinuationSummary = {
key : string | null ;
title : string | null ;
body : string ;
bodyTruncated : boolean ;
updatedAt : string | null ;
} ;
type PaperclipWakeLivenessContinuation = {
attempt : number | null ;
maxAttempts : number | null ;
sourceRunId : string | null ;
state : string | null ;
reason : string | null ;
instruction : string | null ;
} ;
type PaperclipWakeChildIssueSummary = {
id : string | null ;
identifier : string | null ;
title : string | null ;
status : string | null ;
priority : string | null ;
summary : string | null ;
} ;
2026-03-28 09:55:41 -05:00
type PaperclipWakePayload = {
reason : string | null ;
issue : PaperclipWakeIssue | null ;
2026-04-11 10:53:28 -05:00
checkedOutByHarness : boolean ;
2026-04-08 08:05:35 -05:00
executionStage : PaperclipWakeExecutionStage | null ;
[codex] Add run liveness continuations (#4083)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Heartbeat runs are the control-plane record of each agent execution
window.
> - Long-running local agents can exhaust context or stop while still
holding useful next-step state.
> - Operators need that stop reason, next action, and continuation path
to be durable and visible.
> - This pull request adds run liveness metadata, continuation
summaries, and UI surfaces for issue run ledgers.
> - The benefit is that interrupted or long-running work can resume with
clearer context instead of losing the agent's last useful handoff.
## What Changed
- Added heartbeat-run liveness fields, continuation attempt tracking,
and an idempotent `0058` migration.
- Added server services and tests for run liveness, continuation
summaries, stop metadata, and activity backfill.
- Wired local and HTTP adapters to surface continuation/liveness context
through shared adapter utilities.
- Added shared constants, validators, and heartbeat types for liveness
continuation state.
- Added issue-detail UI surfaces for continuation handoffs and the run
ledger, with component tests.
- Updated agent runtime docs, heartbeat protocol docs, prompt guidance,
onboarding assets, and skills instructions to explain continuation
behavior.
- Addressed Greptile feedback by scoping document evidence by run,
excluding system continuation-summary documents from liveness evidence,
importing shared liveness types, surfacing hidden ledger run counts,
documenting bounded retry behavior, and moving run-ledger liveness
backfill off the request path.
## Verification
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/run-continuations.test.ts
server/src/__tests__/run-liveness.test.ts
server/src/__tests__/activity-service.test.ts
server/src/__tests__/documents-service.test.ts
server/src/__tests__/issue-continuation-summary.test.ts
server/src/services/heartbeat-stop-metadata.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/components/IssueContinuationHandoff.test.tsx
ui/src/components/IssueDocumentsSection.test.tsx`
- `pnpm --filter @paperclipai/db build`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/run-continuations.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts -t "treats a
plan document update"`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts -t "activity
service|treats a plan document update"`
- Remote PR checks on head `e53b1a1d`: `verify`, `e2e`, `policy`, and
Snyk all passed.
- Confirmed `public-gh/master` is an ancestor of this branch after
fetching `public-gh master`.
- Confirmed `pnpm-lock.yaml` is not included in the branch diff.
- Confirmed migration `0058_wealthy_starbolt.sql` is ordered after
`0057` and uses `IF NOT EXISTS` guards for repeat application.
- Greptile inline review threads are resolved.
## Risks
- Medium risk: this touches heartbeat execution, liveness recovery,
activity rendering, issue routes, shared contracts, docs, and UI.
- Migration risk is mitigated by additive columns/indexes and idempotent
guards.
- Run-ledger liveness backfill is now asynchronous, so the first ledger
response can briefly show historical missing liveness until the
background backfill completes.
- UI screenshot coverage is not included in this packaging pass;
validation is currently through focused component tests.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4, local tool-use coding agent with terminal, git,
GitHub connector, GitHub CLI, and Paperclip API access.
## 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
Screenshot note: no before/after screenshots were captured in this PR
packaging pass; the UI changes are covered by focused component tests
listed above.
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:01:49 -05:00
continuationSummary : PaperclipWakeContinuationSummary | null ;
livenessContinuation : PaperclipWakeLivenessContinuation | null ;
childIssueSummaries : PaperclipWakeChildIssueSummary [ ] ;
childIssueSummaryTruncated : boolean ;
2026-03-28 09:55:41 -05:00
commentIds : string [ ] ;
latestCommentId : string | null ;
comments : PaperclipWakeComment [ ] ;
requestedCount : number ;
includedCount : number ;
missingCount : number ;
truncated : boolean ;
fallbackFetchNeeded : boolean ;
} ;
function normalizePaperclipWakeIssue ( value : unknown ) : PaperclipWakeIssue | null {
const issue = parseObject ( value ) ;
const id = asString ( issue . id , "" ) . trim ( ) || null ;
const identifier = asString ( issue . identifier , "" ) . trim ( ) || null ;
const title = asString ( issue . title , "" ) . trim ( ) || null ;
const status = asString ( issue . status , "" ) . trim ( ) || null ;
const priority = asString ( issue . priority , "" ) . trim ( ) || null ;
if ( ! id && ! identifier && ! title ) return null ;
return {
id ,
identifier ,
title ,
status ,
priority ,
} ;
}
function normalizePaperclipWakeComment ( value : unknown ) : PaperclipWakeComment | null {
const comment = parseObject ( value ) ;
const author = parseObject ( comment . author ) ;
const body = asString ( comment . body , "" ) ;
if ( ! body . trim ( ) ) return null ;
return {
id : asString ( comment . id , "" ) . trim ( ) || null ,
issueId : asString ( comment . issueId , "" ) . trim ( ) || null ,
body ,
bodyTruncated : asBoolean ( comment . bodyTruncated , false ) ,
createdAt : asString ( comment . createdAt , "" ) . trim ( ) || null ,
authorType : asString ( author . type , "" ) . trim ( ) || null ,
authorId : asString ( author . id , "" ) . trim ( ) || null ,
} ;
}
[codex] Add run liveness continuations (#4083)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Heartbeat runs are the control-plane record of each agent execution
window.
> - Long-running local agents can exhaust context or stop while still
holding useful next-step state.
> - Operators need that stop reason, next action, and continuation path
to be durable and visible.
> - This pull request adds run liveness metadata, continuation
summaries, and UI surfaces for issue run ledgers.
> - The benefit is that interrupted or long-running work can resume with
clearer context instead of losing the agent's last useful handoff.
## What Changed
- Added heartbeat-run liveness fields, continuation attempt tracking,
and an idempotent `0058` migration.
- Added server services and tests for run liveness, continuation
summaries, stop metadata, and activity backfill.
- Wired local and HTTP adapters to surface continuation/liveness context
through shared adapter utilities.
- Added shared constants, validators, and heartbeat types for liveness
continuation state.
- Added issue-detail UI surfaces for continuation handoffs and the run
ledger, with component tests.
- Updated agent runtime docs, heartbeat protocol docs, prompt guidance,
onboarding assets, and skills instructions to explain continuation
behavior.
- Addressed Greptile feedback by scoping document evidence by run,
excluding system continuation-summary documents from liveness evidence,
importing shared liveness types, surfacing hidden ledger run counts,
documenting bounded retry behavior, and moving run-ledger liveness
backfill off the request path.
## Verification
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/run-continuations.test.ts
server/src/__tests__/run-liveness.test.ts
server/src/__tests__/activity-service.test.ts
server/src/__tests__/documents-service.test.ts
server/src/__tests__/issue-continuation-summary.test.ts
server/src/services/heartbeat-stop-metadata.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/components/IssueContinuationHandoff.test.tsx
ui/src/components/IssueDocumentsSection.test.tsx`
- `pnpm --filter @paperclipai/db build`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/run-continuations.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts -t "treats a
plan document update"`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts -t "activity
service|treats a plan document update"`
- Remote PR checks on head `e53b1a1d`: `verify`, `e2e`, `policy`, and
Snyk all passed.
- Confirmed `public-gh/master` is an ancestor of this branch after
fetching `public-gh master`.
- Confirmed `pnpm-lock.yaml` is not included in the branch diff.
- Confirmed migration `0058_wealthy_starbolt.sql` is ordered after
`0057` and uses `IF NOT EXISTS` guards for repeat application.
- Greptile inline review threads are resolved.
## Risks
- Medium risk: this touches heartbeat execution, liveness recovery,
activity rendering, issue routes, shared contracts, docs, and UI.
- Migration risk is mitigated by additive columns/indexes and idempotent
guards.
- Run-ledger liveness backfill is now asynchronous, so the first ledger
response can briefly show historical missing liveness until the
background backfill completes.
- UI screenshot coverage is not included in this packaging pass;
validation is currently through focused component tests.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4, local tool-use coding agent with terminal, git,
GitHub connector, GitHub CLI, and Paperclip API access.
## 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
Screenshot note: no before/after screenshots were captured in this PR
packaging pass; the UI changes are covered by focused component tests
listed above.
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:01:49 -05:00
function normalizePaperclipWakeContinuationSummary ( value : unknown ) : PaperclipWakeContinuationSummary | null {
const summary = parseObject ( value ) ;
const body = asString ( summary . body , "" ) . trim ( ) ;
if ( ! body ) return null ;
return {
key : asString ( summary . key , "" ) . trim ( ) || null ,
title : asString ( summary . title , "" ) . trim ( ) || null ,
body ,
bodyTruncated : asBoolean ( summary . bodyTruncated , false ) ,
updatedAt : asString ( summary . updatedAt , "" ) . trim ( ) || null ,
} ;
}
function normalizePaperclipWakeLivenessContinuation ( value : unknown ) : PaperclipWakeLivenessContinuation | null {
const continuation = parseObject ( value ) ;
const attempt = asNumber ( continuation . attempt , 0 ) ;
const maxAttempts = asNumber ( continuation . maxAttempts , 0 ) ;
const sourceRunId = asString ( continuation . sourceRunId , "" ) . trim ( ) || null ;
const state = asString ( continuation . state , "" ) . trim ( ) || null ;
const reason = asString ( continuation . reason , "" ) . trim ( ) || null ;
const instruction = asString ( continuation . instruction , "" ) . trim ( ) || null ;
if ( ! attempt && ! maxAttempts && ! sourceRunId && ! state && ! reason && ! instruction ) return null ;
return {
attempt : attempt > 0 ? attempt : null ,
maxAttempts : maxAttempts > 0 ? maxAttempts : null ,
sourceRunId ,
state ,
reason ,
instruction ,
} ;
}
function normalizePaperclipWakeChildIssueSummary ( value : unknown ) : PaperclipWakeChildIssueSummary | null {
const child = parseObject ( value ) ;
const id = asString ( child . id , "" ) . trim ( ) || null ;
const identifier = asString ( child . identifier , "" ) . trim ( ) || null ;
const title = asString ( child . title , "" ) . trim ( ) || null ;
const status = asString ( child . status , "" ) . trim ( ) || null ;
const priority = asString ( child . priority , "" ) . trim ( ) || null ;
const summary = asString ( child . summary , "" ) . trim ( ) || null ;
if ( ! id && ! identifier && ! title && ! status && ! summary ) return null ;
return { id , identifier , title , status , priority , summary } ;
}
2026-04-08 08:05:35 -05:00
function normalizePaperclipWakeExecutionPrincipal ( value : unknown ) : PaperclipWakeExecutionPrincipal | null {
const principal = parseObject ( value ) ;
const typeRaw = asString ( principal . type , "" ) . trim ( ) . toLowerCase ( ) ;
if ( typeRaw !== "agent" && typeRaw !== "user" ) return null ;
return {
type : typeRaw ,
agentId : asString ( principal . agentId , "" ) . trim ( ) || null ,
userId : asString ( principal . userId , "" ) . trim ( ) || null ,
} ;
}
function normalizePaperclipWakeExecutionStage ( value : unknown ) : PaperclipWakeExecutionStage | null {
const stage = parseObject ( value ) ;
const wakeRoleRaw = asString ( stage . wakeRole , "" ) . trim ( ) . toLowerCase ( ) ;
const wakeRole =
wakeRoleRaw === "reviewer" || wakeRoleRaw === "approver" || wakeRoleRaw === "executor"
? wakeRoleRaw
: null ;
const allowedActions = Array . isArray ( stage . allowedActions )
? stage . allowedActions
. filter ( ( entry ) : entry is string = > typeof entry === "string" && entry . trim ( ) . length > 0 )
. map ( ( entry ) = > entry . trim ( ) )
: [ ] ;
const currentParticipant = normalizePaperclipWakeExecutionPrincipal ( stage . currentParticipant ) ;
const returnAssignee = normalizePaperclipWakeExecutionPrincipal ( stage . returnAssignee ) ;
const stageId = asString ( stage . stageId , "" ) . trim ( ) || null ;
const stageType = asString ( stage . stageType , "" ) . trim ( ) || null ;
const lastDecisionOutcome = asString ( stage . lastDecisionOutcome , "" ) . trim ( ) || null ;
if ( ! wakeRole && ! stageId && ! stageType && ! currentParticipant && ! returnAssignee && ! lastDecisionOutcome && allowedActions . length === 0 ) {
return null ;
}
return {
wakeRole ,
stageId ,
stageType ,
currentParticipant ,
returnAssignee ,
lastDecisionOutcome ,
allowedActions ,
} ;
}
2026-03-28 09:55:41 -05:00
export function normalizePaperclipWakePayload ( value : unknown ) : PaperclipWakePayload | null {
const payload = parseObject ( value ) ;
const comments = Array . isArray ( payload . comments )
? payload . comments
. map ( ( entry ) = > normalizePaperclipWakeComment ( entry ) )
. filter ( ( entry ) : entry is PaperclipWakeComment = > Boolean ( entry ) )
: [ ] ;
const commentWindow = parseObject ( payload . commentWindow ) ;
const commentIds = Array . isArray ( payload . commentIds )
? payload . commentIds
. filter ( ( entry ) : entry is string = > typeof entry === "string" && entry . trim ( ) . length > 0 )
. map ( ( entry ) = > entry . trim ( ) )
: [ ] ;
2026-04-08 08:05:35 -05:00
const executionStage = normalizePaperclipWakeExecutionStage ( payload . executionStage ) ;
[codex] Add run liveness continuations (#4083)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Heartbeat runs are the control-plane record of each agent execution
window.
> - Long-running local agents can exhaust context or stop while still
holding useful next-step state.
> - Operators need that stop reason, next action, and continuation path
to be durable and visible.
> - This pull request adds run liveness metadata, continuation
summaries, and UI surfaces for issue run ledgers.
> - The benefit is that interrupted or long-running work can resume with
clearer context instead of losing the agent's last useful handoff.
## What Changed
- Added heartbeat-run liveness fields, continuation attempt tracking,
and an idempotent `0058` migration.
- Added server services and tests for run liveness, continuation
summaries, stop metadata, and activity backfill.
- Wired local and HTTP adapters to surface continuation/liveness context
through shared adapter utilities.
- Added shared constants, validators, and heartbeat types for liveness
continuation state.
- Added issue-detail UI surfaces for continuation handoffs and the run
ledger, with component tests.
- Updated agent runtime docs, heartbeat protocol docs, prompt guidance,
onboarding assets, and skills instructions to explain continuation
behavior.
- Addressed Greptile feedback by scoping document evidence by run,
excluding system continuation-summary documents from liveness evidence,
importing shared liveness types, surfacing hidden ledger run counts,
documenting bounded retry behavior, and moving run-ledger liveness
backfill off the request path.
## Verification
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/run-continuations.test.ts
server/src/__tests__/run-liveness.test.ts
server/src/__tests__/activity-service.test.ts
server/src/__tests__/documents-service.test.ts
server/src/__tests__/issue-continuation-summary.test.ts
server/src/services/heartbeat-stop-metadata.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/components/IssueContinuationHandoff.test.tsx
ui/src/components/IssueDocumentsSection.test.tsx`
- `pnpm --filter @paperclipai/db build`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/run-continuations.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts -t "treats a
plan document update"`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts -t "activity
service|treats a plan document update"`
- Remote PR checks on head `e53b1a1d`: `verify`, `e2e`, `policy`, and
Snyk all passed.
- Confirmed `public-gh/master` is an ancestor of this branch after
fetching `public-gh master`.
- Confirmed `pnpm-lock.yaml` is not included in the branch diff.
- Confirmed migration `0058_wealthy_starbolt.sql` is ordered after
`0057` and uses `IF NOT EXISTS` guards for repeat application.
- Greptile inline review threads are resolved.
## Risks
- Medium risk: this touches heartbeat execution, liveness recovery,
activity rendering, issue routes, shared contracts, docs, and UI.
- Migration risk is mitigated by additive columns/indexes and idempotent
guards.
- Run-ledger liveness backfill is now asynchronous, so the first ledger
response can briefly show historical missing liveness until the
background backfill completes.
- UI screenshot coverage is not included in this packaging pass;
validation is currently through focused component tests.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4, local tool-use coding agent with terminal, git,
GitHub connector, GitHub CLI, and Paperclip API access.
## 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
Screenshot note: no before/after screenshots were captured in this PR
packaging pass; the UI changes are covered by focused component tests
listed above.
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:01:49 -05:00
const continuationSummary = normalizePaperclipWakeContinuationSummary ( payload . continuationSummary ) ;
const livenessContinuation = normalizePaperclipWakeLivenessContinuation ( payload . livenessContinuation ) ;
const childIssueSummaries = Array . isArray ( payload . childIssueSummaries )
? payload . childIssueSummaries
. map ( ( entry ) = > normalizePaperclipWakeChildIssueSummary ( entry ) )
. filter ( ( entry ) : entry is PaperclipWakeChildIssueSummary = > Boolean ( entry ) )
: [ ] ;
2026-03-28 09:55:41 -05:00
[codex] Add run liveness continuations (#4083)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Heartbeat runs are the control-plane record of each agent execution
window.
> - Long-running local agents can exhaust context or stop while still
holding useful next-step state.
> - Operators need that stop reason, next action, and continuation path
to be durable and visible.
> - This pull request adds run liveness metadata, continuation
summaries, and UI surfaces for issue run ledgers.
> - The benefit is that interrupted or long-running work can resume with
clearer context instead of losing the agent's last useful handoff.
## What Changed
- Added heartbeat-run liveness fields, continuation attempt tracking,
and an idempotent `0058` migration.
- Added server services and tests for run liveness, continuation
summaries, stop metadata, and activity backfill.
- Wired local and HTTP adapters to surface continuation/liveness context
through shared adapter utilities.
- Added shared constants, validators, and heartbeat types for liveness
continuation state.
- Added issue-detail UI surfaces for continuation handoffs and the run
ledger, with component tests.
- Updated agent runtime docs, heartbeat protocol docs, prompt guidance,
onboarding assets, and skills instructions to explain continuation
behavior.
- Addressed Greptile feedback by scoping document evidence by run,
excluding system continuation-summary documents from liveness evidence,
importing shared liveness types, surfacing hidden ledger run counts,
documenting bounded retry behavior, and moving run-ledger liveness
backfill off the request path.
## Verification
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/run-continuations.test.ts
server/src/__tests__/run-liveness.test.ts
server/src/__tests__/activity-service.test.ts
server/src/__tests__/documents-service.test.ts
server/src/__tests__/issue-continuation-summary.test.ts
server/src/services/heartbeat-stop-metadata.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/components/IssueContinuationHandoff.test.tsx
ui/src/components/IssueDocumentsSection.test.tsx`
- `pnpm --filter @paperclipai/db build`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/run-continuations.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts -t "treats a
plan document update"`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts -t "activity
service|treats a plan document update"`
- Remote PR checks on head `e53b1a1d`: `verify`, `e2e`, `policy`, and
Snyk all passed.
- Confirmed `public-gh/master` is an ancestor of this branch after
fetching `public-gh master`.
- Confirmed `pnpm-lock.yaml` is not included in the branch diff.
- Confirmed migration `0058_wealthy_starbolt.sql` is ordered after
`0057` and uses `IF NOT EXISTS` guards for repeat application.
- Greptile inline review threads are resolved.
## Risks
- Medium risk: this touches heartbeat execution, liveness recovery,
activity rendering, issue routes, shared contracts, docs, and UI.
- Migration risk is mitigated by additive columns/indexes and idempotent
guards.
- Run-ledger liveness backfill is now asynchronous, so the first ledger
response can briefly show historical missing liveness until the
background backfill completes.
- UI screenshot coverage is not included in this packaging pass;
validation is currently through focused component tests.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4, local tool-use coding agent with terminal, git,
GitHub connector, GitHub CLI, and Paperclip API access.
## 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
Screenshot note: no before/after screenshots were captured in this PR
packaging pass; the UI changes are covered by focused component tests
listed above.
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:01:49 -05:00
if ( comments . length === 0 && commentIds . length === 0 && childIssueSummaries . length === 0 && ! executionStage && ! continuationSummary && ! livenessContinuation && ! normalizePaperclipWakeIssue ( payload . issue ) ) {
2026-04-08 08:05:35 -05:00
return null ;
}
2026-03-28 09:55:41 -05:00
return {
reason : asString ( payload . reason , "" ) . trim ( ) || null ,
issue : normalizePaperclipWakeIssue ( payload . issue ) ,
2026-04-11 10:53:28 -05:00
checkedOutByHarness : asBoolean ( payload . checkedOutByHarness , false ) ,
2026-04-08 08:05:35 -05:00
executionStage ,
[codex] Add run liveness continuations (#4083)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Heartbeat runs are the control-plane record of each agent execution
window.
> - Long-running local agents can exhaust context or stop while still
holding useful next-step state.
> - Operators need that stop reason, next action, and continuation path
to be durable and visible.
> - This pull request adds run liveness metadata, continuation
summaries, and UI surfaces for issue run ledgers.
> - The benefit is that interrupted or long-running work can resume with
clearer context instead of losing the agent's last useful handoff.
## What Changed
- Added heartbeat-run liveness fields, continuation attempt tracking,
and an idempotent `0058` migration.
- Added server services and tests for run liveness, continuation
summaries, stop metadata, and activity backfill.
- Wired local and HTTP adapters to surface continuation/liveness context
through shared adapter utilities.
- Added shared constants, validators, and heartbeat types for liveness
continuation state.
- Added issue-detail UI surfaces for continuation handoffs and the run
ledger, with component tests.
- Updated agent runtime docs, heartbeat protocol docs, prompt guidance,
onboarding assets, and skills instructions to explain continuation
behavior.
- Addressed Greptile feedback by scoping document evidence by run,
excluding system continuation-summary documents from liveness evidence,
importing shared liveness types, surfacing hidden ledger run counts,
documenting bounded retry behavior, and moving run-ledger liveness
backfill off the request path.
## Verification
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/run-continuations.test.ts
server/src/__tests__/run-liveness.test.ts
server/src/__tests__/activity-service.test.ts
server/src/__tests__/documents-service.test.ts
server/src/__tests__/issue-continuation-summary.test.ts
server/src/services/heartbeat-stop-metadata.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/components/IssueContinuationHandoff.test.tsx
ui/src/components/IssueDocumentsSection.test.tsx`
- `pnpm --filter @paperclipai/db build`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/run-continuations.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts -t "treats a
plan document update"`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts -t "activity
service|treats a plan document update"`
- Remote PR checks on head `e53b1a1d`: `verify`, `e2e`, `policy`, and
Snyk all passed.
- Confirmed `public-gh/master` is an ancestor of this branch after
fetching `public-gh master`.
- Confirmed `pnpm-lock.yaml` is not included in the branch diff.
- Confirmed migration `0058_wealthy_starbolt.sql` is ordered after
`0057` and uses `IF NOT EXISTS` guards for repeat application.
- Greptile inline review threads are resolved.
## Risks
- Medium risk: this touches heartbeat execution, liveness recovery,
activity rendering, issue routes, shared contracts, docs, and UI.
- Migration risk is mitigated by additive columns/indexes and idempotent
guards.
- Run-ledger liveness backfill is now asynchronous, so the first ledger
response can briefly show historical missing liveness until the
background backfill completes.
- UI screenshot coverage is not included in this packaging pass;
validation is currently through focused component tests.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4, local tool-use coding agent with terminal, git,
GitHub connector, GitHub CLI, and Paperclip API access.
## 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
Screenshot note: no before/after screenshots were captured in this PR
packaging pass; the UI changes are covered by focused component tests
listed above.
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:01:49 -05:00
continuationSummary ,
livenessContinuation ,
childIssueSummaries ,
childIssueSummaryTruncated : asBoolean ( payload . childIssueSummaryTruncated , false ) ,
2026-03-28 09:55:41 -05:00
commentIds ,
latestCommentId : asString ( payload . latestCommentId , "" ) . trim ( ) || null ,
comments ,
requestedCount : asNumber ( commentWindow . requestedCount , comments . length || commentIds . length ) ,
includedCount : asNumber ( commentWindow . includedCount , comments . length ) ,
missingCount : asNumber ( commentWindow . missingCount , 0 ) ,
truncated : asBoolean ( payload . truncated , false ) ,
fallbackFetchNeeded : asBoolean ( payload . fallbackFetchNeeded , false ) ,
} ;
}
export function stringifyPaperclipWakePayload ( value : unknown ) : string | null {
const normalized = normalizePaperclipWakePayload ( value ) ;
if ( ! normalized ) return null ;
return JSON . stringify ( normalized ) ;
}
2026-03-28 10:33:40 -05:00
export function renderPaperclipWakePrompt (
value : unknown ,
options : { resumedSession? : boolean } = { } ,
) : string {
2026-03-28 09:55:41 -05:00
const normalized = normalizePaperclipWakePayload ( value ) ;
if ( ! normalized ) return "" ;
2026-03-28 10:33:40 -05:00
const resumedSession = options . resumedSession === true ;
2026-04-08 08:05:35 -05:00
const executionStage = normalized . executionStage ;
const principalLabel = ( principal : PaperclipWakeExecutionPrincipal | null ) = > {
if ( ! principal || ! principal . type ) return "unknown" ;
if ( principal . type === "agent" ) return principal . agentId ? ` agent ${ principal . agentId } ` : "agent" ;
return principal . userId ? ` user ${ principal . userId } ` : "user" ;
} ;
2026-03-28 10:33:40 -05:00
const lines = resumedSession
2026-03-28 11:40:43 -05:00
? [
2026-03-28 10:33:40 -05:00
"## Paperclip Resume Delta" ,
"" ,
"You are resuming an existing Paperclip session." ,
2026-03-28 11:40:43 -05:00
"This heartbeat is scoped to the issue below. Do not switch to another issue until you have handled this wake." ,
2026-03-28 10:33:40 -05:00
"Focus on the new wake delta below and continue the current task without restating the full heartbeat boilerplate." ,
"Fetch the API thread only when `fallbackFetchNeeded` is true or you need broader history than this batch." ,
"" ,
[codex] Add run liveness continuations (#4083)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Heartbeat runs are the control-plane record of each agent execution
window.
> - Long-running local agents can exhaust context or stop while still
holding useful next-step state.
> - Operators need that stop reason, next action, and continuation path
to be durable and visible.
> - This pull request adds run liveness metadata, continuation
summaries, and UI surfaces for issue run ledgers.
> - The benefit is that interrupted or long-running work can resume with
clearer context instead of losing the agent's last useful handoff.
## What Changed
- Added heartbeat-run liveness fields, continuation attempt tracking,
and an idempotent `0058` migration.
- Added server services and tests for run liveness, continuation
summaries, stop metadata, and activity backfill.
- Wired local and HTTP adapters to surface continuation/liveness context
through shared adapter utilities.
- Added shared constants, validators, and heartbeat types for liveness
continuation state.
- Added issue-detail UI surfaces for continuation handoffs and the run
ledger, with component tests.
- Updated agent runtime docs, heartbeat protocol docs, prompt guidance,
onboarding assets, and skills instructions to explain continuation
behavior.
- Addressed Greptile feedback by scoping document evidence by run,
excluding system continuation-summary documents from liveness evidence,
importing shared liveness types, surfacing hidden ledger run counts,
documenting bounded retry behavior, and moving run-ledger liveness
backfill off the request path.
## Verification
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/run-continuations.test.ts
server/src/__tests__/run-liveness.test.ts
server/src/__tests__/activity-service.test.ts
server/src/__tests__/documents-service.test.ts
server/src/__tests__/issue-continuation-summary.test.ts
server/src/services/heartbeat-stop-metadata.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/components/IssueContinuationHandoff.test.tsx
ui/src/components/IssueDocumentsSection.test.tsx`
- `pnpm --filter @paperclipai/db build`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/run-continuations.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts -t "treats a
plan document update"`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts -t "activity
service|treats a plan document update"`
- Remote PR checks on head `e53b1a1d`: `verify`, `e2e`, `policy`, and
Snyk all passed.
- Confirmed `public-gh/master` is an ancestor of this branch after
fetching `public-gh master`.
- Confirmed `pnpm-lock.yaml` is not included in the branch diff.
- Confirmed migration `0058_wealthy_starbolt.sql` is ordered after
`0057` and uses `IF NOT EXISTS` guards for repeat application.
- Greptile inline review threads are resolved.
## Risks
- Medium risk: this touches heartbeat execution, liveness recovery,
activity rendering, issue routes, shared contracts, docs, and UI.
- Migration risk is mitigated by additive columns/indexes and idempotent
guards.
- Run-ledger liveness backfill is now asynchronous, so the first ledger
response can briefly show historical missing liveness until the
background backfill completes.
- UI screenshot coverage is not included in this packaging pass;
validation is currently through focused component tests.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4, local tool-use coding agent with terminal, git,
GitHub connector, GitHub CLI, and Paperclip API access.
## 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
Screenshot note: no before/after screenshots were captured in this PR
packaging pass; the UI changes are covered by focused component tests
listed above.
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:01:49 -05:00
"Execution contract: take concrete action in this heartbeat when the issue is actionable; do not stop at a plan unless planning was requested. Leave durable progress with a clear next action, use child issues instead of polling for long or parallel work, and mark blocked work with the unblock owner/action." ,
"" ,
2026-03-28 10:33:40 -05:00
` - reason: ${ normalized . reason ? ? "unknown" } ` ,
` - issue: ${ normalized . issue ? . identifier ? ? normalized . issue ? . id ? ? "unknown" } ${ normalized . issue ? . title ? ` ${ normalized . issue . title } ` : "" } ` ,
` - pending comments: ${ normalized . includedCount } / ${ normalized . requestedCount } ` ,
` - latest comment id: ${ normalized . latestCommentId ? ? "unknown" } ` ,
` - fallback fetch needed: ${ normalized . fallbackFetchNeeded ? "yes" : "no" } ` ,
]
: [
"## Paperclip Wake Payload" ,
"" ,
"Treat this wake payload as the highest-priority change for the current heartbeat." ,
2026-03-28 11:40:43 -05:00
"This heartbeat is scoped to the issue below. Do not switch to another issue until you have handled this wake." ,
2026-03-28 10:33:40 -05:00
"Before generic repo exploration or boilerplate heartbeat updates, acknowledge the latest comment and explain how it changes your next action." ,
"Use this inline wake data first before refetching the issue thread." ,
"Only fetch the API thread when `fallbackFetchNeeded` is true or you need broader history than this batch." ,
"" ,
[codex] Add run liveness continuations (#4083)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Heartbeat runs are the control-plane record of each agent execution
window.
> - Long-running local agents can exhaust context or stop while still
holding useful next-step state.
> - Operators need that stop reason, next action, and continuation path
to be durable and visible.
> - This pull request adds run liveness metadata, continuation
summaries, and UI surfaces for issue run ledgers.
> - The benefit is that interrupted or long-running work can resume with
clearer context instead of losing the agent's last useful handoff.
## What Changed
- Added heartbeat-run liveness fields, continuation attempt tracking,
and an idempotent `0058` migration.
- Added server services and tests for run liveness, continuation
summaries, stop metadata, and activity backfill.
- Wired local and HTTP adapters to surface continuation/liveness context
through shared adapter utilities.
- Added shared constants, validators, and heartbeat types for liveness
continuation state.
- Added issue-detail UI surfaces for continuation handoffs and the run
ledger, with component tests.
- Updated agent runtime docs, heartbeat protocol docs, prompt guidance,
onboarding assets, and skills instructions to explain continuation
behavior.
- Addressed Greptile feedback by scoping document evidence by run,
excluding system continuation-summary documents from liveness evidence,
importing shared liveness types, surfacing hidden ledger run counts,
documenting bounded retry behavior, and moving run-ledger liveness
backfill off the request path.
## Verification
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/run-continuations.test.ts
server/src/__tests__/run-liveness.test.ts
server/src/__tests__/activity-service.test.ts
server/src/__tests__/documents-service.test.ts
server/src/__tests__/issue-continuation-summary.test.ts
server/src/services/heartbeat-stop-metadata.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/components/IssueContinuationHandoff.test.tsx
ui/src/components/IssueDocumentsSection.test.tsx`
- `pnpm --filter @paperclipai/db build`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/run-continuations.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts -t "treats a
plan document update"`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts -t "activity
service|treats a plan document update"`
- Remote PR checks on head `e53b1a1d`: `verify`, `e2e`, `policy`, and
Snyk all passed.
- Confirmed `public-gh/master` is an ancestor of this branch after
fetching `public-gh master`.
- Confirmed `pnpm-lock.yaml` is not included in the branch diff.
- Confirmed migration `0058_wealthy_starbolt.sql` is ordered after
`0057` and uses `IF NOT EXISTS` guards for repeat application.
- Greptile inline review threads are resolved.
## Risks
- Medium risk: this touches heartbeat execution, liveness recovery,
activity rendering, issue routes, shared contracts, docs, and UI.
- Migration risk is mitigated by additive columns/indexes and idempotent
guards.
- Run-ledger liveness backfill is now asynchronous, so the first ledger
response can briefly show historical missing liveness until the
background backfill completes.
- UI screenshot coverage is not included in this packaging pass;
validation is currently through focused component tests.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4, local tool-use coding agent with terminal, git,
GitHub connector, GitHub CLI, and Paperclip API access.
## 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
Screenshot note: no before/after screenshots were captured in this PR
packaging pass; the UI changes are covered by focused component tests
listed above.
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:01:49 -05:00
"Execution contract: take concrete action in this heartbeat when the issue is actionable; do not stop at a plan unless planning was requested. Leave durable progress with a clear next action, use child issues instead of polling for long or parallel work, and mark blocked work with the unblock owner/action." ,
"" ,
2026-03-28 10:33:40 -05:00
` - reason: ${ normalized . reason ? ? "unknown" } ` ,
` - issue: ${ normalized . issue ? . identifier ? ? normalized . issue ? . id ? ? "unknown" } ${ normalized . issue ? . title ? ` ${ normalized . issue . title } ` : "" } ` ,
` - pending comments: ${ normalized . includedCount } / ${ normalized . requestedCount } ` ,
` - latest comment id: ${ normalized . latestCommentId ? ? "unknown" } ` ,
` - fallback fetch needed: ${ normalized . fallbackFetchNeeded ? "yes" : "no" } ` ,
] ;
2026-03-28 09:55:41 -05:00
if ( normalized . issue ? . status ) {
lines . push ( ` - issue status: ${ normalized . issue . status } ` ) ;
}
if ( normalized . issue ? . priority ) {
lines . push ( ` - issue priority: ${ normalized . issue . priority } ` ) ;
}
2026-04-11 10:53:28 -05:00
if ( normalized . checkedOutByHarness ) {
lines . push ( "- checkout: already claimed by the harness for this run" ) ;
}
2026-03-28 09:55:41 -05:00
if ( normalized . missingCount > 0 ) {
lines . push ( ` - omitted comments: ${ normalized . missingCount } ` ) ;
}
2026-04-08 08:05:35 -05:00
if ( executionStage ) {
lines . push (
` - execution wake role: ${ executionStage . wakeRole ? ? "unknown" } ` ,
` - execution stage: ${ executionStage . stageType ? ? "unknown" } ` ,
` - execution participant: ${ principalLabel ( executionStage . currentParticipant ) } ` ,
` - execution return assignee: ${ principalLabel ( executionStage . returnAssignee ) } ` ,
` - last decision outcome: ${ executionStage . lastDecisionOutcome ? ? "none" } ` ,
) ;
if ( executionStage . allowedActions . length > 0 ) {
lines . push ( ` - allowed actions: ${ executionStage . allowedActions . join ( ", " ) } ` ) ;
}
lines . push ( "" ) ;
if ( executionStage . wakeRole === "reviewer" || executionStage . wakeRole === "approver" ) {
lines . push (
` You are waking as the active ${ executionStage . wakeRole } for this issue. ` ,
"Do not execute the task itself or continue executor work." ,
"Review the issue and choose one of the allowed actions above." ,
"If you request changes, the workflow routes back to the stored return assignee." ,
"" ,
) ;
} else if ( executionStage . wakeRole === "executor" ) {
lines . push (
"You are waking because changes were requested in the execution workflow." ,
"Address the requested changes on this issue and resubmit when the work is ready." ,
"" ,
) ;
}
}
[codex] Add run liveness continuations (#4083)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Heartbeat runs are the control-plane record of each agent execution
window.
> - Long-running local agents can exhaust context or stop while still
holding useful next-step state.
> - Operators need that stop reason, next action, and continuation path
to be durable and visible.
> - This pull request adds run liveness metadata, continuation
summaries, and UI surfaces for issue run ledgers.
> - The benefit is that interrupted or long-running work can resume with
clearer context instead of losing the agent's last useful handoff.
## What Changed
- Added heartbeat-run liveness fields, continuation attempt tracking,
and an idempotent `0058` migration.
- Added server services and tests for run liveness, continuation
summaries, stop metadata, and activity backfill.
- Wired local and HTTP adapters to surface continuation/liveness context
through shared adapter utilities.
- Added shared constants, validators, and heartbeat types for liveness
continuation state.
- Added issue-detail UI surfaces for continuation handoffs and the run
ledger, with component tests.
- Updated agent runtime docs, heartbeat protocol docs, prompt guidance,
onboarding assets, and skills instructions to explain continuation
behavior.
- Addressed Greptile feedback by scoping document evidence by run,
excluding system continuation-summary documents from liveness evidence,
importing shared liveness types, surfacing hidden ledger run counts,
documenting bounded retry behavior, and moving run-ledger liveness
backfill off the request path.
## Verification
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/run-continuations.test.ts
server/src/__tests__/run-liveness.test.ts
server/src/__tests__/activity-service.test.ts
server/src/__tests__/documents-service.test.ts
server/src/__tests__/issue-continuation-summary.test.ts
server/src/services/heartbeat-stop-metadata.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/components/IssueContinuationHandoff.test.tsx
ui/src/components/IssueDocumentsSection.test.tsx`
- `pnpm --filter @paperclipai/db build`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/run-continuations.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts -t "treats a
plan document update"`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts -t "activity
service|treats a plan document update"`
- Remote PR checks on head `e53b1a1d`: `verify`, `e2e`, `policy`, and
Snyk all passed.
- Confirmed `public-gh/master` is an ancestor of this branch after
fetching `public-gh master`.
- Confirmed `pnpm-lock.yaml` is not included in the branch diff.
- Confirmed migration `0058_wealthy_starbolt.sql` is ordered after
`0057` and uses `IF NOT EXISTS` guards for repeat application.
- Greptile inline review threads are resolved.
## Risks
- Medium risk: this touches heartbeat execution, liveness recovery,
activity rendering, issue routes, shared contracts, docs, and UI.
- Migration risk is mitigated by additive columns/indexes and idempotent
guards.
- Run-ledger liveness backfill is now asynchronous, so the first ledger
response can briefly show historical missing liveness until the
background backfill completes.
- UI screenshot coverage is not included in this packaging pass;
validation is currently through focused component tests.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4, local tool-use coding agent with terminal, git,
GitHub connector, GitHub CLI, and Paperclip API access.
## 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
Screenshot note: no before/after screenshots were captured in this PR
packaging pass; the UI changes are covered by focused component tests
listed above.
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:01:49 -05:00
if ( normalized . continuationSummary ) {
lines . push (
"" ,
"Issue continuation summary:" ,
normalized . continuationSummary . body ,
) ;
if ( normalized . continuationSummary . bodyTruncated ) {
lines . push ( "[continuation summary truncated]" ) ;
}
}
if ( normalized . livenessContinuation ) {
const continuation = normalized . livenessContinuation ;
lines . push ( "" , "Run liveness continuation:" ) ;
if ( continuation . attempt ) {
lines . push (
` - attempt: ${ continuation . attempt } ${ continuation . maxAttempts ? ` / ${ continuation . maxAttempts } ` : "" } ` ,
) ;
}
if ( continuation . sourceRunId ) {
lines . push ( ` - source run: ${ continuation . sourceRunId } ` ) ;
}
if ( continuation . state ) {
lines . push ( ` - liveness state: ${ continuation . state } ` ) ;
}
if ( continuation . reason ) {
lines . push ( ` - reason: ${ continuation . reason } ` ) ;
}
if ( continuation . instruction ) {
lines . push ( ` - instruction: ${ continuation . instruction } ` ) ;
}
}
if ( normalized . childIssueSummaries . length > 0 ) {
lines . push ( "" , "Direct child issue summaries:" ) ;
for ( const child of normalized . childIssueSummaries ) {
const label = child . identifier ? ? child . id ? ? "unknown" ;
lines . push (
` - ${ label } ${ child . title ? ` ${ child . title } ` : "" } ${ child . status ? ` ( ${ child . status } ) ` : "" } ` ,
) ;
if ( child . summary ) {
lines . push ( ` ${ child . summary } ` ) ;
}
}
if ( normalized . childIssueSummaryTruncated ) {
lines . push ( "[child issue summaries truncated]" ) ;
}
}
2026-04-11 10:53:28 -05:00
if ( normalized . checkedOutByHarness ) {
lines . push (
"" ,
"The harness already checked out this issue for the current run." ,
"Do not call `/api/issues/{id}/checkout` again unless you intentionally switch to a different task." ,
"" ,
) ;
}
2026-04-08 08:05:35 -05:00
if ( normalized . comments . length > 0 ) {
lines . push ( "New comments in order:" ) ;
}
2026-03-28 09:55:41 -05:00
for ( const [ index , comment ] of normalized . comments . entries ( ) ) {
const authorLabel = comment . authorId
? ` ${ comment . authorType ? ? "unknown" } ${ comment . authorId } `
: comment . authorType ? ? "unknown" ;
lines . push (
` ${ index + 1 } . comment ${ comment . id ? ? "unknown" } at ${ comment . createdAt ? ? "unknown" } by ${ authorLabel } ` ,
comment . body ,
) ;
if ( comment . bodyTruncated ) {
lines . push ( "[comment body truncated]" ) ;
}
lines . push ( "" ) ;
}
return lines . join ( "\n" ) . trim ( ) ;
}
2026-02-18 14:23:16 -06:00
export function redactEnvForLogs ( env : Record < string , string > ) : Record < string , string > {
const redacted : Record < string , string > = { } ;
for ( const [ key , value ] of Object . entries ( env ) ) {
redacted [ key ] = SENSITIVE_ENV_KEY . test ( key ) ? "***REDACTED***" : value ;
}
return redacted ;
}
2026-03-28 15:42:14 -05:00
export function buildInvocationEnvForLogs (
env : Record < string , string > ,
options : {
runtimeEnv? : NodeJS.ProcessEnv | Record < string , string > ;
includeRuntimeKeys? : string [ ] ;
resolvedCommand? : string | null ;
resolvedCommandEnvKey? : string ;
} = { } ,
) : Record < string , string > {
const merged : Record < string , string > = { . . . env } ;
const runtimeEnv = options . runtimeEnv ? ? { } ;
for ( const key of options . includeRuntimeKeys ? ? [ ] ) {
if ( key in merged ) continue ;
const value = runtimeEnv [ key ] ;
if ( typeof value !== "string" || value . length === 0 ) continue ;
merged [ key ] = value ;
}
const resolvedCommand = options . resolvedCommand ? . trim ( ) ;
if ( resolvedCommand ) {
merged [ options . resolvedCommandEnvKey ? ? "PAPERCLIP_RESOLVED_COMMAND" ] = resolvedCommand ;
}
return redactEnvForLogs ( merged ) ;
}
2026-02-18 14:23:16 -06:00
export function buildPaperclipEnv ( agent : { id : string ; companyId : string } ) : Record < string , string > {
2026-03-03 13:13:47 -06:00
const resolveHostForUrl = ( rawHost : string ) : string = > {
const host = rawHost . trim ( ) ;
if ( ! host || host === "0.0.0.0" || host === "::" ) return "localhost" ;
if ( host . includes ( ":" ) && ! host . startsWith ( "[" ) && ! host . endsWith ( "]" ) ) return ` [ ${ host } ] ` ;
return host ;
} ;
2026-02-18 14:23:16 -06:00
const vars : Record < string , string > = {
PAPERCLIP_AGENT_ID : agent.id ,
PAPERCLIP_COMPANY_ID : agent.companyId ,
} ;
2026-03-03 13:13:47 -06:00
const runtimeHost = resolveHostForUrl (
process . env . PAPERCLIP_LISTEN_HOST ? ? process . env . HOST ? ? "localhost" ,
) ;
const runtimePort = process . env . PAPERCLIP_LISTEN_PORT ? ? process . env . PORT ? ? "3100" ;
const apiUrl = process . env . PAPERCLIP_API_URL ? ? ` http:// ${ runtimeHost } : ${ runtimePort } ` ;
2026-02-18 14:23:16 -06:00
vars . PAPERCLIP_API_URL = apiUrl ;
return vars ;
}
export function defaultPathForPlatform() {
if ( process . platform === "win32" ) {
return "C:\\Windows\\System32;C:\\Windows;C:\\Windows\\System32\\Wbem" ;
}
return "/usr/local/bin:/opt/homebrew/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin" ;
}
2026-03-09 21:52:06 +09:00
function windowsPathExts ( env : NodeJS.ProcessEnv ) : string [ ] {
return ( env . PATHEXT ? ? ".EXE;.CMD;.BAT;.COM" ) . split ( ";" ) . filter ( Boolean ) ;
}
async function pathExists ( candidate : string ) {
try {
2026-03-09 22:08:50 +09:00
await fs . access ( candidate , process . platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK ) ;
2026-03-09 21:52:06 +09:00
return true ;
} catch {
return false ;
}
}
async function resolveCommandPath ( command : string , cwd : string , env : NodeJS.ProcessEnv ) : Promise < string | null > {
const hasPathSeparator = command . includes ( "/" ) || command . includes ( "\\" ) ;
if ( hasPathSeparator ) {
const absolute = path . isAbsolute ( command ) ? command : path.resolve ( cwd , command ) ;
return ( await pathExists ( absolute ) ) ? absolute : null ;
}
const pathValue = env . PATH ? ? env . Path ? ? "" ;
const delimiter = process . platform === "win32" ? ";" : ":" ;
const dirs = pathValue . split ( delimiter ) . filter ( Boolean ) ;
const exts = process . platform === "win32" ? windowsPathExts ( env ) : [ "" ] ;
const hasExtension = process . platform === "win32" && path . extname ( command ) . length > 0 ;
for ( const dir of dirs ) {
const candidates =
process . platform === "win32"
? hasExtension
? [ path . join ( dir , command ) ]
: exts . map ( ( ext ) = > path . join ( dir , ` ${ command } ${ ext } ` ) )
: [ path . join ( dir , command ) ] ;
for ( const candidate of candidates ) {
if ( await pathExists ( candidate ) ) return candidate ;
}
}
return null ;
}
2026-03-28 15:42:14 -05:00
export async function resolveCommandForLogs ( command : string , cwd : string , env : NodeJS.ProcessEnv ) : Promise < string > {
return ( await resolveCommandPath ( command , cwd , env ) ) ? ? command ;
}
2026-03-09 21:52:06 +09:00
function quoteForCmd ( arg : string ) {
if ( ! arg . length ) return '""' ;
2026-03-09 22:08:50 +09:00
const escaped = arg . replace ( /"/g , '""' ) ;
2026-03-09 21:52:06 +09:00
return /[\s"&<>|^()]/ . test ( escaped ) ? ` " ${ escaped } " ` : escaped ;
}
2026-04-01 09:25:39 +02:00
function resolveWindowsCmdShell ( env : NodeJS.ProcessEnv ) : string {
const fallbackRoot = env . SystemRoot || process . env . SystemRoot || "C:\\Windows" ;
2026-04-01 14:37:46 +02:00
return path . join ( fallbackRoot , "System32" , "cmd.exe" ) ;
2026-04-01 09:25:39 +02:00
}
2026-03-09 21:52:06 +09:00
async function resolveSpawnTarget (
command : string ,
args : string [ ] ,
cwd : string ,
env : NodeJS.ProcessEnv ,
) : Promise < SpawnTarget > {
const resolved = await resolveCommandPath ( command , cwd , env ) ;
const executable = resolved ? ? command ;
if ( process . platform !== "win32" ) {
return { command : executable , args } ;
}
if ( /\.(cmd|bat)$/i . test ( executable ) ) {
2026-04-01 09:25:39 +02:00
// Always use cmd.exe for .cmd/.bat wrappers. Some environments override
// ComSpec to PowerShell, which breaks cmd-specific flags like /d /s /c.
const shell = resolveWindowsCmdShell ( env ) ;
2026-03-09 21:52:06 +09:00
const commandLine = [ quoteForCmd ( executable ) , . . . args . map ( quoteForCmd ) ] . join ( " " ) ;
return {
command : shell ,
args : [ "/d" , "/s" , "/c" , commandLine ] ,
} ;
}
return { command : executable , args } ;
}
2026-02-18 14:23:16 -06:00
export function ensurePathInEnv ( env : NodeJS.ProcessEnv ) : NodeJS . ProcessEnv {
if ( typeof env . PATH === "string" && env . PATH . length > 0 ) return env ;
if ( typeof env . Path === "string" && env . Path . length > 0 ) return env ;
return { . . . env , PATH : defaultPathForPlatform ( ) } ;
}
2026-03-03 12:29:32 -06:00
export async function ensureAbsoluteDirectory (
cwd : string ,
opts : { createIfMissing? : boolean } = { } ,
) {
2026-02-18 14:23:16 -06:00
if ( ! path . isAbsolute ( cwd ) ) {
throw new Error ( ` Working directory must be an absolute path: " ${ cwd } " ` ) ;
}
2026-03-03 12:29:32 -06:00
const assertDirectory = async ( ) = > {
const stats = await fs . stat ( cwd ) ;
if ( ! stats . isDirectory ( ) ) {
throw new Error ( ` Working directory is not a directory: " ${ cwd } " ` ) ;
}
} ;
2026-02-18 14:23:16 -06:00
try {
2026-03-03 12:29:32 -06:00
await assertDirectory ( ) ;
return ;
} catch ( err ) {
const code = ( err as NodeJS . ErrnoException ) . code ;
if ( ! opts . createIfMissing || code !== "ENOENT" ) {
if ( code === "ENOENT" ) {
throw new Error ( ` Working directory does not exist: " ${ cwd } " ` ) ;
}
throw err instanceof Error ? err : new Error ( String ( err ) ) ;
}
2026-02-18 14:23:16 -06:00
}
2026-03-03 12:29:32 -06:00
try {
await fs . mkdir ( cwd , { recursive : true } ) ;
await assertDirectory ( ) ;
} catch ( err ) {
const reason = err instanceof Error ? err.message : String ( err ) ;
throw new Error ( ` Could not create working directory " ${ cwd } ": ${ reason } ` ) ;
2026-02-18 14:23:16 -06:00
}
}
2026-03-12 15:57:37 -05:00
export async function resolvePaperclipSkillsDir (
moduleDir : string ,
additionalCandidates : string [ ] = [ ] ,
) : Promise < string | null > {
const candidates = [
. . . PAPERCLIP_SKILL_ROOT_RELATIVE_CANDIDATES . map ( ( relativePath ) = > path . resolve ( moduleDir , relativePath ) ) ,
. . . additionalCandidates . map ( ( candidate ) = > path . resolve ( candidate ) ) ,
] ;
2026-03-12 15:44:44 -05:00
const seenRoots = new Set < string > ( ) ;
2026-03-12 15:57:37 -05:00
for ( const root of candidates ) {
2026-03-12 15:44:44 -05:00
if ( seenRoots . has ( root ) ) continue ;
seenRoots . add ( root ) ;
const isDirectory = await fs . stat ( root ) . then ( ( stats ) = > stats . isDirectory ( ) ) . catch ( ( ) = > false ) ;
2026-03-12 15:57:37 -05:00
if ( isDirectory ) return root ;
}
2026-03-12 15:44:44 -05:00
2026-03-12 15:57:37 -05:00
return null ;
}
2026-03-12 15:44:44 -05:00
2026-03-12 15:57:37 -05:00
export async function listPaperclipSkillEntries (
moduleDir : string ,
additionalCandidates : string [ ] = [ ] ,
) : Promise < PaperclipSkillEntry [ ] > {
const root = await resolvePaperclipSkillsDir ( moduleDir , additionalCandidates ) ;
if ( ! root ) return [ ] ;
try {
const entries = await fs . readdir ( root , { withFileTypes : true } ) ;
return entries
. filter ( ( entry ) = > entry . isDirectory ( ) )
. map ( ( entry ) = > ( {
2026-03-16 18:27:20 -05:00
key : ` paperclipai/paperclip/ ${ entry . name } ` ,
runtimeName : entry.name ,
2026-03-12 15:44:44 -05:00
source : path.join ( root , entry . name ) ,
2026-03-15 07:05:01 -05:00
required : true ,
requiredReason : "Bundled Paperclip skills are always available for local adapters." ,
2026-03-12 15:57:37 -05:00
} ) ) ;
} catch {
return [ ] ;
2026-03-12 15:44:44 -05:00
}
}
2026-03-18 14:21:50 -05:00
export async function readInstalledSkillTargets ( skillsHome : string ) : Promise < Map < string , InstalledSkillTarget > > {
const entries = await fs . readdir ( skillsHome , { withFileTypes : true } ) . catch ( ( ) = > [ ] ) ;
const out = new Map < string , InstalledSkillTarget > ( ) ;
for ( const entry of entries ) {
const fullPath = path . join ( skillsHome , entry . name ) ;
const linkedPath = entry . isSymbolicLink ( ) ? await fs . readlink ( fullPath ) . catch ( ( ) = > null ) : null ;
out . set ( entry . name , resolveInstalledEntryTarget ( skillsHome , entry . name , entry , linkedPath ) ) ;
}
return out ;
}
export function buildPersistentSkillSnapshot (
options : PersistentSkillSnapshotOptions ,
) : AdapterSkillSnapshot {
const {
adapterType ,
availableEntries ,
desiredSkills ,
installed ,
skillsHome ,
locationLabel ,
installedDetail ,
missingDetail ,
externalConflictDetail ,
externalDetail ,
} = options ;
const availableByKey = new Map ( availableEntries . map ( ( entry ) = > [ entry . key , entry ] ) ) ;
const desiredSet = new Set ( desiredSkills ) ;
const entries : AdapterSkillEntry [ ] = [ ] ;
const warnings = [ . . . ( options . warnings ? ? [ ] ) ] ;
for ( const available of availableEntries ) {
const installedEntry = installed . get ( available . runtimeName ) ? ? null ;
const desired = desiredSet . has ( available . key ) ;
let state : AdapterSkillEntry [ "state" ] = "available" ;
let managed = false ;
let detail : string | null = null ;
if ( installedEntry ? . targetPath === available . source ) {
managed = true ;
state = desired ? "installed" : "stale" ;
detail = installedDetail ? ? null ;
} else if ( installedEntry ) {
state = "external" ;
detail = desired ? externalConflictDetail : externalDetail ;
} else if ( desired ) {
state = "missing" ;
detail = missingDetail ;
}
entries . push ( {
key : available.key ,
runtimeName : available.runtimeName ,
desired ,
managed ,
state ,
sourcePath : available.source ,
targetPath : path.join ( skillsHome , available . runtimeName ) ,
detail ,
required : Boolean ( available . required ) ,
requiredReason : available.requiredReason ? ? null ,
. . . buildManagedSkillOrigin ( available ) ,
} ) ;
}
for ( const desiredSkill of desiredSkills ) {
if ( availableByKey . has ( desiredSkill ) ) continue ;
warnings . push ( ` Desired skill " ${ desiredSkill } " is not available from the Paperclip skills directory. ` ) ;
entries . push ( {
key : desiredSkill ,
runtimeName : null ,
desired : true ,
managed : true ,
state : "missing" ,
sourcePath : null ,
targetPath : null ,
detail : "Paperclip cannot find this skill in the local runtime skills directory." ,
origin : "external_unknown" ,
originLabel : "External or unavailable" ,
readOnly : false ,
} ) ;
}
for ( const [ name , installedEntry ] of installed . entries ( ) ) {
if ( availableEntries . some ( ( entry ) = > entry . runtimeName === name ) ) continue ;
entries . push ( {
key : name ,
runtimeName : name ,
desired : false ,
managed : false ,
state : "external" ,
origin : "user_installed" ,
originLabel : "User-installed" ,
locationLabel : skillLocationLabel ( locationLabel ) ,
readOnly : true ,
sourcePath : null ,
targetPath : installedEntry.targetPath ? ? path . join ( skillsHome , name ) ,
detail : externalDetail ,
} ) ;
}
entries . sort ( ( left , right ) = > left . key . localeCompare ( right . key ) ) ;
return {
adapterType ,
supported : true ,
mode : "persistent" ,
desiredSkills ,
entries ,
warnings ,
} ;
}
2026-03-15 07:05:01 -05:00
function normalizeConfiguredPaperclipRuntimeSkills ( value : unknown ) : PaperclipSkillEntry [ ] {
if ( ! Array . isArray ( value ) ) return [ ] ;
const out : PaperclipSkillEntry [ ] = [ ] ;
for ( const rawEntry of value ) {
const entry = parseObject ( rawEntry ) ;
2026-03-16 18:27:20 -05:00
const key = asString ( entry . key , asString ( entry . name , "" ) ) . trim ( ) ;
const runtimeName = asString ( entry . runtimeName , asString ( entry . name , "" ) ) . trim ( ) ;
2026-03-15 07:05:01 -05:00
const source = asString ( entry . source , "" ) . trim ( ) ;
2026-03-16 18:27:20 -05:00
if ( ! key || ! runtimeName || ! source ) continue ;
2026-03-15 07:05:01 -05:00
out . push ( {
2026-03-16 18:27:20 -05:00
key ,
runtimeName ,
2026-03-15 07:05:01 -05:00
source ,
required : asBoolean ( entry . required , false ) ,
requiredReason :
typeof entry . requiredReason === "string" && entry . requiredReason . trim ( ) . length > 0
? entry . requiredReason . trim ( )
: null ,
} ) ;
}
return out ;
}
export async function readPaperclipRuntimeSkillEntries (
config : Record < string , unknown > ,
moduleDir : string ,
additionalCandidates : string [ ] = [ ] ,
) : Promise < PaperclipSkillEntry [ ] > {
const configuredEntries = normalizeConfiguredPaperclipRuntimeSkills ( config . paperclipRuntimeSkills ) ;
if ( configuredEntries . length > 0 ) return configuredEntries ;
return listPaperclipSkillEntries ( moduleDir , additionalCandidates ) ;
}
2026-03-12 15:44:44 -05:00
export async function readPaperclipSkillMarkdown (
moduleDir : string ,
2026-03-16 18:27:20 -05:00
skillKey : string ,
2026-03-12 15:44:44 -05:00
) : Promise < string | null > {
2026-03-16 18:27:20 -05:00
const normalized = skillKey . trim ( ) . toLowerCase ( ) ;
2026-03-12 15:44:44 -05:00
if ( ! normalized ) return null ;
const entries = await listPaperclipSkillEntries ( moduleDir ) ;
2026-03-16 18:27:20 -05:00
const match = entries . find ( ( entry ) = > entry . key === normalized ) ;
2026-03-12 15:44:44 -05:00
if ( ! match ) return null ;
try {
return await fs . readFile ( path . join ( match . source , "SKILL.md" ) , "utf8" ) ;
} catch {
return null ;
}
}
2026-03-13 22:49:42 -05:00
export function readPaperclipSkillSyncPreference ( config : Record < string , unknown > ) : {
explicit : boolean ;
desiredSkills : string [ ] ;
} {
const raw = config . paperclipSkillSync ;
if ( typeof raw !== "object" || raw === null || Array . isArray ( raw ) ) {
return { explicit : false , desiredSkills : [ ] } ;
}
const syncConfig = raw as Record < string , unknown > ;
const desiredValues = syncConfig . desiredSkills ;
const desired = Array . isArray ( desiredValues )
? desiredValues
. filter ( ( value ) : value is string = > typeof value === "string" )
. map ( ( value ) = > value . trim ( ) )
. filter ( Boolean )
: [ ] ;
return {
explicit : Object.prototype.hasOwnProperty.call ( raw , "desiredSkills" ) ,
desiredSkills : Array.from ( new Set ( desired ) ) ,
} ;
}
2026-03-16 19:13:00 -05:00
function canonicalizeDesiredPaperclipSkillReference (
reference : string ,
availableEntries : Array < { key : string ; runtimeName? : string | null } > ,
) : string {
const normalizedReference = reference . trim ( ) . toLowerCase ( ) ;
if ( ! normalizedReference ) return "" ;
const exactKey = availableEntries . find ( ( entry ) = > entry . key . trim ( ) . toLowerCase ( ) === normalizedReference ) ;
if ( exactKey ) return exactKey . key ;
const byRuntimeName = availableEntries . filter ( ( entry ) = >
typeof entry . runtimeName === "string" && entry . runtimeName . trim ( ) . toLowerCase ( ) === normalizedReference ,
) ;
if ( byRuntimeName . length === 1 ) return byRuntimeName [ 0 ] ! . key ;
const slugMatches = availableEntries . filter ( ( entry ) = >
entry . key . trim ( ) . toLowerCase ( ) . split ( "/" ) . pop ( ) === normalizedReference ,
) ;
if ( slugMatches . length === 1 ) return slugMatches [ 0 ] ! . key ;
return normalizedReference ;
}
2026-03-15 07:05:01 -05:00
export function resolvePaperclipDesiredSkillNames (
config : Record < string , unknown > ,
2026-03-16 19:13:00 -05:00
availableEntries : Array < { key : string ; runtimeName? : string | null ; required? : boolean } > ,
2026-03-15 07:05:01 -05:00
) : string [ ] {
const preference = readPaperclipSkillSyncPreference ( config ) ;
const requiredSkills = availableEntries
. filter ( ( entry ) = > entry . required )
2026-03-16 18:27:20 -05:00
. map ( ( entry ) = > entry . key ) ;
2026-03-15 07:05:01 -05:00
if ( ! preference . explicit ) {
return Array . from ( new Set ( requiredSkills ) ) ;
}
2026-03-16 19:13:00 -05:00
const desiredSkills = preference . desiredSkills
. map ( ( reference ) = > canonicalizeDesiredPaperclipSkillReference ( reference , availableEntries ) )
. filter ( Boolean ) ;
return Array . from ( new Set ( [ . . . requiredSkills , . . . desiredSkills ] ) ) ;
2026-03-15 07:05:01 -05:00
}
2026-03-13 22:49:42 -05:00
export function writePaperclipSkillSyncPreference (
config : Record < string , unknown > ,
desiredSkills : string [ ] ,
) : Record < string , unknown > {
const next = { . . . config } ;
const raw = next . paperclipSkillSync ;
const current =
typeof raw === "object" && raw !== null && ! Array . isArray ( raw )
? { . . . ( raw as Record < string , unknown > ) }
: { } ;
current . desiredSkills = Array . from (
new Set (
desiredSkills
. map ( ( value ) = > value . trim ( ) )
. filter ( Boolean ) ,
) ,
) ;
next . paperclipSkillSync = current ;
return next ;
}
2026-03-12 15:44:44 -05:00
export async function ensurePaperclipSkillSymlink (
source : string ,
target : string ,
linkSkill : ( source : string , target : string ) = > Promise < void > = ( linkSource , linkTarget ) = >
fs . symlink ( linkSource , linkTarget ) ,
) : Promise < "created" | "repaired" | "skipped" > {
const existing = await fs . lstat ( target ) . catch ( ( ) = > null ) ;
if ( ! existing ) {
await linkSkill ( source , target ) ;
return "created" ;
}
if ( ! existing . isSymbolicLink ( ) ) {
return "skipped" ;
}
const linkedPath = await fs . readlink ( target ) . catch ( ( ) = > null ) ;
if ( ! linkedPath ) return "skipped" ;
const resolvedLinkedPath = path . resolve ( path . dirname ( target ) , linkedPath ) ;
if ( resolvedLinkedPath === source ) {
return "skipped" ;
}
const linkedPathExists = await fs . stat ( resolvedLinkedPath ) . then ( ( ) = > true ) . catch ( ( ) = > false ) ;
if ( linkedPathExists ) {
return "skipped" ;
}
await fs . unlink ( target ) ;
await linkSkill ( source , target ) ;
return "repaired" ;
}
2026-03-12 15:57:37 -05:00
export async function removeMaintainerOnlySkillSymlinks (
skillsHome : string ,
allowedSkillNames : Iterable < string > ,
) : Promise < string [ ] > {
const allowed = new Set ( Array . from ( allowedSkillNames ) ) ;
try {
const entries = await fs . readdir ( skillsHome , { withFileTypes : true } ) ;
const removed : string [ ] = [ ] ;
for ( const entry of entries ) {
if ( allowed . has ( entry . name ) ) continue ;
const target = path . join ( skillsHome , entry . name ) ;
const existing = await fs . lstat ( target ) . catch ( ( ) = > null ) ;
if ( ! existing ? . isSymbolicLink ( ) ) continue ;
const linkedPath = await fs . readlink ( target ) . catch ( ( ) = > null ) ;
if ( ! linkedPath ) continue ;
const resolvedLinkedPath = path . isAbsolute ( linkedPath )
? linkedPath
: path . resolve ( path . dirname ( target ) , linkedPath ) ;
if (
! isMaintainerOnlySkillTarget ( linkedPath ) &&
! isMaintainerOnlySkillTarget ( resolvedLinkedPath )
) {
continue ;
}
await fs . unlink ( target ) ;
removed . push ( entry . name ) ;
}
return removed ;
} catch {
return [ ] ;
}
}
2026-02-18 14:23:16 -06:00
export async function ensureCommandResolvable ( command : string , cwd : string , env : NodeJS.ProcessEnv ) {
2026-03-09 21:52:06 +09:00
const resolved = await resolveCommandPath ( command , cwd , env ) ;
if ( resolved ) return ;
if ( command . includes ( "/" ) || command . includes ( "\\" ) ) {
2026-02-18 14:23:16 -06:00
const absolute = path . isAbsolute ( command ) ? command : path.resolve ( cwd , command ) ;
2026-03-09 21:52:06 +09:00
throw new Error ( ` Command is not executable: " ${ command } " (resolved: " ${ absolute } ") ` ) ;
2026-02-18 14:23:16 -06:00
}
throw new Error ( ` Command not found in PATH: " ${ command } " ` ) ;
}
export async function runChildProcess (
runId : string ,
command : string ,
args : string [ ] ,
opts : {
cwd : string ;
env : Record < string , string > ;
timeoutSec : number ;
graceSec : number ;
onLog : ( stream : "stdout" | "stderr" , chunk : string ) = > Promise < void > ;
onLogError ? : ( err : unknown , runId : string , message : string ) = > void ;
2026-04-10 22:26:21 -05:00
onSpawn ? : ( meta : { pid : number ; processGroupId : number | null ; startedAt : string } ) = > Promise < void > ;
2026-04-20 10:38:57 -05:00
terminalResultCleanup? : TerminalResultCleanupOptions ;
2026-02-18 15:29:24 -06:00
stdin? : string ;
2026-02-18 14:23:16 -06:00
} ,
) : Promise < RunProcessResult > {
const onLogError = opts . onLogError ? ? ( ( err , id , msg ) = > console . warn ( { err , runId : id } , msg ) ) ;
return new Promise < RunProcessResult > ( ( resolve , reject ) = > {
2026-03-10 12:01:46 +00:00
const rawMerged : NodeJS.ProcessEnv = { . . . process . env , . . . opts . env } ;
// Strip Claude Code nesting-guard env vars so spawned `claude` processes
// don't refuse to start with "cannot be launched inside another session".
// These vars leak in when the Paperclip server itself is started from
// within a Claude Code session (e.g. `npx paperclipai run` in a terminal
// owned by Claude Code) or when cron inherits a contaminated shell env.
2026-03-10 07:24:48 -05:00
const CLAUDE_CODE_NESTING_VARS = [
"CLAUDECODE" ,
"CLAUDE_CODE_ENTRYPOINT" ,
"CLAUDE_CODE_SESSION" ,
"CLAUDE_CODE_PARENT_SESSION" ,
] as const ;
for ( const key of CLAUDE_CODE_NESTING_VARS ) {
delete rawMerged [ key ] ;
}
2026-03-10 12:01:46 +00:00
const mergedEnv = ensurePathInEnv ( rawMerged ) ;
2026-03-09 21:52:06 +09:00
void resolveSpawnTarget ( command , args , opts . cwd , mergedEnv )
. then ( ( target ) = > {
const child = spawn ( target . command , target . args , {
cwd : opts.cwd ,
env : mergedEnv ,
2026-04-10 22:26:21 -05:00
detached : process.platform !== "win32" ,
2026-03-09 21:52:06 +09:00
shell : false ,
stdio : [ opts . stdin != null ? "pipe" : "ignore" , "pipe" , "pipe" ] ,
} ) as ChildProcessWithEvents ;
2026-03-19 11:20:36 -05:00
const startedAt = new Date ( ) . toISOString ( ) ;
2026-04-10 22:26:21 -05:00
const processGroupId = resolveProcessGroupId ( child ) ;
2026-03-09 21:52:06 +09:00
2026-04-08 09:47:02 -05:00
const spawnPersistPromise =
typeof child . pid === "number" && child . pid > 0 && opts . onSpawn
2026-04-10 22:26:21 -05:00
? opts . onSpawn ( { pid : child.pid , processGroupId , startedAt } ) . catch ( ( err ) = > {
2026-04-08 09:47:02 -05:00
onLogError ( err , runId , "failed to record child process metadata" ) ;
} )
: Promise . resolve ( ) ;
2026-03-19 11:20:36 -05:00
2026-04-10 22:26:21 -05:00
runningProcesses . set ( runId , { child , graceSec : opts.graceSec , processGroupId } ) ;
2026-03-09 21:52:06 +09:00
let timedOut = false ;
let stdout = "" ;
let stderr = "" ;
let logChain : Promise < void > = Promise . resolve ( ) ;
2026-04-20 10:38:57 -05:00
let childExited = false ;
let terminalResultSeen = false ;
let terminalCleanupStarted = false ;
let terminalCleanupTimer : NodeJS.Timeout | null = null ;
let terminalCleanupKillTimer : NodeJS.Timeout | null = null ;
let terminalResultStdoutScanOffset = 0 ;
let terminalResultStderrScanOffset = 0 ;
const clearTerminalCleanupTimers = ( ) = > {
if ( terminalCleanupTimer ) clearTimeout ( terminalCleanupTimer ) ;
if ( terminalCleanupKillTimer ) clearTimeout ( terminalCleanupKillTimer ) ;
terminalCleanupTimer = null ;
terminalCleanupKillTimer = null ;
} ;
const maybeArmTerminalResultCleanup = ( ) = > {
const terminalCleanup = opts . terminalResultCleanup ;
if ( ! terminalCleanup || terminalCleanupStarted || timedOut ) return ;
if ( ! terminalResultSeen ) {
const stdoutStart = Math . max ( 0 , terminalResultStdoutScanOffset - TERMINAL_RESULT_SCAN_OVERLAP_CHARS ) ;
const stderrStart = Math . max ( 0 , terminalResultStderrScanOffset - TERMINAL_RESULT_SCAN_OVERLAP_CHARS ) ;
const scanOutput = {
stdout : stdout.slice ( stdoutStart ) ,
stderr : stderr.slice ( stderrStart ) ,
} ;
terminalResultStdoutScanOffset = stdout . length ;
terminalResultStderrScanOffset = stderr . length ;
if ( scanOutput . stdout . length === 0 && scanOutput . stderr . length === 0 ) return ;
try {
terminalResultSeen = terminalCleanup . hasTerminalResult ( scanOutput ) ;
} catch ( err ) {
onLogError ( err , runId , "failed to inspect terminal adapter output" ) ;
}
}
if ( ! terminalResultSeen || ! childExited ) return ;
if ( terminalCleanupTimer ) return ;
const graceMs = Math . max ( 0 , terminalCleanup . graceMs ? ? 5 _000 ) ;
terminalCleanupTimer = setTimeout ( ( ) = > {
terminalCleanupTimer = null ;
if ( terminalCleanupStarted || timedOut ) return ;
terminalCleanupStarted = true ;
signalRunningProcess ( { child , processGroupId } , "SIGTERM" ) ;
terminalCleanupKillTimer = setTimeout ( ( ) = > {
terminalCleanupKillTimer = null ;
signalRunningProcess ( { child , processGroupId } , "SIGKILL" ) ;
} , Math . max ( 1 , opts . graceSec ) * 1000 ) ;
} , graceMs ) ;
} ;
2026-03-09 21:52:06 +09:00
const timeout =
opts . timeoutSec > 0
? setTimeout ( ( ) = > {
timedOut = true ;
2026-04-20 10:38:57 -05:00
clearTerminalCleanupTimers ( ) ;
2026-04-10 22:26:21 -05:00
signalRunningProcess ( { child , processGroupId } , "SIGTERM" ) ;
2026-03-09 21:52:06 +09:00
setTimeout ( ( ) = > {
2026-04-10 22:26:21 -05:00
signalRunningProcess ( { child , processGroupId } , "SIGKILL" ) ;
2026-03-09 21:52:06 +09:00
} , Math . max ( 1 , opts . graceSec ) * 1000 ) ;
} , opts . timeoutSec * 1000 )
: null ;
child . stdout ? . on ( "data" , ( chunk : unknown ) = > {
[codex] Improve agent runtime recovery and governance (#4086)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The heartbeat runtime, agent import path, and agent configuration
defaults determine whether work is dispatched safely and predictably.
> - Several accumulated fixes all touched agent execution recovery, wake
routing, import behavior, and runtime concurrency defaults.
> - Those changes need to land together so the heartbeat service and
agent creation defaults stay internally consistent.
> - This pull request groups the runtime/governance changes from the
split branch into one standalone branch.
> - The benefit is safer recovery for stranded runs, bounded high-volume
reads, imported-agent approval correctness, skill-template support, and
a clearer default concurrency policy.
## What Changed
- Fixed stranded continuation recovery so successful automatic retries
are requeued instead of incorrectly blocking the issue.
- Bounded high-volume issue/log reads across issue, heartbeat, agent,
project, and workspace paths.
- Fixed imported-agent approval and instruction-path permission
handling.
- Quarantined seeded worktree execution state during worktree
provisioning.
- Queued approval follow-up wakes and hardened SQL_ASCII heartbeat
output handling.
- Added reusable agent instruction templates for hiring flows.
- Set the default max concurrent agent runs to five and updated related
UI/tests/docs.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run server/src/__tests__/company-portability.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/heartbeat-comment-wake-batching.test.ts
server/src/__tests__/heartbeat-list.test.ts
server/src/__tests__/issues-service.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
packages/adapter-utils/src/server-utils.test.ts
ui/src/lib/new-agent-runtime-config.test.ts`
- Split integration check: merged this branch first, followed by the
other [PAP-1614](/PAP/issues/PAP-1614) branches, with no merge
conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Medium risk: touches heartbeat recovery, queueing, and issue list
bounds in central runtime paths.
- Imported-agent and concurrency default behavior changes may affect
existing automation that assumes one-at-a-time default runs.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## 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>
2026-04-20 06:19:48 -05:00
const readable = child . stdout ;
if ( ! readable ) return ;
readable . pause ( ) ;
2026-03-09 21:52:06 +09:00
const text = String ( chunk ) ;
stdout = appendWithCap ( stdout , text ) ;
2026-04-20 10:38:57 -05:00
maybeArmTerminalResultCleanup ( ) ;
2026-03-09 21:52:06 +09:00
logChain = logChain
. then ( ( ) = > opts . onLog ( "stdout" , text ) )
[codex] Improve agent runtime recovery and governance (#4086)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The heartbeat runtime, agent import path, and agent configuration
defaults determine whether work is dispatched safely and predictably.
> - Several accumulated fixes all touched agent execution recovery, wake
routing, import behavior, and runtime concurrency defaults.
> - Those changes need to land together so the heartbeat service and
agent creation defaults stay internally consistent.
> - This pull request groups the runtime/governance changes from the
split branch into one standalone branch.
> - The benefit is safer recovery for stranded runs, bounded high-volume
reads, imported-agent approval correctness, skill-template support, and
a clearer default concurrency policy.
## What Changed
- Fixed stranded continuation recovery so successful automatic retries
are requeued instead of incorrectly blocking the issue.
- Bounded high-volume issue/log reads across issue, heartbeat, agent,
project, and workspace paths.
- Fixed imported-agent approval and instruction-path permission
handling.
- Quarantined seeded worktree execution state during worktree
provisioning.
- Queued approval follow-up wakes and hardened SQL_ASCII heartbeat
output handling.
- Added reusable agent instruction templates for hiring flows.
- Set the default max concurrent agent runs to five and updated related
UI/tests/docs.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run server/src/__tests__/company-portability.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/heartbeat-comment-wake-batching.test.ts
server/src/__tests__/heartbeat-list.test.ts
server/src/__tests__/issues-service.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
packages/adapter-utils/src/server-utils.test.ts
ui/src/lib/new-agent-runtime-config.test.ts`
- Split integration check: merged this branch first, followed by the
other [PAP-1614](/PAP/issues/PAP-1614) branches, with no merge
conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Medium risk: touches heartbeat recovery, queueing, and issue list
bounds in central runtime paths.
- Imported-agent and concurrency default behavior changes may affect
existing automation that assumes one-at-a-time default runs.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## 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>
2026-04-20 06:19:48 -05:00
. catch ( ( err ) = > onLogError ( err , runId , "failed to append stdout log chunk" ) )
2026-04-20 10:38:57 -05:00
. finally ( ( ) = > {
maybeArmTerminalResultCleanup ( ) ;
resumeReadable ( readable ) ;
} ) ;
2026-03-09 21:52:06 +09:00
} ) ;
child . stderr ? . on ( "data" , ( chunk : unknown ) = > {
[codex] Improve agent runtime recovery and governance (#4086)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The heartbeat runtime, agent import path, and agent configuration
defaults determine whether work is dispatched safely and predictably.
> - Several accumulated fixes all touched agent execution recovery, wake
routing, import behavior, and runtime concurrency defaults.
> - Those changes need to land together so the heartbeat service and
agent creation defaults stay internally consistent.
> - This pull request groups the runtime/governance changes from the
split branch into one standalone branch.
> - The benefit is safer recovery for stranded runs, bounded high-volume
reads, imported-agent approval correctness, skill-template support, and
a clearer default concurrency policy.
## What Changed
- Fixed stranded continuation recovery so successful automatic retries
are requeued instead of incorrectly blocking the issue.
- Bounded high-volume issue/log reads across issue, heartbeat, agent,
project, and workspace paths.
- Fixed imported-agent approval and instruction-path permission
handling.
- Quarantined seeded worktree execution state during worktree
provisioning.
- Queued approval follow-up wakes and hardened SQL_ASCII heartbeat
output handling.
- Added reusable agent instruction templates for hiring flows.
- Set the default max concurrent agent runs to five and updated related
UI/tests/docs.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run server/src/__tests__/company-portability.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/heartbeat-comment-wake-batching.test.ts
server/src/__tests__/heartbeat-list.test.ts
server/src/__tests__/issues-service.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
packages/adapter-utils/src/server-utils.test.ts
ui/src/lib/new-agent-runtime-config.test.ts`
- Split integration check: merged this branch first, followed by the
other [PAP-1614](/PAP/issues/PAP-1614) branches, with no merge
conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Medium risk: touches heartbeat recovery, queueing, and issue list
bounds in central runtime paths.
- Imported-agent and concurrency default behavior changes may affect
existing automation that assumes one-at-a-time default runs.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## 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>
2026-04-20 06:19:48 -05:00
const readable = child . stderr ;
if ( ! readable ) return ;
readable . pause ( ) ;
2026-03-09 21:52:06 +09:00
const text = String ( chunk ) ;
stderr = appendWithCap ( stderr , text ) ;
2026-04-20 10:38:57 -05:00
maybeArmTerminalResultCleanup ( ) ;
2026-03-09 21:52:06 +09:00
logChain = logChain
. then ( ( ) = > opts . onLog ( "stderr" , text ) )
[codex] Improve agent runtime recovery and governance (#4086)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The heartbeat runtime, agent import path, and agent configuration
defaults determine whether work is dispatched safely and predictably.
> - Several accumulated fixes all touched agent execution recovery, wake
routing, import behavior, and runtime concurrency defaults.
> - Those changes need to land together so the heartbeat service and
agent creation defaults stay internally consistent.
> - This pull request groups the runtime/governance changes from the
split branch into one standalone branch.
> - The benefit is safer recovery for stranded runs, bounded high-volume
reads, imported-agent approval correctness, skill-template support, and
a clearer default concurrency policy.
## What Changed
- Fixed stranded continuation recovery so successful automatic retries
are requeued instead of incorrectly blocking the issue.
- Bounded high-volume issue/log reads across issue, heartbeat, agent,
project, and workspace paths.
- Fixed imported-agent approval and instruction-path permission
handling.
- Quarantined seeded worktree execution state during worktree
provisioning.
- Queued approval follow-up wakes and hardened SQL_ASCII heartbeat
output handling.
- Added reusable agent instruction templates for hiring flows.
- Set the default max concurrent agent runs to five and updated related
UI/tests/docs.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run server/src/__tests__/company-portability.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/heartbeat-comment-wake-batching.test.ts
server/src/__tests__/heartbeat-list.test.ts
server/src/__tests__/issues-service.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
packages/adapter-utils/src/server-utils.test.ts
ui/src/lib/new-agent-runtime-config.test.ts`
- Split integration check: merged this branch first, followed by the
other [PAP-1614](/PAP/issues/PAP-1614) branches, with no merge
conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Medium risk: touches heartbeat recovery, queueing, and issue list
bounds in central runtime paths.
- Imported-agent and concurrency default behavior changes may affect
existing automation that assumes one-at-a-time default runs.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## 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>
2026-04-20 06:19:48 -05:00
. catch ( ( err ) = > onLogError ( err , runId , "failed to append stderr log chunk" ) )
2026-04-20 10:38:57 -05:00
. finally ( ( ) = > {
maybeArmTerminalResultCleanup ( ) ;
resumeReadable ( readable ) ;
} ) ;
2026-03-09 21:52:06 +09:00
} ) ;
2026-04-08 09:47:02 -05:00
const stdin = child . stdin ;
if ( opts . stdin != null && stdin ) {
void spawnPersistPromise . finally ( ( ) = > {
if ( child . killed || stdin . destroyed ) return ;
stdin . write ( opts . stdin as string ) ;
stdin . end ( ) ;
} ) ;
}
2026-03-09 21:52:06 +09:00
child . on ( "error" , ( err : Error ) = > {
if ( timeout ) clearTimeout ( timeout ) ;
2026-04-20 10:38:57 -05:00
clearTerminalCleanupTimers ( ) ;
2026-03-09 21:52:06 +09:00
runningProcesses . delete ( runId ) ;
const errno = ( err as NodeJS . ErrnoException ) . code ;
const pathValue = mergedEnv . PATH ? ? mergedEnv . Path ? ? "" ;
const msg =
errno === "ENOENT"
? ` Failed to start command " ${ command } " in " ${ opts . cwd } ". Verify adapter command, working directory, and PATH ( ${ pathValue } ). `
: ` Failed to start command " ${ command } " in " ${ opts . cwd } ": ${ err . message } ` ;
reject ( new Error ( msg ) ) ;
} ) ;
2026-02-18 15:29:24 -06:00
2026-04-20 10:38:57 -05:00
child . on ( "exit" , ( ) = > {
childExited = true ;
maybeArmTerminalResultCleanup ( ) ;
} ) ;
2026-03-09 21:52:06 +09:00
child . on ( "close" , ( code : number | null , signal : NodeJS.Signals | null ) = > {
if ( timeout ) clearTimeout ( timeout ) ;
2026-04-20 10:38:57 -05:00
clearTerminalCleanupTimers ( ) ;
2026-03-09 21:52:06 +09:00
runningProcesses . delete ( runId ) ;
void logChain . finally ( ( ) = > {
resolve ( {
exitCode : code ,
signal ,
timedOut ,
stdout ,
stderr ,
2026-03-19 11:20:36 -05:00
pid : child.pid ? ? null ,
startedAt ,
2026-03-09 21:52:06 +09:00
} ) ;
} ) ;
2026-02-18 14:23:16 -06:00
} ) ;
2026-03-09 21:52:06 +09:00
} )
. catch ( reject ) ;
2026-02-18 14:23:16 -06:00
} ) ;
}