nomadcode/agent-task/m-plane-work-item-webhook-intake/03+02_idempotency/CODE_REVIEW-cloud-G06.md
toki bbb1179194 feat(work-item-pipeline): 작업자 외부 참조 중복 처리 및 고유 인덱스 추가
Tasks 테이블에 external_provider, external_id 기반 UPSERT 로직을 추가하고
GetTaskByExternalRef 쿼리를 신설하여 중복 작업을 효율적으로 처리한다.

외부 참조 고유 인덱스 마이그레이션(00008)을 추가한다.

Workspace slots 쿼리에서 alias를 명시하여 FOR UPDATE SKIP LOCKED 동시성 처리를 개선한다.

Storage, workflow, workitempipeline의 중복 처리 로직을 일관되게 적용하고
Plane 웹훅 테스트를 보강한다.
2026-06-15 14:58:59 +09:00

8.2 KiB

Code Review Reference - WEBHOOK_IDEMPOTENCY

[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation. The task is NOT complete until every implementation-owned section below is filled in. Complete the 구현 체크리스트; the final checklist item is mandatory before saving. Fill implementation-owned sections, then stop with active files in place and report ready for review. If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill 사용자 리뷰 요청 with evidence and stop with active files in place; code-review decides whether to write USER_REVIEW.md. Do not ask the user directly, present choices in chat, or call request_user_input during implementation. Finalization is review-agent-only.

개요

date=2026-06-14 task=m-plane-work-item-webhook-intake/03+02_idempotency, plan=0, tag=WEBHOOK_IDEMPOTENCY

Roadmap Targets

  • Milestone: agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/plane-work-item-webhook-intake.md
  • Task ids:
    • loop-idempotency: duplicate webhook, provider retry, NomadCode self actor event를 중복 creation task 없이 처리한다.
  • Completion mode: check-on-pass

이 파일을 읽는 리뷰 에이전트에게

[REVIEW AGENT ONLY] 구현 결과를 실제 소스와 대조하고, PASS이면 code-review 절차에 따라 log/complete/archive를 처리한다. 구현 에이전트는 이 섹션을 실행하지 않는다.

구현 항목별 완료 여부

항목 완료 여부
[WEBHOOK_IDEMPOTENCY-1] Storage Idempotency Boundary [x]
[WEBHOOK_IDEMPOTENCY-2] Workflow And Pipeline Semantics [x]
[WEBHOOK_IDEMPOTENCY-3] Webhook Duplicate/Self Tests [x]

구현 체크리스트

  • Plane-origin task idempotency key를 external provider/id로 고정하고 DB unique/index 또는 upsert semantics를 추가한다.
  • duplicate webhook/provider retry가 기존 task를 반환하거나 ignored 처리되어 새 task, workspace provision, slot reservation을 만들지 않도록 workflow/workitempipeline 경계를 조정한다. 검증: 같은 provider/work item/revision 재처리와 self actor 입력이 task 중복 생성으로 이어지지 않는다.
  • storage/workflow/workitempipeline/http tests를 추가해 duplicate external ref, duplicate webhook dispatch, self actor ignored를 검증한다.
  • CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

코드리뷰 전용 체크리스트

[REVIEW AGENT ONLY] 이 체크리스트는 코드리뷰 에이전트만 사용한다.

  • 코드리뷰 결과PASS, WARN, FAIL 중 하나의 판정을 append한다.
  • active CODE_REVIEW-*-G??.mdPLAN-*-G??.md.log로 아카이브한다.
  • PASS이면 complete.log를 작성하고 active task 디렉터리를 agent-task/archive/YYYY/MM/m-plane-work-item-webhook-intake/03+02_idempotency/로 이동한다.
  • PASS이고 task group이 m-plane-work-item-webhook-intake이므로 완료 이벤트 메타데이터를 보고한다. roadmap 수정은 런타임 책임이다.

계획 대비 변경 사항

  • ReserveAvailableWorkspaceSlot 쿼리(queries/workspace_slots.sql)에서 sqlc v1.31.1 파싱 시 project_sync_setting_id 모호성(ambiguity) 에러가 발생하여, 서브쿼리 테이블에 ws 에일리어스를 적용하여 수정하였습니다.

주요 설계 결정

  • workitempipeline.Service에서 CreateTaskFromWorkItem 시작 지점에 GetTaskByExternalRef로 사전에 존재하는 task가 있는지 체크하도록 하여, workspace provision이나 slot reservation 등의 부가적인 side effect가 일어나기 전에 멱등하게 조기 반환하도록 설계했습니다.
  • queries/tasks.sqlCreateTask 쿼리에 ON CONFLICT (external_provider, external_id) DO UPDATE SET updated_at = tasks.updated_at 절을 추가하여 DB 레벨에서도 멱등성을 제공하도록 했습니다.
  • workflow.ServiceGetTaskByExternalRef를 추가하고, input 파라미터(provider, id)의 validation 방어 코드를 작성했습니다.

사용자 리뷰 요청

  • 상태: 없음
  • 사유 유형: 없음
  • 결정 필요: 없음
  • 차단 근거: 없음
  • 실행한 검증/명령: 없음
  • 자동 후속 불가 이유: 없음
  • 재개 조건: 없음

리뷰어를 위한 체크포인트

  • Duplicate prevention happens before workspace provision/slot reservation, or tests prove no duplicate side effect.
  • DB unique/upsert semantics do not break tasks without external refs.
  • Generated sqlc code matches queries/tasks.sql.

검증 결과

WEBHOOK_IDEMPOTENCY-1 중간 검증

$ cd services/core && go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate
(정상 완료)

WEBHOOK_IDEMPOTENCY-2 중간 검증

$ cd services/core && go test -count=1 ./internal/workflow ./internal/workitempipeline
ok  	github.com/nomadcode/nomadcode-core/internal/workflow	0.003s
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	0.003s

WEBHOOK_IDEMPOTENCY-3 중간 검증

$ cd services/core && go test -count=1 ./internal/http ./internal/workitempipeline
ok  	github.com/nomadcode/nomadcode-core/internal/http	0.004s
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	0.003s

최종 검증

$ cd services/core && go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate
(정상 완료)
$ cd services/core && gofmt -w internal/db/tasks.sql.go internal/storage/store.go internal/workflow/service.go internal/workflow/service_test.go internal/workitempipeline/service.go internal/workitempipeline/service_test.go internal/http/plane_webhook_test.go
(정상 완료)
$ cd services/core && go test -count=1 ./internal/storage ./internal/workflow ./internal/workitempipeline ./internal/http
ok  	github.com/nomadcode/nomadcode-core/internal/storage	0.003s
ok  	github.com/nomadcode/nomadcode-core/internal/workflow	0.003s
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	0.003s
ok  	github.com/nomadcode/nomadcode-core/internal/http	0.009s
$ tar -C services/core -czf - . | ssh toki@toki-labs.com 'mkdir -p ~/agent-work/nomadcode/services/core && tar -C ~/agent-work/nomadcode/services/core -xzf -'
(정상 완료)
$ ssh toki@toki-labs.com 'zsh -lc "cd ~/agent-work/nomadcode/services/core && go test -count=1 ./..."'
ok  	github.com/nomadcode/nomadcode-core/cmd/plane-smoke	0.499s
?   	github.com/nomadcode/nomadcode-core/cmd/server	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/a2a	0.422s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/jira	0.841s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/mattermost	1.236s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/openai	1.636s
ok  	github.com/nomadcode/nomadcode-core/internal/adapters/plane	2.003s
?   	github.com/nomadcode/nomadcode-core/internal/agent	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/authoring	2.342s
ok  	github.com/nomadcode/nomadcode-core/internal/config	2.682s
?   	github.com/nomadcode/nomadcode-core/internal/db	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/gitoevents	3.095s
ok  	github.com/nomadcode/nomadcode-core/internal/gitosync	3.398s
ok  	github.com/nomadcode/nomadcode-core/internal/http	3.600s
?   	github.com/nomadcode/nomadcode-core/internal/model	[no test files]
ok  	github.com/nomadcode/nomadcode-core/internal/notification	3.928s
ok  	github.com/nomadcode/nomadcode-core/internal/projectsync	3.891s
ok  	github.com/nomadcode/nomadcode-core/internal/protosocket	3.957s
ok  	github.com/nomadcode/nomadcode-core/internal/roadmapsync	3.905s
ok  	github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline	3.885s
ok  	github.com/nomadcode/nomadcode-core/internal/scheduler	5.913s
ok  	github.com/nomadcode/nomadcode-core/internal/storage	3.947s
ok  	github.com/nomadcode/nomadcode-core/internal/workflow	3.908s
ok  	github.com/nomadcode/nomadcode-core/internal/workitem	3.873s
ok  	github.com/nomadcode/nomadcode-core/internal/workitempipeline	3.852s