From 48936510d7f69d0b871734cf6ed592beb10a3ede Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 15 Jun 2026 13:03:23 +0200 Subject: [PATCH 1/4] Add outbound webhook SDK client --- packages/sdk/typescript/src/client.test.ts | 99 ++++++++++++++++++++++ packages/sdk/typescript/src/client.ts | 76 +++++++++++++++++ packages/sdk/typescript/src/index.ts | 9 ++ packages/sdk/typescript/src/types.ts | 70 +++++++++++++++ 4 files changed, 254 insertions(+) diff --git a/packages/sdk/typescript/src/client.test.ts b/packages/sdk/typescript/src/client.test.ts index ce159840..254ab485 100644 --- a/packages/sdk/typescript/src/client.test.ts +++ b/packages/sdk/typescript/src/client.test.ts @@ -2032,6 +2032,105 @@ describe("RelayFileClient — new webhook/writeback methods", () => { expect(body.headers["X-GitHub-Event"]).toBe("issues"); }); + it("registerWebhook sends outbound subscription details", async () => { + const f = mockFetch({ subscriptionId: "whsub_1" }); + const client = makeClient(f); + + const res = await client.registerWebhook({ + workspaceId: "ws_acme", + url: "https://factory.example.com/relayfile", + pathGlobs: ["/github/repos/acme/api/issues/by-id/**"], + secret: "whsec_test", + }); + + expect(res.subscriptionId).toBe("whsub_1"); + const url = f.mock.calls[0]![0] as string; + expect(url).toContain("/v1/workspaces/ws_acme/webhooks"); + const init = f.mock.calls[0]![1] as RequestInit; + expect(init.method).toBe("POST"); + const body = JSON.parse(init.body as string); + expect(body.url).toBe("https://factory.example.com/relayfile"); + expect(body.pathGlobs).toEqual(["/github/repos/acme/api/issues/by-id/**"]); + expect(body.secret).toBe("whsec_test"); + }); + + it("listWebhooks and deleteWebhook target outbound subscription routes", async () => { + const f = mockFetch([ + { + id: "whsub_1", + url: "https://factory.example.com/relayfile", + pathGlobs: ["/linear/issues/**"], + createdAt: "2026-06-15T00:00:00.000Z", + updatedAt: "2026-06-15T00:00:00.000Z", + health: { + lastDeliveryAt: null, + lastSuccessAt: null, + lastError: null, + consecutiveFailures: 0, + }, + }, + ]); + const client = makeClient(f); + + const hooks = await client.listWebhooks("ws_acme"); + expect(hooks[0]?.id).toBe("whsub_1"); + expect(f.mock.calls[0]![0] as string).toContain("/v1/workspaces/ws_acme/webhooks"); + + f.mockResolvedValueOnce({ + ok: true, + status: 204, + headers: new Headers(), + json: () => Promise.resolve(null), + text: () => Promise.resolve(""), + } as unknown as Response); + await client.deleteWebhook("ws_acme", "whsub_1"); + expect(f.mock.calls[1]![0] as string).toContain("/v1/workspaces/ws_acme/webhooks/whsub_1"); + expect((f.mock.calls[1]![1] as RequestInit).method).toBe("DELETE"); + }); + + it("lists and replays outbound webhook delivery dead letters", async () => { + const f = mockFetch({ + items: [ + { + deliveryId: "whdel_1", + workspaceId: "ws_acme", + subscriptionId: "whsub_1", + eventId: "evt_10", + url: "https://factory.example.com/relayfile", + failedAt: "2026-06-15T00:00:00.000Z", + attemptCount: 3, + lastError: "webhook endpoint returned 500", + replayCount: 0, + status: "dead_lettered", + }, + ], + nextCursor: null, + }); + const client = makeClient(f); + + const dlq = await client.getWebhookDeadLetters("ws_acme", { + cursor: "2026-06-14T00:00:00.000Z", + limit: 10, + }); + expect(dlq.items[0]?.eventId).toBe("evt_10"); + expect(f.mock.calls[0]![0] as string).toContain( + "/v1/workspaces/ws_acme/webhooks/dlq?cursor=2026-06-14T00%3A00%3A00.000Z&limit=10", + ); + + f.mockResolvedValueOnce({ + ok: true, + status: 202, + headers: new Headers({ "content-type": "application/json" }), + json: () => Promise.resolve({ status: "queued", id: "whdel_1" }), + text: () => Promise.resolve(JSON.stringify({ status: "queued", id: "whdel_1" })), + } as unknown as Response); + const replay = await client.replayWebhookDeadLetter("ws_acme", "whdel_1"); + expect(replay.status).toBe("queued"); + expect(f.mock.calls[1]![0] as string).toContain( + "/v1/workspaces/ws_acme/webhooks/dlq/whdel_1/replay", + ); + }); + it("listPendingWritebacks GETs writeback/pending", async () => { const payload: WritebackItem[] = [ { diff --git a/packages/sdk/typescript/src/client.ts b/packages/sdk/typescript/src/client.ts index f1a0b58b..f3c60b56 100644 --- a/packages/sdk/typescript/src/client.ts +++ b/packages/sdk/typescript/src/client.ts @@ -11,6 +11,7 @@ import { type DeleteFileInput, type DeadLetterItem, type DeadLetterFeedResponse, + type DeleteWebhookOptions, type DiscardForkInput, type ErrorResponse, type EventFeedResponse, @@ -26,6 +27,8 @@ import { type GetSyncDeadLettersOptions, type GetSyncIngressStatusOptions, type GetSyncStatusOptions, + type GetWebhookDeadLettersOptions, + type ListWebhooksOptions, type ListTreeOptions, type OperationFeedResponse, type OperationStatusResponse, @@ -33,6 +36,8 @@ import { type ResourceAtEventResult, type ReadFileInput, type QueryFilesOptions, + type RegisterWebhookInput, + type RegisterWebhookResponse, type Subscription, type SyncIngressStatusResponse, type SyncStatusResponse, @@ -41,6 +46,8 @@ import { type WriteQueuedResponse, type IngestWebhookInput, type WritebackItem, + type WebhookDeliveryDeadLetterFeedResponse, + type WebhookSubscription, type AckWritebackInput, type AckWritebackResponse, type SweepWritebackDraftsInput, @@ -1861,6 +1868,75 @@ export class RelayFileClient { }); } + async registerWebhook(input: RegisterWebhookInput): Promise { + return this.request({ + method: "POST", + path: `/v1/workspaces/${encodeURIComponent(input.workspaceId)}/webhooks`, + correlationId: input.correlationId, + body: { + url: input.url, + pathGlobs: input.pathGlobs, + secret: input.secret + }, + signal: input.signal + }); + } + + async listWebhooks( + workspaceId: string, + options: ListWebhooksOptions = {} + ): Promise { + return this.request({ + method: "GET", + path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks`, + correlationId: options.correlationId, + signal: options.signal + }); + } + + async deleteWebhook( + workspaceId: string, + subscriptionId: string, + options: DeleteWebhookOptions = {} + ): Promise { + await this.performRequest({ + method: "DELETE", + path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/${encodeURIComponent(subscriptionId)}`, + correlationId: options.correlationId, + signal: options.signal + }); + } + + async getWebhookDeadLetters( + workspaceId: string, + options: GetWebhookDeadLettersOptions = {} + ): Promise { + const query = buildQuery({ + cursor: options.cursor, + limit: options.limit + }); + return this.request({ + method: "GET", + path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/dlq${query}`, + correlationId: options.correlationId, + signal: options.signal + }); + } + + async replayWebhookDeadLetter( + workspaceId: string, + deliveryId: string, + correlationId?: string, + signal?: AbortSignal + ): Promise { + return this.request({ + method: "POST", + path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/dlq/${encodeURIComponent(deliveryId)}/replay`, + correlationId, + signal + }); + } + async listPendingWritebacks( workspaceId: string, correlationId?: string, diff --git a/packages/sdk/typescript/src/index.ts b/packages/sdk/typescript/src/index.ts index d0f08410..a0b2669d 100644 --- a/packages/sdk/typescript/src/index.ts +++ b/packages/sdk/typescript/src/index.ts @@ -144,6 +144,7 @@ export type { CreateForkInput, ContentIdentity, DeleteFileInput, + DeleteWebhookOptions, DeadLetterFeedResponse, DeadLetterItem, DigestBullet, @@ -175,10 +176,12 @@ export type { GetSyncDeadLettersOptions, GetSyncIngressStatusOptions, GetSyncStatusOptions, + GetWebhookDeadLettersOptions, IngestWebhookInput, LayoutManifest, LayoutManifestAlias, LayoutManifestResource, + ListWebhooksOptions, ListTreeOptions, OperationFeedResponse, OperationStatus, @@ -186,6 +189,8 @@ export type { QueuedResponse, QueryFilesOptions, ReadFileInput, + RegisterWebhookInput, + RegisterWebhookResponse, ReplayOptions, ResourceAtEventResult, SummaryExpansion, @@ -204,6 +209,10 @@ export type { SweepWritebackDraftsResponse, TreeEntry, TreeResponse, + WebhookDeliveryDeadLetterFeedResponse, + WebhookDeliveryDeadLetterItem, + WebhookSubscription, + WebhookSubscriptionHealth, WritebackActionType, WritebackDeadLetterError, WritebackDeadLetterErrorCode, diff --git a/packages/sdk/typescript/src/types.ts b/packages/sdk/typescript/src/types.ts index 496cd923..05cdd392 100644 --- a/packages/sdk/typescript/src/types.ts +++ b/packages/sdk/typescript/src/types.ts @@ -172,6 +172,7 @@ export interface FilesystemEvent { type: FilesystemEventType; path: string; revision: string; + contentHash?: string; origin?: EventOrigin; provider?: string; correlationId?: string; @@ -722,6 +723,75 @@ export interface DeadLetterFeedResponse { nextCursor: string | null; } +export interface WebhookSubscriptionHealth { + lastDeliveryAt: string | null; + lastSuccessAt: string | null; + lastError: string | null; + consecutiveFailures: number; +} + +export interface WebhookSubscription { + id: string; + url: string; + pathGlobs: string[]; + createdAt: string; + updatedAt: string; + health: WebhookSubscriptionHealth; +} + +export interface RegisterWebhookInput { + workspaceId: string; + url: string; + pathGlobs: string[]; + /** + * Optional HMAC secret. If omitted, relayfile generates a secret and returns + * it once from the registration response. + */ + secret?: string; + correlationId?: string; + signal?: AbortSignal; +} + +export interface RegisterWebhookResponse { + subscriptionId: string; + secret?: string; +} + +export interface ListWebhooksOptions { + correlationId?: string; + signal?: AbortSignal; +} + +export interface DeleteWebhookOptions { + correlationId?: string; + signal?: AbortSignal; +} + +export interface GetWebhookDeadLettersOptions { + cursor?: string; + limit?: number; + correlationId?: string; + signal?: AbortSignal; +} + +export interface WebhookDeliveryDeadLetterItem { + deliveryId: string; + workspaceId: string; + subscriptionId: string; + eventId: string; + url: string; + failedAt: string; + attemptCount: number; + lastError: string; + replayCount: number; + status: "dead_lettered" | "queued" | "delivered"; +} + +export interface WebhookDeliveryDeadLetterFeedResponse { + items: WebhookDeliveryDeadLetterItem[]; + nextCursor: string | null; +} + export interface WriteFileInput { workspaceId: string; path: string; From 2432065c6190d1c6a66e57712150d9a3106d2812 Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Mon, 15 Jun 2026 11:11:51 +0000 Subject: [PATCH 2/4] chore: apply pr-reviewer fixes for #281 --- .../active/traj_pmzcmccpttxb/trajectory.json | 19 ++++ .../active/traj_4pvrlmqfnzng/trajectory.json | 46 ---------- .../2026-06/traj_4pvrlmqfnzng/summary.md | 33 +++++++ .../2026-06/traj_4pvrlmqfnzng/trajectory.json | 87 +++++++++++++++++++ .trajectories/index.json | 13 --- 5 files changed, 139 insertions(+), 59 deletions(-) create mode 100644 .agentworkforce/trajectories/active/traj_pmzcmccpttxb/trajectory.json delete mode 100644 .trajectories/active/traj_4pvrlmqfnzng/trajectory.json create mode 100644 .trajectories/completed/2026-06/traj_4pvrlmqfnzng/summary.md create mode 100644 .trajectories/completed/2026-06/traj_4pvrlmqfnzng/trajectory.json delete mode 100644 .trajectories/index.json diff --git a/.agentworkforce/trajectories/active/traj_pmzcmccpttxb/trajectory.json b/.agentworkforce/trajectories/active/traj_pmzcmccpttxb/trajectory.json new file mode 100644 index 00000000..7552aed4 --- /dev/null +++ b/.agentworkforce/trajectories/active/traj_pmzcmccpttxb/trajectory.json @@ -0,0 +1,19 @@ +{ + "id": "traj_pmzcmccpttxb", + "version": 1, + "task": { + "title": "Review PR #281 in AgentWorkforce/relayfile" + }, + "status": "active", + "startedAt": "2026-06-15T11:05:55.976Z", + "agents": [], + "chapters": [], + "commits": [], + "filesChanged": [], + "projectId": "AgentWorkforce/relayfile", + "tags": [], + "_trace": { + "startRef": "48936510d7f69d0b871734cf6ed592beb10a3ede", + "endRef": "48936510d7f69d0b871734cf6ed592beb10a3ede" + } +} \ No newline at end of file diff --git a/.trajectories/active/traj_4pvrlmqfnzng/trajectory.json b/.trajectories/active/traj_4pvrlmqfnzng/trajectory.json deleted file mode 100644 index 7777d4fa..00000000 --- a/.trajectories/active/traj_4pvrlmqfnzng/trajectory.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "id": "traj_4pvrlmqfnzng", - "version": 1, - "task": { - "title": "Review PR #278 in AgentWorkforce/relayfile" - }, - "status": "active", - "startedAt": "2026-06-15T07:37:32.072Z", - "agents": [ - { - "name": "default", - "role": "lead", - "joinedAt": "2026-06-15T07:37:32.451Z" - } - ], - "chapters": [ - { - "id": "chap_ihldqr43vlno", - "title": "Work", - "agentName": "default", - "startedAt": "2026-06-15T07:37:32.451Z", - "events": [ - { - "ts": 1781509052452, - "type": "reflection", - "content": "Validated PR #278 SDK change; no mechanical edits applied. Found one behavior risk around re-probing after permanent WebSocket factory failure; SDK build/typecheck/tests pass, root Node tests pass until missing Go, npm ci blocked by pre-existing lock drift.", - "raw": { - "confidence": 0.82 - }, - "significance": "high", - "tags": [ - "confidence:0.82" - ] - } - ] - } - ], - "commits": [], - "filesChanged": [], - "projectId": "/home/daytona/workspace", - "tags": [], - "_trace": { - "startRef": "b951fa3b01f705af8555c98a23814a545532b699", - "endRef": "b951fa3b01f705af8555c98a23814a545532b699" - } -} \ No newline at end of file diff --git a/.trajectories/completed/2026-06/traj_4pvrlmqfnzng/summary.md b/.trajectories/completed/2026-06/traj_4pvrlmqfnzng/summary.md new file mode 100644 index 00000000..7d935789 --- /dev/null +++ b/.trajectories/completed/2026-06/traj_4pvrlmqfnzng/summary.md @@ -0,0 +1,33 @@ +# Trajectory: Review PR #278 in AgentWorkforce/relayfile + +> **Status:** ✅ Completed +> **Confidence:** 82% +> **Started:** June 15, 2026 at 07:37 AM +> **Completed:** June 15, 2026 at 11:11 AM + +--- + +## Summary + +Reviewed PR #281 SDK outbound webhook additions. No mechanical edits applied. Found SDK methods for outbound webhook subscription/DLQ routes that are absent from current server router and OpenAPI contract; verified SDK build/typecheck/tests and contract surface pass, while Go-gated root checks cannot run because go is unavailable. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Leave outbound webhook SDK/server contract mismatch as review finding +- **Chose:** Leave outbound webhook SDK/server contract mismatch as review finding +- **Reasoning:** Fix requires semantic server/OpenAPI design and implementation; reviewer instructions allow only mechanical edits + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Validated PR #278 SDK change; no mechanical edits applied. Found one behavior risk around re-probing after permanent WebSocket factory failure; SDK build/typecheck/tests pass, root Node tests pass until missing Go, npm ci blocked by pre-existing lock drift. +- Leave outbound webhook SDK/server contract mismatch as review finding: Leave outbound webhook SDK/server contract mismatch as review finding +- Review found no mechanical cleanup to apply; SDK compile/tests pass, but added outbound webhook methods target routes absent from router and OpenAPI diff --git a/.trajectories/completed/2026-06/traj_4pvrlmqfnzng/trajectory.json b/.trajectories/completed/2026-06/traj_4pvrlmqfnzng/trajectory.json new file mode 100644 index 00000000..35c9042e --- /dev/null +++ b/.trajectories/completed/2026-06/traj_4pvrlmqfnzng/trajectory.json @@ -0,0 +1,87 @@ +{ + "id": "traj_4pvrlmqfnzng", + "version": 1, + "task": { + "title": "Review PR #278 in AgentWorkforce/relayfile" + }, + "status": "completed", + "startedAt": "2026-06-15T07:37:32.072Z", + "completedAt": "2026-06-15T11:11:23.101Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-06-15T07:37:32.451Z" + } + ], + "chapters": [ + { + "id": "chap_ihldqr43vlno", + "title": "Work", + "agentName": "default", + "startedAt": "2026-06-15T07:37:32.451Z", + "endedAt": "2026-06-15T11:11:23.101Z", + "events": [ + { + "ts": 1781509052452, + "type": "reflection", + "content": "Validated PR #278 SDK change; no mechanical edits applied. Found one behavior risk around re-probing after permanent WebSocket factory failure; SDK build/typecheck/tests pass, root Node tests pass until missing Go, npm ci blocked by pre-existing lock drift.", + "raw": { + "confidence": 0.82 + }, + "significance": "high", + "tags": [ + "confidence:0.82" + ] + }, + { + "ts": 1781521862043, + "type": "decision", + "content": "Leave outbound webhook SDK/server contract mismatch as review finding: Leave outbound webhook SDK/server contract mismatch as review finding", + "raw": { + "question": "Leave outbound webhook SDK/server contract mismatch as review finding", + "chosen": "Leave outbound webhook SDK/server contract mismatch as review finding", + "alternatives": [], + "reasoning": "Fix requires semantic server/OpenAPI design and implementation; reviewer instructions allow only mechanical edits" + }, + "significance": "high" + }, + { + "ts": 1781521869157, + "type": "reflection", + "content": "Review found no mechanical cleanup to apply; SDK compile/tests pass, but added outbound webhook methods target routes absent from router and OpenAPI", + "raw": { + "focalPoints": [ + "sdk", + "openapi", + "server-contract", + "verification" + ], + "confidence": 0.82 + }, + "significance": "high", + "tags": [ + "focal:sdk", + "focal:openapi", + "focal:server-contract", + "focal:verification", + "confidence:0.82" + ] + } + ] + } + ], + "retrospective": { + "summary": "Reviewed PR #281 SDK outbound webhook additions. No mechanical edits applied. Found SDK methods for outbound webhook subscription/DLQ routes that are absent from current server router and OpenAPI contract; verified SDK build/typecheck/tests and contract surface pass, while Go-gated root checks cannot run because go is unavailable.", + "approach": "Standard approach", + "confidence": 0.82 + }, + "commits": [], + "filesChanged": [], + "projectId": "/home/daytona/workspace", + "tags": [], + "_trace": { + "startRef": "b951fa3b01f705af8555c98a23814a545532b699", + "endRef": "b951fa3b01f705af8555c98a23814a545532b699" + } +} \ No newline at end of file diff --git a/.trajectories/index.json b/.trajectories/index.json deleted file mode 100644 index d298fc7a..00000000 --- a/.trajectories/index.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": 1, - "lastUpdated": "2026-06-15T07:44:13.407Z", - "trajectories": { - "traj_7c0yqouml6gq": { - "title": "Review PR #277 in AgentWorkforce/relayfile", - "status": "completed", - "startedAt": "2026-06-14T19:15:23.611Z", - "completedAt": "2026-06-14T19:15:24.233Z", - "path": "/home/daytona/workspace/.trajectories/completed/2026-06/traj_7c0yqouml6gq.json" - } - } -} \ No newline at end of file From 1bb39c32fcb745d42c178483521c3679ac0af110 Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Mon, 15 Jun 2026 11:21:37 +0000 Subject: [PATCH 3/4] chore: apply pr-reviewer fixes for #281 --- .../active/traj_pmzcmccpttxb/trajectory.json | 19 ----- .../2026-06/traj_pmzcmccpttxb.trace.json | 81 ++++++++++++++++++ .../2026-06/traj_pmzcmccpttxb/summary.md | 39 +++++++++ .../2026-06/traj_pmzcmccpttxb/trajectory.json | 84 +++++++++++++++++++ .../2026-06/traj_4pvrlmqfnzng/trajectory.json | 4 +- 5 files changed, 206 insertions(+), 21 deletions(-) delete mode 100644 .agentworkforce/trajectories/active/traj_pmzcmccpttxb/trajectory.json create mode 100644 .agentworkforce/trajectories/completed/2026-06/traj_pmzcmccpttxb.trace.json create mode 100644 .agentworkforce/trajectories/completed/2026-06/traj_pmzcmccpttxb/summary.md create mode 100644 .agentworkforce/trajectories/completed/2026-06/traj_pmzcmccpttxb/trajectory.json diff --git a/.agentworkforce/trajectories/active/traj_pmzcmccpttxb/trajectory.json b/.agentworkforce/trajectories/active/traj_pmzcmccpttxb/trajectory.json deleted file mode 100644 index 7552aed4..00000000 --- a/.agentworkforce/trajectories/active/traj_pmzcmccpttxb/trajectory.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "traj_pmzcmccpttxb", - "version": 1, - "task": { - "title": "Review PR #281 in AgentWorkforce/relayfile" - }, - "status": "active", - "startedAt": "2026-06-15T11:05:55.976Z", - "agents": [], - "chapters": [], - "commits": [], - "filesChanged": [], - "projectId": "AgentWorkforce/relayfile", - "tags": [], - "_trace": { - "startRef": "48936510d7f69d0b871734cf6ed592beb10a3ede", - "endRef": "48936510d7f69d0b871734cf6ed592beb10a3ede" - } -} \ No newline at end of file diff --git a/.agentworkforce/trajectories/completed/2026-06/traj_pmzcmccpttxb.trace.json b/.agentworkforce/trajectories/completed/2026-06/traj_pmzcmccpttxb.trace.json new file mode 100644 index 00000000..347f8800 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-06/traj_pmzcmccpttxb.trace.json @@ -0,0 +1,81 @@ +{ + "version": "1.0.0", + "id": "e0d9dc38-17eb-4800-b660-a3437ea9b378", + "timestamp": "2026-06-15T11:21:06.653Z", + "trajectory": "traj_pmzcmccpttxb", + "files": [ + { + "path": ".agentworkforce/trajectories/active/traj_pmzcmccpttxb/trajectory.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 19, + "revision": "2432065c6190d1c6a66e57712150d9a3106d2812" + } + ] + } + ] + }, + { + "path": ".trajectories/active/traj_4pvrlmqfnzng/trajectory.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [] + } + ] + }, + { + "path": ".trajectories/completed/2026-06/traj_4pvrlmqfnzng/summary.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 33, + "revision": "2432065c6190d1c6a66e57712150d9a3106d2812" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-06/traj_4pvrlmqfnzng/trajectory.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 87, + "revision": "2432065c6190d1c6a66e57712150d9a3106d2812" + } + ] + } + ] + }, + { + "path": ".trajectories/index.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/.agentworkforce/trajectories/completed/2026-06/traj_pmzcmccpttxb/summary.md b/.agentworkforce/trajectories/completed/2026-06/traj_pmzcmccpttxb/summary.md new file mode 100644 index 00000000..ca8c844a --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-06/traj_pmzcmccpttxb/summary.md @@ -0,0 +1,39 @@ +# Trajectory: Review PR #281 in AgentWorkforce/relayfile + +> **Status:** ✅ Completed +> **Confidence:** 80% +> **Started:** June 15, 2026 at 11:05 AM +> **Completed:** June 15, 2026 at 11:21 AM + +--- + +## Summary + +Reviewed PR #281 outbound webhook SDK additions. Applied one mechanical duplicate-text cleanup in trajectory metadata. Found a blocking SDK/server/OpenAPI contract mismatch for newly added outbound webhook management and DLQ methods; validation showed SDK build/typecheck and contract-surface check pass, while canonical install/test paths are blocked by root lockfile drift and missing Go/Vitest dependencies in this sandbox. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Applied mechanical duplicate-text cleanup in trajectory JSON +- **Chose:** Applied mechanical duplicate-text cleanup in trajectory JSON +- **Reasoning:** CodeRabbit finding was valid in current checkout and changing duplicated metadata text is non-semantic + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Applied mechanical duplicate-text cleanup in trajectory JSON: Applied mechanical duplicate-text cleanup in trajectory JSON +- Reviewed PR #281 SDK outbound webhook additions. Applied one mechanical metadata cleanup. Main review finding remains SDK methods for outbound webhook management target routes absent from current server router and OpenAPI; SDK build/typecheck pass, contract surface passes, full CI path blocked by pre-existing lock drift and missing Go/Vitest install. + +--- + +## Artifacts + +**Commits:** 2432065 +**Files changed:** 5 diff --git a/.agentworkforce/trajectories/completed/2026-06/traj_pmzcmccpttxb/trajectory.json b/.agentworkforce/trajectories/completed/2026-06/traj_pmzcmccpttxb/trajectory.json new file mode 100644 index 00000000..b10766bc --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-06/traj_pmzcmccpttxb/trajectory.json @@ -0,0 +1,84 @@ +{ + "id": "traj_pmzcmccpttxb", + "version": 1, + "task": { + "title": "Review PR #281 in AgentWorkforce/relayfile" + }, + "status": "completed", + "startedAt": "2026-06-15T11:05:55.976Z", + "completedAt": "2026-06-15T11:21:06.560Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-06-15T11:17:46.230Z" + } + ], + "chapters": [ + { + "id": "chap_pasypmwj27pa", + "title": "Work", + "agentName": "default", + "startedAt": "2026-06-15T11:17:46.230Z", + "endedAt": "2026-06-15T11:21:06.560Z", + "events": [ + { + "ts": 1781522266231, + "type": "decision", + "content": "Applied mechanical duplicate-text cleanup in trajectory JSON: Applied mechanical duplicate-text cleanup in trajectory JSON", + "raw": { + "question": "Applied mechanical duplicate-text cleanup in trajectory JSON", + "chosen": "Applied mechanical duplicate-text cleanup in trajectory JSON", + "alternatives": [], + "reasoning": "CodeRabbit finding was valid in current checkout and changing duplicated metadata text is non-semantic" + }, + "significance": "high" + }, + { + "ts": 1781522448355, + "type": "reflection", + "content": "Reviewed PR #281 SDK outbound webhook additions. Applied one mechanical metadata cleanup. Main review finding remains SDK methods for outbound webhook management target routes absent from current server router and OpenAPI; SDK build/typecheck pass, contract surface passes, full CI path blocked by pre-existing lock drift and missing Go/Vitest install.", + "raw": { + "focalPoints": [ + "sdk", + "openapi", + "server-contract", + "ci" + ], + "confidence": 0.8 + }, + "significance": "high", + "tags": [ + "focal:sdk", + "focal:openapi", + "focal:server-contract", + "focal:ci", + "confidence:0.8" + ] + } + ] + } + ], + "retrospective": { + "summary": "Reviewed PR #281 outbound webhook SDK additions. Applied one mechanical duplicate-text cleanup in trajectory metadata. Found a blocking SDK/server/OpenAPI contract mismatch for newly added outbound webhook management and DLQ methods; validation showed SDK build/typecheck and contract-surface check pass, while canonical install/test paths are blocked by root lockfile drift and missing Go/Vitest dependencies in this sandbox.", + "approach": "Standard approach", + "confidence": 0.8 + }, + "commits": [ + "2432065" + ], + "filesChanged": [ + ".agentworkforce/trajectories/active/traj_pmzcmccpttxb/trajectory.json", + ".trajectories/active/traj_4pvrlmqfnzng/trajectory.json", + ".trajectories/completed/2026-06/traj_4pvrlmqfnzng/summary.md", + ".trajectories/completed/2026-06/traj_4pvrlmqfnzng/trajectory.json", + ".trajectories/index.json" + ], + "projectId": "AgentWorkforce/relayfile", + "tags": [], + "_trace": { + "startRef": "48936510d7f69d0b871734cf6ed592beb10a3ede", + "endRef": "2432065c6190d1c6a66e57712150d9a3106d2812", + "traceId": "e0d9dc38-17eb-4800-b660-a3437ea9b378" + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-06/traj_4pvrlmqfnzng/trajectory.json b/.trajectories/completed/2026-06/traj_4pvrlmqfnzng/trajectory.json index 35c9042e..5c25397d 100644 --- a/.trajectories/completed/2026-06/traj_4pvrlmqfnzng/trajectory.json +++ b/.trajectories/completed/2026-06/traj_4pvrlmqfnzng/trajectory.json @@ -37,7 +37,7 @@ { "ts": 1781521862043, "type": "decision", - "content": "Leave outbound webhook SDK/server contract mismatch as review finding: Leave outbound webhook SDK/server contract mismatch as review finding", + "content": "Leave outbound webhook SDK/server contract mismatch as review finding", "raw": { "question": "Leave outbound webhook SDK/server contract mismatch as review finding", "chosen": "Leave outbound webhook SDK/server contract mismatch as review finding", @@ -84,4 +84,4 @@ "startRef": "b951fa3b01f705af8555c98a23814a545532b699", "endRef": "b951fa3b01f705af8555c98a23814a545532b699" } -} \ No newline at end of file +} From 76abab30d4b658dc66de4769db8040313adc0592 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 15 Jun 2026 13:58:49 +0200 Subject: [PATCH 4/4] fix(sdk): sync lockfile, harden webhook methods, preserve contentHash - Regenerate root package-lock.json to 0.8.28 mount packages, fixing the stale-lock `npm ci` failures across Contract, E2E, SDK Typecheck, and Provider-backed evals (lock still pinned mount-* @ 0.8.27 after the v0.8.28 release bump). - Add defensive required-arg guards to registerWebhook/listWebhooks/ deleteWebhook/getWebhookDeadLetters/replayWebhookDeadLetter so empty workspaceId/url/subscriptionId/deliveryId throw instead of issuing a malformed request (addresses gemini-code-assist findings). - Carry contentHash through RelayFileSyncWireEvent and normalizeFilesystemEvent so the new typed field survives the higher-level sync API, not just raw REST/WS (addresses codex P2 finding). - Add tests for the validation guards and contentHash passthrough. Co-Authored-By: Claude Opus 4.8 (1M context) --- package-lock.json | 59 +++++++++++++--------- packages/sdk/typescript/src/client.test.ts | 24 +++++++++ packages/sdk/typescript/src/client.ts | 24 +++++++++ packages/sdk/typescript/src/sync.test.ts | 37 ++++++++++++++ packages/sdk/typescript/src/sync.ts | 2 + 5 files changed, 122 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 247a570a..82750239 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "relayfile", - "version": "0.8.27", + "version": "0.8.28", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "relayfile", - "version": "0.8.27", + "version": "0.8.28", "license": "Apache-2.0", "workspaces": [ "packages/core", @@ -1999,9 +1999,9 @@ "link": true }, "node_modules/@relayfile/mount-darwin-arm64": { - "version": "0.8.27", - "resolved": "https://registry.npmjs.org/@relayfile/mount-darwin-arm64/-/mount-darwin-arm64-0.8.27.tgz", - "integrity": "sha512-J/Y5vasUkUVWvlDwye4F1LF1QYdiZd5ZvhcIZwtbNnoYGl6klPK8xTnLkx4lgbxeGbjnkkC1dOZlcB9cthKTLQ==", + "version": "0.8.28", + "resolved": "https://registry.npmjs.org/@relayfile/mount-darwin-arm64/-/mount-darwin-arm64-0.8.28.tgz", + "integrity": "sha512-M6Yhqz6TAw/VgAh5URmFb0sMvjTbHDC6qjz6sjifkXpFfqn0v4gJsLeuYYN1rh2/e5KInTTk+QvwplIlMa17GA==", "cpu": [ "arm64" ], @@ -2012,9 +2012,9 @@ ] }, "node_modules/@relayfile/mount-darwin-x64": { - "version": "0.8.27", - "resolved": "https://registry.npmjs.org/@relayfile/mount-darwin-x64/-/mount-darwin-x64-0.8.27.tgz", - "integrity": "sha512-WXTC+rjjN6qa02D+HMNcwTc2JWNTj4oXRqGEmJZNTZlhVzK88kwuRqnoDxLjDEwZ7d5HYAyQHX3coVEdkYetuw==", + "version": "0.8.28", + "resolved": "https://registry.npmjs.org/@relayfile/mount-darwin-x64/-/mount-darwin-x64-0.8.28.tgz", + "integrity": "sha512-017dDLPCyF9OJIRAadwGd3z/SricGMucnqIakA1kVS82UJQYSZR1U6kq6mA0l7Fyz/he5JvQ8U9A6q1EwSEolw==", "cpu": [ "x64" ], @@ -2025,9 +2025,9 @@ ] }, "node_modules/@relayfile/mount-linux-arm64": { - "version": "0.8.27", - "resolved": "https://registry.npmjs.org/@relayfile/mount-linux-arm64/-/mount-linux-arm64-0.8.27.tgz", - "integrity": "sha512-6xriucInU5mMVki7kHNDMRC33o9xBFbWpg7i06I9PAm6ArEE45lWWrPGQ3TNWDJ2pCbasTrFweikkBfHExRd7w==", + "version": "0.8.28", + "resolved": "https://registry.npmjs.org/@relayfile/mount-linux-arm64/-/mount-linux-arm64-0.8.28.tgz", + "integrity": "sha512-vdPX0w4JQhbF5xUS2ep+PwRaneKmiA9Ue7c/rVC/DWpxfLb1nKVW9Avb/T5I648CefNHJ+nlRyOz66NnWUpxMQ==", "cpu": [ "arm64" ], @@ -2038,9 +2038,9 @@ ] }, "node_modules/@relayfile/mount-linux-x64": { - "version": "0.8.27", - "resolved": "https://registry.npmjs.org/@relayfile/mount-linux-x64/-/mount-linux-x64-0.8.27.tgz", - "integrity": "sha512-53abVBh/xv87hUTm5hMtmebpjsO0MyAWMNO6RE5qJtY+VGgrLBOqujzPCx4TAcAI4TtYXWX6bXiI2HyoNi7BBQ==", + "version": "0.8.28", + "resolved": "https://registry.npmjs.org/@relayfile/mount-linux-x64/-/mount-linux-x64-0.8.28.tgz", + "integrity": "sha512-lv/YCFEjmQkNBZQ+AFa9ChW3wqDfPlelINU1tviRcLms1R5cXpp0/HERo152VtAq8WyJIlPcauJfI1kjxnTYZQ==", "cpu": [ "x64" ], @@ -3739,6 +3739,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3760,6 +3761,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3781,6 +3783,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3802,6 +3805,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3823,6 +3827,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3844,6 +3849,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3865,6 +3871,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3886,6 +3893,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3907,6 +3915,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3928,6 +3937,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3949,6 +3959,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5276,7 +5287,7 @@ }, "packages/cli": { "name": "relayfile", - "version": "0.8.27", + "version": "0.8.28", "hasInstallScript": true, "license": "MIT", "bin": { @@ -5288,7 +5299,7 @@ }, "packages/core": { "name": "@relayfile/core", - "version": "0.8.27", + "version": "0.8.28", "license": "MIT", "devDependencies": { "@types/node": "^22.0.0", @@ -5301,7 +5312,7 @@ }, "packages/file-observer": { "name": "@relayfile/file-observer", - "version": "0.8.27", + "version": "0.8.28", "license": "MIT", "dependencies": { "class-variance-authority": "^0.7.0", @@ -6109,7 +6120,7 @@ }, "packages/local-mount": { "name": "@relayfile/local-mount", - "version": "0.8.27", + "version": "0.8.28", "license": "MIT", "dependencies": { "@parcel/watcher": "^2.5.6", @@ -6138,10 +6149,10 @@ }, "packages/sdk/typescript": { "name": "@relayfile/sdk", - "version": "0.8.27", + "version": "0.8.28", "license": "MIT", "dependencies": { - "@relayfile/core": "0.8.27", + "@relayfile/core": "0.8.28", "ignore": "^7.0.5", "tar": "^7.5.10" }, @@ -6153,10 +6164,10 @@ "node": ">=18" }, "optionalDependencies": { - "@relayfile/mount-darwin-arm64": "0.8.27", - "@relayfile/mount-darwin-x64": "0.8.27", - "@relayfile/mount-linux-arm64": "0.8.27", - "@relayfile/mount-linux-x64": "0.8.27" + "@relayfile/mount-darwin-arm64": "0.8.28", + "@relayfile/mount-darwin-x64": "0.8.28", + "@relayfile/mount-linux-arm64": "0.8.28", + "@relayfile/mount-linux-x64": "0.8.28" } } } diff --git a/packages/sdk/typescript/src/client.test.ts b/packages/sdk/typescript/src/client.test.ts index 254ab485..efd9d6a1 100644 --- a/packages/sdk/typescript/src/client.test.ts +++ b/packages/sdk/typescript/src/client.test.ts @@ -2131,6 +2131,30 @@ describe("RelayFileClient — new webhook/writeback methods", () => { ); }); + it("webhook methods reject missing required identifiers before issuing a request", async () => { + const f = mockFetch({}); + const client = makeClient(f); + + await expect( + client.registerWebhook({ workspaceId: "", url: "https://x", pathGlobs: ["/a"] }), + ).rejects.toThrow("workspaceId is required"); + await expect( + client.registerWebhook({ workspaceId: "ws_acme", url: "", pathGlobs: ["/a"] }), + ).rejects.toThrow("url is required"); + await expect(client.listWebhooks("")).rejects.toThrow("workspaceId is required"); + await expect(client.deleteWebhook("ws_acme", "")).rejects.toThrow( + "subscriptionId is required", + ); + await expect(client.getWebhookDeadLetters("")).rejects.toThrow( + "workspaceId is required", + ); + await expect(client.replayWebhookDeadLetter("ws_acme", "")).rejects.toThrow( + "deliveryId is required", + ); + + expect(f).not.toHaveBeenCalled(); + }); + it("listPendingWritebacks GETs writeback/pending", async () => { const payload: WritebackItem[] = [ { diff --git a/packages/sdk/typescript/src/client.ts b/packages/sdk/typescript/src/client.ts index f3c60b56..ee5b2c12 100644 --- a/packages/sdk/typescript/src/client.ts +++ b/packages/sdk/typescript/src/client.ts @@ -1869,6 +1869,12 @@ export class RelayFileClient { } async registerWebhook(input: RegisterWebhookInput): Promise { + if (!input.workspaceId) { + throw new Error("workspaceId is required"); + } + if (!input.url) { + throw new Error("url is required"); + } return this.request({ method: "POST", path: `/v1/workspaces/${encodeURIComponent(input.workspaceId)}/webhooks`, @@ -1886,6 +1892,9 @@ export class RelayFileClient { workspaceId: string, options: ListWebhooksOptions = {} ): Promise { + if (!workspaceId) { + throw new Error("workspaceId is required"); + } return this.request({ method: "GET", path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks`, @@ -1899,6 +1908,12 @@ export class RelayFileClient { subscriptionId: string, options: DeleteWebhookOptions = {} ): Promise { + if (!workspaceId) { + throw new Error("workspaceId is required"); + } + if (!subscriptionId) { + throw new Error("subscriptionId is required"); + } await this.performRequest({ method: "DELETE", path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/${encodeURIComponent(subscriptionId)}`, @@ -1911,6 +1926,9 @@ export class RelayFileClient { workspaceId: string, options: GetWebhookDeadLettersOptions = {} ): Promise { + if (!workspaceId) { + throw new Error("workspaceId is required"); + } const query = buildQuery({ cursor: options.cursor, limit: options.limit @@ -1929,6 +1947,12 @@ export class RelayFileClient { correlationId?: string, signal?: AbortSignal ): Promise { + if (!workspaceId) { + throw new Error("workspaceId is required"); + } + if (!deliveryId) { + throw new Error("deliveryId is required"); + } return this.request({ method: "POST", path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/dlq/${encodeURIComponent(deliveryId)}/replay`, diff --git a/packages/sdk/typescript/src/sync.test.ts b/packages/sdk/typescript/src/sync.test.ts index 1fb9a788..d6874477 100644 --- a/packages/sdk/typescript/src/sync.test.ts +++ b/packages/sdk/typescript/src/sync.test.ts @@ -103,6 +103,43 @@ describe("RelayFileSync", () => { await sync.stop(); }); + it("preserves contentHash from WebSocket event payloads", async () => { + const sockets: MockWebSocket[] = []; + const sync = new RelayFileSync({ + client: makeClient(), + workspaceId: "ws_acme", + baseUrl: "https://relay.test", + token: "ws_token", + webSocketFactory: (url) => { + const socket = new MockWebSocket(url); + sockets.push(socket); + return socket; + } + }); + + const events: FilesystemEvent[] = []; + sync.on("event", (event) => events.push(event)); + + sync.start(); + sockets[0]!.emit("open", {}); + sockets[0]!.emit("message", { + data: JSON.stringify({ + type: "file.updated", + path: "/docs/readme.md", + revision: "rev_2", + contentHash: "sha256:abc123", + timestamp: "2026-03-26T00:00:00Z" + }) + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(events).toHaveLength(1); + expect(events[0]!.contentHash).toBe("sha256:abc123"); + + await sync.stop(); + }); + it("sends exclusive cursor and path filters on WebSocket connect and reconnect", async () => { vi.useFakeTimers(); const sockets: MockWebSocket[] = []; diff --git a/packages/sdk/typescript/src/sync.ts b/packages/sdk/typescript/src/sync.ts index 6df075e1..4c20839a 100644 --- a/packages/sdk/typescript/src/sync.ts +++ b/packages/sdk/typescript/src/sync.ts @@ -139,6 +139,7 @@ interface RelayFileSyncWireEvent { eventId?: string; path?: string; revision?: string; + contentHash?: string; origin?: EventOrigin; provider?: string; correlationId?: string; @@ -254,6 +255,7 @@ function normalizeFilesystemEvent(message: RelayFileSyncWireEvent): FilesystemEv type: message.type as FilesystemEvent["type"], path: message.path ?? "", revision: message.revision ?? "", + contentHash: message.contentHash, origin: message.origin, provider: message.provider, correlationId: message.correlationId,