From 50bd35e1333decae09b3cb1a747a179ced2abcee Mon Sep 17 00:00:00 2001 From: Proactive Runtime Bot Date: Sun, 7 Jun 2026 22:33:51 +0200 Subject: [PATCH 1/6] Canonicalize Slack webhook paths in core --- package-lock.json | 34 ++-- packages/core/CHANGELOG.md | 9 +- packages/core/package.json | 2 +- packages/core/src/webhooks.test.ts | 283 +++++++++++++++++++++++++++++ packages/core/src/webhooks.ts | 121 ++++++++++-- 5 files changed, 420 insertions(+), 29 deletions(-) create mode 100644 packages/core/src/webhooks.test.ts diff --git a/package-lock.json b/package-lock.json index 38845530..8f9605c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { "name": "relayfile", - "version": "0.7.21", + "version": "0.8.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "relayfile", - "version": "0.7.21", + "version": "0.8.16", + "license": "Apache-2.0", "workspaces": [ "packages/core", "packages/sdk/typescript", @@ -1443,7 +1444,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -3149,7 +3149,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -4095,7 +4094,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -4105,7 +4103,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -4719,7 +4716,6 @@ "version": "7.5.13", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -5200,7 +5196,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -5240,7 +5235,7 @@ }, "packages/cli": { "name": "relayfile", - "version": "0.7.21", + "version": "0.8.16", "hasInstallScript": true, "license": "MIT", "bin": { @@ -5252,7 +5247,7 @@ }, "packages/core": { "name": "@relayfile/core", - "version": "0.7.21", + "version": "0.8.17", "license": "MIT", "devDependencies": { "@types/node": "^22.0.0", @@ -5265,7 +5260,7 @@ }, "packages/file-observer": { "name": "@relayfile/file-observer", - "version": "0.7.21", + "version": "0.8.16", "license": "MIT", "dependencies": { "class-variance-authority": "^0.7.0", @@ -6073,7 +6068,7 @@ }, "packages/local-mount": { "name": "@relayfile/local-mount", - "version": "0.7.21", + "version": "0.8.16", "license": "MIT", "dependencies": { "@parcel/watcher": "^2.5.6", @@ -6102,10 +6097,12 @@ }, "packages/sdk/typescript": { "name": "@relayfile/sdk", - "version": "0.7.21", + "version": "0.8.16", "license": "MIT", "dependencies": { - "@relayfile/core": "0.7.21" + "@relayfile/core": "0.8.16", + "ignore": "^7.0.5", + "tar": "^7.5.10" }, "devDependencies": { "typescript": "^5.7.3", @@ -6114,6 +6111,15 @@ "engines": { "node": ">=18" } + }, + "packages/sdk/typescript/node_modules/@relayfile/core": { + "version": "0.8.16", + "resolved": "https://registry.npmjs.org/@relayfile/core/-/core-0.8.16.tgz", + "integrity": "sha512-G4TfP9wZAOPl4TfSz/J+mYSTNJItcZCxt+lm/JTIMAMgVNpt2e3A5DJfpf3nNf24wekj+Q8c/BiskAlscL+KBg==", + "license": "MIT", + "engines": { + "node": ">=18" + } } } } diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index a088fa98..cdfd846e 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 _No unreleased changes._ +## [0.8.17] - 2026-06-07 + +### Fixed + +- Canonicalize Slack webhook envelope paths so hosted ingestion treats provider-relative channel paths and `/slack/...` paths equivalently, resolves raw channel IDs to existing `channelId__name` aliases, and ignores out-of-provider Slack paths. + ## [0.8.16] - 2026-06-07 _No user-visible changes in this release._ @@ -330,7 +336,8 @@ _No user-visible changes in this release._ ### Added - Optional `contentIdentity` on write operations, enabling server-side deduplication of identical payloads. ([#54]) -[Unreleased]: https://github.com/AgentWorkforce/relayfile/compare/v0.8.16...HEAD +[Unreleased]: https://github.com/AgentWorkforce/relayfile/compare/v0.8.17...HEAD +[0.8.17]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.8.17 [0.8.16]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.8.16 [0.8.15]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.8.15 [0.8.14]: https://github.com/AgentWorkforce/relayfile/releases/tag/v0.8.14 diff --git a/packages/core/package.json b/packages/core/package.json index f7dbb7c7..170dfa27 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@relayfile/core", - "version": "0.8.16", + "version": "0.8.17", "description": "Shared business logic for relayfile — file operations, ACL, queries, events, and writeback lifecycle", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/core/src/webhooks.test.ts b/packages/core/src/webhooks.test.ts new file mode 100644 index 00000000..fcd97851 --- /dev/null +++ b/packages/core/src/webhooks.test.ts @@ -0,0 +1,283 @@ +import { describe, expect, it } from "vitest"; + +import type { + EnvelopeQueryOptions, + EnvelopeRow, + EventRow, + FileRow, + OperationRow, + Paginated, + PaginationOptions, + StorageAdapter, + WritebackItem, +} from "./storage.js"; +import { + applyWebhookEnvelope, + ingestWebhook, + normalizeEnvelopeEvent, + normalizeEnvelopePath, +} from "./webhooks.js"; + +describe("webhook Slack path canonicalization", () => { + it("canonicalizes Slack provider-relative envelope paths", () => { + const event = normalizeEnvelopeEvent({ + provider: "slack", + receivedAt: "2026-06-07T20:00:00.000Z", + payload: { + provider: "slack", + event_type: "file.updated", + path: "/channels/C123/messages/1711111111_000100/meta.json", + }, + }); + + expect(event?.path).toBe( + "/slack/channels/C123/messages/1711111111_000100/meta.json", + ); + expect( + normalizeEnvelopePath({ + provider: "slack", + payload: { + provider: "slack", + path: "/channels/C123/messages/1711111111_000100/meta.json", + }, + }), + ).toBe("/slack/channels/C123/messages/1711111111_000100/meta.json"); + }); + + it("resolves raw Slack channel IDs to existing channelId__name aliases before writes and events", () => { + const storage = new MemoryStorage([ + fileRow("/slack/channels/C123__engineering/meta.json", { + content: '{"id":"C123","name":"engineering"}', + provider: "slack", + }), + ]); + + const result = applyWebhookEnvelope(storage, { + envelopeId: "env_1", + workspaceId: "ws_core", + provider: "slack", + deliveryId: "delivery_1", + receivedAt: "2026-06-07T20:00:00.000Z", + payload: { + provider: "slack", + event_type: "file.updated", + path: "/slack/channels/C123/messages/1711111111_000100/meta.json", + content: '{"text":"hello"}', + contentType: "application/json", + }, + correlationId: "corr_1", + status: "queued", + attemptCount: 0, + lastError: null, + }); + + const canonicalPath = + "/slack/channels/C123__engineering/messages/1711111111_000100/meta.json"; + expect(result).toMatchObject({ + status: "processed", + path: canonicalPath, + }); + expect(storage.getFile(canonicalPath)?.content).toBe('{"text":"hello"}'); + expect( + storage.getFile("/slack/channels/C123/messages/1711111111_000100/meta.json"), + ).toBeNull(); + expect(storage.events.at(-1)?.path).toBe(canonicalPath); + }); + + it("ignores Slack envelopes targeting paths outside the Slack provider root", () => { + const storage = new MemoryStorage(); + + const result = applyWebhookEnvelope(storage, { + envelopeId: "env_1", + workspaceId: "ws_core", + provider: "slack", + deliveryId: "delivery_1", + receivedAt: "2026-06-07T20:00:00.000Z", + payload: { + provider: "slack", + event_type: "file.updated", + path: "/github/repos/acme/cloud/issues/1.json", + content: '{"title":"wrong provider"}', + }, + correlationId: "corr_1", + status: "queued", + attemptCount: 0, + lastError: null, + }); + + expect(result).toEqual({ + status: "ignored", + eventType: null, + path: null, + revision: null, + }); + expect(storage.listFiles()).toEqual([]); + expect(storage.events).toEqual([]); + }); + + it("coalesces Slack provider-relative and canonical paths by canonical path", () => { + const storage = new MemoryStorage(); + let nextEnvelopeId = 1; + + const first = ingestWebhook(storage, { + provider: "slack", + eventType: "file.updated", + path: "/channels/C123/messages/1711111111_000100/meta.json", + deliveryId: "delivery_1", + timestamp: "2026-06-07T20:00:00.000Z", + correlationId: "corr_1", + }, { + generateEnvelopeId: () => `env_${nextEnvelopeId++}`, + coalesceWindowMs: 10_000, + }); + const second = ingestWebhook(storage, { + provider: "slack", + eventType: "file.updated", + path: "/slack/channels/C123/messages/1711111111_000100/meta.json", + deliveryId: "delivery_2", + timestamp: "2026-06-07T20:00:01.000Z", + correlationId: "corr_2", + }, { + generateEnvelopeId: () => `env_${nextEnvelopeId++}`, + coalesceWindowMs: 10_000, + }); + + expect(first).toMatchObject({ status: "queued", envelopeId: "env_1" }); + expect(second).toMatchObject({ status: "queued", envelopeId: "env_1" }); + expect(Array.from(storage.envelopes)).toHaveLength(1); + const envelope = storage.envelopes.get("env_1"); + expect(envelope?.deliveryIds).toEqual(["delivery_1", "delivery_2"]); + expect(envelope?.payload.path).toBe( + "/slack/channels/C123/messages/1711111111_000100/meta.json", + ); + }); +}); + +function fileRow(path: string, overrides: Partial = {}): FileRow { + return { + path, + revision: "rev_1", + contentType: "application/json", + content: "{}", + encoding: "utf-8", + provider: "", + lastEditedAt: "2026-06-07T20:00:00.000Z", + semantics: {}, + ...overrides, + }; +} + +class MemoryStorage implements StorageAdapter { + files = new Map(); + events: EventRow[] = []; + envelopes = new Map(); + deliveryAliases = new Map(); + revisionCounter = 1; + eventCounter = 1; + + constructor(files: FileRow[] = []) { + for (const file of files) { + this.files.set(file.path, file); + } + } + + getFile(path: string): FileRow | null { + return this.files.get(path) ?? null; + } + + listFiles(): FileRow[] { + return Array.from(this.files.values()); + } + + putFile(file: FileRow): void { + this.files.set(file.path, file); + } + + deleteFile(path: string): void { + this.files.delete(path); + } + + appendEvent(event: EventRow): void { + this.events.push(event); + } + + listEvents(_options: PaginationOptions & { provider?: string }): Paginated { + return { items: this.events, nextCursor: null }; + } + + getRecentEvents(limit: number): EventRow[] { + return this.events.slice(-limit); + } + + getOperation(_opId: string): OperationRow | null { + return null; + } + + putOperation(_op: OperationRow): void { + return undefined; + } + + listOperations(_options: PaginationOptions): Paginated { + return { items: [], nextCursor: null }; + } + + nextRevision(): string { + return `rev_${this.revisionCounter++}`; + } + + nextOperationId(): string { + return "op_1"; + } + + nextEventId(): string { + return `evt_${this.eventCounter++}`; + } + + enqueueWriteback(_item: WritebackItem): void { + return undefined; + } + + getPendingWritebacks(): WritebackItem[] { + return []; + } + + getEnvelopeByDelivery( + workspaceId: string, + provider: string, + deliveryId: string, + ): EnvelopeRow | null { + const envelopeId = this.deliveryAliases.get( + `${workspaceId}|${provider}|${deliveryId}`, + ); + return envelopeId ? this.envelopes.get(envelopeId) ?? null : null; + } + + putEnvelope(envelope: EnvelopeRow): void { + this.envelopes.set(envelope.envelopeId, envelope); + } + + putEnvelopeDeliveryAlias( + workspaceId: string, + provider: string, + deliveryId: string, + envelopeId: string, + ): void { + this.deliveryAliases.set(`${workspaceId}|${provider}|${deliveryId}`, envelopeId); + } + + listEnvelopes(options: EnvelopeQueryOptions): Paginated { + return { + items: Array.from(this.envelopes.values()).filter( + (envelope) => + (!options.workspaceId || envelope.workspaceId === options.workspaceId) && + (!options.provider || envelope.provider === options.provider) && + (!options.status || envelope.status === options.status), + ), + nextCursor: null, + }; + } + + getWorkspaceId(): string { + return "ws_core"; + } +} diff --git a/packages/core/src/webhooks.ts b/packages/core/src/webhooks.ts index 72b324c2..c0a112fa 100644 --- a/packages/core/src/webhooks.ts +++ b/packages/core/src/webhooks.ts @@ -184,6 +184,7 @@ export function ingestWebhook( const coalesced = findCoalescedEnvelope( queued, payload, + provider, receivedAt, options.coalesceWindowMs ?? DEFAULT_COALESCE_WINDOW_MS, ); @@ -248,12 +249,14 @@ export function normalizeEnvelope( ): Partial { const provider = normalizeProvider(input.provider); const eventType = normalizeEventType(input.eventType); - const path = normalizePath(input.path ?? ""); + const canonicalPath = input.path?.trim() + ? canonicalProviderEnvelopePath(provider, input.path) + : null; const correlationId = input.correlationId?.trim() ?? ""; const receivedAt = normalizeIsoDate(input.timestamp) ?? now(); const deliveryId = input.deliveryId?.trim(); - if (!provider || !eventType || !input.path?.trim()) { + if (!provider || !eventType || !input.path?.trim() || !canonicalPath) { return { provider, deliveryId, @@ -272,7 +275,7 @@ export function normalizeEnvelope( payload: { provider, event_type: eventType, - path, + path: canonicalPath, timestamp: receivedAt, data: asRecord(input.data), delivery_id: deliveryId, @@ -282,7 +285,7 @@ export function normalizeEnvelope( } export function normalizeEnvelopeEvent( - envelope: Pick, + envelope: Pick & Partial>, ): EnvelopeEvent | null { const payload = envelope.payload; const eventType = normalizeEnvelopeEventType(payload); @@ -290,7 +293,10 @@ export function normalizeEnvelopeEvent( return null; } - const path = normalizePath(asOptionalString(payload.path) ?? "/"); + const path = normalizeEnvelopePath(envelope); + if (!path) { + return null; + } const data = asRecord(payload.data); const body = Object.keys(data).length > 0 ? data : payload; const timestamp = @@ -322,10 +328,13 @@ export function normalizeEnvelopeEvent( } export function normalizeEnvelopePath( - envelope: Pick, + envelope: Pick & Partial>, ): string | null { const path = asOptionalString(envelope.payload.path); - return path ? normalizePath(path) : null; + const provider = + asOptionalString(envelope.provider) ?? + asOptionalString(envelope.payload.provider); + return path ? canonicalProviderEnvelopePath(provider, path) : null; } export function applyWebhookEnvelope( @@ -333,8 +342,8 @@ export function applyWebhookEnvelope( envelope: EnvelopeRow, options: ApplyEnvelopeOptions = {}, ): ApplyEnvelopeResult { - const event = normalizeEnvelopeEvent(envelope); - if (!event) { + const normalizedEvent = normalizeEnvelopeEvent(envelope); + if (!normalizedEvent) { return { status: "ignored", eventType: null, @@ -342,6 +351,7 @@ export function applyWebhookEnvelope( revision: null, }; } + const event = canonicalizeEnvelopeEventPath(storage, envelope.provider, normalizedEvent); if (options.shouldSuppress?.(envelope, event)) { const revision = appendSyncEvent( @@ -573,17 +583,18 @@ function appendSyncEvent( function findCoalescedEnvelope( envelopes: EnvelopeRow[], payload: Record, + provider: string, receivedAt: string, windowMs: number, ): EnvelopeRow | null { - const key = coalesceObjectKey(payload); + const key = coalesceObjectKey(payload, provider); if (!key) { return null; } let match: EnvelopeRow | null = null; for (const envelope of envelopes) { - if (coalesceObjectKey(envelope.payload) !== key) { + if (coalesceObjectKey(envelope.payload, envelope.provider) !== key) { continue; } if (!withinCoalesceWindow(envelope.receivedAt, receivedAt, windowMs)) { @@ -606,7 +617,7 @@ function deliveryMatches(envelope: EnvelopeRow, deliveryId: string): boolean { return envelope.deliveryId === deliveryId || envelope.deliveryIds?.includes(deliveryId) === true; } -function coalesceObjectKey(payload: Record): string { +function coalesceObjectKey(payload: Record, provider?: string): string { const data = asRecord(payload.data); const providerObjectId = asOptionalString(data.providerObjectId) ?? @@ -619,10 +630,94 @@ function coalesceObjectKey(payload: Record): string { return `object:${providerObjectId}`; } - const path = normalizeEnvelopePath({ payload }); + const path = normalizeEnvelopePath({ payload, provider }); return path && path !== "/" ? `path:${path}` : ""; } +const PROVIDER_RELATIVE_PATH_ROOTS: Record> = { + slack: new Set(["channels", "dms", "teams", "users"]), +}; + +function canonicalProviderEnvelopePath(provider: string | undefined, rawPath: string): string | null { + const path = normalizePath(rawPath); + const normalizedProvider = normalizeProvider(provider); + if (!normalizedProvider || path === "/") { + return path; + } + + const relativeRoots = PROVIDER_RELATIVE_PATH_ROOTS[normalizedProvider]; + if (!relativeRoots) { + return path; + } + + const trimmed = path.slice(1); + if ( + trimmed === normalizedProvider || + trimmed.startsWith(`${normalizedProvider}/`) + ) { + return path; + } + + const [firstSegment] = trimmed.split("/", 1); + if (firstSegment && relativeRoots.has(firstSegment)) { + return normalizePath(`/${normalizedProvider}/${trimmed}`); + } + + return null; +} + +function canonicalizeEnvelopeEventPath( + storage: StorageAdapter, + provider: string, + event: EnvelopeEvent, +): EnvelopeEvent { + const path = canonicalizeExistingProviderAliasPath(storage, provider, event.path); + return path === event.path ? event : { ...event, path }; +} + +function canonicalizeExistingProviderAliasPath( + storage: StorageAdapter, + provider: string, + rawPath: string, +): string { + const path = normalizePath(rawPath); + if (normalizeProvider(provider) !== "slack") { + return path; + } + return canonicalizeSlackChannelAliasPath(storage.listFiles(), path); +} + +function canonicalizeSlackChannelAliasPath(files: { path: string }[], rawPath: string): string { + const path = normalizePath(rawPath); + const parts = path.slice(1).split("/"); + if (parts.length < 3 || parts[0] !== "slack" || parts[1] !== "channels") { + return path; + } + const channelSegment = parts[2]; + if (!channelSegment || channelSegment.includes("__")) { + return path; + } + + const candidates = files + .map((file) => normalizePath(file.path).slice(1).split("/")) + .filter( + (fileParts) => + fileParts.length >= 3 && + fileParts[0] === "slack" && + fileParts[1] === "channels" && + fileParts[2]?.startsWith(`${channelSegment}__`), + ) + .map((fileParts) => fileParts[2] as string) + .sort(); + + if (candidates.length === 0) { + return path; + } + + parts[2] = candidates[0] as string; + return normalizePath(`/${parts.join("/")}`); +} + function withinCoalesceWindow( existingReceivedAt: string, incomingReceivedAt: string, From 7f15ed427d10928da103d423e6a1eb50cba198b8 Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Sun, 7 Jun 2026 20:43:07 +0000 Subject: [PATCH 2/6] chore: apply pr-reviewer fixes for #257 --- .../active/traj_y5jru5dh9ku6/trajectory.json | 12 ++ .../completed/2026-06/traj_hk2sfepahlu0.json | 104 ++++++++++ .../completed/2026-06/traj_hk2sfepahlu0.md | 38 ++++ .trajectories/index.json | 188 ++++++++++++++++++ package-lock.json | 13 +- packages/core/src/webhooks.ts | 15 +- 6 files changed, 348 insertions(+), 22 deletions(-) create mode 100644 .trajectories/completed/2026-06/traj_hk2sfepahlu0.json create mode 100644 .trajectories/completed/2026-06/traj_hk2sfepahlu0.md create mode 100644 .trajectories/index.json diff --git a/.trajectories/active/traj_y5jru5dh9ku6/trajectory.json b/.trajectories/active/traj_y5jru5dh9ku6/trajectory.json index cab9d1e6..1af9c38d 100644 --- a/.trajectories/active/traj_y5jru5dh9ku6/trajectory.json +++ b/.trajectories/active/traj_y5jru5dh9ku6/trajectory.json @@ -91,6 +91,18 @@ "tags": [ "confidence:0.86" ] + }, + { + "ts": 1780864647303, + "type": "decision", + "content": "Patch ingestWebhook to pass provider into normalizeEnvelopePath: Patch ingestWebhook to pass provider into normalizeEnvelopePath", + "raw": { + "question": "Patch ingestWebhook to pass provider into normalizeEnvelopePath", + "chosen": "Patch ingestWebhook to pass provider into normalizeEnvelopePath", + "alternatives": [], + "reasoning": "Current checkout canonicalizes Slack provider-relative paths in normalizeEnvelope, but ingestWebhook re-normalizes payload.path without provider and rejects /channels/... as invalid_input before queueing." + }, + "significance": "high" } ] } diff --git a/.trajectories/completed/2026-06/traj_hk2sfepahlu0.json b/.trajectories/completed/2026-06/traj_hk2sfepahlu0.json new file mode 100644 index 00000000..49669344 --- /dev/null +++ b/.trajectories/completed/2026-06/traj_hk2sfepahlu0.json @@ -0,0 +1,104 @@ +{ + "id": "traj_hk2sfepahlu0", + "version": 1, + "task": { + "title": "Review and fix PR #256/#257 Slack webhook canonicalization" + }, + "status": "completed", + "startedAt": "2026-06-07T20:39:45.290Z", + "completedAt": "2026-06-07T20:42:27.896Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-06-07T20:40:04.615Z" + } + ], + "chapters": [ + { + "id": "chap_npg3si0tuioo", + "title": "Work", + "agentName": "default", + "startedAt": "2026-06-07T20:40:04.615Z", + "endedAt": "2026-06-07T20:42:27.896Z", + "events": [ + { + "ts": 1780864804616, + "type": "decision", + "content": "Apply prefix filtering in Slack channel alias resolution: Apply prefix filtering in Slack channel alias resolution", + "raw": { + "question": "Apply prefix filtering in Slack channel alias resolution", + "chosen": "Apply prefix filtering in Slack channel alias resolution", + "alternatives": [], + "reasoning": "Gemini's review is still valid in the current checkout: canonicalizeSlackChannelAliasPath splits every workspace file path before filtering. Prefix filtering preserves behavior while avoiding splits for unrelated files." + }, + "significance": "high" + }, + { + "ts": 1780864806988, + "type": "decision", + "content": "Expanded verification to SDK build and typecheck: Expanded verification to SDK build and typecheck", + "raw": { + "question": "Expanded verification to SDK build and typecheck", + "chosen": "Expanded verification to SDK build and typecheck", + "alternatives": [], + "reasoning": "package-lock and SDK dependency metadata changed in the PR diff, so downstream TypeScript SDK compilation is part of the impacted surface." + }, + "significance": "high" + }, + { + "ts": 1780864861688, + "type": "reflection", + "content": "PR review fixes are narrow and verified locally: ingestion now validates Slack provider-relative paths with provider context, and alias resolution now filters by Slack channel prefix before splitting paths.", + "raw": { + "focalPoints": [ + "webhook-canonicalization", + "bot-review", + "verification" + ], + "confidence": 0.86 + }, + "significance": "high", + "tags": [ + "focal:webhook-canonicalization", + "focal:bot-review", + "focal:verification", + "confidence:0.86" + ] + }, + { + "ts": 1780864909198, + "type": "reflection", + "content": "Full test run exposed a local dependency install problem in file-observer, not a core webhook failure", + "raw": { + "focalPoints": [ + "verification", + "dependency-install" + ], + "adjustments": "Repair node_modules and rerun the failed workspace plus Go before concluding.", + "confidence": 0.75 + }, + "significance": "high", + "tags": [ + "focal:verification", + "focal:dependency-install", + "confidence:0.75" + ] + } + ] + } + ], + "retrospective": { + "summary": "Reviewed PR #257, fixed Slack webhook ingestion provider-context validation, addressed Gemini's alias-resolution performance review with prefix filtering, and verified core tests/build/package dry-run plus contract check locally.", + "approach": "Standard approach", + "confidence": 0.86 + }, + "commits": [], + "filesChanged": [], + "projectId": "/home/daytona/workspace", + "tags": [], + "_trace": { + "startRef": "50bd35e1333decae09b3cb1a747a179ced2abcee", + "endRef": "50bd35e1333decae09b3cb1a747a179ced2abcee" + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-06/traj_hk2sfepahlu0.md b/.trajectories/completed/2026-06/traj_hk2sfepahlu0.md new file mode 100644 index 00000000..b6fc16af --- /dev/null +++ b/.trajectories/completed/2026-06/traj_hk2sfepahlu0.md @@ -0,0 +1,38 @@ +# Trajectory: Review and fix PR #256/#257 Slack webhook canonicalization + +> **Status:** ✅ Completed +> **Confidence:** 86% +> **Started:** June 7, 2026 at 08:39 PM +> **Completed:** June 7, 2026 at 08:42 PM + +--- + +## Summary + +Reviewed PR #257, fixed Slack webhook ingestion provider-context validation, addressed Gemini's alias-resolution performance review with prefix filtering, and verified core tests/build/package dry-run plus contract check locally. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Apply prefix filtering in Slack channel alias resolution +- **Chose:** Apply prefix filtering in Slack channel alias resolution +- **Reasoning:** Gemini's review is still valid in the current checkout: canonicalizeSlackChannelAliasPath splits every workspace file path before filtering. Prefix filtering preserves behavior while avoiding splits for unrelated files. + +### Expanded verification to SDK build and typecheck +- **Chose:** Expanded verification to SDK build and typecheck +- **Reasoning:** package-lock and SDK dependency metadata changed in the PR diff, so downstream TypeScript SDK compilation is part of the impacted surface. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Apply prefix filtering in Slack channel alias resolution: Apply prefix filtering in Slack channel alias resolution +- Expanded verification to SDK build and typecheck: Expanded verification to SDK build and typecheck +- PR review fixes are narrow and verified locally: ingestion now validates Slack provider-relative paths with provider context, and alias resolution now filters by Slack channel prefix before splitting paths. +- Full test run exposed a local dependency install problem in file-observer, not a core webhook failure diff --git a/.trajectories/index.json b/.trajectories/index.json new file mode 100644 index 00000000..0f97be60 --- /dev/null +++ b/.trajectories/index.json @@ -0,0 +1,188 @@ +{ + "version": 1, + "lastUpdated": "2026-06-07T20:42:28.588Z", + "trajectories": { + "traj_7x9nltybo08h": { + "title": "Update PR 65 usage instructions", + "status": "completed", + "startedAt": "2026-04-30T21:07:25.621Z", + "completedAt": "2026-04-30T21:08:20.135Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_7x9nltybo08h.json" + }, + "traj_82lywlk9dcnc": { + "title": "Write SDK setup client workflow from spec", + "status": "completed", + "startedAt": "2026-04-30T16:43:24.651Z", + "completedAt": "2026-04-30T16:51:07.147Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_82lywlk9dcnc.json" + }, + "traj_cdist8i8vdmd": { + "title": "Address PR 65 feedback", + "status": "completed", + "startedAt": "2026-04-30T20:10:45.916Z", + "completedAt": "2026-04-30T20:11:29.126Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_cdist8i8vdmd.json" + }, + "traj_dmoc4slub7ox": { + "title": "Fix SDK setup workflow evidence path", + "status": "completed", + "startedAt": "2026-04-30T17:33:11.390Z", + "completedAt": "2026-04-30T17:34:29.265Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_dmoc4slub7ox.json" + }, + "traj_em3hvzpg1xmx": { + "title": "062-sdk-setup-client-workflow", + "status": "completed", + "startedAt": "2026-04-30T17:43:56.110Z", + "completedAt": "2026-04-30T17:43:59.041Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_em3hvzpg1xmx.json" + }, + "traj_i1f02867dkxn": { + "title": "Fix SDK setup workflow verify gate", + "status": "completed", + "startedAt": "2026-04-30T17:07:38.811Z", + "completedAt": "2026-04-30T17:10:18.837Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_i1f02867dkxn.json" + }, + "traj_iuzm83ogm43k": { + "title": "Replace chokidar with @parcel/watcher in local-mount", + "status": "completed", + "startedAt": "2026-04-20T20:35:15.759Z", + "completedAt": "2026-04-20T20:58:15.412Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_iuzm83ogm43k.json" + }, + "traj_mfyus7zfgxt2": { + "title": "062-sdk-setup-client-workflow", + "status": "completed", + "startedAt": "2026-04-30T16:53:07.629Z", + "completedAt": "2026-04-30T17:05:01.326Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_mfyus7zfgxt2.json" + }, + "traj_nixaonkglri1": { + "title": "Migrate relayfile e2e and conformance scripts to RS256 local JWKS", + "status": "completed", + "startedAt": "2026-04-24T09:06:31.046Z", + "completedAt": "2026-04-24T09:10:42.425Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_nixaonkglri1.json" + }, + "traj_qi3qmy5oveab": { + "title": "Resolve SDK setup review gate", + "status": "completed", + "startedAt": "2026-04-30T17:41:15.457Z", + "completedAt": "2026-04-30T17:43:16.297Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_qi3qmy5oveab.json" + }, + "traj_ubq95azheqpt": { + "title": "062-sdk-setup-client-workflow", + "status": "completed", + "startedAt": "2026-04-30T17:12:22.684Z", + "completedAt": "2026-04-30T17:23:57.490Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_ubq95azheqpt.json" + }, + "traj_wez7rl7pkfpn": { + "title": "Review PR 65 implementation against spec", + "status": "completed", + "startedAt": "2026-04-30T20:31:59.188Z", + "completedAt": "2026-04-30T20:31:59.361Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_wez7rl7pkfpn.json" + }, + "traj_6fjv0fnvrc5e": { + "title": "Relayfile follow-up PRs: cloud conventions, cloud sdk/core bump, adapters release pipeline investigation", + "status": "completed", + "startedAt": "2026-05-09T13:35:32.701Z", + "completedAt": "2026-05-09T13:45:18.302Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_6fjv0fnvrc5e.json" + }, + "traj_6lyjg41p6a28": { + "title": "Address PR comments on cloud#504 Linear conventions", + "status": "completed", + "startedAt": "2026-05-09T13:55:09.128Z", + "completedAt": "2026-05-09T13:57:04.293Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_6lyjg41p6a28.json" + }, + "traj_9khc36ax639i": { + "title": "Add relayfile eval harness", + "status": "completed", + "startedAt": "2026-05-08T23:08:09.607Z", + "completedAt": "2026-05-08T23:18:24.282Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_9khc36ax639i.json" + }, + "traj_a6rfc30zag40": { + "title": "Draft agent workspace golden path spec", + "status": "completed", + "startedAt": "2026-05-01T15:09:58.013Z", + "completedAt": "2026-05-01T15:12:08.390Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_a6rfc30zag40.json" + }, + "traj_ailh4waboewf": { + "title": "Design relayfile low-friction cloud login and integration mount flow", + "status": "completed", + "startedAt": "2026-05-01T23:39:39.687Z", + "completedAt": "2026-05-01T23:45:39.611Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_ailh4waboewf.json" + }, + "traj_d3drzvodqpn7": { + "title": "Address PR 114 comments", + "status": "completed", + "startedAt": "2026-05-09T08:39:35.473Z", + "completedAt": "2026-05-09T08:42:45.202Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_d3drzvodqpn7.json" + }, + "traj_hyqnsfininh5": { + "title": "Write agent workspace implementation workflow", + "status": "completed", + "startedAt": "2026-05-01T15:22:05.684Z", + "completedAt": "2026-05-01T15:27:12.578Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_hyqnsfininh5.json" + }, + "traj_nxbsptr6c5q0": { + "title": "Investigate local lag and open status diagnostics PR", + "status": "completed", + "startedAt": "2026-05-14T09:46:57.122Z", + "completedAt": "2026-05-14T09:50:46.065Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_nxbsptr6c5q0.json" + }, + "traj_qrt7hh3ht8nk": { + "title": "Investigate confusing relayfile lagging status", + "status": "completed", + "startedAt": "2026-05-14T09:38:05.074Z", + "completedAt": "2026-05-14T09:42:53.566Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_qrt7hh3ht8nk.json" + }, + "traj_v1un6n66y38i": { + "title": "Async createMount — issue #104", + "status": "completed", + "startedAt": "2026-05-08T17:27:24.218Z", + "completedAt": "2026-05-08T17:31:22.965Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_v1un6n66y38i.json" + }, + "traj_xf18gkmtr3ib": { + "title": "Address PR comments on relayfile-adapters#59", + "status": "completed", + "startedAt": "2026-05-09T13:50:45.476Z", + "completedAt": "2026-05-09T13:54:43.281Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_xf18gkmtr3ib.json" + }, + "traj_z2klijcrwqed": { + "title": "Design simple agent workspace connect flow", + "status": "completed", + "startedAt": "2026-05-01T14:58:15.412Z", + "completedAt": "2026-05-01T15:06:23.351Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_z2klijcrwqed.json" + }, + "traj_cf89ajbo2ast": { + "title": "Review and fix PR #243", + "status": "completed", + "startedAt": "2026-06-06T00:23:06.923Z", + "completedAt": "2026-06-06T00:23:16.097Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-06/traj_cf89ajbo2ast.json" + }, + "traj_hk2sfepahlu0": { + "title": "Review and fix PR #256/#257 Slack webhook canonicalization", + "status": "completed", + "startedAt": "2026-06-07T20:39:45.290Z", + "completedAt": "2026-06-07T20:42:27.896Z", + "path": "/home/daytona/workspace/.trajectories/completed/2026-06/traj_hk2sfepahlu0.json" + } + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8f9605c7..6156caa4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -830,7 +830,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "dev": true, + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -3687,7 +3687,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3709,7 +3708,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3731,7 +3729,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3753,7 +3750,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3775,7 +3771,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3797,7 +3792,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3819,7 +3813,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3841,7 +3834,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3863,7 +3855,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3885,7 +3876,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3907,7 +3897,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, diff --git a/packages/core/src/webhooks.ts b/packages/core/src/webhooks.ts index c0a112fa..19661c5e 100644 --- a/packages/core/src/webhooks.ts +++ b/packages/core/src/webhooks.ts @@ -146,7 +146,7 @@ export function ingestWebhook( const normalized = normalizeEnvelope(input, now); const provider = asOptionalString(normalized.provider) ?? ""; const payload = asRecord(normalized.payload); - const path = normalizeEnvelopePath({ payload }); + const path = normalizeEnvelopePath({ payload, provider }); const receivedAt = asOptionalString(normalized.receivedAt) ?? now(); const workspaceId = storage.getWorkspaceId(); const deliveryId = @@ -698,16 +698,11 @@ function canonicalizeSlackChannelAliasPath(files: { path: string }[], rawPath: s return path; } + const prefix = `/slack/channels/${channelSegment}__`; const candidates = files - .map((file) => normalizePath(file.path).slice(1).split("/")) - .filter( - (fileParts) => - fileParts.length >= 3 && - fileParts[0] === "slack" && - fileParts[1] === "channels" && - fileParts[2]?.startsWith(`${channelSegment}__`), - ) - .map((fileParts) => fileParts[2] as string) + .map((file) => normalizePath(file.path)) + .filter((filePath) => filePath.startsWith(prefix)) + .map((filePath) => filePath.slice(1).split("/")[2] as string) .sort(); if (candidates.length === 0) { From 1aa330c51eafda9ad58cdff30b356efdbe10ee34 Mon Sep 17 00:00:00 2001 From: Proactive Runtime Bot Date: Sun, 7 Jun 2026 22:49:11 +0200 Subject: [PATCH 3/6] Avoid unnecessary Slack alias storage scans --- packages/core/src/webhooks.test.ts | 59 ++++++++++++++++++++++++++++++ packages/core/src/webhooks.ts | 8 ++++ 2 files changed, 67 insertions(+) diff --git a/packages/core/src/webhooks.test.ts b/packages/core/src/webhooks.test.ts index fcd97851..d461af97 100644 --- a/packages/core/src/webhooks.test.ts +++ b/packages/core/src/webhooks.test.ts @@ -82,6 +82,63 @@ describe("webhook Slack path canonicalization", () => { storage.getFile("/slack/channels/C123/messages/1711111111_000100/meta.json"), ).toBeNull(); expect(storage.events.at(-1)?.path).toBe(canonicalPath); + expect(storage.listFilesCalls).toBe(1); + }); + + it("does not scan storage when Slack channel alias resolution is unnecessary", () => { + const storage = new MemoryStorage([ + fileRow("/slack/channels/C123__engineering/meta.json", { + content: '{"id":"C123","name":"engineering"}', + provider: "slack", + }), + ]); + + const aliasedResult = applyWebhookEnvelope(storage, { + envelopeId: "env_aliased", + workspaceId: "ws_core", + provider: "slack", + deliveryId: "delivery_aliased", + receivedAt: "2026-06-07T20:00:00.000Z", + payload: { + provider: "slack", + event_type: "file.updated", + path: "/slack/channels/C123__engineering/messages/1711111111_000100/meta.json", + content: '{"text":"hello"}', + contentType: "application/json", + }, + correlationId: "corr_aliased", + status: "queued", + attemptCount: 0, + lastError: null, + }); + const dmResult = applyWebhookEnvelope(storage, { + envelopeId: "env_dm", + workspaceId: "ws_core", + provider: "slack", + deliveryId: "delivery_dm", + receivedAt: "2026-06-07T20:00:01.000Z", + payload: { + provider: "slack", + event_type: "file.updated", + path: "/slack/dms/D123/messages/1711111111_000100/meta.json", + content: '{"text":"dm"}', + contentType: "application/json", + }, + correlationId: "corr_dm", + status: "queued", + attemptCount: 0, + lastError: null, + }); + + expect(aliasedResult).toMatchObject({ + status: "processed", + path: "/slack/channels/C123__engineering/messages/1711111111_000100/meta.json", + }); + expect(dmResult).toMatchObject({ + status: "processed", + path: "/slack/dms/D123/messages/1711111111_000100/meta.json", + }); + expect(storage.listFilesCalls).toBe(0); }); it("ignores Slack envelopes targeting paths outside the Slack provider root", () => { @@ -174,6 +231,7 @@ class MemoryStorage implements StorageAdapter { deliveryAliases = new Map(); revisionCounter = 1; eventCounter = 1; + listFilesCalls = 0; constructor(files: FileRow[] = []) { for (const file of files) { @@ -186,6 +244,7 @@ class MemoryStorage implements StorageAdapter { } listFiles(): FileRow[] { + this.listFilesCalls += 1; return Array.from(this.files.values()); } diff --git a/packages/core/src/webhooks.ts b/packages/core/src/webhooks.ts index 19661c5e..9c08b4df 100644 --- a/packages/core/src/webhooks.ts +++ b/packages/core/src/webhooks.ts @@ -684,6 +684,14 @@ function canonicalizeExistingProviderAliasPath( if (normalizeProvider(provider) !== "slack") { return path; } + const parts = path.slice(1).split("/"); + if (parts.length < 3 || parts[0] !== "slack" || parts[1] !== "channels") { + return path; + } + const channelSegment = parts[2]; + if (!channelSegment || channelSegment.includes("__")) { + return path; + } return canonicalizeSlackChannelAliasPath(storage.listFiles(), path); } From 5db0ef0b2f025e1713a997d1997fa759c79e1262 Mon Sep 17 00:00:00 2001 From: Proactive Runtime Bot Date: Sun, 7 Jun 2026 22:51:39 +0200 Subject: [PATCH 4/6] Stabilize SDK change event test wait --- packages/sdk/typescript/src/client.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sdk/typescript/src/client.test.ts b/packages/sdk/typescript/src/client.test.ts index b807db7f..d104343f 100644 --- a/packages/sdk/typescript/src/client.test.ts +++ b/packages/sdk/typescript/src/client.test.ts @@ -82,13 +82,14 @@ async function waitForWebSocket(): Promise { async function waitForExpectation(check: () => void): Promise { let lastError: unknown; - for (let attempt = 0; attempt < 20; attempt += 1) { + const deadline = Date.now() + 5_000; + while (Date.now() <= deadline) { try { check(); return; } catch (error) { lastError = error; - await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 10)); } } throw lastError; From e52dfc02c987b6ceef508c6d66c3e5a6406fb7e0 Mon Sep 17 00:00:00 2001 From: Proactive Runtime Bot Date: Sun, 7 Jun 2026 22:55:33 +0200 Subject: [PATCH 5/6] Align SDK core dependency for 0.8.17 --- package-lock.json | 11 +---------- packages/sdk/typescript/package.json | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6156caa4..980b7737 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6089,7 +6089,7 @@ "version": "0.8.16", "license": "MIT", "dependencies": { - "@relayfile/core": "0.8.16", + "@relayfile/core": "0.8.17", "ignore": "^7.0.5", "tar": "^7.5.10" }, @@ -6100,15 +6100,6 @@ "engines": { "node": ">=18" } - }, - "packages/sdk/typescript/node_modules/@relayfile/core": { - "version": "0.8.16", - "resolved": "https://registry.npmjs.org/@relayfile/core/-/core-0.8.16.tgz", - "integrity": "sha512-G4TfP9wZAOPl4TfSz/J+mYSTNJItcZCxt+lm/JTIMAMgVNpt2e3A5DJfpf3nNf24wekj+Q8c/BiskAlscL+KBg==", - "license": "MIT", - "engines": { - "node": ">=18" - } } } } diff --git a/packages/sdk/typescript/package.json b/packages/sdk/typescript/package.json index 748bcb77..2fb0eede 100644 --- a/packages/sdk/typescript/package.json +++ b/packages/sdk/typescript/package.json @@ -55,7 +55,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@relayfile/core": "0.8.16", + "@relayfile/core": "0.8.17", "ignore": "^7.0.5", "tar": "^7.5.10" }, From c5a3f28143cd575b02ee3f419cb632af58d5a1b8 Mon Sep 17 00:00:00 2001 From: Proactive Runtime Bot Date: Sun, 7 Jun 2026 22:58:41 +0200 Subject: [PATCH 6/6] Address review follow-ups for alias and test waits --- packages/core/src/webhooks.ts | 38 ++++++++++++++-------- packages/sdk/typescript/src/client.test.ts | 3 +- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/core/src/webhooks.ts b/packages/core/src/webhooks.ts index 9c08b4df..6d4fe7cb 100644 --- a/packages/core/src/webhooks.ts +++ b/packages/core/src/webhooks.ts @@ -638,6 +638,12 @@ const PROVIDER_RELATIVE_PATH_ROOTS: Record> = { slack: new Set(["channels", "dms", "teams", "users"]), }; +type RawSlackChannelAliasPath = { + path: string; + parts: string[]; + channelSegment: string; +}; + function canonicalProviderEnvelopePath(provider: string | undefined, rawPath: string): string | null { const path = normalizePath(rawPath); const normalizedProvider = normalizeProvider(provider); @@ -684,27 +690,18 @@ function canonicalizeExistingProviderAliasPath( if (normalizeProvider(provider) !== "slack") { return path; } - const parts = path.slice(1).split("/"); - if (parts.length < 3 || parts[0] !== "slack" || parts[1] !== "channels") { - return path; - } - const channelSegment = parts[2]; - if (!channelSegment || channelSegment.includes("__")) { + if (!parseRawSlackChannelAliasPath(path)) { return path; } return canonicalizeSlackChannelAliasPath(storage.listFiles(), path); } function canonicalizeSlackChannelAliasPath(files: { path: string }[], rawPath: string): string { - const path = normalizePath(rawPath); - const parts = path.slice(1).split("/"); - if (parts.length < 3 || parts[0] !== "slack" || parts[1] !== "channels") { - return path; - } - const channelSegment = parts[2]; - if (!channelSegment || channelSegment.includes("__")) { - return path; + const parsed = parseRawSlackChannelAliasPath(rawPath); + if (!parsed) { + return normalizePath(rawPath); } + const { path, parts, channelSegment } = parsed; const prefix = `/slack/channels/${channelSegment}__`; const candidates = files @@ -721,6 +718,19 @@ function canonicalizeSlackChannelAliasPath(files: { path: string }[], rawPath: s return normalizePath(`/${parts.join("/")}`); } +function parseRawSlackChannelAliasPath(rawPath: string): RawSlackChannelAliasPath | null { + const path = normalizePath(rawPath); + const parts = path.slice(1).split("/"); + if (parts.length < 3 || parts[0] !== "slack" || parts[1] !== "channels") { + return null; + } + const channelSegment = parts[2]; + if (!channelSegment || channelSegment.includes("__")) { + return null; + } + return { path, parts, channelSegment }; +} + function withinCoalesceWindow( existingReceivedAt: string, incomingReceivedAt: string, diff --git a/packages/sdk/typescript/src/client.test.ts b/packages/sdk/typescript/src/client.test.ts index d104343f..ce159840 100644 --- a/packages/sdk/typescript/src/client.test.ts +++ b/packages/sdk/typescript/src/client.test.ts @@ -82,8 +82,7 @@ async function waitForWebSocket(): Promise { async function waitForExpectation(check: () => void): Promise { let lastError: unknown; - const deadline = Date.now() + 5_000; - while (Date.now() <= deadline) { + for (let attempt = 0; attempt < 500; attempt += 1) { try { check(); return;