From 746e287e33f94e8d3d3ab26d6c578e1825826fe2 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:00:03 -0500 Subject: [PATCH] feat(control-plane): add annotation and workspace controls (#8229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI-agent companies. > - The control plane coordinates issues, workspaces, documents, routines, and board review flows across company-scoped data. > - The local source branch contained related schema, service, and UI changes for workspace issue scoping and document/routine annotations. > - These changes need to move together because db schema, shared types, server services, and UI consumers form one contract. > - This pull request extracts the migration-bearing control-plane work from the source branch onto `origin/master`. > - The benefit is a standalone branch with deterministic migration order and focused review for the highest-risk part of the split. ## Linked Issues or Issue Description No GitHub issue exists for this branch split. Internal source task: [PAP-11234](/PAP/issues/PAP-11234). Problem/motivation: - Workspace operations need explicit issue scoping so readiness and blocker handling can be derived correctly. - Document annotations need reliable live updates, save failure surfacing, normalized activity keys, and better comment panel behavior. - Routine descriptions need the same annotation contract as issue documents so operators can discuss and edit routine text without special-case infrastructure. Proposed solution: - Add the workspace-operation `issueId` migration and readiness scoping. - Add routine document/annotation schema, shared types, services, routes, and UI editing support. - Keep the related migrations in one PR so the renumbered `0106` and `0107` migrations land in a deterministic order after current `master`. Alternatives considered: - Split migrations into separate PRs, rejected because that would create migration-numbering conflicts and make each branch less standalone. - Merge this with UI polish, rejected because this branch needs deeper server/db review. Roadmap alignment: - Checked `ROADMAP.md`; the roadmap mentions future recurring routine capabilities generally, but no duplicate implementation PR for these annotation/workspace changes was found. ## What Changed - Added `0106_workspace_operations_issue_id.sql` and `0107_routine_description_annotations.sql`, plus schema exports. - Scoped workspace readiness to blocker issues and attached workspace operation issue ids. - Scoped issue-thread interaction accept finalization to the source run. - Added routine document annotation contracts across db/shared/server/UI. - Improved document annotation live updates, activity-key normalization, save failure surfacing, and comment panel behavior. - Added issue workspace property controls and compact blocked-by/quick-control UI updates. - Added focused server and UI regression tests for the new contracts. ## Verification - `CI=true NODE_ENV=development pnpm install --frozen-lockfile --prefer-offline` - `NODE_ENV=test pnpm exec vitest server/src/__tests__/document-annotation-routes.test.ts server/src/__tests__/issue-thread-interactions-service.test.ts server/src/__tests__/issues-service.test.ts server/src/__tests__/routine-document-annotation-routes.test.ts server/src/__tests__/routines-routes.test.ts server/src/__tests__/workspace-runtime.test.ts ui/src/components/IssueDocumentAnnotations.test.tsx ui/src/components/IssueProperties.test.tsx ui/src/components/WorkspaceRuntimeControls.test.tsx ui/src/context/LiveUpdatesProvider.test.ts --run` — 10 files, 273 tests passed. - `NODE_ENV=test pnpm -r --filter @paperclipai/db --filter @paperclipai/shared --filter @paperclipai/server --filter @paperclipai/ui typecheck` — passed, including db migration numbering check. ## Risks - Migration-bearing PR; merge this branch before any later PR that adds migrations with higher numbers. - Cross-layer contract risk across db/shared/server/ui, mitigated with targeted tests and affected-package typecheck. - Review should pay special attention to company scoping in new routine/document annotation paths. > 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 GPT-5 Codex via Paperclip `codex_local` / CodexCoder, GPT-5-class coding model with tool use and shell execution. Exact runtime snapshot and context-window setting were not exposed by the Paperclip run context. ## 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 available from the run context) - [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 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 (N/A per source task: do not add screenshots/images unless specifically part of the work) - [x] I have updated relevant documentation to reflect my changes (N/A; no public docs changed) - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- .../schema/document_annotation_comments.ts | 6 +-- .../src/schema/document_annotation_threads.ts | 6 +-- .../heartbeat-dependency-scheduling.test.ts | 2 + server/src/__tests__/issues-service.test.ts | 26 +++++++++++- .../__tests__/plugin-managed-routines.test.ts | 6 +++ .../__tests__/qa-routine-secrets-e2e.test.ts | 6 +++ .../__tests__/routine-run-telemetry.test.ts | 6 +++ server/src/__tests__/routines-e2e.test.ts | 12 ++++++ server/src/__tests__/routines-service.test.ts | 6 +++ server/src/routes/openapi.ts | 40 +++++++++++++++++++ server/src/services/document-annotations.ts | 23 +++++++++-- server/src/services/issues.ts | 34 ++++++++++++---- server/src/services/routines.ts | 6 +++ ui/src/context/LiveUpdatesProvider.tsx | 5 +++ 14 files changed, 165 insertions(+), 19 deletions(-) diff --git a/packages/db/src/schema/document_annotation_comments.ts b/packages/db/src/schema/document_annotation_comments.ts index afa8069e43..f5273ad1e0 100644 --- a/packages/db/src/schema/document_annotation_comments.ts +++ b/packages/db/src/schema/document_annotation_comments.ts @@ -51,9 +51,9 @@ export const documentAnnotationComments = pgTable( ), issueCommentIdx: index("document_annotation_comments_issue_comment_idx").on(table.issueCommentId), bodySearchIdx: index("document_annotation_comments_body_search_idx").using("gin", table.body.op("gin_trgm_ops")), - ownerCheck: check( - "document_annotation_comments_owner_check", - sql`${table.issueId} IS NOT NULL OR ${table.routineId} IS NOT NULL`, + exactlyOneOwnerChk: check( + "document_annotation_comments_exactly_one_owner_chk", + sql`num_nonnulls(${table.issueId}, ${table.routineId}) = 1`, ), }), ); diff --git a/packages/db/src/schema/document_annotation_threads.ts b/packages/db/src/schema/document_annotation_threads.ts index 2d24751c8f..d09c40d28d 100644 --- a/packages/db/src/schema/document_annotation_threads.ts +++ b/packages/db/src/schema/document_annotation_threads.ts @@ -74,9 +74,9 @@ export const documentAnnotationThreads = pgTable( table.companyId, table.anchorState, ), - ownerCheck: check( - "document_annotation_threads_owner_check", - sql`${table.issueId} IS NOT NULL OR ${table.routineId} IS NOT NULL`, + exactlyOneOwnerChk: check( + "document_annotation_threads_exactly_one_owner_chk", + sql`num_nonnulls(${table.issueId}, ${table.routineId}) = 1`, ), }), ); diff --git a/server/src/__tests__/heartbeat-dependency-scheduling.test.ts b/server/src/__tests__/heartbeat-dependency-scheduling.test.ts index 12b70a4526..ea9987d19c 100644 --- a/server/src/__tests__/heartbeat-dependency-scheduling.test.ts +++ b/server/src/__tests__/heartbeat-dependency-scheduling.test.ts @@ -146,6 +146,8 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () = await db.delete(environments); await db.delete(workspaceOperations); await db.delete(executionWorkspaces); + await db.delete(environmentLeases); + await db.delete(companySkills); await db.delete(companies); }); diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 1074c6869e..8064953df4 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -3208,7 +3208,7 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness", }); }); - it("ignores unattributed pre-backfill workspace operations when checking blocker readiness", async () => { + it("keeps dependents blocked on unattributed workspace operations for the blocker workspace", async () => { const { companyId, executionWorkspaceId, @@ -3225,6 +3225,22 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness", startedAt: new Date("2026-05-23T22:00:00.000Z"), }); + await expect(svc.listWakeableBlockedDependents(blockerId)).resolves.toEqual([]); + await expect(svc.getDependencyReadiness(dependentId)).resolves.toMatchObject({ + isDependencyReady: false, + pendingFinalizeBlockerIssueIds: [blockerId], + unresolvedBlockerIssueIds: [blockerId], + }); + + await db.insert(workspaceOperations).values({ + companyId, + executionWorkspaceId, + issueId: null, + phase: "workspace_finalize", + status: "succeeded", + startedAt: new Date("2026-05-23T22:05:00.000Z"), + }); + await expect(svc.listWakeableBlockedDependents(blockerId)).resolves.toEqual([ expect.objectContaining({ id: dependentId, @@ -3286,6 +3302,14 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness", status: "succeeded", startedAt: new Date("2026-05-23T22:10:00.000Z"), }); + await db.insert(workspaceOperations).values({ + companyId, + executionWorkspaceId, + issueId: null, + phase: "worktree_prepare", + status: "succeeded", + startedAt: new Date("2026-05-23T22:15:00.000Z"), + }); await expect(svc.listWakeableBlockedDependents(blockerId)).resolves.toEqual([ expect.objectContaining({ diff --git a/server/src/__tests__/plugin-managed-routines.test.ts b/server/src/__tests__/plugin-managed-routines.test.ts index 6f89cac17a..97f93ac6f0 100644 --- a/server/src/__tests__/plugin-managed-routines.test.ts +++ b/server/src/__tests__/plugin-managed-routines.test.ts @@ -7,10 +7,13 @@ import { agents, companies, createDb, + documentRevisions, + documents, issues, pluginManagedResources, plugins, projects, + routineDocuments, routineRuns, routineTriggers, routines, @@ -108,7 +111,10 @@ describeEmbeddedPostgres("plugin-managed routines", () => { afterEach(async () => { await db.delete(routineRuns); await db.delete(routineTriggers); + await db.delete(routineDocuments); await db.delete(routines); + await db.delete(documentRevisions); + await db.delete(documents); await db.delete(issues); await db.delete(agentConfigRevisions); await db.delete(activityLog); diff --git a/server/src/__tests__/qa-routine-secrets-e2e.test.ts b/server/src/__tests__/qa-routine-secrets-e2e.test.ts index 43d7910ac1..d1b11d2c3f 100644 --- a/server/src/__tests__/qa-routine-secrets-e2e.test.ts +++ b/server/src/__tests__/qa-routine-secrets-e2e.test.ts @@ -25,7 +25,10 @@ import { companySecrets, companySecretVersions, createDb, + documentRevisions, + documents, projects, + routineDocuments, routineRuns, routines, secretAccessEvents, @@ -61,7 +64,10 @@ describeEmbedded("PAP-9522 QA: routine secrets end-to-end", () => { await db.delete(secretAccessEvents); await db.delete(companySecretBindings); await db.delete(routineRuns); + await db.delete(routineDocuments); await db.delete(routines); + await db.delete(documents); + await db.delete(documentRevisions); await db.delete(companySecretVersions); await db.delete(companySecrets); await db.delete(projects); diff --git a/server/src/__tests__/routine-run-telemetry.test.ts b/server/src/__tests__/routine-run-telemetry.test.ts index ded455971a..f5e77ac2b3 100644 --- a/server/src/__tests__/routine-run-telemetry.test.ts +++ b/server/src/__tests__/routine-run-telemetry.test.ts @@ -5,11 +5,14 @@ import { agents, companies, createDb, + documentRevisions, + documents, executionWorkspaces, heartbeatRuns, issues, projectWorkspaces, projects, + routineDocuments, routineRuns, routines, routineTriggers, @@ -54,7 +57,10 @@ describeEmbeddedPostgres("routine run telemetry", () => { vi.clearAllMocks(); await db.delete(routineRuns); await db.delete(routineTriggers); + await db.delete(routineDocuments); await db.delete(routines); + await db.delete(documents); + await db.delete(documentRevisions); await db.delete(heartbeatRuns); await db.delete(issues); await db.delete(executionWorkspaces); diff --git a/server/src/__tests__/routines-e2e.test.ts b/server/src/__tests__/routines-e2e.test.ts index 8e2f0f8855..b93c148a03 100644 --- a/server/src/__tests__/routines-e2e.test.ts +++ b/server/src/__tests__/routines-e2e.test.ts @@ -10,6 +10,11 @@ import { companies, companyMemberships, createDb, + documentAnnotationAnchorSnapshots, + documentAnnotationComments, + documentAnnotationThreads, + documentRevisions, + documents, executionWorkspaces, heartbeatRunEvents, heartbeatRuns, @@ -18,6 +23,7 @@ import { principalPermissionGrants, projectWorkspaces, projects, + routineDocuments, routineRuns, routines, routineTriggers, @@ -96,6 +102,9 @@ describeEmbeddedPostgres("routine routes end-to-end", () => { afterEach(async () => { await db.delete(activityLog); + await db.delete(documentAnnotationAnchorSnapshots); + await db.delete(documentAnnotationComments); + await db.delete(documentAnnotationThreads); await db.delete(routineRuns); await db.delete(routineTriggers); await db.delete(heartbeatRunEvents); @@ -106,7 +115,10 @@ describeEmbeddedPostgres("routine routes end-to-end", () => { await db.delete(projectWorkspaces); await db.delete(principalPermissionGrants); await db.delete(companyMemberships); + await db.delete(routineDocuments); await db.delete(routines); + await db.delete(documentRevisions); + await db.delete(documents); await db.delete(projects); await db.delete(agents); await db.delete(companies); diff --git a/server/src/__tests__/routines-service.test.ts b/server/src/__tests__/routines-service.test.ts index f1797c7948..c5f6a1a470 100644 --- a/server/src/__tests__/routines-service.test.ts +++ b/server/src/__tests__/routines-service.test.ts @@ -9,6 +9,8 @@ import { companySecrets, companySecretVersions, createDb, + documentRevisions, + documents, executionWorkspaces, heartbeatRuns, instanceSettings, @@ -17,6 +19,7 @@ import { issues, projectWorkspaces, projects, + routineDocuments, routineRuns, routines, routineTriggers, @@ -65,6 +68,9 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => { await db.delete(routineRuns); await db.delete(routineTriggers); await db.delete(routines); + await db.delete(routineDocuments); + await db.delete(documents); + await db.delete(documentRevisions); await db.delete(companySecretVersions); await db.delete(companySecrets); await db.delete(heartbeatRuns); diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index 808fd7d955..e8849da6c0 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -621,6 +621,8 @@ const CREATED_OPERATIONS = new Set([ "POST /api/companies/{companyId}/labels", "POST /api/issues/{id}/documents/{key}/annotations", "POST /api/issues/{id}/documents/{key}/annotations/{threadId}/comments", + "POST /api/routines/{id}/description/annotations", + "POST /api/routines/{id}/description/annotations/{threadId}/comments", "POST /api/issues/{id}/work-products", "POST /api/issues/{id}/low-trust/promotions", "POST /api/issues/{id}/approvals", @@ -4504,6 +4506,44 @@ registerCurrentRoute({ body: updateDocumentAnnotationThreadSchema, }); +for (const route of [ + ["get", "/api/routines/{id}/description/annotations", "List routine description annotation threads"], + ["get", "/api/routines/{id}/description/annotations/{threadId}", "Get a routine description annotation thread"], +] as const) { + registerCurrentRoute({ + method: route[0], + path: route[1], + tags: ["routines"], + summary: route[2], + }); +} + +registerCurrentRoute({ + method: "post", + path: "/api/routines/{id}/description/annotations", + tags: ["routines"], + summary: "Create a routine description annotation thread", + body: createDocumentAnnotationThreadSchema, + responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound }, +}); + +registerCurrentRoute({ + method: "post", + path: "/api/routines/{id}/description/annotations/{threadId}/comments", + tags: ["routines"], + summary: "Add a routine description annotation comment", + body: createDocumentAnnotationCommentSchema, + responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound }, +}); + +registerCurrentRoute({ + method: "patch", + path: "/api/routines/{id}/description/annotations/{threadId}", + tags: ["routines"], + summary: "Update a routine description annotation thread", + body: updateDocumentAnnotationThreadSchema, +}); + registerCurrentRoute({ method: "get", path: "/api/issues/{id}/recovery-actions", diff --git a/server/src/services/document-annotations.ts b/server/src/services/document-annotations.ts index f98ce42686..207b7f2f52 100644 --- a/server/src/services/document-annotations.ts +++ b/server/src/services/document-annotations.ts @@ -146,7 +146,11 @@ export function documentAnnotationService(db: Db) { }) .from(routineDocuments) .innerJoin(documents, eq(routineDocuments.documentId, documents.id)) - .where(and(eq(routineDocuments.routineId, routineId), eq(routineDocuments.key, key))) + .where(and( + eq(routineDocuments.routineId, routineId), + eq(routineDocuments.key, key), + eq(routineDocuments.companyId, documents.companyId), + )) .then((rows: RoutineDocumentRow[]) => rows[0] ?? null); } @@ -171,6 +175,8 @@ export function documentAnnotationService(db: Db) { routineId: string, documentKey: string, threadId: string, + companyId: string, + documentId: string, dbOrTx: any = db, ): Promise { return dbOrTx @@ -178,7 +184,9 @@ export function documentAnnotationService(db: Db) { .from(documentAnnotationThreads) .where(and( eq(documentAnnotationThreads.id, threadId), + eq(documentAnnotationThreads.companyId, companyId), eq(documentAnnotationThreads.routineId, routineId), + eq(documentAnnotationThreads.documentId, documentId), eq(documentAnnotationThreads.documentKey, documentKey), )) .then((rows: DocumentAnnotationThread[]) => rows[0] ?? null); @@ -256,6 +264,7 @@ export function documentAnnotationService(db: Db) { const doc = await getRoutineDocument(routineId, key); if (!doc) throw notFound("Document not found"); const conditions = [ + eq(documentAnnotationThreads.companyId, doc.companyId), eq(documentAnnotationThreads.routineId, routineId), eq(documentAnnotationThreads.documentId, doc.documentId), ]; @@ -289,7 +298,9 @@ export function documentAnnotationService(db: Db) { }, getThreadForRoutineDocument: async (routineId: string, key: string, threadId: string) => { - const thread = await getThreadForRoutine(routineId, key, threadId); + const doc = await getRoutineDocument(routineId, key); + if (!doc) return null; + const thread = await getThreadForRoutine(routineId, key, threadId, doc.companyId, doc.documentId); if (!thread) return null; const comments = await commentsForThreads([thread.id]); return { ...thread, comments }; @@ -512,7 +523,9 @@ export function documentAnnotationService(db: Db) { input: CreateDocumentAnnotationComment, actor: ActorInput, ) => db.transaction(async (tx) => { - const thread = await getThreadForRoutine(routineId, key, threadId, tx); + const doc = await getRoutineDocument(routineId, key, tx); + if (!doc) throw notFound("Document not found"); + const thread = await getThreadForRoutine(routineId, key, threadId, doc.companyId, doc.documentId, tx); if (!thread) throw notFound("Annotation thread not found"); const now = new Date(); const [comment] = await tx @@ -646,7 +659,9 @@ export function documentAnnotationService(db: Db) { input: UpdateDocumentAnnotationThread, actor: ActorInput, ) => db.transaction(async (tx) => { - const thread = await getThreadForRoutine(routineId, key, threadId, tx); + const doc = await getRoutineDocument(routineId, key, tx); + if (!doc) throw notFound("Document not found"); + const thread = await getThreadForRoutine(routineId, key, threadId, doc.companyId, doc.documentId, tx); if (!thread) throw notFound("Annotation thread not found"); if (!input.status || input.status === thread.status) return thread; diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 3b51e6900e..e3442c17d9 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -698,6 +698,9 @@ async function listPendingFinalizeBlockerIssueIds( const blockerIssueIds = [...new Set(blockerWorkspacePairs.map((pair) => pair.blockerIssueId))]; const executionWorkspaceIds = [...new Set(blockerWorkspacePairs.map((pair) => pair.executionWorkspaceId))]; if (blockerIssueIds.length === 0 || executionWorkspaceIds.length === 0) return pending; + const blockerWorkspaceKeys = new Set( + blockerWorkspacePairs.map((pair) => `${pair.blockerIssueId}:${pair.executionWorkspaceId}`), + ); const rows = await dbOrTx .select({ @@ -711,18 +714,32 @@ async function listPendingFinalizeBlockerIssueIds( .where( and( eq(workspaceOperations.companyId, companyId), - inArray(workspaceOperations.issueId, blockerIssueIds), inArray(workspaceOperations.executionWorkspaceId, executionWorkspaceIds), + or(inArray(workspaceOperations.issueId, blockerIssueIds), isNull(workspaceOperations.issueId)), ), ); - const latestByBlockerWorkspace = new Map(); + const latestAttributedByBlockerWorkspace = new Map(); + const latestUnattributedByWorkspace = new Map(); for (const row of rows) { - if (!row.issueId || !row.executionWorkspaceId) continue; - const key = `${row.issueId}:${row.executionWorkspaceId}`; - const current = latestByBlockerWorkspace.get(key); + if (!row.executionWorkspaceId) continue; + if (row.issueId) { + const key = `${row.issueId}:${row.executionWorkspaceId}`; + if (!blockerWorkspaceKeys.has(key)) continue; + const current = latestAttributedByBlockerWorkspace.get(key); + if (!current || row.startedAt > current.startedAt) { + latestAttributedByBlockerWorkspace.set(key, { + phase: row.phase, + status: row.status, + startedAt: row.startedAt, + }); + } + continue; + } + + const current = latestUnattributedByWorkspace.get(row.executionWorkspaceId); if (!current || row.startedAt > current.startedAt) { - latestByBlockerWorkspace.set(key, { + latestUnattributedByWorkspace.set(row.executionWorkspaceId, { phase: row.phase, status: row.status, startedAt: row.startedAt, @@ -731,8 +748,9 @@ async function listPendingFinalizeBlockerIssueIds( } for (const pair of blockerWorkspacePairs) { - const latest = latestByBlockerWorkspace.get(`${pair.blockerIssueId}:${pair.executionWorkspaceId}`); - if (!latest) continue; // no attributed ops recorded -> nothing to finalize for this blocker + const latest = latestAttributedByBlockerWorkspace.get(`${pair.blockerIssueId}:${pair.executionWorkspaceId}`) + ?? latestUnattributedByWorkspace.get(pair.executionWorkspaceId); + if (!latest) continue; // no ops recorded -> nothing to finalize for this blocker if (latest.phase === "workspace_finalize" && latest.status === "succeeded") continue; pending.add(pair.blockerIssueId); } diff --git a/server/src/services/routines.ts b/server/src/services/routines.ts index f0147197de..9ad737f3c5 100644 --- a/server/src/services/routines.ts +++ b/server/src/services/routines.ts @@ -654,6 +654,12 @@ export function routineService( actor: Actor, options: { changeSummary?: string | null } = {}, ): Promise { + if (executor === db) { + return db.transaction(async (tx) => ( + upsertRoutineDescriptionDocument(tx as unknown as Db, routine, actor, options) + )); + } + const now = new Date(); const body = routine.description ?? ""; const existing = await getRoutineDescriptionDocument(routine.id, executor); diff --git a/ui/src/context/LiveUpdatesProvider.tsx b/ui/src/context/LiveUpdatesProvider.tsx index 1e1b908360..086a8d4790 100644 --- a/ui/src/context/LiveUpdatesProvider.tsx +++ b/ui/src/context/LiveUpdatesProvider.tsx @@ -667,6 +667,9 @@ function invalidateActivityQueries( const actorType = readString(payload.actorType); const actorId = readString(payload.actorId); const details = readRecord(payload.details); + const ownActorActivity = + (actorType === "user" && !!currentActor.userId && actorId === currentActor.userId) || + (actorType === "agent" && !!currentActor.agentId && actorId === currentActor.agentId); if (action?.startsWith("resource_membership.")) { const targetUserId = readString(details?.userId); @@ -789,8 +792,10 @@ function invalidateActivityQueries( queryClient.invalidateQueries({ queryKey: ["routines"] }); if (entityType === "routine" && action && ROUTINE_DOCUMENT_ANNOTATION_ACTIVITY_ACTIONS.has(action) && entityId) { const documentKey = readString(details?.key) ?? readString(details?.documentKey) ?? "description"; + const routineInvalidationOptions = ownActorActivity ? { refetchType: "inactive" as const } : undefined; queryClient.invalidateQueries({ queryKey: ["routines", "document-annotations", entityId, documentKey], + ...routineInvalidationOptions, }); } return;