Files
PaperClipAI/packages/db/src/migrations/0109_routine_description_annotations.sql
T
DottaandPaperclip e68188c438 fix: upstream deployed document-comment, routine-annotation, workspace & board-polling fixes (#8536)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - This change touches several already-shipped subsystems —
document-comment annotations, the selected-agent Conference Room chat
surface, routine description annotations, workspace-operation tracking,
and the board polling/inbox UI
> - A batch of incremental fixes and two small backend additions had
accumulated on a local mainline and were deployed to a live instance,
but never landed upstream — so each `origin/master` sync kept
re-diverging
> - Leaving them un-upstreamed means the same delta has to be re-merged
on every sync and risks being lost or silently reverted
> - This pull request rebases that delta cleanly on top of current
`origin/master` (preserving recent upstream work such as reusable
sandbox leases and the relation-list collapse controls) and brings it up
for review
> - The benefit is that mainline and the deployed instance converge, and
these fixes/additions get normal review + CI + Greptile coverage

## Linked Issues or Issue Description

No single GitHub issue tracks this; it is a bundle of bug fixes and two
small feature additions. Following the issue-template fields:

**Bug fixes (what was wrong → what this does):**
- Document comments rendered out of document order, didn't live-update
across clients, swallowed save failures, and lost the markdown text
selection on re-render. Now: doc-order sort, live updates, surfaced
save-failure state, stable selection across re-renders.
- The board polling hot path returned oversized payloads on every poll.
Now: an opt-in `summary` projection trims the heartbeat-run list
payload.
- Assorted UI fixes: sidebar nav peek/streamlining edge cases, markdown
file-viewer re-mount/line-height issues on iOS Safari, inbox badge/skill
deep-link tab selection, and `⌘.` work-mode cycling on iOS.

**Feature additions:**
- `workspace_operations.issue_id` — associate a workspace operation with
the issue that triggered it (new migration `0106`, schema, service,
shared type).
- Routine **description annotations** — comment threads on a routine's
description document, mirroring issue document annotations (new
migration `0107`, `routine_documents` schema, routes/service,
editable-sections UI).
- Selected-agent **Conference Room chat** surface wiring and live
issue-thread updates.

**Related PR:** #8229 (`feat(control-plane): add annotation and
workspace controls`, draft) covers overlapping
annotation/workspace-control territory — flagging it so a reviewer can
reconcile the two rather than double-merging.

## What Changed

- `feat(workspace)`: `workspace_operations.issue_id` migration +
schema/service/type; issue workspace property controls reconciled with
upstream's evolved "Service" row.
- `feat(routines)`: routine description annotations —
`routine_documents` schema, migration `0107`, routes/service,
`editable-sections` UI.
- `fix(document-comments)`: doc-order sort, live updates, save-failure
surfacing, stable markdown selection (+ storybook story, rerender test).
- `feat(chat)`: selected-agent Conference Room chat surface and live
issue-thread updates (`LiveUpdatesProvider`, `issue-chat-messages`,
interactions service).
- `perf(board)`: opt-in `summary` projection for the board
polling/heartbeat-run list payload, plus assorted sidebar / file-viewer
/ inbox / IssueProperties UI fixes.

Organized into 5 logical commits. Migrations are numbered incrementally
after upstream's latest (`0105`) — `0106` then `0107`, no journal
collision.

## Verification

- Built by 3-way merging the deployed delta onto current
`origin/master`; the only merge conflict (`IssueProperties.test.tsx`,
two adjacent test blocks) was resolved in favor of upstream's evolved
"green service link above the workspace row" layout, which matches the
merged component's rendered output.
- Confirmed recent upstream work is preserved post-merge: reusable
sandbox lease teardown (`#8513`), the IssueProperties relation-list
collapse controls, and the sidebar streamlined-nav default.
- Confirmed the net diff vs `origin/master` is exactly the intended
feature delta (65 files) and that overlapping server files (`issues.ts`,
`agents.ts`, `heartbeat.ts`) only add feature code without disturbing
upstream logic.
- This delta is already running on a live deployed instance.
- Full typecheck/test suite + Greptile to run in CI (see checklist).

## Risks

- Two new migrations (`0106`, `0107`). Both are additive (new table /
new nullable column) and ordered after upstream's `0105`; no data
backfill, low risk. If another migration-bearing PR merges first,
renumber before merge.
- Largest blast radius is in the merged overlapping UI/service files;
covered by the existing test suites for those files plus CI.
- Overlaps thematically with draft PR #8229 — reviewers should reconcile
rather than merge both blindly.

## Model Used

Claude Opus 4.8 (`claude-opus-4-8`), extended thinking, with tool use
(git, shell). Agentic coding workflow.

## 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)
- [ ] 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>
2026-06-23 09:00:14 -05:00

171 lines
6.2 KiB
SQL

CREATE TABLE IF NOT EXISTS "routine_documents" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"routine_id" uuid NOT NULL,
"document_id" uuid NOT NULL,
"key" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM "pg_constraint" WHERE "conname" = 'routine_documents_company_id_companies_id_fk'
) THEN
ALTER TABLE "routine_documents" ADD CONSTRAINT "routine_documents_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;
END IF;
END $$;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM "pg_constraint" WHERE "conname" = 'routine_documents_routine_id_routines_id_fk'
) THEN
ALTER TABLE "routine_documents" ADD CONSTRAINT "routine_documents_routine_id_routines_id_fk" FOREIGN KEY ("routine_id") REFERENCES "public"."routines"("id") ON DELETE cascade ON UPDATE no action;
END IF;
END $$;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM "pg_constraint" WHERE "conname" = 'routine_documents_document_id_documents_id_fk'
) THEN
ALTER TABLE "routine_documents" ADD CONSTRAINT "routine_documents_document_id_documents_id_fk" FOREIGN KEY ("document_id") REFERENCES "public"."documents"("id") ON DELETE cascade ON UPDATE no action;
END IF;
END $$;
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "routine_documents_company_routine_key_uq" ON "routine_documents" USING btree ("company_id","routine_id","key");
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "routine_documents_document_uq" ON "routine_documents" USING btree ("document_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "routine_documents_company_routine_updated_idx" ON "routine_documents" USING btree ("company_id","routine_id","updated_at");
--> statement-breakpoint
ALTER TABLE "document_annotation_threads" ADD COLUMN IF NOT EXISTS "routine_id" uuid;
--> statement-breakpoint
ALTER TABLE "document_annotation_comments" ADD COLUMN IF NOT EXISTS "routine_id" uuid;
--> statement-breakpoint
ALTER TABLE "document_annotation_threads" ALTER COLUMN "issue_id" DROP NOT NULL;
--> statement-breakpoint
ALTER TABLE "document_annotation_comments" ALTER COLUMN "issue_id" DROP NOT NULL;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM "pg_constraint" WHERE "conname" = 'document_annotation_threads_routine_id_routines_id_fk'
) THEN
ALTER TABLE "document_annotation_threads" ADD CONSTRAINT "document_annotation_threads_routine_id_routines_id_fk" FOREIGN KEY ("routine_id") REFERENCES "public"."routines"("id") ON DELETE cascade ON UPDATE no action;
END IF;
END $$;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM "pg_constraint" WHERE "conname" = 'document_annotation_comments_routine_id_routines_id_fk'
) THEN
ALTER TABLE "document_annotation_comments" ADD CONSTRAINT "document_annotation_comments_routine_id_routines_id_fk" FOREIGN KEY ("routine_id") REFERENCES "public"."routines"("id") ON DELETE cascade ON UPDATE no action;
END IF;
END $$;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM "pg_constraint" WHERE "conname" = 'document_annotation_threads_owner_check'
) THEN
ALTER TABLE "document_annotation_threads" ADD CONSTRAINT "document_annotation_threads_owner_check" CHECK ("issue_id" IS NOT NULL OR "routine_id" IS NOT NULL);
END IF;
END $$;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM "pg_constraint" WHERE "conname" = 'document_annotation_comments_owner_check'
) THEN
ALTER TABLE "document_annotation_comments" ADD CONSTRAINT "document_annotation_comments_owner_check" CHECK ("issue_id" IS NOT NULL OR "routine_id" IS NOT NULL);
END IF;
END $$;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "document_annotation_threads_company_routine_status_idx" ON "document_annotation_threads" USING btree ("company_id","routine_id","status");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "document_annotation_comments_company_routine_created_at_idx" ON "document_annotation_comments" USING btree ("company_id","routine_id","created_at");
--> statement-breakpoint
DO $$
DECLARE
routine_row RECORD;
created_document_id uuid;
created_revision_id uuid;
BEGIN
FOR routine_row IN
SELECT *
FROM "routines"
WHERE NOT EXISTS (
SELECT 1
FROM "routine_documents"
WHERE "routine_documents"."routine_id" = "routines"."id"
AND "routine_documents"."key" = 'description'
)
LOOP
INSERT INTO "documents" (
"company_id",
"title",
"format",
"latest_body",
"latest_revision_number",
"created_by_agent_id",
"created_by_user_id",
"updated_by_agent_id",
"updated_by_user_id",
"created_at",
"updated_at"
)
VALUES (
routine_row."company_id",
'routine description',
'markdown',
coalesce(routine_row."description", ''),
1,
routine_row."created_by_agent_id",
routine_row."created_by_user_id",
routine_row."updated_by_agent_id",
routine_row."updated_by_user_id",
coalesce(routine_row."created_at", now()),
coalesce(routine_row."updated_at", now())
)
RETURNING "id" INTO created_document_id;
INSERT INTO "document_revisions" (
"company_id",
"document_id",
"revision_number",
"title",
"format",
"body",
"change_summary",
"created_by_agent_id",
"created_by_user_id",
"created_at"
)
VALUES (
routine_row."company_id",
created_document_id,
1,
'routine description',
'markdown',
coalesce(routine_row."description", ''),
'Backfilled routine description',
routine_row."created_by_agent_id",
routine_row."created_by_user_id",
coalesce(routine_row."created_at", now())
)
RETURNING "id" INTO created_revision_id;
UPDATE "documents"
SET "latest_revision_id" = created_revision_id
WHERE "id" = created_document_id;
INSERT INTO "routine_documents" ("company_id", "routine_id", "document_id", "key", "created_at", "updated_at")
VALUES (
routine_row."company_id",
routine_row."id",
created_document_id,
'description',
coalesce(routine_row."created_at", now()),
coalesce(routine_row."updated_at", now())
)
ON CONFLICT DO NOTHING;
END LOOP;
END $$;