From 31e8b94933c003160c061a5bbc6f60367fdbbac6 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Wed, 8 Jul 2026 19:37:07 +0700 Subject: [PATCH 1/4] fix: keep Temporal workflow when a scheduled post is edited without a time change Editing a future scheduled post (dashboard or public API) sent type 'schedule', which always terminated and recreated the post's Temporal workflow - even when only content or settings changed and the publish time stayed the same. At 100k+ sleeping workflows that is needless churn, and it diverges from the agent's updatePostTool, which already leaves the workflow alone for non-time changes. Decide it server-side in createPost: capture the post's current publish time before the upsert, and only (re)start the workflow when it's a new post or an existing QUEUE post whose time actually moved. Content and settings are re-read from the DB at publish time, so an unchanged time needs no restart; a genuine time change still restarts it. Done in the service rather than the edit modal so it's authoritative and covers every caller (dashboard, public API), and can't be fooled by client-side date state. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../database/prisma/posts/posts.service.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 57f73373ec..007715b21d 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -900,10 +900,22 @@ export class PostsService { content: removeLinks ? stripLinks(updateContent[i]) : updateContent[i], })); + const scheduledDate = + body.type === 'now' + ? dayjs().format('YYYY-MM-DDTHH:mm:00') + : body.date; + + // Grab the current publish time BEFORE the upsert overwrites it, so we + // can tell (below) whether an edit actually moved the post's time. + const existingRootId = post.value?.[0]?.id; + const existingRoot = existingRootId + ? await this._postRepository.getPostById(existingRootId, orgId) + : null; + const { posts } = await this._postRepository.createOrUpdatePost( body.type, orgId, - body.type === 'now' ? dayjs().format('YYYY-MM-DDTHH:mm:00') : body.date, + scheduledDate, post, body.tags, creationMethod, @@ -914,7 +926,19 @@ export class PostsService { return [] as any[]; } - if (body.type !== 'update') { + // Only (re)start the Temporal workflow when it's actually needed: a new + // post, or a scheduled post whose publish time changed. Editing a queued + // post without moving its time keeps the existing workflow - content and + // settings are re-read from the DB at publish time, so a restart (which + // would terminate + recreate the workflow) is unnecessary churn. + const publishTimeUnchanged = + !!existingRoot && + existingRoot.state === 'QUEUE' && + dayjs + .utc(existingRoot.publishDate) + .isSame(dayjs.utc(scheduledDate), 'minute'); + + if (body.type !== 'update' && !publishTimeUnchanged) { this.startWorkflow( post.settings.__type.split('-')[0].toLowerCase(), posts[0].id, From b55d70294ffbecabb45965cd6155250195ce85d2 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Tue, 14 Jul 2026 18:35:06 +0700 Subject: [PATCH 2/4] fix: parse scheduledDate like the repository does when comparing publish times createOrUpdatePost persists publishDate with a local-time parse (dayjs(date).toDate()), but the unchanged-time guard compared with dayjs.utc(scheduledDate). On non-UTC servers the guard was off by the server offset: usually failing open (needless workflow restart), and wrongly skipping the restart when a time moved by exactly that offset. Parsing both sides the way the repository does makes the guard exact on any server timezone; on UTC servers behavior is unchanged. Co-Authored-By: Claude Fable 5 --- .../src/database/prisma/posts/posts.service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 007715b21d..988023cfd2 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -934,9 +934,7 @@ export class PostsService { const publishTimeUnchanged = !!existingRoot && existingRoot.state === 'QUEUE' && - dayjs - .utc(existingRoot.publishDate) - .isSame(dayjs.utc(scheduledDate), 'minute'); + dayjs(existingRoot.publishDate).isSame(dayjs(scheduledDate), 'minute'); if (body.type !== 'update' && !publishTimeUnchanged) { this.startWorkflow( From 61df6d71af3e18e9989677db71cbb0fc144488e9 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Tue, 14 Jul 2026 19:19:45 +0700 Subject: [PATCH 3/4] fix: don't skip startWorkflow when a queued post is saved as draft or posted now publishTimeUnchanged only looked at the pre-update state, so saving a queued post as a draft (or posting it now) with an unchanged time skipped startWorkflow and left the old Temporal workflow running, which would publish the draft at its original time. Gate the skip on body.type === 'schedule' so only queued-stays-queued edits keep the existing workflow. Co-Authored-By: Claude Fable 5 --- .../src/database/prisma/posts/posts.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 988023cfd2..ce8d2fba83 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -931,7 +931,10 @@ export class PostsService { // post without moving its time keeps the existing workflow - content and // settings are re-read from the DB at publish time, so a restart (which // would terminate + recreate the workflow) is unnecessary churn. + // Only a 'schedule' save may skip: 'draft'/'now' saves must still go + // through startWorkflow so the old run is terminated. const publishTimeUnchanged = + body.type === 'schedule' && !!existingRoot && existingRoot.state === 'QUEUE' && dayjs(existingRoot.publishDate).isSame(dayjs(scheduledDate), 'minute'); From 043e2da1ebf3ff70b25dc88de15150f50b50f374 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Tue, 14 Jul 2026 19:47:00 +0700 Subject: [PATCH 4/4] fix: only skip startWorkflow when the unchanged post row is the root If an edit removes or reorders the thread head, value[0] is a promoted child row that passes the QUEUE/same-time checks, while the workflow is keyed to the old root, which gets soft-deleted by the group cleanup. Require existingRoot to have no parentPostId so such edits start a workflow for the new root. Co-Authored-By: Claude Fable 5 --- .../src/database/prisma/posts/posts.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index ce8d2fba83..93934f4616 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -932,10 +932,14 @@ export class PostsService { // settings are re-read from the DB at publish time, so a restart (which // would terminate + recreate the workflow) is unnecessary churn. // Only a 'schedule' save may skip: 'draft'/'now' saves must still go - // through startWorkflow so the old run is terminated. + // through startWorkflow so the old run is terminated. The existing row + // must also BE the root: if the edit promoted a child row (thread head + // removed/reordered), the workflow is keyed to the old root, which gets + // soft-deleted - the new root needs its own workflow. const publishTimeUnchanged = body.type === 'schedule' && !!existingRoot && + !existingRoot.parentPostId && existingRoot.state === 'QUEUE' && dayjs(existingRoot.publishDate).isSame(dayjs(scheduledDate), 'minute');