2026-03-21 12:20:48 -05:00
|
|
|
import { randomUUID } from "node:crypto";
|
2026-04-02 11:38:57 -05:00
|
|
|
import { eq } from "drizzle-orm";
|
2026-03-21 12:20:48 -05:00
|
|
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
2026-04-04 13:56:04 -05:00
|
|
|
import { sql } from "drizzle-orm";
|
2026-03-21 12:20:48 -05:00
|
|
|
import {
|
|
|
|
|
activityLog,
|
|
|
|
|
agents,
|
|
|
|
|
companies,
|
|
|
|
|
createDb,
|
2026-03-28 22:21:24 -05:00
|
|
|
executionWorkspaces,
|
[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
|
|
|
goals,
|
[codex] Fix stale issue execution run locks (#4258)
## Thinking Path
> - Paperclip is a control plane for AI-agent companies, so issue
checkout and execution ownership are core safety contracts.
> - The affected subsystem is the issue service and route layer that
gates agent writes by `checkoutRunId` and `executionRunId`.
> - PAP-1982 exposed a stale-lock failure mode where a terminal
heartbeat run could leave `executionRunId` pinned after checkout
ownership had moved or been cleared.
> - That stale execution lock could reject legitimate
PATCH/comment/release requests from the rightful assignee after a
harness restart.
> - This pull request centralizes terminal-run cleanup, applies it
before ownership-gated writes, and adds a board-only recovery endpoint
for operator intervention.
> - The benefit is that crashed or terminal runs no longer strand issues
behind stale execution locks, while live execution locks still block
conflicting writes.
## What Changed
- Added `issueService.clearExecutionRunIfTerminal()` to atomically lock
the issue/run rows and clear terminal or missing execution-run locks.
- Reused stale execution-lock cleanup from checkout,
`assertCheckoutOwner()`, and `release()`.
- Allowed the same assigned agent/current run to adopt an unowned
`in_progress` checkout after stale execution-lock cleanup.
- Updated release to clear `executionRunId`, `executionAgentNameKey`,
and `executionLockedAt`.
- Added board-only `POST /api/issues/:id/admin/force-release` with
company access checks, optional `clearAssignee=true`, and
`issue.admin_force_release` audit logging.
- Added embedded Postgres service tests and route integration tests for
stale-lock recovery, release behavior, and admin force-release
authorization/audit behavior.
- Documented the new force-release API in `doc/SPEC-implementation.md`.
## Verification
- `pnpm vitest run server/src/__tests__/issues-service.test.ts
server/src/__tests__/issue-stale-execution-lock-routes.test.ts` passed.
- `pnpm vitest run
server/src/__tests__/issue-stale-execution-lock-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/issue-telemetry-routes.test.ts` passed.
- `pnpm -r typecheck` passed.
- `pnpm build` passed.
- `git diff --check` passed.
- `pnpm lint` could not run because this repo has no `lint` command.
- Full `pnpm test:run` completed with 4 failures in existing route
suites: `approval-routes-idempotency.test.ts` (2),
`issue-comment-reopen-routes.test.ts` (1), and
`issue-telemetry-routes.test.ts` (1). Those same files pass when run
isolated and when run together with the new stale-lock route test, so
this appears to be a whole-suite ordering/mock-isolation issue outside
this patch path.
## Risks
- Medium: this changes ownership-gated write behavior. The new adoption
path is limited to the current run, the current assignee, `in_progress`
issues, and rows with no checkout owner after terminal-lock cleanup.
- Low: the admin force-release endpoint is board-only and
company-scoped, but misuse can intentionally clear a live lock. It
writes an audit event with prior lock IDs.
- No schema or migration changes.
> 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 coding agent (`gpt-5`), agentic coding with
terminal/tool use and local test execution.
## 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
2026-04-22 10:43:38 -05:00
|
|
|
heartbeatRuns,
|
2026-03-30 14:08:44 -05:00
|
|
|
instanceSettings,
|
2026-03-21 12:20:48 -05:00
|
|
|
issueComments,
|
2026-03-26 08:19:16 -05:00
|
|
|
issueInboxArchives,
|
2026-04-04 13:56:04 -05:00
|
|
|
issueRelations,
|
2026-03-21 12:20:48 -05:00
|
|
|
issues,
|
2026-03-30 14:08:44 -05:00
|
|
|
projectWorkspaces,
|
2026-03-28 22:21:24 -05:00
|
|
|
projects,
|
2026-03-21 12:20:48 -05:00
|
|
|
} from "@paperclipai/db";
|
2026-03-26 11:04:07 -05:00
|
|
|
import {
|
|
|
|
|
getEmbeddedPostgresTestSupport,
|
|
|
|
|
startEmbeddedPostgresTestDatabase,
|
|
|
|
|
} from "./helpers/embedded-postgres.js";
|
2026-03-30 14:08:44 -05:00
|
|
|
import { instanceSettingsService } from "../services/instance-settings.ts";
|
[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
|
|
|
import { clampIssueListLimit, ISSUE_LIST_MAX_LIMIT, issueService } from "../services/issues.ts";
|
[codex] Split backend control-plane QoL slice (#4700)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies, so
backend task ownership, recovery, review visibility, and company-scoped
limits need to stay enforceable without UI-only coupling.
> - Closed PR #4692 bundled those backend changes with UI workflow,
docs, skills, workflow, and lockfile churn.
> - PAP-2694 asks for a clean backend/control-plane slice from that
closed branch.
> - This branch starts from current `master` and mines only the `cli`,
`packages/db`, `packages/shared`, and `server` contracts/tests needed
for the backend behavior.
> - It explicitly excludes UI workflow/performance work,
`.github/workflows/pr.yml`, `pnpm-lock.yaml`, docs, skills,
package-script, adapter UI build-config, and perf fixture script
changes; the only UI files are fixture/test updates required by the
tightened shared `Company` contract.
> - The benefit is a smaller reviewable PR that preserves the
control-plane fixes while staying under Greptile s 100-file review
limit.
## What Changed
- Added company-scoped attachment-size limits through DB
schema/migrations, shared company portability contracts, CLI
import/export coverage, and server attachment upload enforcement.
- Added productivity review service/API behavior for no-comment streak,
long-active, and high-churn review issues, including request-depth
clamping and issue summary exposure.
- Hardened issue ownership and recovery/control-plane paths: peer-agent
mutation denial, issue tree pause/resume behavior, stranded recovery
origins, and related activity/test coverage.
- Preserved related backend contract updates for routine timestamp
variables and managed agent instruction bundles because they live in
shared/server contracts from the source branch.
- Addressed Greptile feedback by making `Company.attachmentMaxBytes`
non-optional, simplifying review request-depth clamping, fixing the
migration final newline, and enforcing the process-level attachment cap
as the final ceiling for uploads.
- Added minimal company fixtures needed for repo-wide typecheck/build
and kept the PR to 66 changed files with forbidden/non-slice paths
excluded.
## Verification
- `pnpm install --frozen-lockfile`
- `git diff --check origin/master..HEAD`
- `git diff --name-only origin/master..HEAD | wc -l` -> 66 files
- `git diff --name-only origin/master..HEAD -- .github/workflows/pr.yml
pnpm-lock.yaml package.json doc skills .agents scripts
packages/adapters` -> no output
- `pnpm exec vitest run --config vitest.config.ts
packages/shared/src/validators/issue.test.ts
packages/shared/src/routine-variables.test.ts
packages/shared/src/adapter-types.test.ts
cli/src/__tests__/company-import-export-e2e.test.ts
cli/src/__tests__/company.test.ts
server/src/__tests__/productivity-review-service.test.ts
server/src/__tests__/issue-tree-control-service.test.ts
server/src/__tests__/issue-tree-control-routes.test.ts
server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts
server/src/__tests__/issue-attachment-routes.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/issues-service.test.ts` -> 12 files, 147 tests
passed
- `pnpm exec vitest run --config vitest.config.ts
cli/src/__tests__/company-delete.test.ts
cli/src/__tests__/company-import-export-e2e.test.ts
server/src/__tests__/productivity-review-service.test.ts` -> 3 files, 18
tests passed
- `pnpm exec vitest run --config vitest.config.ts
server/src/__tests__/issue-attachment-routes.test.ts` -> 1 file, 6 tests
passed
- `pnpm --filter @paperclipai/db typecheck && pnpm --filter
@paperclipai/shared typecheck && pnpm --filter @paperclipai/server
typecheck && pnpm --filter paperclipai typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck && pnpm --filter
@paperclipai/ui build`
## Risks
- Includes migrations `0073_shiny_salo.sql` and
`0074_striped_genesis.sql`; merge ordering matters if another PR adds
migrations first.
- This is intentionally backend-only apart from fixture/test updates
forced by shared type correctness; UI affordances from PR #4692 are not
present here and should land in separate UI slices.
- The worktree install emitted plugin SDK bin-link warnings for unbuilt
plugin packages, but the targeted tests and package typechecks completed
successfully.
> 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 coding agent, tool-enabled terminal/GitHub
workflow. Exact runtime context window was not exposed by the 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-28 16:46:45 -05:00
|
|
|
import { buildProjectMentionHref, MAX_ISSUE_REQUEST_DEPTH } from "@paperclipai/shared";
|
2026-03-21 12:20:48 -05:00
|
|
|
|
2026-03-26 11:04:07 -05:00
|
|
|
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
|
|
|
|
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
2026-03-21 12:20:48 -05:00
|
|
|
|
[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
|
|
|
describe("issue list limit helpers", () => {
|
|
|
|
|
it("clamps untrusted issue-list limits to the server maximum", () => {
|
|
|
|
|
expect(clampIssueListLimit(0)).toBe(1);
|
|
|
|
|
expect(clampIssueListLimit(25.9)).toBe(25);
|
|
|
|
|
expect(clampIssueListLimit(ISSUE_LIST_MAX_LIMIT + 10)).toBe(ISSUE_LIST_MAX_LIMIT);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-04 13:56:04 -05:00
|
|
|
async function ensureIssueRelationsTable(db: ReturnType<typeof createDb>) {
|
|
|
|
|
await db.execute(sql.raw(`
|
|
|
|
|
CREATE TABLE IF NOT EXISTS "issue_relations" (
|
|
|
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
|
"company_id" uuid NOT NULL,
|
|
|
|
|
"issue_id" uuid NOT NULL,
|
|
|
|
|
"related_issue_id" uuid NOT NULL,
|
|
|
|
|
"type" text NOT NULL,
|
|
|
|
|
"created_by_agent_id" uuid,
|
|
|
|
|
"created_by_user_id" text,
|
|
|
|
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
|
|
|
"updated_at" timestamptz NOT NULL DEFAULT now()
|
|
|
|
|
);
|
|
|
|
|
`));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 11:04:07 -05:00
|
|
|
if (!embeddedPostgresSupport.supported) {
|
|
|
|
|
console.warn(
|
|
|
|
|
`Skipping embedded Postgres issue service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
|
|
|
|
|
);
|
2026-03-21 12:20:48 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 11:04:07 -05:00
|
|
|
describeEmbeddedPostgres("issueService.list participantAgentId", () => {
|
2026-03-21 12:20:48 -05:00
|
|
|
let db!: ReturnType<typeof createDb>;
|
|
|
|
|
let svc!: ReturnType<typeof issueService>;
|
2026-03-26 11:04:07 -05:00
|
|
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
2026-03-21 12:20:48 -05:00
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
2026-03-26 11:04:07 -05:00
|
|
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-issues-service-");
|
|
|
|
|
db = createDb(tempDb.connectionString);
|
2026-03-21 12:20:48 -05:00
|
|
|
svc = issueService(db);
|
2026-04-04 13:56:04 -05:00
|
|
|
await ensureIssueRelationsTable(db);
|
2026-03-21 12:20:48 -05:00
|
|
|
}, 20_000);
|
|
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
await db.delete(issueComments);
|
2026-04-04 13:56:04 -05:00
|
|
|
await db.delete(issueRelations);
|
2026-03-26 08:19:16 -05:00
|
|
|
await db.delete(issueInboxArchives);
|
2026-03-21 12:20:48 -05:00
|
|
|
await db.delete(activityLog);
|
|
|
|
|
await db.delete(issues);
|
2026-03-28 22:21:24 -05:00
|
|
|
await db.delete(executionWorkspaces);
|
2026-03-30 14:08:44 -05:00
|
|
|
await db.delete(projectWorkspaces);
|
2026-03-28 22:21:24 -05:00
|
|
|
await db.delete(projects);
|
[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
|
|
|
await db.delete(goals);
|
2026-03-21 12:20:48 -05:00
|
|
|
await db.delete(agents);
|
2026-03-30 14:08:44 -05:00
|
|
|
await db.delete(instanceSettings);
|
2026-03-21 12:20:48 -05:00
|
|
|
await db.delete(companies);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
2026-03-26 11:04:07 -05:00
|
|
|
await tempDb?.cleanup();
|
2026-03-21 12:20:48 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns issues an agent participated in across the supported signals", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const agentId = randomUUID();
|
|
|
|
|
const otherAgentId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(agents).values([
|
|
|
|
|
{
|
|
|
|
|
id: agentId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "CodexCoder",
|
|
|
|
|
role: "engineer",
|
|
|
|
|
status: "active",
|
|
|
|
|
adapterType: "codex_local",
|
|
|
|
|
adapterConfig: {},
|
|
|
|
|
runtimeConfig: {},
|
|
|
|
|
permissions: {},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: otherAgentId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "OtherAgent",
|
|
|
|
|
role: "engineer",
|
|
|
|
|
status: "active",
|
|
|
|
|
adapterType: "codex_local",
|
|
|
|
|
adapterConfig: {},
|
|
|
|
|
runtimeConfig: {},
|
|
|
|
|
permissions: {},
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const assignedIssueId = randomUUID();
|
|
|
|
|
const createdIssueId = randomUUID();
|
|
|
|
|
const commentedIssueId = randomUUID();
|
|
|
|
|
const activityIssueId = randomUUID();
|
|
|
|
|
const excludedIssueId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{
|
|
|
|
|
id: assignedIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Assigned issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
assigneeAgentId: agentId,
|
|
|
|
|
createdByAgentId: otherAgentId,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: createdIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Created issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
createdByAgentId: agentId,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: commentedIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Commented issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
createdByAgentId: otherAgentId,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: activityIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Activity issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
createdByAgentId: otherAgentId,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: excludedIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Excluded issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
createdByAgentId: otherAgentId,
|
|
|
|
|
assigneeAgentId: otherAgentId,
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await db.insert(issueComments).values({
|
|
|
|
|
companyId,
|
|
|
|
|
issueId: commentedIssueId,
|
|
|
|
|
authorAgentId: agentId,
|
|
|
|
|
body: "Investigating this issue.",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(activityLog).values({
|
|
|
|
|
companyId,
|
|
|
|
|
actorType: "agent",
|
|
|
|
|
actorId: agentId,
|
|
|
|
|
action: "issue.updated",
|
|
|
|
|
entityType: "issue",
|
|
|
|
|
entityId: activityIssueId,
|
|
|
|
|
agentId,
|
|
|
|
|
details: { changed: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await svc.list(companyId, { participantAgentId: agentId });
|
|
|
|
|
const resultIds = new Set(result.map((issue) => issue.id));
|
|
|
|
|
|
|
|
|
|
expect(resultIds).toEqual(new Set([
|
|
|
|
|
assignedIssueId,
|
|
|
|
|
createdIssueId,
|
|
|
|
|
commentedIssueId,
|
|
|
|
|
activityIssueId,
|
|
|
|
|
]));
|
|
|
|
|
expect(resultIds.has(excludedIssueId)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("combines participation filtering with search", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const agentId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(agents).values({
|
|
|
|
|
id: agentId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "CodexCoder",
|
|
|
|
|
role: "engineer",
|
|
|
|
|
status: "active",
|
|
|
|
|
adapterType: "codex_local",
|
|
|
|
|
adapterConfig: {},
|
|
|
|
|
runtimeConfig: {},
|
|
|
|
|
permissions: {},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const matchedIssueId = randomUUID();
|
|
|
|
|
const otherIssueId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{
|
|
|
|
|
id: matchedIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Invoice reconciliation",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
createdByAgentId: agentId,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: otherIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Weekly planning",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
createdByAgentId: agentId,
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await svc.list(companyId, {
|
|
|
|
|
participantAgentId: agentId,
|
|
|
|
|
q: "invoice",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.map((issue) => issue.id)).toEqual([matchedIssueId]);
|
|
|
|
|
});
|
2026-03-26 08:19:16 -05:00
|
|
|
|
2026-04-06 20:30:50 -05:00
|
|
|
it("applies result limits to issue search", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const exactIdentifierId = randomUUID();
|
|
|
|
|
const titleMatchId = randomUUID();
|
|
|
|
|
const descriptionMatchId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{
|
|
|
|
|
id: exactIdentifierId,
|
|
|
|
|
companyId,
|
|
|
|
|
issueNumber: 42,
|
|
|
|
|
identifier: "PAP-42",
|
|
|
|
|
title: "Completely unrelated",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: titleMatchId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Search ranking issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: descriptionMatchId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Another item",
|
|
|
|
|
description: "Contains the search keyword",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await svc.list(companyId, {
|
|
|
|
|
q: "search",
|
|
|
|
|
limit: 2,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.map((issue) => issue.id)).toEqual([titleMatchId, descriptionMatchId]);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-11 06:57:49 -05:00
|
|
|
it("ranks comment matches ahead of description-only matches", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const commentMatchId = randomUUID();
|
|
|
|
|
const descriptionMatchId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{
|
|
|
|
|
id: commentMatchId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Comment match",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: descriptionMatchId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Description match",
|
|
|
|
|
description: "Contains pull/3303 in the description",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await db.insert(issueComments).values({
|
|
|
|
|
companyId,
|
|
|
|
|
issueId: commentMatchId,
|
|
|
|
|
body: "Reference: https://github.com/paperclipai/paperclip/pull/3303",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await svc.list(companyId, {
|
|
|
|
|
q: "pull/3303",
|
|
|
|
|
limit: 2,
|
|
|
|
|
includeRoutineExecutions: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.map((issue) => issue.id)).toEqual([commentMatchId, descriptionMatchId]);
|
|
|
|
|
});
|
|
|
|
|
|
[codex] Improve issue thread review flow (#4381)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Issue detail is where operators coordinate review, approvals, and
follow-up work with active runs
> - That thread UI needs to surface blockers, descendants, review
handoffs, and reply ergonomics clearly enough for humans to guide agent
work
> - Several small gaps in the issue-thread flow were making review and
navigation clunkier than necessary
> - This pull request improves the reply composer, descendant/blocker
presentation, interaction folding, and review-request handoff plumbing
together as one cohesive issue-thread workflow slice
> - The benefit is a cleaner operator review loop without changing the
broader task model
## What Changed
- restored and refined the floating reply composer behavior in the issue
thread
- folded expired confirmation interactions and improved post-submit
thread scrolling behavior
- surfaced descendant issue context and inline blocker/paused-assignee
notices on the issue detail view
- tightened large-board first paint behavior in `IssuesList`
- added loose review-request handoffs through the issue
execution-policy/update path and covered them with tests
## Verification
- `pnpm vitest run ui/src/pages/IssueDetail.test.tsx`
- `pnpm vitest run server/src/__tests__/issues-service.test.ts
server/src/__tests__/issue-execution-policy.test.ts`
- `pnpm exec vitest run --project @paperclipai/ui
ui/src/components/IssueChatThread.test.tsx
ui/src/components/IssueProperties.test.tsx
ui/src/components/IssuesList.test.tsx ui/src/lib/issue-tree.test.ts
ui/src/api/issues.test.ts`
- `pnpm exec vitest run --project @paperclipai/adapter-utils
packages/adapter-utils/src/server-utils.test.ts`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/issue-comment-reopen-routes.test.ts -t "coerces
executor handoff patches into workflow-controlled review wakes|wakes the
return assignee with execution_changes_requested"`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/issue-execution-policy.test.ts
server/src/__tests__/issues-service.test.ts`
## Visual Evidence
- UI layout changes are covered by the focused issue-thread component
and issue-detail tests listed above. Browser screenshots were not
attachable from this automated greploop environment, so reviewers should
use the running preview for final visual confirmation.
## Risks
- Moderate UI-flow risk: these changes touch the issue detail experience
in multiple spots, so regressions would most likely show up as
thread-layout quirks or incorrect review-handoff behavior
> 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-based coding agent with tool use and code execution
in the Codex CLI environment
## 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 or documented the visual verification path
- [ ] 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-24 08:02:45 -05:00
|
|
|
it("filters issue lists to the full descendant tree for a root issue", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const rootId = randomUUID();
|
|
|
|
|
const childId = randomUUID();
|
|
|
|
|
const grandchildId = randomUUID();
|
|
|
|
|
const siblingId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{
|
|
|
|
|
id: rootId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Root",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: childId,
|
|
|
|
|
companyId,
|
|
|
|
|
parentId: rootId,
|
|
|
|
|
title: "Child",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: grandchildId,
|
|
|
|
|
companyId,
|
|
|
|
|
parentId: childId,
|
|
|
|
|
title: "Grandchild",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: siblingId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Sibling",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await svc.list(companyId, { descendantOf: rootId });
|
|
|
|
|
|
|
|
|
|
expect(new Set(result.map((issue) => issue.id))).toEqual(new Set([childId, grandchildId]));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("combines descendant filtering with search", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const rootId = randomUUID();
|
|
|
|
|
const childId = randomUUID();
|
|
|
|
|
const grandchildId = randomUUID();
|
|
|
|
|
const outsideMatchId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{
|
|
|
|
|
id: rootId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Root",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: childId,
|
|
|
|
|
companyId,
|
|
|
|
|
parentId: rootId,
|
|
|
|
|
title: "Relevant parent",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: grandchildId,
|
|
|
|
|
companyId,
|
|
|
|
|
parentId: childId,
|
|
|
|
|
title: "Needle grandchild",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: outsideMatchId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Needle outside",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await svc.list(companyId, { descendantOf: rootId, q: "needle" });
|
|
|
|
|
|
|
|
|
|
expect(result.map((issue) => issue.id)).toEqual([grandchildId]);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-02 09:11:49 -05:00
|
|
|
it("accepts issue identifiers through getById", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const issueId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: "PAP",
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: issueId,
|
|
|
|
|
companyId,
|
|
|
|
|
issueNumber: 1064,
|
|
|
|
|
identifier: "PAP-1064",
|
|
|
|
|
title: "Feedback votes error",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
createdByUserId: "user-1",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const issue = await svc.getById("PAP-1064");
|
|
|
|
|
|
|
|
|
|
expect(issue).toEqual(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
id: issueId,
|
|
|
|
|
identifier: "PAP-1064",
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns null instead of throwing for malformed non-uuid issue refs", async () => {
|
|
|
|
|
await expect(svc.getById("not-a-uuid")).resolves.toBeNull();
|
|
|
|
|
});
|
2026-03-28 22:21:24 -05:00
|
|
|
it("filters issues by execution workspace id", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const projectId = randomUUID();
|
|
|
|
|
const targetWorkspaceId = randomUUID();
|
|
|
|
|
const otherWorkspaceId = randomUUID();
|
|
|
|
|
const linkedIssueId = randomUUID();
|
|
|
|
|
const otherLinkedIssueId = randomUUID();
|
|
|
|
|
const unlinkedIssueId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projects).values({
|
|
|
|
|
id: projectId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "Workspace project",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(executionWorkspaces).values([
|
|
|
|
|
{
|
|
|
|
|
id: targetWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
mode: "shared_workspace",
|
|
|
|
|
strategyType: "project_primary",
|
|
|
|
|
name: "Target workspace",
|
|
|
|
|
status: "active",
|
|
|
|
|
providerType: "local_fs",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: otherWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
mode: "shared_workspace",
|
|
|
|
|
strategyType: "project_primary",
|
|
|
|
|
name: "Other workspace",
|
|
|
|
|
status: "active",
|
|
|
|
|
providerType: "local_fs",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{
|
|
|
|
|
id: linkedIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
title: "Linked issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
executionWorkspaceId: targetWorkspaceId,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: otherLinkedIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
title: "Other linked issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
executionWorkspaceId: otherWorkspaceId,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: unlinkedIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
title: "Unlinked issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await svc.list(companyId, { executionWorkspaceId: targetWorkspaceId });
|
|
|
|
|
|
|
|
|
|
expect(result.map((issue) => issue.id)).toEqual([linkedIssueId]);
|
|
|
|
|
});
|
|
|
|
|
|
[codex] Harden heartbeat scheduling and runtime controls (#4223)
## Thinking Path
> - Paperclip orchestrates AI agents through issue checkout, heartbeat
runs, routines, and auditable control-plane state
> - The runtime path has to recover from lost local processes, transient
adapter failures, blocked dependencies, and routine coalescing without
stranding work
> - The existing branch carried several reliability fixes across
heartbeat scheduling, issue runtime controls, routine dispatch, and
operator-facing run state
> - These changes belong together because they share backend contracts,
migrations, and runtime status semantics
> - This pull request groups the control-plane/runtime slice so it can
merge independently from board UI polish and adapter sandbox work
> - The benefit is safer heartbeat recovery, clearer runtime controls,
and more predictable recurring execution behavior
## What Changed
- Adds bounded heartbeat retry scheduling, scheduled retry state, and
Codex transient failure recovery handling.
- Tightens heartbeat process recovery, blocker wake behavior, issue
comment wake handling, routine dispatch coalescing, and
activity/dashboard bounds.
- Adds runtime-control MCP tools and Paperclip skill docs for issue
workspace runtime management.
- Adds migrations `0061_lively_thor_girl.sql` and
`0062_routine_run_dispatch_fingerprint.sql`.
- Surfaces retry state in run ledger/agent UI and keeps related shared
types synchronized.
## Verification
- `pnpm exec vitest run
server/src/__tests__/heartbeat-retry-scheduling.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/routines-service.test.ts`
- `pnpm exec vitest run src/tools.test.ts` from `packages/mcp-server`
## Risks
- Medium risk: this touches heartbeat recovery and routine dispatch,
which are central execution paths.
- Migration order matters if split branches land out of order: merge
this PR before branches that assume the new runtime/routine fields.
- Runtime retry behavior should be watched in CI and in local operator
smoke tests because it changes how transient failures are resumed.
> 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-based coding agent runtime, shell/git tool use
enabled. Exact hosted model build and context window are not exposed in
this Paperclip heartbeat environment.
## 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
- [ ] 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
2026-04-21 12:24:11 -05:00
|
|
|
it("filters issues by generic workspace id across execution and project workspace links", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const projectId = randomUUID();
|
|
|
|
|
const projectWorkspaceId = randomUUID();
|
|
|
|
|
const executionWorkspaceId = randomUUID();
|
|
|
|
|
const executionLinkedIssueId = randomUUID();
|
|
|
|
|
const projectLinkedIssueId = randomUUID();
|
|
|
|
|
const otherIssueId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projects).values({
|
|
|
|
|
id: projectId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "Workspace project",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projectWorkspaces).values({
|
|
|
|
|
id: projectWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
name: "Feature workspace",
|
|
|
|
|
sourceType: "local_path",
|
|
|
|
|
visibility: "default",
|
|
|
|
|
isPrimary: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(executionWorkspaces).values({
|
|
|
|
|
id: executionWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
strategyType: "git_worktree",
|
|
|
|
|
name: "Execution workspace",
|
|
|
|
|
status: "active",
|
|
|
|
|
providerType: "git_worktree",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{
|
|
|
|
|
id: executionLinkedIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
title: "Execution linked issue",
|
|
|
|
|
status: "done",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
executionWorkspaceId,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: projectLinkedIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
title: "Project linked issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: otherIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
title: "Other issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const executionResult = await svc.list(companyId, { workspaceId: executionWorkspaceId });
|
|
|
|
|
const projectResult = await svc.list(companyId, { workspaceId: projectWorkspaceId });
|
|
|
|
|
|
|
|
|
|
expect(executionResult.map((issue) => issue.id)).toEqual([executionLinkedIssueId]);
|
|
|
|
|
expect(projectResult.map((issue) => issue.id).sort()).toEqual([executionLinkedIssueId, projectLinkedIssueId].sort());
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-26 08:19:16 -05:00
|
|
|
it("hides archived inbox issues until new external activity arrives", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const userId = "user-1";
|
|
|
|
|
const otherUserId = "user-2";
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const visibleIssueId = randomUUID();
|
|
|
|
|
const archivedIssueId = randomUUID();
|
|
|
|
|
const resurfacedIssueId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{
|
|
|
|
|
id: visibleIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Visible issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
createdByUserId: userId,
|
|
|
|
|
createdAt: new Date("2026-03-26T10:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-26T10:00:00.000Z"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: archivedIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Archived issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
createdByUserId: userId,
|
|
|
|
|
createdAt: new Date("2026-03-26T11:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-26T11:00:00.000Z"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: resurfacedIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Resurfaced issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
createdByUserId: userId,
|
|
|
|
|
createdAt: new Date("2026-03-26T12:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-26T12:00:00.000Z"),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
2026-04-02 09:11:49 -05:00
|
|
|
await svc.archiveInbox(companyId, archivedIssueId, userId, new Date("2026-03-26T12:30:00.000Z"));
|
|
|
|
|
await svc.archiveInbox(companyId, resurfacedIssueId, userId, new Date("2026-03-26T13:00:00.000Z"));
|
2026-03-26 08:19:16 -05:00
|
|
|
|
|
|
|
|
await db.insert(issueComments).values({
|
|
|
|
|
companyId,
|
|
|
|
|
issueId: resurfacedIssueId,
|
|
|
|
|
authorUserId: otherUserId,
|
|
|
|
|
body: "This should bring the issue back into Mine.",
|
|
|
|
|
createdAt: new Date("2026-03-26T13:30:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-26T13:30:00.000Z"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const archivedFiltered = await svc.list(companyId, {
|
|
|
|
|
touchedByUserId: userId,
|
|
|
|
|
inboxArchivedByUserId: userId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(archivedFiltered.map((issue) => issue.id)).toEqual([
|
|
|
|
|
resurfacedIssueId,
|
|
|
|
|
visibleIssueId,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await svc.unarchiveInbox(companyId, archivedIssueId, userId);
|
|
|
|
|
|
|
|
|
|
const afterUnarchive = await svc.list(companyId, {
|
|
|
|
|
touchedByUserId: userId,
|
|
|
|
|
inboxArchivedByUserId: userId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(new Set(afterUnarchive.map((issue) => issue.id))).toEqual(new Set([
|
|
|
|
|
visibleIssueId,
|
|
|
|
|
archivedIssueId,
|
|
|
|
|
resurfacedIssueId,
|
|
|
|
|
]));
|
|
|
|
|
});
|
2026-04-02 11:38:57 -05:00
|
|
|
|
|
|
|
|
it("resurfaces archived issue when status/updatedAt changes after archiving", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const userId = "user-1";
|
|
|
|
|
const otherUserId = "user-2";
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const issueId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: issueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Issue with old comment then status change",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
createdByUserId: userId,
|
|
|
|
|
createdAt: new Date("2026-03-26T10:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-26T10:00:00.000Z"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Old external comment before archiving
|
|
|
|
|
await db.insert(issueComments).values({
|
|
|
|
|
companyId,
|
|
|
|
|
issueId,
|
|
|
|
|
authorUserId: otherUserId,
|
|
|
|
|
body: "Old comment before archive",
|
|
|
|
|
createdAt: new Date("2026-03-26T11:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-26T11:00:00.000Z"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Archive after seeing the comment
|
|
|
|
|
await svc.archiveInbox(
|
|
|
|
|
companyId,
|
|
|
|
|
issueId,
|
|
|
|
|
userId,
|
|
|
|
|
new Date("2026-03-26T12:00:00.000Z"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Verify it's archived
|
|
|
|
|
const afterArchive = await svc.list(companyId, {
|
|
|
|
|
touchedByUserId: userId,
|
|
|
|
|
inboxArchivedByUserId: userId,
|
|
|
|
|
});
|
|
|
|
|
expect(afterArchive.map((i) => i.id)).not.toContain(issueId);
|
|
|
|
|
|
|
|
|
|
// Status/work update changes updatedAt (no new comment)
|
|
|
|
|
await db
|
|
|
|
|
.update(issues)
|
|
|
|
|
.set({
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
updatedAt: new Date("2026-03-26T13:00:00.000Z"),
|
|
|
|
|
})
|
|
|
|
|
.where(eq(issues.id, issueId));
|
|
|
|
|
|
|
|
|
|
// Should resurface because updatedAt > archivedAt
|
|
|
|
|
const afterUpdate = await svc.list(companyId, {
|
|
|
|
|
touchedByUserId: userId,
|
|
|
|
|
inboxArchivedByUserId: userId,
|
|
|
|
|
});
|
|
|
|
|
expect(afterUpdate.map((i) => i.id)).toContain(issueId);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("sorts and exposes last activity from comments and non-local issue activity logs", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const olderIssueId = randomUUID();
|
|
|
|
|
const commentIssueId = randomUUID();
|
|
|
|
|
const activityIssueId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{
|
|
|
|
|
id: olderIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Older issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
updatedAt: new Date("2026-03-26T10:00:00.000Z"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: commentIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Comment activity issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
updatedAt: new Date("2026-03-26T10:00:00.000Z"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: activityIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Logged activity issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
updatedAt: new Date("2026-03-26T10:00:00.000Z"),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await db.insert(issueComments).values({
|
|
|
|
|
companyId,
|
|
|
|
|
issueId: commentIssueId,
|
|
|
|
|
body: "New comment without touching issue.updatedAt",
|
|
|
|
|
createdAt: new Date("2026-03-26T11:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-26T11:00:00.000Z"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(activityLog).values([
|
|
|
|
|
{
|
|
|
|
|
companyId,
|
|
|
|
|
actorType: "system",
|
|
|
|
|
actorId: "system",
|
|
|
|
|
action: "issue.document_updated",
|
|
|
|
|
entityType: "issue",
|
|
|
|
|
entityId: activityIssueId,
|
|
|
|
|
createdAt: new Date("2026-03-26T12:00:00.000Z"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
companyId,
|
|
|
|
|
actorType: "user",
|
|
|
|
|
actorId: "user-1",
|
|
|
|
|
action: "issue.read_marked",
|
|
|
|
|
entityType: "issue",
|
|
|
|
|
entityId: olderIssueId,
|
|
|
|
|
createdAt: new Date("2026-03-26T13:00:00.000Z"),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await svc.list(companyId, {});
|
|
|
|
|
|
|
|
|
|
expect(result.map((issue) => issue.id)).toEqual([
|
|
|
|
|
activityIssueId,
|
|
|
|
|
commentIssueId,
|
|
|
|
|
olderIssueId,
|
|
|
|
|
]);
|
|
|
|
|
expect(result.find((issue) => issue.id === activityIssueId)?.lastActivityAt?.toISOString()).toBe(
|
|
|
|
|
"2026-03-26T12:00:00.000Z",
|
|
|
|
|
);
|
|
|
|
|
expect(result.find((issue) => issue.id === commentIssueId)?.lastActivityAt?.toISOString()).toBe(
|
|
|
|
|
"2026-03-26T11:00:00.000Z",
|
|
|
|
|
);
|
|
|
|
|
expect(result.find((issue) => issue.id === olderIssueId)?.lastActivityAt?.toISOString()).toBe(
|
|
|
|
|
"2026-03-26T10:00:00.000Z",
|
|
|
|
|
);
|
|
|
|
|
});
|
Sync/master post pap1497 followups 2026 04 15 (#3779)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The board depends on issue, inbox, cost, and company-skill surfaces
to stay accurate and fast while agents are actively working
> - The PAP-1497 follow-up branch exposed a few rough edges in those
surfaces: stale active-run state on completed issues, missing creator
filters, oversized issue payload scans, and placeholder issue-route
parsing
> - Those gaps make the control plane harder to trust because operators
can see misleading run state, miss the right subset of work, or pay
extra query/render cost on large issue records
> - This pull request tightens those follow-ups across server and UI
code, and adds regression coverage for the affected paths
> - The benefit is a more reliable issue workflow, safer high-volume
cost aggregation, and clearer board/operator navigation
## What Changed
- Added the `v2026.415.0` release changelog entry.
- Fixed stale issue-run presentation after completion and reused the
shared issue-path parser so literal route placeholders no longer become
issue links.
- Added creator filters to the Issues page and Inbox, including
persisted filter-state normalization and regression coverage.
- Bounded issue detail/list project-mention scans and trimmed large
issue-list payload fields to keep issue reads lighter.
- Hardened company-skill list projection and cost/finance aggregation so
large markdown blobs and large summed values do not leak into list
responses or overflow 32-bit casts.
- Added targeted server/UI regression tests for company skills,
costs/finance, issue mention scanning, creator filters, inbox
normalization, and issue reference parsing.
## Verification
- `pnpm exec vitest run
server/src/__tests__/company-skills-service.test.ts
server/src/__tests__/costs-service.test.ts
server/src/__tests__/issues-goal-context-routes.test.ts
server/src/__tests__/issues-service.test.ts ui/src/lib/inbox.test.ts
ui/src/lib/issue-filters.test.ts ui/src/lib/issue-reference.test.ts`
- `gh pr checks 3779`
Current pass set on the PR head: `policy`, `verify`, `e2e`,
`security/snyk (cryppadotta)`, `Greptile Review`
## Risks
- Creator filter options are derived from the currently loaded
issue/agent data, so very sparse result sets may not surface every
historical creator until they appear in the active dataset.
- Cost/finance aggregate casts now use `double precision`; that removes
the current overflow risk, but future schema changes should keep
large-value aggregation behavior under review.
- Issue detail mention scanning now skips comment-body scans on the
detail route, so any consumer that relied on comment-only project
mentions there would need to fetch them separately.
## Model Used
- OpenAI Codex, GPT-5-based coding agent with terminal tool use and
local code execution in the Paperclip workspace. Exact internal model
ID/context-window exposure is not surfaced in this session.
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 21:13:56 -05:00
|
|
|
|
2026-04-26 16:23:53 -07:00
|
|
|
it("paginates earlier comments in descending order from an anchor comment", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const issueId = randomUUID();
|
|
|
|
|
const firstCommentId = randomUUID();
|
|
|
|
|
const anchorCommentId = randomUUID();
|
|
|
|
|
const latestCommentId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: issueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Paged comments issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issueComments).values([
|
|
|
|
|
{
|
|
|
|
|
id: firstCommentId,
|
|
|
|
|
companyId,
|
|
|
|
|
issueId,
|
|
|
|
|
body: "First comment",
|
|
|
|
|
createdAt: new Date("2026-03-26T10:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-26T10:00:00.000Z"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: anchorCommentId,
|
|
|
|
|
companyId,
|
|
|
|
|
issueId,
|
|
|
|
|
body: "Anchor comment",
|
|
|
|
|
createdAt: new Date("2026-03-26T11:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-26T11:00:00.000Z"),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: latestCommentId,
|
|
|
|
|
companyId,
|
|
|
|
|
issueId,
|
|
|
|
|
body: "Latest comment",
|
|
|
|
|
createdAt: new Date("2026-03-26T12:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-26T12:00:00.000Z"),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const comments = await svc.listComments(issueId, {
|
|
|
|
|
afterCommentId: anchorCommentId,
|
|
|
|
|
order: "desc",
|
|
|
|
|
limit: 50,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(comments.map((comment) => comment.id)).toEqual([firstCommentId]);
|
|
|
|
|
});
|
|
|
|
|
|
Present ordered sub-issues as a workflow checklist (#4523)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Operators use issue detail pages and child issue lists to understand
multi-step execution plans.
> - Ordered sub-issues currently read like a flat table, so dependency
chains and current next steps are harder to scan.
> - The branch work adds a workflow-oriented presentation for child
issues without changing the single-assignee task model.
> - This pull request makes ordered sub-issues read more like a progress
checklist while preserving normal issue list controls.
> - The benefit is that operators can see completed steps, active work,
blocked follow-ups, and dependency order at a glance.
## What Changed
- Added workflow sorting utilities and tests for dependency-aware child
issue ordering.
- Added sub-issue progress summary, checklist numbering, current-step
affordances, blocker context, and done-state de-emphasis in the issue
list UI.
- Wired issue detail sub-issue panels to use the workflow sort/progress
checklist presentation.
- Updated issue service behavior/tests for child issue ordering inputs
used by the UI.
- Added a Storybook visual review fixture and screenshot helper for the
sub-issue workflow checklist surface.
## Verification
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/issues-service.test.ts
ui/src/components/IssueRow.test.tsx
ui/src/components/IssuesList.test.tsx ui/src/pages/IssueDetail.test.tsx
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/workflow-sort.test.ts`
- Result: 6 test files passed, 55 tests passed, 34 embedded Postgres
issue-service tests skipped because `@embedded-postgres/darwin-x64` is
unavailable on this host.
- Visual review: generated Storybook screenshots from the existing local
Storybook server on port 6006 with `node
scripts/screenshot-subissues.mjs /tmp/pap-2189-subissues-screens
http://localhost:6006`.
- Screenshot artifacts:
- Desktop dark: 
- Desktop light: 
- Mobile dark: 
- Mobile light: 
- Local Storybook note: starting a second Storybook process selected
port 6008 because 6006 was occupied, then Vite failed with an esbuild
host/binary version mismatch (`0.25.12` host vs `0.27.3` binary). The
already-running Storybook server on 6006 served the fixture successfully
for screenshots.
## Risks
- Medium UI risk: the issue list now has additional sub-issue-specific
visual states, so dense lists should be checked for spacing and
scanability.
- Low ordering risk: workflow sorting is covered by focused unit tests,
but unusual dependency topologies may still need reviewer attention.
- No migration risk: this PR does not add database migrations or touch
`pnpm-lock.yaml`.
> 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 coding agent, tool-enabled shell/git/GitHub
workflow. Context window is runtime-provided and not exposed in this
environment.
## 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-26 07:36:49 -05:00
|
|
|
it("includes blockedBy summaries on list rows in one batched pass", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const blockerId = randomUUID();
|
|
|
|
|
const blockedId = randomUUID();
|
|
|
|
|
const unblockedId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{
|
|
|
|
|
id: blockerId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Blocker issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "high",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: blockedId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Blocked issue",
|
|
|
|
|
status: "blocked",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: unblockedId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Unblocked issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await db.insert(issueRelations).values({
|
|
|
|
|
companyId,
|
|
|
|
|
issueId: blockerId,
|
|
|
|
|
relatedIssueId: blockedId,
|
|
|
|
|
type: "blocks",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const defaultResult = await svc.list(companyId);
|
|
|
|
|
expect(defaultResult.find((issue) => issue.id === blockedId)?.blockedBy).toBeUndefined();
|
|
|
|
|
|
|
|
|
|
const result = await svc.list(companyId, { includeBlockedBy: true });
|
|
|
|
|
const byId = new Map(result.map((issue) => [issue.id, issue]));
|
|
|
|
|
|
|
|
|
|
expect(byId.get(blockedId)?.blockedBy).toEqual([
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
id: blockerId,
|
|
|
|
|
identifier: null,
|
|
|
|
|
title: "Blocker issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "high",
|
|
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
expect(byId.get(blockerId)?.blockedBy).toEqual([]);
|
|
|
|
|
expect(byId.get(unblockedId)?.blockedBy).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
Sync/master post pap1497 followups 2026 04 15 (#3779)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The board depends on issue, inbox, cost, and company-skill surfaces
to stay accurate and fast while agents are actively working
> - The PAP-1497 follow-up branch exposed a few rough edges in those
surfaces: stale active-run state on completed issues, missing creator
filters, oversized issue payload scans, and placeholder issue-route
parsing
> - Those gaps make the control plane harder to trust because operators
can see misleading run state, miss the right subset of work, or pay
extra query/render cost on large issue records
> - This pull request tightens those follow-ups across server and UI
code, and adds regression coverage for the affected paths
> - The benefit is a more reliable issue workflow, safer high-volume
cost aggregation, and clearer board/operator navigation
## What Changed
- Added the `v2026.415.0` release changelog entry.
- Fixed stale issue-run presentation after completion and reused the
shared issue-path parser so literal route placeholders no longer become
issue links.
- Added creator filters to the Issues page and Inbox, including
persisted filter-state normalization and regression coverage.
- Bounded issue detail/list project-mention scans and trimmed large
issue-list payload fields to keep issue reads lighter.
- Hardened company-skill list projection and cost/finance aggregation so
large markdown blobs and large summed values do not leak into list
responses or overflow 32-bit casts.
- Added targeted server/UI regression tests for company skills,
costs/finance, issue mention scanning, creator filters, inbox
normalization, and issue reference parsing.
## Verification
- `pnpm exec vitest run
server/src/__tests__/company-skills-service.test.ts
server/src/__tests__/costs-service.test.ts
server/src/__tests__/issues-goal-context-routes.test.ts
server/src/__tests__/issues-service.test.ts ui/src/lib/inbox.test.ts
ui/src/lib/issue-filters.test.ts ui/src/lib/issue-reference.test.ts`
- `gh pr checks 3779`
Current pass set on the PR head: `policy`, `verify`, `e2e`,
`security/snyk (cryppadotta)`, `Greptile Review`
## Risks
- Creator filter options are derived from the currently loaded
issue/agent data, so very sparse result sets may not surface every
historical creator until they appear in the active dataset.
- Cost/finance aggregate casts now use `double precision`; that removes
the current overflow risk, but future schema changes should keep
large-value aggregation behavior under review.
- Issue detail mention scanning now skips comment-body scans on the
detail route, so any consumer that relied on comment-only project
mentions there would need to fetch them separately.
## Model Used
- OpenAI Codex, GPT-5-based coding agent with terminal tool use and
local code execution in the Paperclip workspace. Exact internal model
ID/context-window exposure is not surfaced in this session.
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 21:13:56 -05:00
|
|
|
it("trims list payload fields that can grow large on issue index routes", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const issueId = randomUUID();
|
|
|
|
|
const longDescription = "x".repeat(5_000);
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: issueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Large issue",
|
|
|
|
|
description: longDescription,
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
executionPolicy: { stages: Array.from({ length: 20 }, (_, index) => ({ index, kind: "review", notes: "y".repeat(400) })) },
|
|
|
|
|
executionState: { history: Array.from({ length: 20 }, (_, index) => ({ index, body: "z".repeat(400) })) },
|
|
|
|
|
executionWorkspaceSettings: { notes: "w".repeat(2_000) },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [result] = await svc.list(companyId);
|
|
|
|
|
|
|
|
|
|
expect(result).toBeTruthy();
|
|
|
|
|
expect(result?.description).toHaveLength(1200);
|
|
|
|
|
expect(result?.executionPolicy).toBeNull();
|
|
|
|
|
expect(result?.executionState).toBeNull();
|
|
|
|
|
expect(result?.executionWorkspaceSettings).toBeNull();
|
|
|
|
|
});
|
[codex] Harden heartbeat scheduling and runtime controls (#4223)
## Thinking Path
> - Paperclip orchestrates AI agents through issue checkout, heartbeat
runs, routines, and auditable control-plane state
> - The runtime path has to recover from lost local processes, transient
adapter failures, blocked dependencies, and routine coalescing without
stranding work
> - The existing branch carried several reliability fixes across
heartbeat scheduling, issue runtime controls, routine dispatch, and
operator-facing run state
> - These changes belong together because they share backend contracts,
migrations, and runtime status semantics
> - This pull request groups the control-plane/runtime slice so it can
merge independently from board UI polish and adapter sandbox work
> - The benefit is safer heartbeat recovery, clearer runtime controls,
and more predictable recurring execution behavior
## What Changed
- Adds bounded heartbeat retry scheduling, scheduled retry state, and
Codex transient failure recovery handling.
- Tightens heartbeat process recovery, blocker wake behavior, issue
comment wake handling, routine dispatch coalescing, and
activity/dashboard bounds.
- Adds runtime-control MCP tools and Paperclip skill docs for issue
workspace runtime management.
- Adds migrations `0061_lively_thor_girl.sql` and
`0062_routine_run_dispatch_fingerprint.sql`.
- Surfaces retry state in run ledger/agent UI and keeps related shared
types synchronized.
## Verification
- `pnpm exec vitest run
server/src/__tests__/heartbeat-retry-scheduling.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/routines-service.test.ts`
- `pnpm exec vitest run src/tools.test.ts` from `packages/mcp-server`
## Risks
- Medium risk: this touches heartbeat recovery and routine dispatch,
which are central execution paths.
- Migration order matters if split branches land out of order: merge
this PR before branches that assume the new runtime/routine fields.
- Runtime retry behavior should be watched in CI and in local operator
smoke tests because it changes how transient failures are resumed.
> 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-based coding agent runtime, shell/git tool use
enabled. Exact hosted model build and context window are not exposed in
this Paperclip heartbeat environment.
## 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
- [ ] 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
2026-04-21 12:24:11 -05:00
|
|
|
|
|
|
|
|
it("does not let description preview truncation split multibyte characters", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const issueId = randomUUID();
|
|
|
|
|
const description = `${"x".repeat(1199)}— still valid after truncation`;
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: issueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Multibyte boundary issue",
|
|
|
|
|
description,
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [result] = await svc.list(companyId);
|
|
|
|
|
|
|
|
|
|
expect(result?.description).toHaveLength(1200);
|
|
|
|
|
expect(result?.description?.endsWith("—")).toBe(true);
|
|
|
|
|
});
|
2026-03-21 12:20:48 -05:00
|
|
|
});
|
2026-03-30 14:08:44 -05:00
|
|
|
|
|
|
|
|
describeEmbeddedPostgres("issueService.create workspace inheritance", () => {
|
|
|
|
|
let db!: ReturnType<typeof createDb>;
|
|
|
|
|
let svc!: ReturnType<typeof issueService>;
|
|
|
|
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-issues-create-");
|
|
|
|
|
db = createDb(tempDb.connectionString);
|
|
|
|
|
svc = issueService(db);
|
2026-04-04 13:56:04 -05:00
|
|
|
await ensureIssueRelationsTable(db);
|
2026-03-30 14:08:44 -05:00
|
|
|
}, 20_000);
|
|
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
await db.delete(issueComments);
|
2026-04-04 13:56:04 -05:00
|
|
|
await db.delete(issueRelations);
|
2026-03-30 14:08:44 -05:00
|
|
|
await db.delete(issueInboxArchives);
|
|
|
|
|
await db.delete(activityLog);
|
|
|
|
|
await db.delete(issues);
|
|
|
|
|
await db.delete(executionWorkspaces);
|
|
|
|
|
await db.delete(projectWorkspaces);
|
|
|
|
|
await db.delete(projects);
|
[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
|
|
|
await db.delete(goals);
|
2026-03-30 14:08:44 -05:00
|
|
|
await db.delete(agents);
|
|
|
|
|
await db.delete(instanceSettings);
|
|
|
|
|
await db.delete(companies);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
await tempDb?.cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("inherits the parent issue workspace linkage when child workspace fields are omitted", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const projectId = randomUUID();
|
|
|
|
|
const parentIssueId = randomUUID();
|
|
|
|
|
const projectWorkspaceId = randomUUID();
|
|
|
|
|
const executionWorkspaceId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
|
|
|
|
|
|
|
|
|
|
await db.insert(projects).values({
|
|
|
|
|
id: projectId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "Workspace project",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projectWorkspaces).values({
|
|
|
|
|
id: projectWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
name: "Primary workspace",
|
|
|
|
|
isPrimary: true,
|
|
|
|
|
sharedWorkspaceKey: "workspace-key",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(executionWorkspaces).values({
|
|
|
|
|
id: executionWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
strategyType: "git_worktree",
|
|
|
|
|
name: "Issue worktree",
|
|
|
|
|
status: "active",
|
|
|
|
|
providerType: "git_worktree",
|
|
|
|
|
providerRef: `/tmp/${executionWorkspaceId}`,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: parentIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
title: "Parent issue",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
executionWorkspaceId,
|
|
|
|
|
executionWorkspacePreference: "reuse_existing",
|
|
|
|
|
executionWorkspaceSettings: {
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
workspaceRuntime: { profile: "agent" },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const child = await svc.create(companyId, {
|
|
|
|
|
parentId: parentIssueId,
|
|
|
|
|
projectId,
|
|
|
|
|
title: "Child issue",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(child.parentId).toBe(parentIssueId);
|
|
|
|
|
expect(child.projectWorkspaceId).toBe(projectWorkspaceId);
|
|
|
|
|
expect(child.executionWorkspaceId).toBe(executionWorkspaceId);
|
|
|
|
|
expect(child.executionWorkspacePreference).toBe("reuse_existing");
|
|
|
|
|
expect(child.executionWorkspaceSettings).toEqual({
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
workspaceRuntime: { profile: "agent" },
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("keeps explicit workspace fields instead of inheriting the parent linkage", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const projectId = randomUUID();
|
|
|
|
|
const parentIssueId = randomUUID();
|
|
|
|
|
const parentProjectWorkspaceId = randomUUID();
|
|
|
|
|
const parentExecutionWorkspaceId = randomUUID();
|
|
|
|
|
const explicitProjectWorkspaceId = randomUUID();
|
|
|
|
|
const explicitExecutionWorkspaceId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
|
|
|
|
|
|
|
|
|
|
await db.insert(projects).values({
|
|
|
|
|
id: projectId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "Workspace project",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projectWorkspaces).values([
|
|
|
|
|
{
|
|
|
|
|
id: parentProjectWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
name: "Parent workspace",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: explicitProjectWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
name: "Explicit workspace",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await db.insert(executionWorkspaces).values([
|
|
|
|
|
{
|
|
|
|
|
id: parentExecutionWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId: parentProjectWorkspaceId,
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
strategyType: "git_worktree",
|
|
|
|
|
name: "Parent worktree",
|
|
|
|
|
status: "active",
|
|
|
|
|
providerType: "git_worktree",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: explicitExecutionWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId: explicitProjectWorkspaceId,
|
|
|
|
|
mode: "shared_workspace",
|
|
|
|
|
strategyType: "project_primary",
|
|
|
|
|
name: "Explicit shared workspace",
|
|
|
|
|
status: "active",
|
|
|
|
|
providerType: "local_fs",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: parentIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId: parentProjectWorkspaceId,
|
|
|
|
|
title: "Parent issue",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
executionWorkspaceId: parentExecutionWorkspaceId,
|
|
|
|
|
executionWorkspacePreference: "reuse_existing",
|
|
|
|
|
executionWorkspaceSettings: {
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const child = await svc.create(companyId, {
|
|
|
|
|
parentId: parentIssueId,
|
|
|
|
|
projectId,
|
|
|
|
|
title: "Child issue",
|
|
|
|
|
projectWorkspaceId: explicitProjectWorkspaceId,
|
|
|
|
|
executionWorkspaceId: explicitExecutionWorkspaceId,
|
|
|
|
|
executionWorkspacePreference: "reuse_existing",
|
|
|
|
|
executionWorkspaceSettings: {
|
|
|
|
|
mode: "shared_workspace",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(child.projectWorkspaceId).toBe(explicitProjectWorkspaceId);
|
|
|
|
|
expect(child.executionWorkspaceId).toBe(explicitExecutionWorkspaceId);
|
|
|
|
|
expect(child.executionWorkspacePreference).toBe("reuse_existing");
|
|
|
|
|
expect(child.executionWorkspaceSettings).toEqual({
|
|
|
|
|
mode: "shared_workspace",
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("inherits workspace linkage from an explicit source issue without creating a parent-child relationship", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const projectId = randomUUID();
|
|
|
|
|
const sourceIssueId = randomUUID();
|
|
|
|
|
const projectWorkspaceId = randomUUID();
|
|
|
|
|
const executionWorkspaceId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
|
|
|
|
|
|
|
|
|
|
await db.insert(projects).values({
|
|
|
|
|
id: projectId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "Workspace project",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projectWorkspaces).values({
|
|
|
|
|
id: projectWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
name: "Primary workspace",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(executionWorkspaces).values({
|
|
|
|
|
id: executionWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
mode: "operator_branch",
|
|
|
|
|
strategyType: "git_worktree",
|
|
|
|
|
name: "Operator branch",
|
|
|
|
|
status: "active",
|
|
|
|
|
providerType: "git_worktree",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: sourceIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
title: "Source issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
executionWorkspaceId,
|
|
|
|
|
executionWorkspacePreference: "reuse_existing",
|
|
|
|
|
executionWorkspaceSettings: {
|
|
|
|
|
mode: "operator_branch",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const followUp = await svc.create(companyId, {
|
|
|
|
|
projectId,
|
|
|
|
|
title: "Follow-up issue",
|
|
|
|
|
inheritExecutionWorkspaceFromIssueId: sourceIssueId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(followUp.parentId).toBeNull();
|
|
|
|
|
expect(followUp.projectWorkspaceId).toBe(projectWorkspaceId);
|
|
|
|
|
expect(followUp.executionWorkspaceId).toBe(executionWorkspaceId);
|
|
|
|
|
expect(followUp.executionWorkspacePreference).toBe("reuse_existing");
|
|
|
|
|
expect(followUp.executionWorkspaceSettings).toEqual({
|
|
|
|
|
mode: "operator_branch",
|
|
|
|
|
});
|
|
|
|
|
});
|
[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
|
|
|
|
|
|
|
|
it("createChild applies parent defaults, acceptance criteria, workspace inheritance, and optional parent blocker chaining", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const projectId = randomUUID();
|
|
|
|
|
const goalId = randomUUID();
|
|
|
|
|
const parentIssueId = randomUUID();
|
|
|
|
|
const projectWorkspaceId = randomUUID();
|
|
|
|
|
const executionWorkspaceId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
|
|
|
|
|
|
|
|
|
|
await db.insert(goals).values({
|
|
|
|
|
id: goalId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Ship child helpers",
|
|
|
|
|
level: "task",
|
|
|
|
|
status: "active",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projects).values({
|
|
|
|
|
id: projectId,
|
|
|
|
|
companyId,
|
|
|
|
|
goalId,
|
|
|
|
|
name: "Workspace project",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projectWorkspaces).values({
|
|
|
|
|
id: projectWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
name: "Primary workspace",
|
|
|
|
|
isPrimary: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(executionWorkspaces).values({
|
|
|
|
|
id: executionWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
strategyType: "git_worktree",
|
|
|
|
|
name: "Issue worktree",
|
|
|
|
|
status: "active",
|
|
|
|
|
providerType: "git_worktree",
|
|
|
|
|
providerRef: `/tmp/${executionWorkspaceId}`,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: parentIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
goalId,
|
|
|
|
|
title: "Parent issue",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
requestDepth: 1,
|
|
|
|
|
executionWorkspaceId,
|
|
|
|
|
executionWorkspacePreference: "reuse_existing",
|
|
|
|
|
executionWorkspaceSettings: {
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { issue: child, parentBlockerAdded } = await svc.createChild(parentIssueId, {
|
|
|
|
|
title: "Child helper",
|
|
|
|
|
status: "todo",
|
|
|
|
|
description: "Implement the helper.",
|
|
|
|
|
acceptanceCriteria: ["Uses the parent issue as parentId", "Reuses the parent execution workspace"],
|
|
|
|
|
blockParentUntilDone: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(parentBlockerAdded).toBe(true);
|
|
|
|
|
expect(child.parentId).toBe(parentIssueId);
|
|
|
|
|
expect(child.projectId).toBe(projectId);
|
|
|
|
|
expect(child.goalId).toBe(goalId);
|
|
|
|
|
expect(child.requestDepth).toBe(2);
|
|
|
|
|
expect(child.description).toContain("## Acceptance Criteria");
|
|
|
|
|
expect(child.description).toContain("- Uses the parent issue as parentId");
|
|
|
|
|
expect(child.projectWorkspaceId).toBe(projectWorkspaceId);
|
|
|
|
|
expect(child.executionWorkspaceId).toBe(executionWorkspaceId);
|
|
|
|
|
expect(child.executionWorkspacePreference).toBe("reuse_existing");
|
|
|
|
|
|
|
|
|
|
const parentRelations = await svc.getRelationSummaries(parentIssueId);
|
|
|
|
|
expect(parentRelations.blockedBy).toEqual([
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
id: child.id,
|
|
|
|
|
title: "Child helper",
|
|
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
});
|
[codex] Split backend control-plane QoL slice (#4700)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies, so
backend task ownership, recovery, review visibility, and company-scoped
limits need to stay enforceable without UI-only coupling.
> - Closed PR #4692 bundled those backend changes with UI workflow,
docs, skills, workflow, and lockfile churn.
> - PAP-2694 asks for a clean backend/control-plane slice from that
closed branch.
> - This branch starts from current `master` and mines only the `cli`,
`packages/db`, `packages/shared`, and `server` contracts/tests needed
for the backend behavior.
> - It explicitly excludes UI workflow/performance work,
`.github/workflows/pr.yml`, `pnpm-lock.yaml`, docs, skills,
package-script, adapter UI build-config, and perf fixture script
changes; the only UI files are fixture/test updates required by the
tightened shared `Company` contract.
> - The benefit is a smaller reviewable PR that preserves the
control-plane fixes while staying under Greptile s 100-file review
limit.
## What Changed
- Added company-scoped attachment-size limits through DB
schema/migrations, shared company portability contracts, CLI
import/export coverage, and server attachment upload enforcement.
- Added productivity review service/API behavior for no-comment streak,
long-active, and high-churn review issues, including request-depth
clamping and issue summary exposure.
- Hardened issue ownership and recovery/control-plane paths: peer-agent
mutation denial, issue tree pause/resume behavior, stranded recovery
origins, and related activity/test coverage.
- Preserved related backend contract updates for routine timestamp
variables and managed agent instruction bundles because they live in
shared/server contracts from the source branch.
- Addressed Greptile feedback by making `Company.attachmentMaxBytes`
non-optional, simplifying review request-depth clamping, fixing the
migration final newline, and enforcing the process-level attachment cap
as the final ceiling for uploads.
- Added minimal company fixtures needed for repo-wide typecheck/build
and kept the PR to 66 changed files with forbidden/non-slice paths
excluded.
## Verification
- `pnpm install --frozen-lockfile`
- `git diff --check origin/master..HEAD`
- `git diff --name-only origin/master..HEAD | wc -l` -> 66 files
- `git diff --name-only origin/master..HEAD -- .github/workflows/pr.yml
pnpm-lock.yaml package.json doc skills .agents scripts
packages/adapters` -> no output
- `pnpm exec vitest run --config vitest.config.ts
packages/shared/src/validators/issue.test.ts
packages/shared/src/routine-variables.test.ts
packages/shared/src/adapter-types.test.ts
cli/src/__tests__/company-import-export-e2e.test.ts
cli/src/__tests__/company.test.ts
server/src/__tests__/productivity-review-service.test.ts
server/src/__tests__/issue-tree-control-service.test.ts
server/src/__tests__/issue-tree-control-routes.test.ts
server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts
server/src/__tests__/issue-attachment-routes.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/issues-service.test.ts` -> 12 files, 147 tests
passed
- `pnpm exec vitest run --config vitest.config.ts
cli/src/__tests__/company-delete.test.ts
cli/src/__tests__/company-import-export-e2e.test.ts
server/src/__tests__/productivity-review-service.test.ts` -> 3 files, 18
tests passed
- `pnpm exec vitest run --config vitest.config.ts
server/src/__tests__/issue-attachment-routes.test.ts` -> 1 file, 6 tests
passed
- `pnpm --filter @paperclipai/db typecheck && pnpm --filter
@paperclipai/shared typecheck && pnpm --filter @paperclipai/server
typecheck && pnpm --filter paperclipai typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck && pnpm --filter
@paperclipai/ui build`
## Risks
- Includes migrations `0073_shiny_salo.sql` and
`0074_striped_genesis.sql`; merge ordering matters if another PR adds
migrations first.
- This is intentionally backend-only apart from fixture/test updates
forced by shared type correctness; UI affordances from PR #4692 are not
present here and should land in separate UI slices.
- The worktree install emitted plugin SDK bin-link warnings for unbuilt
plugin packages, but the targeted tests and package typechecks completed
successfully.
> 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 coding agent, tool-enabled terminal/GitHub
workflow. Exact runtime context window was not exposed by the 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-28 16:46:45 -05:00
|
|
|
|
|
|
|
|
it("clamps helper-created child requestDepth to the safe maximum", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const projectId = randomUUID();
|
|
|
|
|
const goalId = randomUUID();
|
|
|
|
|
const parentIssueId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: false });
|
|
|
|
|
|
|
|
|
|
await db.insert(goals).values({
|
|
|
|
|
id: goalId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Ship child helpers",
|
|
|
|
|
level: "task",
|
|
|
|
|
status: "active",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projects).values({
|
|
|
|
|
id: projectId,
|
|
|
|
|
companyId,
|
|
|
|
|
goalId,
|
|
|
|
|
name: "Workspace project",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: parentIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
goalId,
|
|
|
|
|
title: "Parent issue",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
requestDepth: MAX_ISSUE_REQUEST_DEPTH,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { issue: child } = await svc.createChild(parentIssueId, {
|
|
|
|
|
title: "Child helper",
|
|
|
|
|
status: "todo",
|
|
|
|
|
requestDepth: MAX_ISSUE_REQUEST_DEPTH + 100,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(child.requestDepth).toBe(MAX_ISSUE_REQUEST_DEPTH);
|
|
|
|
|
});
|
2026-03-30 14:08:44 -05:00
|
|
|
});
|
2026-04-04 13:56:04 -05:00
|
|
|
|
|
|
|
|
describeEmbeddedPostgres("issueService blockers and dependency wake readiness", () => {
|
|
|
|
|
let db!: ReturnType<typeof createDb>;
|
|
|
|
|
let svc!: ReturnType<typeof issueService>;
|
|
|
|
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-issues-blockers-");
|
|
|
|
|
db = createDb(tempDb.connectionString);
|
|
|
|
|
svc = issueService(db);
|
|
|
|
|
await ensureIssueRelationsTable(db);
|
|
|
|
|
}, 20_000);
|
|
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
await db.delete(issueComments);
|
|
|
|
|
await db.delete(issueRelations);
|
|
|
|
|
await db.delete(issueInboxArchives);
|
|
|
|
|
await db.delete(activityLog);
|
|
|
|
|
await db.delete(issues);
|
|
|
|
|
await db.delete(executionWorkspaces);
|
|
|
|
|
await db.delete(projectWorkspaces);
|
|
|
|
|
await db.delete(projects);
|
|
|
|
|
await db.delete(agents);
|
|
|
|
|
await db.delete(instanceSettings);
|
|
|
|
|
await db.delete(companies);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
await tempDb?.cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("persists blocked-by relations and exposes both blockedBy and blocks summaries", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const blockerId = randomUUID();
|
|
|
|
|
const blockedId = randomUUID();
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{
|
|
|
|
|
id: blockerId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Blocker",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "high",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: blockedId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Blocked issue",
|
|
|
|
|
status: "blocked",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await svc.update(blockedId, {
|
|
|
|
|
blockedByIssueIds: [blockerId],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const blockerRelations = await svc.getRelationSummaries(blockerId);
|
|
|
|
|
const blockedRelations = await svc.getRelationSummaries(blockedId);
|
|
|
|
|
|
|
|
|
|
expect(blockerRelations.blocks.map((relation) => relation.id)).toEqual([blockedId]);
|
|
|
|
|
expect(blockedRelations.blockedBy.map((relation) => relation.id)).toEqual([blockerId]);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-24 15:50:32 -05:00
|
|
|
it("adds terminal blockers to immediate blocked-by summaries", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const issueA = randomUUID();
|
|
|
|
|
const issueB = randomUUID();
|
|
|
|
|
const issueC = randomUUID();
|
|
|
|
|
const issueD = randomUUID();
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{ id: issueA, companyId, identifier: "PAP-1", title: "Issue A", status: "blocked", priority: "medium" },
|
|
|
|
|
{ id: issueB, companyId, identifier: "PAP-2", title: "Issue B", status: "blocked", priority: "medium" },
|
|
|
|
|
{ id: issueC, companyId, identifier: "PAP-3", title: "Issue C", status: "blocked", priority: "medium" },
|
|
|
|
|
{ id: issueD, companyId, identifier: "PAP-4", title: "Issue D", status: "todo", priority: "high" },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await svc.update(issueC, { blockedByIssueIds: [issueD] });
|
|
|
|
|
await svc.update(issueB, { blockedByIssueIds: [issueC] });
|
|
|
|
|
await svc.update(issueA, { blockedByIssueIds: [issueB] });
|
|
|
|
|
|
|
|
|
|
const relations = await svc.getRelationSummaries(issueA);
|
|
|
|
|
|
|
|
|
|
expect(relations.blockedBy).toHaveLength(1);
|
|
|
|
|
expect(relations.blockedBy[0]).toMatchObject({
|
|
|
|
|
id: issueB,
|
|
|
|
|
identifier: "PAP-2",
|
|
|
|
|
title: "Issue B",
|
|
|
|
|
terminalBlockers: [
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
id: issueD,
|
|
|
|
|
identifier: "PAP-4",
|
|
|
|
|
title: "Issue D",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "high",
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-04 13:56:04 -05:00
|
|
|
it("rejects blocking cycles", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const issueA = randomUUID();
|
|
|
|
|
const issueB = randomUUID();
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{ id: issueA, companyId, title: "Issue A", status: "todo", priority: "medium" },
|
|
|
|
|
{ id: issueB, companyId, title: "Issue B", status: "todo", priority: "medium" },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await svc.update(issueA, { blockedByIssueIds: [issueB] });
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
svc.update(issueB, { blockedByIssueIds: [issueA] }),
|
|
|
|
|
).rejects.toMatchObject({ status: 422 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("only returns dependents once every blocker is done", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const assigneeAgentId = randomUUID();
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
await db.insert(agents).values({
|
|
|
|
|
id: assigneeAgentId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "CodexCoder",
|
|
|
|
|
role: "engineer",
|
|
|
|
|
status: "active",
|
|
|
|
|
adapterType: "codex_local",
|
|
|
|
|
adapterConfig: {},
|
|
|
|
|
runtimeConfig: {},
|
|
|
|
|
permissions: {},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const blockerA = randomUUID();
|
|
|
|
|
const blockerB = randomUUID();
|
|
|
|
|
const blockedIssueId = randomUUID();
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{ id: blockerA, companyId, title: "Blocker A", status: "done", priority: "medium" },
|
|
|
|
|
{ id: blockerB, companyId, title: "Blocker B", status: "todo", priority: "medium" },
|
|
|
|
|
{
|
|
|
|
|
id: blockedIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Blocked issue",
|
|
|
|
|
status: "blocked",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
assigneeAgentId,
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await svc.update(blockedIssueId, { blockedByIssueIds: [blockerA, blockerB] });
|
|
|
|
|
|
|
|
|
|
expect(await svc.listWakeableBlockedDependents(blockerA)).toEqual([]);
|
|
|
|
|
|
|
|
|
|
await svc.update(blockerB, { status: "done" });
|
|
|
|
|
|
2026-04-04 20:00:29 -05:00
|
|
|
await expect(svc.listWakeableBlockedDependents(blockerA)).resolves.toEqual([
|
|
|
|
|
expect.objectContaining({
|
2026-04-04 13:56:04 -05:00
|
|
|
id: blockedIssueId,
|
|
|
|
|
assigneeAgentId,
|
2026-04-04 20:00:29 -05:00
|
|
|
blockerIssueIds: expect.arrayContaining([blockerA, blockerB]),
|
|
|
|
|
}),
|
2026-04-04 13:56:04 -05:00
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-20 16:03:57 -05:00
|
|
|
it("reports dependency readiness for blocked issue chains", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const blockerId = randomUUID();
|
|
|
|
|
const blockedId = randomUUID();
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{ id: blockerId, companyId, title: "Blocker", status: "todo", priority: "medium" },
|
|
|
|
|
{ id: blockedId, companyId, title: "Blocked", status: "todo", priority: "medium" },
|
|
|
|
|
]);
|
|
|
|
|
await svc.update(blockedId, { blockedByIssueIds: [blockerId] });
|
|
|
|
|
|
|
|
|
|
await expect(svc.getDependencyReadiness(blockedId)).resolves.toMatchObject({
|
|
|
|
|
issueId: blockedId,
|
|
|
|
|
blockerIssueIds: [blockerId],
|
|
|
|
|
unresolvedBlockerIssueIds: [blockerId],
|
|
|
|
|
unresolvedBlockerCount: 1,
|
|
|
|
|
allBlockersDone: false,
|
|
|
|
|
isDependencyReady: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await svc.update(blockerId, { status: "done" });
|
|
|
|
|
|
|
|
|
|
await expect(svc.getDependencyReadiness(blockedId)).resolves.toMatchObject({
|
|
|
|
|
issueId: blockedId,
|
|
|
|
|
blockerIssueIds: [blockerId],
|
|
|
|
|
unresolvedBlockerIssueIds: [],
|
|
|
|
|
unresolvedBlockerCount: 0,
|
|
|
|
|
allBlockersDone: true,
|
|
|
|
|
isDependencyReady: true,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects execution when unresolved blockers remain", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const assigneeAgentId = randomUUID();
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
await db.insert(agents).values({
|
|
|
|
|
id: assigneeAgentId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "CodexCoder",
|
|
|
|
|
role: "engineer",
|
|
|
|
|
status: "active",
|
|
|
|
|
adapterType: "codex_local",
|
|
|
|
|
adapterConfig: {},
|
|
|
|
|
runtimeConfig: {},
|
|
|
|
|
permissions: {},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const blockerId = randomUUID();
|
|
|
|
|
const blockedId = randomUUID();
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{ id: blockerId, companyId, title: "Blocker", status: "todo", priority: "medium" },
|
|
|
|
|
{
|
|
|
|
|
id: blockedId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Blocked",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
assigneeAgentId,
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
await svc.update(blockedId, { blockedByIssueIds: [blockerId] });
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
svc.update(blockedId, { status: "in_progress" }),
|
|
|
|
|
).rejects.toMatchObject({ status: 422 });
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
svc.checkout(blockedId, assigneeAgentId, ["todo", "blocked"], null),
|
|
|
|
|
).rejects.toMatchObject({ status: 422 });
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-04 13:56:04 -05:00
|
|
|
it("wakes parents only when all direct children are terminal", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const assigneeAgentId = randomUUID();
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
await db.insert(agents).values({
|
|
|
|
|
id: assigneeAgentId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "CodexCoder",
|
|
|
|
|
role: "engineer",
|
|
|
|
|
status: "active",
|
|
|
|
|
adapterType: "codex_local",
|
|
|
|
|
adapterConfig: {},
|
|
|
|
|
runtimeConfig: {},
|
|
|
|
|
permissions: {},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const parentId = randomUUID();
|
|
|
|
|
const childA = randomUUID();
|
|
|
|
|
const childB = randomUUID();
|
|
|
|
|
await db.insert(issues).values([
|
|
|
|
|
{
|
|
|
|
|
id: parentId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Parent issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
assigneeAgentId,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: childA,
|
|
|
|
|
companyId,
|
|
|
|
|
parentId,
|
|
|
|
|
title: "Child A",
|
|
|
|
|
status: "done",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: childB,
|
|
|
|
|
companyId,
|
|
|
|
|
parentId,
|
|
|
|
|
title: "Child B",
|
|
|
|
|
status: "blocked",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
expect(await svc.getWakeableParentAfterChildCompletion(parentId)).toBeNull();
|
|
|
|
|
|
|
|
|
|
await svc.update(childB, { status: "cancelled" });
|
|
|
|
|
|
[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
|
|
|
expect(await svc.getWakeableParentAfterChildCompletion(parentId)).toMatchObject({
|
2026-04-04 13:56:04 -05:00
|
|
|
id: parentId,
|
|
|
|
|
assigneeAgentId,
|
|
|
|
|
childIssueIds: [childA, childB],
|
[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
|
|
|
childIssueSummaries: [
|
|
|
|
|
expect.objectContaining({ id: childA, title: "Child A", status: "done" }),
|
|
|
|
|
expect.objectContaining({ id: childB, title: "Child B", status: "cancelled" }),
|
|
|
|
|
],
|
|
|
|
|
childIssueSummaryTruncated: false,
|
2026-04-04 13:56:04 -05:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
Sync/master post pap1497 followups 2026 04 15 (#3779)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The board depends on issue, inbox, cost, and company-skill surfaces
to stay accurate and fast while agents are actively working
> - The PAP-1497 follow-up branch exposed a few rough edges in those
surfaces: stale active-run state on completed issues, missing creator
filters, oversized issue payload scans, and placeholder issue-route
parsing
> - Those gaps make the control plane harder to trust because operators
can see misleading run state, miss the right subset of work, or pay
extra query/render cost on large issue records
> - This pull request tightens those follow-ups across server and UI
code, and adds regression coverage for the affected paths
> - The benefit is a more reliable issue workflow, safer high-volume
cost aggregation, and clearer board/operator navigation
## What Changed
- Added the `v2026.415.0` release changelog entry.
- Fixed stale issue-run presentation after completion and reused the
shared issue-path parser so literal route placeholders no longer become
issue links.
- Added creator filters to the Issues page and Inbox, including
persisted filter-state normalization and regression coverage.
- Bounded issue detail/list project-mention scans and trimmed large
issue-list payload fields to keep issue reads lighter.
- Hardened company-skill list projection and cost/finance aggregation so
large markdown blobs and large summed values do not leak into list
responses or overflow 32-bit casts.
- Added targeted server/UI regression tests for company skills,
costs/finance, issue mention scanning, creator filters, inbox
normalization, and issue reference parsing.
## Verification
- `pnpm exec vitest run
server/src/__tests__/company-skills-service.test.ts
server/src/__tests__/costs-service.test.ts
server/src/__tests__/issues-goal-context-routes.test.ts
server/src/__tests__/issues-service.test.ts ui/src/lib/inbox.test.ts
ui/src/lib/issue-filters.test.ts ui/src/lib/issue-reference.test.ts`
- `gh pr checks 3779`
Current pass set on the PR head: `policy`, `verify`, `e2e`,
`security/snyk (cryppadotta)`, `Greptile Review`
## Risks
- Creator filter options are derived from the currently loaded
issue/agent data, so very sparse result sets may not surface every
historical creator until they appear in the active dataset.
- Cost/finance aggregate casts now use `double precision`; that removes
the current overflow risk, but future schema changes should keep
large-value aggregation behavior under review.
- Issue detail mention scanning now skips comment-body scans on the
detail route, so any consumer that relied on comment-only project
mentions there would need to fetch them separately.
## Model Used
- OpenAI Codex, GPT-5-based coding agent with terminal tool use and
local code execution in the Paperclip workspace. Exact internal model
ID/context-window exposure is not surfaced in this session.
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 21:13:56 -05:00
|
|
|
|
feat: implement multi-user access and invite flows (#3784)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## 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 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
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
|
|
|
describeEmbeddedPostgres("issueService.create workspace inheritance", () => {
|
|
|
|
|
let db!: ReturnType<typeof createDb>;
|
|
|
|
|
let svc!: ReturnType<typeof issueService>;
|
|
|
|
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-issues-create-");
|
|
|
|
|
db = createDb(tempDb.connectionString);
|
|
|
|
|
svc = issueService(db);
|
|
|
|
|
await ensureIssueRelationsTable(db);
|
|
|
|
|
}, 20_000);
|
|
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
await db.delete(issueComments);
|
|
|
|
|
await db.delete(issueRelations);
|
|
|
|
|
await db.delete(issueInboxArchives);
|
|
|
|
|
await db.delete(activityLog);
|
|
|
|
|
await db.delete(issues);
|
|
|
|
|
await db.delete(executionWorkspaces);
|
|
|
|
|
await db.delete(projectWorkspaces);
|
|
|
|
|
await db.delete(projects);
|
|
|
|
|
await db.delete(agents);
|
|
|
|
|
await db.delete(instanceSettings);
|
|
|
|
|
await db.delete(companies);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
await tempDb?.cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("inherits the parent issue workspace linkage when child workspace fields are omitted", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const projectId = randomUUID();
|
|
|
|
|
const parentIssueId = randomUUID();
|
|
|
|
|
const projectWorkspaceId = randomUUID();
|
|
|
|
|
const executionWorkspaceId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
|
|
|
|
|
|
|
|
|
|
await db.insert(projects).values({
|
|
|
|
|
id: projectId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "Workspace project",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projectWorkspaces).values({
|
|
|
|
|
id: projectWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
name: "Primary workspace",
|
|
|
|
|
isPrimary: true,
|
|
|
|
|
sharedWorkspaceKey: "workspace-key",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(executionWorkspaces).values({
|
|
|
|
|
id: executionWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
strategyType: "git_worktree",
|
|
|
|
|
name: "Issue worktree",
|
|
|
|
|
status: "active",
|
|
|
|
|
providerType: "git_worktree",
|
|
|
|
|
providerRef: `/tmp/${executionWorkspaceId}`,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: parentIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
title: "Parent issue",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
executionWorkspaceId,
|
|
|
|
|
executionWorkspacePreference: "reuse_existing",
|
|
|
|
|
executionWorkspaceSettings: {
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
workspaceRuntime: { profile: "agent" },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const child = await svc.create(companyId, {
|
|
|
|
|
parentId: parentIssueId,
|
|
|
|
|
projectId,
|
|
|
|
|
title: "Child issue",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(child.parentId).toBe(parentIssueId);
|
|
|
|
|
expect(child.projectWorkspaceId).toBe(projectWorkspaceId);
|
|
|
|
|
expect(child.executionWorkspaceId).toBe(executionWorkspaceId);
|
|
|
|
|
expect(child.executionWorkspacePreference).toBe("reuse_existing");
|
|
|
|
|
expect(child.executionWorkspaceSettings).toEqual({
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
workspaceRuntime: { profile: "agent" },
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("keeps explicit workspace fields instead of inheriting the parent linkage", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const projectId = randomUUID();
|
|
|
|
|
const parentIssueId = randomUUID();
|
|
|
|
|
const parentProjectWorkspaceId = randomUUID();
|
|
|
|
|
const parentExecutionWorkspaceId = randomUUID();
|
|
|
|
|
const explicitProjectWorkspaceId = randomUUID();
|
|
|
|
|
const explicitExecutionWorkspaceId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
|
|
|
|
|
|
|
|
|
|
await db.insert(projects).values({
|
|
|
|
|
id: projectId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "Workspace project",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projectWorkspaces).values([
|
|
|
|
|
{
|
|
|
|
|
id: parentProjectWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
name: "Parent workspace",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: explicitProjectWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
name: "Explicit workspace",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await db.insert(executionWorkspaces).values([
|
|
|
|
|
{
|
|
|
|
|
id: parentExecutionWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId: parentProjectWorkspaceId,
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
strategyType: "git_worktree",
|
|
|
|
|
name: "Parent worktree",
|
|
|
|
|
status: "active",
|
|
|
|
|
providerType: "git_worktree",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: explicitExecutionWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId: explicitProjectWorkspaceId,
|
|
|
|
|
mode: "shared_workspace",
|
|
|
|
|
strategyType: "project_primary",
|
|
|
|
|
name: "Explicit shared workspace",
|
|
|
|
|
status: "active",
|
|
|
|
|
providerType: "local_fs",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: parentIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId: parentProjectWorkspaceId,
|
|
|
|
|
title: "Parent issue",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
executionWorkspaceId: parentExecutionWorkspaceId,
|
|
|
|
|
executionWorkspacePreference: "reuse_existing",
|
|
|
|
|
executionWorkspaceSettings: {
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const child = await svc.create(companyId, {
|
|
|
|
|
parentId: parentIssueId,
|
|
|
|
|
projectId,
|
|
|
|
|
title: "Child issue",
|
|
|
|
|
projectWorkspaceId: explicitProjectWorkspaceId,
|
|
|
|
|
executionWorkspaceId: explicitExecutionWorkspaceId,
|
|
|
|
|
executionWorkspacePreference: "reuse_existing",
|
|
|
|
|
executionWorkspaceSettings: {
|
|
|
|
|
mode: "shared_workspace",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(child.projectWorkspaceId).toBe(explicitProjectWorkspaceId);
|
|
|
|
|
expect(child.executionWorkspaceId).toBe(explicitExecutionWorkspaceId);
|
|
|
|
|
expect(child.executionWorkspacePreference).toBe("reuse_existing");
|
|
|
|
|
expect(child.executionWorkspaceSettings).toEqual({
|
|
|
|
|
mode: "shared_workspace",
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("inherits workspace linkage from an explicit source issue without creating a parent-child relationship", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const projectId = randomUUID();
|
|
|
|
|
const sourceIssueId = randomUUID();
|
|
|
|
|
const projectWorkspaceId = randomUUID();
|
|
|
|
|
const executionWorkspaceId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
|
|
|
|
|
|
|
|
|
|
await db.insert(projects).values({
|
|
|
|
|
id: projectId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "Workspace project",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projectWorkspaces).values({
|
|
|
|
|
id: projectWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
name: "Primary workspace",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(executionWorkspaces).values({
|
|
|
|
|
id: executionWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
mode: "operator_branch",
|
|
|
|
|
strategyType: "git_worktree",
|
|
|
|
|
name: "Operator branch",
|
|
|
|
|
status: "active",
|
|
|
|
|
providerType: "git_worktree",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: sourceIssueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
title: "Source issue",
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
executionWorkspaceId,
|
|
|
|
|
executionWorkspacePreference: "reuse_existing",
|
|
|
|
|
executionWorkspaceSettings: {
|
|
|
|
|
mode: "operator_branch",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const followUp = await svc.create(companyId, {
|
|
|
|
|
projectId,
|
|
|
|
|
title: "Follow-up issue",
|
|
|
|
|
inheritExecutionWorkspaceFromIssueId: sourceIssueId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(followUp.parentId).toBeNull();
|
|
|
|
|
expect(followUp.projectWorkspaceId).toBe(projectWorkspaceId);
|
|
|
|
|
expect(followUp.executionWorkspaceId).toBe(executionWorkspaceId);
|
|
|
|
|
expect(followUp.executionWorkspacePreference).toBe("reuse_existing");
|
|
|
|
|
expect(followUp.executionWorkspaceSettings).toEqual({
|
|
|
|
|
mode: "operator_branch",
|
|
|
|
|
});
|
|
|
|
|
});
|
Fix runtime state race, workspace sync, plugin startup, and orphaned leases (#4804)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Agents run inside environments that are leased, and the server
manages runtime state, workspace configuration, and plugin lifecycle
> - Several edge cases caused failures during concurrent operations: a
race condition in runtime state insertion could produce duplicate-key
errors, reused workspaces didn't sync their configuration when the
parent issue was updated, sandbox provider plugins could be queried
before registration completed, and orphaned environment leases from
failed runs were never released
> - This PR fixes these four runtime/environment issues
> - The benefit is more reliable concurrent agent execution and proper
resource cleanup
## What Changed
- `services/heartbeat.ts`: Fixed a race condition where concurrent
runtime state inserts could fail with a duplicate-key error by using an
upsert pattern
- `services/issues.ts`: Sync reused workspace configuration when an
issue is updated, so the workspace reflects the latest issue state
- `services/environment-runtime.ts`: Fixed a startup race where sandbox
provider plugins could be queried before registration completed, by
awaiting plugin readiness before resolving environment drivers
- `services/heartbeat.ts`: Release environment leases for orphaned runs
that lost their process without cleanup
## Verification
- `pnpm test` — all existing and new tests pass, including new tests for
runtime state upsert and process recovery lease cleanup
- `pnpm typecheck` — clean
- Manual: trigger concurrent agent runs to verify no duplicate-key
failures; verify orphaned leases are released after process loss
## Risks
- Low risk. The runtime state upsert changes insert-to-upsert behavior,
which could mask a legitimate duplicate if two different runs produce
the same key — but this is prevented by the run ID being part of the
key. The plugin startup await is bounded by the existing registration
timeout.
## Model Used
Codex GPT 5.4 high via Paperclip.
## 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
- [ ] 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
2026-04-29 16:37:10 -07:00
|
|
|
|
|
|
|
|
it("syncs reused execution workspace config when issue workspace settings are updated", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const projectId = randomUUID();
|
|
|
|
|
const projectWorkspaceId = randomUUID();
|
|
|
|
|
const executionWorkspaceId = randomUUID();
|
|
|
|
|
const issueId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
|
|
|
|
|
|
|
|
|
|
await db.insert(projects).values({
|
|
|
|
|
id: projectId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "Workspace project",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projectWorkspaces).values({
|
|
|
|
|
id: projectWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
name: "Primary workspace",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(executionWorkspaces).values({
|
|
|
|
|
id: executionWorkspaceId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
strategyType: "git_worktree",
|
|
|
|
|
name: "Issue worktree",
|
|
|
|
|
status: "active",
|
|
|
|
|
providerType: "git_worktree",
|
|
|
|
|
metadata: {
|
|
|
|
|
config: {
|
|
|
|
|
environmentId: "env-old",
|
|
|
|
|
provisionCommand: "bash ./scripts/provision-old.sh",
|
|
|
|
|
teardownCommand: "bash ./scripts/teardown-old.sh",
|
|
|
|
|
workspaceRuntime: { profile: "old" },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: issueId,
|
|
|
|
|
companyId,
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId,
|
|
|
|
|
title: "Recovery issue",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
executionWorkspaceId,
|
|
|
|
|
executionWorkspacePreference: "reuse_existing",
|
|
|
|
|
executionWorkspaceSettings: {
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
environmentId: "env-old",
|
|
|
|
|
workspaceStrategy: {
|
|
|
|
|
type: "git_worktree",
|
|
|
|
|
provisionCommand: "bash ./scripts/provision-old.sh",
|
|
|
|
|
teardownCommand: "bash ./scripts/teardown-old.sh",
|
|
|
|
|
},
|
|
|
|
|
workspaceRuntime: { profile: "old" },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await svc.update(issueId, {
|
|
|
|
|
executionWorkspaceSettings: {
|
|
|
|
|
mode: "isolated_workspace",
|
|
|
|
|
environmentId: "env-new",
|
|
|
|
|
workspaceStrategy: {
|
|
|
|
|
type: "cloud_sandbox",
|
|
|
|
|
provisionCommand: "bash ./scripts/provision-new.sh",
|
|
|
|
|
teardownCommand: "bash ./scripts/teardown-new.sh",
|
|
|
|
|
},
|
|
|
|
|
workspaceRuntime: { profile: "new" },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const workspace = await db
|
|
|
|
|
.select({ metadata: executionWorkspaces.metadata })
|
|
|
|
|
.from(executionWorkspaces)
|
|
|
|
|
.where(eq(executionWorkspaces.id, executionWorkspaceId))
|
|
|
|
|
.then((rows) => rows[0] ?? null);
|
|
|
|
|
|
|
|
|
|
expect(workspace?.metadata).toEqual({
|
|
|
|
|
config: {
|
|
|
|
|
environmentId: "env-new",
|
|
|
|
|
provisionCommand: "bash ./scripts/provision-new.sh",
|
|
|
|
|
teardownCommand: "bash ./scripts/teardown-new.sh",
|
|
|
|
|
cleanupCommand: null,
|
|
|
|
|
workspaceRuntime: { profile: "new" },
|
|
|
|
|
desiredState: null,
|
|
|
|
|
serviceStates: null,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
feat: implement multi-user access and invite flows (#3784)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## 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 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
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
|
|
|
});
|
|
|
|
|
|
Sync/master post pap1497 followups 2026 04 15 (#3779)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The board depends on issue, inbox, cost, and company-skill surfaces
to stay accurate and fast while agents are actively working
> - The PAP-1497 follow-up branch exposed a few rough edges in those
surfaces: stale active-run state on completed issues, missing creator
filters, oversized issue payload scans, and placeholder issue-route
parsing
> - Those gaps make the control plane harder to trust because operators
can see misleading run state, miss the right subset of work, or pay
extra query/render cost on large issue records
> - This pull request tightens those follow-ups across server and UI
code, and adds regression coverage for the affected paths
> - The benefit is a more reliable issue workflow, safer high-volume
cost aggregation, and clearer board/operator navigation
## What Changed
- Added the `v2026.415.0` release changelog entry.
- Fixed stale issue-run presentation after completion and reused the
shared issue-path parser so literal route placeholders no longer become
issue links.
- Added creator filters to the Issues page and Inbox, including
persisted filter-state normalization and regression coverage.
- Bounded issue detail/list project-mention scans and trimmed large
issue-list payload fields to keep issue reads lighter.
- Hardened company-skill list projection and cost/finance aggregation so
large markdown blobs and large summed values do not leak into list
responses or overflow 32-bit casts.
- Added targeted server/UI regression tests for company skills,
costs/finance, issue mention scanning, creator filters, inbox
normalization, and issue reference parsing.
## Verification
- `pnpm exec vitest run
server/src/__tests__/company-skills-service.test.ts
server/src/__tests__/costs-service.test.ts
server/src/__tests__/issues-goal-context-routes.test.ts
server/src/__tests__/issues-service.test.ts ui/src/lib/inbox.test.ts
ui/src/lib/issue-filters.test.ts ui/src/lib/issue-reference.test.ts`
- `gh pr checks 3779`
Current pass set on the PR head: `policy`, `verify`, `e2e`,
`security/snyk (cryppadotta)`, `Greptile Review`
## Risks
- Creator filter options are derived from the currently loaded
issue/agent data, so very sparse result sets may not surface every
historical creator until they appear in the active dataset.
- Cost/finance aggregate casts now use `double precision`; that removes
the current overflow risk, but future schema changes should keep
large-value aggregation behavior under review.
- Issue detail mention scanning now skips comment-body scans on the
detail route, so any consumer that relied on comment-only project
mentions there would need to fetch them separately.
## Model Used
- OpenAI Codex, GPT-5-based coding agent with terminal tool use and
local code execution in the Paperclip workspace. Exact internal model
ID/context-window exposure is not surfaced in this session.
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 21:13:56 -05:00
|
|
|
describeEmbeddedPostgres("issueService.findMentionedProjectIds", () => {
|
|
|
|
|
let db!: ReturnType<typeof createDb>;
|
|
|
|
|
let svc!: ReturnType<typeof issueService>;
|
|
|
|
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-issues-mentioned-projects-");
|
|
|
|
|
db = createDb(tempDb.connectionString);
|
|
|
|
|
svc = issueService(db);
|
|
|
|
|
await ensureIssueRelationsTable(db);
|
|
|
|
|
}, 20_000);
|
|
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
await db.delete(issueComments);
|
|
|
|
|
await db.delete(issueRelations);
|
|
|
|
|
await db.delete(issueInboxArchives);
|
|
|
|
|
await db.delete(activityLog);
|
|
|
|
|
await db.delete(issues);
|
|
|
|
|
await db.delete(executionWorkspaces);
|
|
|
|
|
await db.delete(projectWorkspaces);
|
|
|
|
|
await db.delete(projects);
|
|
|
|
|
await db.delete(agents);
|
|
|
|
|
await db.delete(instanceSettings);
|
|
|
|
|
await db.delete(companies);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
await tempDb?.cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("can skip comment-body scans for bounded issue detail reads", async () => {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const issueId = randomUUID();
|
|
|
|
|
const titleProjectId = randomUUID();
|
|
|
|
|
const commentProjectId = randomUUID();
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(projects).values([
|
|
|
|
|
{
|
|
|
|
|
id: titleProjectId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "Title project",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: commentProjectId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "Comment project",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: issueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: `Link [Title](${buildProjectMentionHref(titleProjectId)})`,
|
|
|
|
|
description: null,
|
|
|
|
|
status: "todo",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await db.insert(issueComments).values({
|
|
|
|
|
companyId,
|
|
|
|
|
issueId,
|
|
|
|
|
body: `Comment link [Comment](${buildProjectMentionHref(commentProjectId)})`,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(await svc.findMentionedProjectIds(issueId, { includeCommentBodies: false })).toEqual([titleProjectId]);
|
|
|
|
|
expect(await svc.findMentionedProjectIds(issueId)).toEqual([
|
|
|
|
|
titleProjectId,
|
|
|
|
|
commentProjectId,
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
});
|
[codex] Fix stale issue execution run locks (#4258)
## Thinking Path
> - Paperclip is a control plane for AI-agent companies, so issue
checkout and execution ownership are core safety contracts.
> - The affected subsystem is the issue service and route layer that
gates agent writes by `checkoutRunId` and `executionRunId`.
> - PAP-1982 exposed a stale-lock failure mode where a terminal
heartbeat run could leave `executionRunId` pinned after checkout
ownership had moved or been cleared.
> - That stale execution lock could reject legitimate
PATCH/comment/release requests from the rightful assignee after a
harness restart.
> - This pull request centralizes terminal-run cleanup, applies it
before ownership-gated writes, and adds a board-only recovery endpoint
for operator intervention.
> - The benefit is that crashed or terminal runs no longer strand issues
behind stale execution locks, while live execution locks still block
conflicting writes.
## What Changed
- Added `issueService.clearExecutionRunIfTerminal()` to atomically lock
the issue/run rows and clear terminal or missing execution-run locks.
- Reused stale execution-lock cleanup from checkout,
`assertCheckoutOwner()`, and `release()`.
- Allowed the same assigned agent/current run to adopt an unowned
`in_progress` checkout after stale execution-lock cleanup.
- Updated release to clear `executionRunId`, `executionAgentNameKey`,
and `executionLockedAt`.
- Added board-only `POST /api/issues/:id/admin/force-release` with
company access checks, optional `clearAssignee=true`, and
`issue.admin_force_release` audit logging.
- Added embedded Postgres service tests and route integration tests for
stale-lock recovery, release behavior, and admin force-release
authorization/audit behavior.
- Documented the new force-release API in `doc/SPEC-implementation.md`.
## Verification
- `pnpm vitest run server/src/__tests__/issues-service.test.ts
server/src/__tests__/issue-stale-execution-lock-routes.test.ts` passed.
- `pnpm vitest run
server/src/__tests__/issue-stale-execution-lock-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/issue-telemetry-routes.test.ts` passed.
- `pnpm -r typecheck` passed.
- `pnpm build` passed.
- `git diff --check` passed.
- `pnpm lint` could not run because this repo has no `lint` command.
- Full `pnpm test:run` completed with 4 failures in existing route
suites: `approval-routes-idempotency.test.ts` (2),
`issue-comment-reopen-routes.test.ts` (1), and
`issue-telemetry-routes.test.ts` (1). Those same files pass when run
isolated and when run together with the new stale-lock route test, so
this appears to be a whole-suite ordering/mock-isolation issue outside
this patch path.
## Risks
- Medium: this changes ownership-gated write behavior. The new adoption
path is limited to the current run, the current assignee, `in_progress`
issues, and rows with no checkout owner after terminal-lock cleanup.
- Low: the admin force-release endpoint is board-only and
company-scoped, but misuse can intentionally clear a live lock. It
writes an audit event with prior lock IDs.
- No schema or migration changes.
> 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 coding agent (`gpt-5`), agentic coding with
terminal/tool use and local test execution.
## 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
2026-04-22 10:43:38 -05:00
|
|
|
|
|
|
|
|
describeEmbeddedPostgres("issueService.clearExecutionRunIfTerminal", () => {
|
|
|
|
|
let db!: ReturnType<typeof createDb>;
|
|
|
|
|
let svc!: ReturnType<typeof issueService>;
|
|
|
|
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-issues-execution-lock-");
|
|
|
|
|
db = createDb(tempDb.connectionString);
|
|
|
|
|
svc = issueService(db);
|
|
|
|
|
}, 20_000);
|
|
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
await db.delete(issueComments);
|
|
|
|
|
await db.delete(issueRelations);
|
|
|
|
|
await db.delete(issueInboxArchives);
|
|
|
|
|
await db.delete(activityLog);
|
|
|
|
|
await db.delete(issues);
|
|
|
|
|
await db.delete(heartbeatRuns);
|
|
|
|
|
await db.delete(executionWorkspaces);
|
|
|
|
|
await db.delete(projectWorkspaces);
|
|
|
|
|
await db.delete(projects);
|
|
|
|
|
await db.delete(goals);
|
|
|
|
|
await db.delete(agents);
|
|
|
|
|
await db.delete(instanceSettings);
|
|
|
|
|
await db.delete(companies);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
await tempDb?.cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function seedIssueWithRun(status: string | null) {
|
|
|
|
|
const companyId = randomUUID();
|
|
|
|
|
const agentId = randomUUID();
|
|
|
|
|
const issueId = randomUUID();
|
|
|
|
|
const runId = status ? randomUUID() : null;
|
|
|
|
|
|
|
|
|
|
await db.insert(companies).values({
|
|
|
|
|
id: companyId,
|
|
|
|
|
name: "Paperclip",
|
|
|
|
|
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
|
|
|
|
requireBoardApprovalForNewAgents: false,
|
|
|
|
|
});
|
|
|
|
|
await db.insert(agents).values({
|
|
|
|
|
id: agentId,
|
|
|
|
|
companyId,
|
|
|
|
|
name: "CodexCoder",
|
|
|
|
|
role: "engineer",
|
|
|
|
|
status: "active",
|
|
|
|
|
adapterType: "codex_local",
|
|
|
|
|
adapterConfig: {},
|
|
|
|
|
runtimeConfig: {},
|
|
|
|
|
permissions: {},
|
|
|
|
|
});
|
|
|
|
|
if (runId) {
|
|
|
|
|
await db.insert(heartbeatRuns).values({
|
|
|
|
|
id: runId,
|
|
|
|
|
companyId,
|
|
|
|
|
agentId,
|
|
|
|
|
status,
|
|
|
|
|
invocationSource: "manual",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
await db.insert(issues).values({
|
|
|
|
|
id: issueId,
|
|
|
|
|
companyId,
|
|
|
|
|
title: "Execution lock",
|
|
|
|
|
status: "in_progress",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
assigneeAgentId: agentId,
|
|
|
|
|
executionRunId: runId,
|
|
|
|
|
executionAgentNameKey: runId ? "codexcoder" : null,
|
|
|
|
|
executionLockedAt: runId ? new Date() : null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { issueId, runId };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
it("clears execution locks owned by terminal runs", async () => {
|
|
|
|
|
const { issueId } = await seedIssueWithRun("failed");
|
|
|
|
|
|
|
|
|
|
await expect(svc.clearExecutionRunIfTerminal(issueId)).resolves.toBe(true);
|
|
|
|
|
|
|
|
|
|
const row = await db
|
|
|
|
|
.select({
|
|
|
|
|
executionRunId: issues.executionRunId,
|
|
|
|
|
executionAgentNameKey: issues.executionAgentNameKey,
|
|
|
|
|
executionLockedAt: issues.executionLockedAt,
|
|
|
|
|
})
|
|
|
|
|
.from(issues)
|
|
|
|
|
.where(eq(issues.id, issueId))
|
|
|
|
|
.then((rows) => rows[0]);
|
|
|
|
|
expect(row).toEqual({
|
|
|
|
|
executionRunId: null,
|
|
|
|
|
executionAgentNameKey: null,
|
|
|
|
|
executionLockedAt: null,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not clear execution locks owned by live runs", async () => {
|
|
|
|
|
const { issueId, runId } = await seedIssueWithRun("running");
|
|
|
|
|
|
|
|
|
|
await expect(svc.clearExecutionRunIfTerminal(issueId)).resolves.toBe(false);
|
|
|
|
|
|
|
|
|
|
const row = await db
|
|
|
|
|
.select({
|
|
|
|
|
executionRunId: issues.executionRunId,
|
|
|
|
|
executionAgentNameKey: issues.executionAgentNameKey,
|
|
|
|
|
executionLockedAt: issues.executionLockedAt,
|
|
|
|
|
})
|
|
|
|
|
.from(issues)
|
|
|
|
|
.where(eq(issues.id, issueId))
|
|
|
|
|
.then((rows) => rows[0]);
|
|
|
|
|
expect(row?.executionRunId).toBe(runId);
|
|
|
|
|
expect(row?.executionAgentNameKey).toBe("codexcoder");
|
|
|
|
|
expect(row?.executionLockedAt).toBeInstanceOf(Date);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not update issues without an execution lock", async () => {
|
|
|
|
|
const { issueId } = await seedIssueWithRun(null);
|
|
|
|
|
|
|
|
|
|
await expect(svc.clearExecutionRunIfTerminal(issueId)).resolves.toBe(false);
|
|
|
|
|
|
|
|
|
|
const row = await db
|
|
|
|
|
.select({ executionRunId: issues.executionRunId, executionLockedAt: issues.executionLockedAt })
|
|
|
|
|
.from(issues)
|
|
|
|
|
.where(eq(issues.id, issueId))
|
|
|
|
|
.then((rows) => rows[0]);
|
|
|
|
|
expect(row).toEqual({ executionRunId: null, executionLockedAt: null });
|
|
|
|
|
});
|
|
|
|
|
});
|