From e7331282f06bf0c03a52b2545cc40ec44df93f47 Mon Sep 17 00:00:00 2001 From: snomiao Date: Sat, 2 May 2026 19:19:02 +0000 Subject: [PATCH 1/3] fix(gh-priority-sync): use last_edited_time filter for Notion checkpoint resume Notion's start_cursor must be a token returned by a previous query (next_cursor), not an arbitrary page ID. Passing checkpoint.id caused "The start_cursor provided is invalid" errors that failed CI. Switch to filtering by last_edited_time on_or_after checkpoint.editedAt, and let pagination start from undefined. The existing post-filter that skips checkpoint.id continues to drop the duplicate boundary entry. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/tasks/gh-priority-sync/index.ts | 42 +++++++++++++++++------------ 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/app/tasks/gh-priority-sync/index.ts b/app/tasks/gh-priority-sync/index.ts index 19626e05..4d2e7e48 100644 --- a/app/tasks/gh-priority-sync/index.ts +++ b/app/tasks/gh-priority-sync/index.ts @@ -147,24 +147,32 @@ async function SyncPriorityBetweenComfyTaskAndGithubIssue() { console.log("[notion] comfy-task scan resuming from checkpoint:", checkpoint); // Sync Recent edited Comfy Tasks to GitHub Issues/PRs - const tasks = await pageFlow( - checkpoint?.id ?? (undefined as string | undefined), - async (cursor, page_size = 100) => { - // console.log(`Querying Notion data source ${data_source_id} with cursor=${cursor} page_size=${page_size}...`); - const ret = await notion.dataSources.query({ - data_source_id, - result_type: "page", - filter: { - and: [{ property: "[GH🤖] Link", url: { is_not_empty: true } }], + // Notion's start_cursor must be a token returned by a previous query (next_cursor), + // not an arbitrary page ID. To resume from a checkpoint, filter by last_edited_time + // and start pagination from undefined. + const checkpointFilter = checkpoint?.editedAt + ? [ + { + timestamp: "last_edited_time" as const, + last_edited_time: { on_or_after: checkpoint.editedAt }, }, - sorts: [{ direction: "ascending", timestamp: "last_edited_time" }], - page_size, - start_cursor: cursor, - }); - // ret.next_cursor && await State.set(CHECKPOINT, ret.next_cursor); - return { next: ret.next_cursor, data: ret.results }; - }, - ) + ] + : []; + const tasks = await pageFlow(undefined as string | undefined, async (cursor, page_size = 100) => { + // console.log(`Querying Notion data source ${data_source_id} with cursor=${cursor} page_size=${page_size}...`); + const ret = await notion.dataSources.query({ + data_source_id, + result_type: "page", + filter: { + and: [{ property: "[GH🤖] Link", url: { is_not_empty: true } }, ...checkpointFilter], + }, + sorts: [{ direction: "ascending", timestamp: "last_edited_time" }], + page_size, + start_cursor: cursor, + }); + // ret.next_cursor && await State.set(CHECKPOINT, ret.next_cursor); + return { next: ret.next_cursor, data: ret.results }; + }) .flat() .map((e) => e as Notion.PageObjectResponse) .filter((e) => e.id !== checkpoint?.id) // skip checkpoint entry as it's already processed From 8c70240ecc85393117df9bf1a893fc03cb3fc66b Mon Sep 17 00:00:00 2001 From: snomiao Date: Sat, 2 May 2026 19:36:01 +0000 Subject: [PATCH 2/3] chore(gh-priority-sync): drop stale CHECKPOINT comment Removes a dead snippet that referenced an undefined CHECKPOINT identifier (the actual constant is NotionCheckpoint). Addresses Copilot review on PR #206. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/tasks/gh-priority-sync/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/tasks/gh-priority-sync/index.ts b/app/tasks/gh-priority-sync/index.ts index 4d2e7e48..32cab5f9 100644 --- a/app/tasks/gh-priority-sync/index.ts +++ b/app/tasks/gh-priority-sync/index.ts @@ -170,7 +170,6 @@ async function SyncPriorityBetweenComfyTaskAndGithubIssue() { page_size, start_cursor: cursor, }); - // ret.next_cursor && await State.set(CHECKPOINT, ret.next_cursor); return { next: ret.next_cursor, data: ret.results }; }) .flat() From aaae089423e0d9e0c1fd97dc0b3bded82ef1e1c8 Mon Sep 17 00:00:00 2001 From: snomiao Date: Sat, 2 May 2026 19:37:39 +0000 Subject: [PATCH 3/3] fix(gh-priority-sync): handle ties at checkpoint last_edited_time When multiple Notion pages share the same last_edited_time as the checkpoint, the prior implementation only skipped the single checkpoint id and reprocessed (or got stuck on) the rest each run. Extend the checkpoint to track all ids processed at the boundary timestamp (processedIdsAtEditedAt) and use it as a post-filter. As we process items, we either append to the boundary set (same timestamp) or reset it (new timestamp), so the persisted checkpoint always reflects every id already handled at the current boundary. Legacy { id, editedAt } checkpoints are upgraded by treating { id } as the only processed id at editedAt. Addresses Copilot review on PR #206. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/tasks/gh-priority-sync/index.ts | 40 ++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/app/tasks/gh-priority-sync/index.ts b/app/tasks/gh-priority-sync/index.ts index 32cab5f9..4fbff630 100644 --- a/app/tasks/gh-priority-sync/index.ts +++ b/app/tasks/gh-priority-sync/index.ts @@ -143,18 +143,33 @@ async function SyncPriorityBetweenComfyTaskAndGithubIssue() { // // Query the database to get tasks // console.log('\nFetching tasks from database...'); // // full scan + incremental watching - const checkpoint = (await State.get(NotionCheckpoint)) as { id: string; editedAt: string }; + // Checkpoint shape: + // { editedAt, processedIdsAtEditedAt[] } — robust against ties at the same last_edited_time. + // Legacy { id, editedAt } is upgraded by treating { id } as the only processed id at editedAt. + const checkpoint = (await State.get(NotionCheckpoint)) as + | { id?: string; editedAt?: string; processedIdsAtEditedAt?: string[] } + | undefined; console.log("[notion] comfy-task scan resuming from checkpoint:", checkpoint); + const boundaryEditedAt = checkpoint?.editedAt; + const processedIdsAtBoundary = new Set( + checkpoint?.processedIdsAtEditedAt ?? (checkpoint?.id ? [checkpoint.id] : []), + ); + // Mutable copies used while processing this run, so subsequent items at the same + // boundary timestamp persist into the checkpoint and are skipped on the next run. + let currentBoundaryEditedAt: string | undefined = boundaryEditedAt; + const currentProcessedIdsAtBoundary = new Set(processedIdsAtBoundary); // Sync Recent edited Comfy Tasks to GitHub Issues/PRs // Notion's start_cursor must be a token returned by a previous query (next_cursor), // not an arbitrary page ID. To resume from a checkpoint, filter by last_edited_time - // and start pagination from undefined. - const checkpointFilter = checkpoint?.editedAt + // and start pagination from undefined. To handle multiple pages sharing the same + // last_edited_time as the checkpoint, skip those whose ids are already in + // processedIdsAtBoundary. + const checkpointFilter = boundaryEditedAt ? [ { timestamp: "last_edited_time" as const, - last_edited_time: { on_or_after: checkpoint.editedAt }, + last_edited_time: { on_or_after: boundaryEditedAt }, }, ] : []; @@ -174,7 +189,7 @@ async function SyncPriorityBetweenComfyTaskAndGithubIssue() { }) .flat() .map((e) => e as Notion.PageObjectResponse) - .filter((e) => e.id !== checkpoint?.id) // skip checkpoint entry as it's already processed + .filter((e) => !(e.last_edited_time === boundaryEditedAt && processedIdsAtBoundary.has(e.id))) .map((e) => { return { ...e, @@ -218,7 +233,20 @@ async function SyncPriorityBetweenComfyTaskAndGithubIssue() { async (e) => { const task = e as Notion.PageObjectResponse; await ComfyTaskPrioritySync(task); - await State.set(NotionCheckpoint, { id: task.id, editedAt: task.last_edited_time }); // per-item checkpoint, can resume from last processed page + // Per-item checkpoint. When advancing past the previous boundary timestamp, + // reset the processed-id set; otherwise append the id so future runs skip + // every page already handled at this exact timestamp. + const isNewBoundary = task.last_edited_time !== currentBoundaryEditedAt; + if (isNewBoundary) { + currentBoundaryEditedAt = task.last_edited_time; + currentProcessedIdsAtBoundary.clear(); + } + currentProcessedIdsAtBoundary.add(task.id); + await State.set(NotionCheckpoint, { + id: task.id, + editedAt: currentBoundaryEditedAt, + processedIdsAtEditedAt: [...currentProcessedIdsAtBoundary], + }); }, ), )