mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 07:10:34 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The database migration layer keeps local and deployed instances moving forward safely as schema/data contracts evolve. > - The responsible-user backfill migration could make historical issues look newly updated by bumping user-visible `updated_at` columns during a backfill. > - That timestamp churn can invalidate inbox/archive state and make old work appear fresh even though no user-facing activity happened. > - This pull request moves the responsible-user invariant migration later in the current migration sequence, keeps it from touching user-visible timestamps, and adds a repair sweep for databases that already saw the timestamp bump. > - The benefit is safer migration replay and regression coverage for future backfills that might otherwise mutate visible timestamps. ## Linked Issues or Issue Description No public GitHub issue exists. Inline bug report: **Pre-submission checklist** - [x] I have searched existing open and closed issues and this is not a duplicate. - [x] I am on the latest released version of Paperclip (or can reproduce on `master`). - [x] I have confirmed the error originates in Paperclip itself — not in my agent adapter, API provider, or local configuration. **What happened?** A responsible-user migration backfill could update user-visible `updated_at` columns while filling missing responsible-user data. That makes historical issues/runs/routines appear newer even when no user-facing activity happened. **Expected behavior** Responsible-user backfills should populate ownership metadata without mutating user-visible recency fields, and databases already affected by a timestamp sweep should be repairable. **Steps to reproduce** 1. Start from current `master` with the responsible-user migration sequence. 2. Apply migrations to a database containing historical issues, runs, routines, and companies with older activity timestamps. 3. Replay the responsible-user invariant migration and inspect user-visible `updated_at` values. **Paperclip version or commit** `master` at the PR base. **Deployment mode** Local dev (`pnpm dev`) and deployed instances using the same migrations. **Installation method** Built from source (`pnpm dev` / `pnpm build`). **Agent adapter(s) involved** - [x] Not adapter-specific (core bug) **Database mode** External Postgres and embedded development Postgres migration paths. **Access context** Board and agent-visible issue recency can both be affected. **Relevant logs or output** Covered by the added embedded-Postgres regression tests. **Relevant config (if applicable)** Not applicable. **Additional context** This PR adapts an extracted local migration fix onto the current master migration sequence, where `0133` is already occupied. **Privacy checklist** - [x] I have reviewed all pasted output for PII (usernames, file paths, API keys, tokens, company names) and redacted where necessary. ## What Changed - Renumbered the responsible-user invariant migration onto the current master migration sequence. - Added a repair migration that detects broad timestamp sweeps and restores safer `updated_at` values for issues, heartbeat runs, routines, routine runs, and companies. - Added focused embedded-Postgres regression coverage for the relocated migration, repair migration, and updated-at backfill allowlist. ## Verification - `pnpm --filter @paperclipai/db run check:migrations` - `/srv/paperclip/home/paperclipai/paperclip/node_modules/.bin/vitest run packages/db/src/client.test.ts -t "migration 0134|migration 0135|unallowlisted migration backfills"` ## Risks Migration behavior is the main risk. The PR intentionally changes the active migration sequence by removing the old responsible-user invariant slot and replaying that work later with a repair migration. Reviewers should confirm this matches the intended release/migration policy for installations that may already have applied the earlier migration. > 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.5 coding agent with repository tool use and local shell execution. Context window was not surfaced 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 checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [ ] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
212 lines
7.0 KiB
SQL
212 lines
7.0 KiB
SQL
ALTER TABLE "companies" ADD COLUMN IF NOT EXISTS "default_responsible_user_id" text;
|
|
--> statement-breakpoint
|
|
WITH owner_defaults AS (
|
|
SELECT DISTINCT ON ("company_id")
|
|
"company_id",
|
|
"principal_id" AS "user_id"
|
|
FROM "company_memberships"
|
|
WHERE "principal_type" = 'user'
|
|
AND "status" = 'active'
|
|
AND "membership_role" = 'owner'
|
|
ORDER BY "company_id", "created_at" ASC, "id" ASC
|
|
)
|
|
UPDATE "companies" AS c
|
|
SET "default_responsible_user_id" = owner_defaults."user_id"
|
|
FROM owner_defaults
|
|
WHERE c."id" = owner_defaults."company_id"
|
|
AND c."default_responsible_user_id" IS NULL;
|
|
--> statement-breakpoint
|
|
WITH RECURSIVE issue_chain AS (
|
|
SELECT
|
|
child."id" AS "issue_id",
|
|
child."company_id",
|
|
child."parent_id",
|
|
child."responsible_user_id",
|
|
child."created_by_user_id",
|
|
0 AS "depth"
|
|
FROM "issues" AS child
|
|
WHERE child."responsible_user_id" IS NULL
|
|
UNION ALL
|
|
SELECT
|
|
issue_chain."issue_id",
|
|
parent."company_id",
|
|
parent."parent_id",
|
|
parent."responsible_user_id",
|
|
parent."created_by_user_id",
|
|
issue_chain."depth" + 1
|
|
FROM issue_chain
|
|
JOIN "issues" AS parent
|
|
ON parent."id" = issue_chain."parent_id"
|
|
AND parent."company_id" = issue_chain."company_id"
|
|
WHERE issue_chain."depth" < 50
|
|
),
|
|
resolved_issue_users AS (
|
|
SELECT DISTINCT ON ("issue_id")
|
|
"issue_id",
|
|
COALESCE("responsible_user_id", "created_by_user_id") AS "user_id"
|
|
FROM issue_chain
|
|
WHERE COALESCE("responsible_user_id", "created_by_user_id") IS NOT NULL
|
|
ORDER BY "issue_id", "depth" ASC
|
|
)
|
|
UPDATE "issues" AS i
|
|
SET "responsible_user_id" = resolved_issue_users."user_id"
|
|
FROM resolved_issue_users
|
|
WHERE i."id" = resolved_issue_users."issue_id"
|
|
AND i."responsible_user_id" IS NULL;
|
|
--> statement-breakpoint
|
|
UPDATE "issues" AS i
|
|
SET "responsible_user_id" = c."default_responsible_user_id"
|
|
FROM "companies" AS c
|
|
WHERE i."company_id" = c."id"
|
|
AND i."responsible_user_id" IS NULL
|
|
AND c."default_responsible_user_id" IS NOT NULL;
|
|
--> statement-breakpoint
|
|
WITH routine_responsible_users AS (
|
|
SELECT
|
|
r."id",
|
|
COALESCE(r."created_by_user_id", parent_issue."responsible_user_id", c."default_responsible_user_id") AS "user_id"
|
|
FROM "routines" AS r
|
|
JOIN "companies" AS c ON c."id" = r."company_id"
|
|
LEFT JOIN "issues" AS parent_issue
|
|
ON parent_issue."id" = r."parent_issue_id"
|
|
AND parent_issue."company_id" = r."company_id"
|
|
WHERE r."responsible_user_id" IS NULL
|
|
)
|
|
UPDATE "routines" AS r
|
|
SET "responsible_user_id" = routine_responsible_users."user_id"
|
|
FROM routine_responsible_users
|
|
WHERE r."id" = routine_responsible_users."id"
|
|
AND routine_responsible_users."user_id" IS NOT NULL;
|
|
--> statement-breakpoint
|
|
WITH routine_revision_responsible_users AS (
|
|
SELECT
|
|
rr."id",
|
|
COALESCE(rr."created_by_user_id", r."responsible_user_id", c."default_responsible_user_id") AS "user_id"
|
|
FROM "routine_revisions" AS rr
|
|
JOIN "routines" AS r
|
|
ON rr."routine_id" = r."id"
|
|
AND rr."company_id" = r."company_id"
|
|
JOIN "companies" AS c ON c."id" = rr."company_id"
|
|
WHERE rr."responsible_user_id" IS NULL
|
|
)
|
|
UPDATE "routine_revisions" AS rr
|
|
SET "responsible_user_id" = routine_revision_responsible_users."user_id"
|
|
FROM routine_revision_responsible_users
|
|
WHERE rr."id" = routine_revision_responsible_users."id"
|
|
AND routine_revision_responsible_users."user_id" IS NOT NULL;
|
|
--> statement-breakpoint
|
|
WITH routine_run_responsible_users AS (
|
|
SELECT
|
|
rr."id",
|
|
COALESCE(linked_issue."responsible_user_id", r."responsible_user_id", c."default_responsible_user_id") AS "user_id"
|
|
FROM "routine_runs" AS rr
|
|
JOIN "routines" AS r
|
|
ON rr."routine_id" = r."id"
|
|
AND rr."company_id" = r."company_id"
|
|
JOIN "companies" AS c ON c."id" = rr."company_id"
|
|
LEFT JOIN "issues" AS linked_issue
|
|
ON linked_issue."id" = rr."linked_issue_id"
|
|
AND linked_issue."company_id" = rr."company_id"
|
|
WHERE rr."responsible_user_id" IS NULL
|
|
)
|
|
UPDATE "routine_runs" AS rr
|
|
SET "responsible_user_id" = routine_run_responsible_users."user_id"
|
|
FROM routine_run_responsible_users
|
|
WHERE rr."id" = routine_run_responsible_users."id"
|
|
AND routine_run_responsible_users."user_id" IS NOT NULL;
|
|
--> statement-breakpoint
|
|
UPDATE "heartbeat_runs" AS h
|
|
SET "responsible_user_id" = original."responsible_user_id"
|
|
FROM "heartbeat_runs" AS original
|
|
WHERE h."retry_of_run_id" = original."id"
|
|
AND h."company_id" = original."company_id"
|
|
AND h."responsible_user_id" IS NULL
|
|
AND original."responsible_user_id" IS NOT NULL;
|
|
--> statement-breakpoint
|
|
WITH extracted_run_refs AS (
|
|
SELECT
|
|
h."id" AS "run_id",
|
|
h."company_id",
|
|
NULLIF(h."context_snapshot" ->> 'issueId', '') AS "issue_ref",
|
|
1 AS "ref_priority"
|
|
FROM "heartbeat_runs" AS h
|
|
WHERE h."responsible_user_id" IS NULL
|
|
AND NULLIF(h."context_snapshot" ->> 'issueId', '') IS NOT NULL
|
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
h."id" AS "run_id",
|
|
h."company_id",
|
|
NULLIF(h."context_snapshot" ->> 'taskId', '') AS "issue_ref",
|
|
2 AS "ref_priority"
|
|
FROM "heartbeat_runs" AS h
|
|
WHERE h."responsible_user_id" IS NULL
|
|
AND NULLIF(h."context_snapshot" ->> 'taskId', '') IS NOT NULL
|
|
),
|
|
uuid_run_refs AS (
|
|
SELECT
|
|
"run_id",
|
|
"company_id",
|
|
"issue_ref"::uuid AS "issue_id",
|
|
"ref_priority"
|
|
FROM extracted_run_refs
|
|
WHERE "issue_ref" ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
),
|
|
resolved_run_users AS (
|
|
SELECT DISTINCT ON ("run_id")
|
|
"run_id",
|
|
"responsible_user_id"
|
|
FROM (
|
|
SELECT
|
|
refs."run_id",
|
|
i."responsible_user_id",
|
|
refs."ref_priority",
|
|
1 AS "match_priority"
|
|
FROM uuid_run_refs AS refs
|
|
JOIN "issues" AS i
|
|
ON i."id" = refs."issue_id"
|
|
AND i."company_id" = refs."company_id"
|
|
WHERE i."responsible_user_id" IS NOT NULL
|
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
refs."run_id",
|
|
i."responsible_user_id",
|
|
refs."ref_priority",
|
|
2 AS "match_priority"
|
|
FROM extracted_run_refs AS refs
|
|
JOIN "issues" AS i
|
|
ON i."identifier" = refs."issue_ref"
|
|
AND i."company_id" = refs."company_id"
|
|
WHERE refs."issue_ref" IS NOT NULL
|
|
AND i."responsible_user_id" IS NOT NULL
|
|
) AS candidates
|
|
ORDER BY "run_id", "ref_priority" ASC, "match_priority" ASC
|
|
)
|
|
UPDATE "heartbeat_runs" AS h
|
|
SET "responsible_user_id" = resolved_run_users."responsible_user_id"
|
|
FROM resolved_run_users
|
|
WHERE h."id" = resolved_run_users."run_id"
|
|
AND h."responsible_user_id" IS NULL;
|
|
--> statement-breakpoint
|
|
UPDATE "heartbeat_runs" AS h
|
|
SET "responsible_user_id" = awr."requested_by_actor_id"
|
|
FROM "agent_wakeup_requests" AS awr
|
|
WHERE h."wakeup_request_id" = awr."id"
|
|
AND h."company_id" = awr."company_id"
|
|
AND h."responsible_user_id" IS NULL
|
|
AND awr."requested_by_actor_type" = 'user'
|
|
AND awr."requested_by_actor_id" IS NOT NULL;
|
|
--> statement-breakpoint
|
|
UPDATE "heartbeat_runs" AS h
|
|
SET "responsible_user_id" = c."default_responsible_user_id"
|
|
FROM "companies" AS c
|
|
WHERE h."company_id" = c."id"
|
|
AND h."responsible_user_id" IS NULL
|
|
AND c."default_responsible_user_id" IS NOT NULL;
|
|
--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "companies_default_responsible_user_idx"
|
|
ON "companies" ("default_responsible_user_id");
|