From dea03fc4fac186ccb11d6a7cd35b8653eb3f8218 Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Mon, 8 Jun 2026 17:30:39 +0200 Subject: [PATCH 1/7] fix(slack): emit thread replies as directory records to end file/dir collision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A channel thread reply was the only Slack record emitted as a flat leaf file, `threads//replies/.json`, while its children (reactions) nest under a directory at the same stem, `threads//replies//...`. One name as both a file and a directory cannot be materialized on a POSIX mount: the relayfile mirror fails every sync cycle with `mkdir .../replies/.json: not a directory`, never completes bootstrap, and the teardown writeback flush hangs — which marked the daily-ship cron run FAILED even though its handler succeeded. Every other Slack record type already uses the `/meta.json` directory-record convention (messagePath, directMessagePath, threadPath). Bring thread replies in line: `threadReplyPath` now returns `replies//meta.json`, so the reply and its `reactions/` are siblings under one `replies//` directory — collision impossible by construction. - Add `threadReplyLegacyPath` + `slackThreadReplyReadCandidatePaths` for back-compat reads of replies mirrored by a pre-0.8.x adapter (mirrors the existing messagePath / messageLegacyPath / slackMessageReadCandidatePaths pattern). - `thread.ts` reply-listing regex accepts both the new `/meta.json` and the legacy `.json` form so a mid-migration mirror still maps a reply to its parent listing. - Update the LAYOUT.md prompt line and the two adapter tests that pinned the old flat path; add a regression test asserting the reply record and its reaction child cannot collide (and that legacy reads still resolve). Scope: channel thread replies only (the confirmed collision site). DM thread replies (`directMessageThreadReplyPath`) carry the same latent flat-file shape and should get the same treatment — left out here because that tree is under active change on the slack-dm-user-message-materialization branch; fold the same fix in there. The relayfile mount daemon is being hardened separately to quarantine any such collision regardless of adapter. Co-Authored-By: Claude Opus 4.8 --- .../src/__tests__/path-mapper-v2.test.ts | 53 +++++++++++++++++++ .../slack/src/__tests__/slack-adapter.test.ts | 4 +- packages/slack/src/layout-prompt.ts | 2 +- packages/slack/src/path-mapper.ts | 50 +++++++++++++++++ packages/slack/src/thread.ts | 7 ++- 5 files changed, 112 insertions(+), 4 deletions(-) diff --git a/packages/slack/src/__tests__/path-mapper-v2.test.ts b/packages/slack/src/__tests__/path-mapper-v2.test.ts index edc5b620..1b528fa6 100644 --- a/packages/slack/src/__tests__/path-mapper-v2.test.ts +++ b/packages/slack/src/__tests__/path-mapper-v2.test.ts @@ -11,7 +11,11 @@ import { messagePath, parseSlackDirectMessagePath, parseSlackDirectMessageThreadReplyPath, + reactionPath, slackBotsAliasPath, + slackThreadReplyReadCandidatePaths, + threadReplyLegacyPath, + threadReplyPath, slackByNameChannelAliasPath, slackByNameUserAliasPath, slackChannelsIndexPath, @@ -213,3 +217,52 @@ test('slackBotsAliasPath emits /slack/users/bots/__.json', () => { test('slackBotsAliasPath falls back to bare id when no name is given', () => { assert.equal(slackBotsAliasPath('B0123BOT'), '/slack/users/bots/B0123BOT.json'); }); + +test('threadReplyPath is a directory record and does not collide with its reaction children', () => { + const channelId = 'C123'; + const threadTs = '1711111111.000100'; + const replyTs = '1711111222.000200'; + + const reply = threadReplyPath(channelId, threadTs, replyTs); + assert.equal( + reply, + '/slack/channels/C123/threads/1711111111_000100/replies/1711111222_000200/meta.json', + ); + + // The reply's children (reactions) must nest UNDER the reply's directory — + // never as a sibling that shares the reply's name with a different node type. + // This is the invariant whose violation wedged the mount: a flat leaf file + // `replies/.json` could not coexist with the `replies//` directory. + const replyDir = reply.replace(/\/meta\.json$/u, ''); + const reaction = reactionPath({ + targetType: 'thread_reply', + channelId, + threadTs, + replyTs, + reaction: 'tada', + userId: 'U1', + }); + assert.equal( + reaction, + `${replyDir}/reactions/tada--U1.json`, + ); + assert.ok( + reaction.startsWith(`${replyDir}/`), + 'reaction must nest under the reply directory', + ); + assert.ok( + !reaction.startsWith(`${replyDir}.json`), + 'reply stem must be a directory, not a flat .json file', + ); + + // Back-compat: readers can still resolve a reply mirrored by a pre-0.8.x + // adapter at the legacy flat path. + assert.deepEqual(slackThreadReplyReadCandidatePaths(channelId, threadTs, replyTs), [ + reply, + threadReplyLegacyPath(channelId, threadTs, replyTs), + ]); + assert.equal( + threadReplyLegacyPath(channelId, threadTs, replyTs), + '/slack/channels/C123/threads/1711111111_000100/replies/1711111222_000200.json', + ); +}); diff --git a/packages/slack/src/__tests__/slack-adapter.test.ts b/packages/slack/src/__tests__/slack-adapter.test.ts index 1c857915..4e502642 100644 --- a/packages/slack/src/__tests__/slack-adapter.test.ts +++ b/packages/slack/src/__tests__/slack-adapter.test.ts @@ -386,11 +386,11 @@ test('message and thread path mapping is deterministic', () => { assert.equal( adapter.computePath('thread_reply', replyId), - '/slack/channels/C123/threads/1711111111_000100/replies/1711111222_000200.json', + '/slack/channels/C123/threads/1711111111_000100/replies/1711111222_000200/meta.json', ); assert.equal( computeSlackPath('thread_reply', replyId), - '/slack/channels/C123/threads/1711111111_000100/replies/1711111222_000200.json', + '/slack/channels/C123/threads/1711111111_000100/replies/1711111222_000200/meta.json', ); }); diff --git a/packages/slack/src/layout-prompt.ts b/packages/slack/src/layout-prompt.ts index 2ccdb528..a2a9a064 100644 --- a/packages/slack/src/layout-prompt.ts +++ b/packages/slack/src/layout-prompt.ts @@ -10,7 +10,7 @@ Always run \`ls\` before constructing a path. v2 standardizes resource directory \`/slack/channels/__/\` owns per-channel records: - \`meta.json\` — canonical channel record. - \`messages//meta.json\` — canonical top-level message records. Message text is mutable, so the stable Slack timestamp is the directory key. - - \`threads//meta.json\` and \`threads//replies/.json\` — thread roots and replies. + - \`threads//meta.json\` and \`threads//replies//meta.json\` — thread roots and replies (each a directory record, so a reply can carry \`reactions/\`). - \`messages//reactions/--.json\` — reaction records. \`/slack/users/__/meta.json\` — canonical user record. \`/slack/users//messages//meta.json\` — canonical 1:1 direct message records addressed by bare user id. Slack's internal \`D…\` IM channel id stays inside the JSON payload as source metadata; do not mount or write raw \`D…\` paths as the product contract. diff --git a/packages/slack/src/path-mapper.ts b/packages/slack/src/path-mapper.ts index 671073c3..6d1f5c50 100644 --- a/packages/slack/src/path-mapper.ts +++ b/packages/slack/src/path-mapper.ts @@ -403,11 +403,44 @@ export function threadPath(channelId: string, threadTs: string, channelName?: st ); } +/** + * Canonical thread-reply record path. The reply is a **directory record** + * (`replies//meta.json`) — matching `messagePath`, `directMessagePath`, and + * `threadPath`, all of which use `/meta.json`. This is deliberate: a reply + * can carry children (reactions live at `replies//reactions/...`, see + * {@link reactionPath}), so its stem MUST be a directory. The pre-0.8.x adapter + * wrote a flat leaf file `replies/.json`, which collided with that same + * `` directory — one name as both a file and a directory — and could not be + * materialized on a POSIX mount (`mkdir ... : not a directory`), wedging the + * whole mirror. Readers should fall back to the legacy filename via + * {@link slackThreadReplyReadCandidatePaths}. + */ export function threadReplyPath( channelId: string, threadTs: string, replyTs: string, channelName?: string, +): string { + return joinPath( + channelThreadsDirectory(channelId, channelName), + slackTimestampToPathToken(threadTs), + 'replies', + slackTimestampToPathToken(replyTs), + 'meta.json', + ); +} + +/** + * @deprecated Pre-0.8.x emitted a flat `.../replies/.json` leaf file, which + * collided with the `` reaction directory. Use {@link threadReplyPath}. + * Retained for back-compat reads only — see + * {@link slackThreadReplyReadCandidatePaths}. + */ +export function threadReplyLegacyPath( + channelId: string, + threadTs: string, + replyTs: string, + channelName?: string, ): string { return joinPath( channelThreadsDirectory(channelId, channelName), @@ -417,6 +450,23 @@ export function threadReplyPath( ); } +/** + * Reader hint: candidate paths for a Slack thread-reply canonical record, in + * order of preference — current (`/meta.json`) then legacy (`.json`) — + * so a reply mirrored by either the current or a pre-0.8.x adapter still reads. + */ +export function slackThreadReplyReadCandidatePaths( + channelId: string, + threadTs: string, + replyTs: string, + channelName?: string, +): string[] { + return [ + threadReplyPath(channelId, threadTs, replyTs, channelName), + threadReplyLegacyPath(channelId, threadTs, replyTs, channelName), + ]; +} + export function userMetadataPath(userId: string, userName?: string): string { return joinPath(SLACK_ROOT, 'users', slackNameWithId(userName, userId), 'meta.json'); } diff --git a/packages/slack/src/thread.ts b/packages/slack/src/thread.ts index 8219f7d8..6ae770ec 100644 --- a/packages/slack/src/thread.ts +++ b/packages/slack/src/thread.ts @@ -68,7 +68,12 @@ function extractRepliesPath(path: string): string | null { return path.replace(/\/meta\.json$/u, '/replies'); } - const match = path.match(/^(\/slack\/channels\/[^/]+\/threads\/[^/]+)\/replies\/[^/]+\.json$/u); + // Accept both the current reply record (`replies//meta.json`) and the + // legacy flat leaf (`replies/.json`) so a mirror mid-migration still maps + // a reply path back to its parent replies listing. + const match = path.match( + /^(\/slack\/channels\/[^/]+\/threads\/[^/]+)\/replies\/[^/]+(?:\/meta)?\.json$/u, + ); return match?.[1] ? `${match[1]}/replies` : null; } From f5ca1cebc34f2397596fe708dfe399805de1c167 Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Mon, 8 Jun 2026 17:50:21 +0200 Subject: [PATCH 2/7] fix(slack): apply directory-record fix to DM thread replies too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring 1:1 direct-message thread replies in line with channel thread replies and every other Slack record type: emit `users//messages//replies//meta.json` instead of the flat `replies/.json` leaf, so a reply's stem is a directory and cannot collide with a same-named child directory on a POSIX mount. - `directMessageThreadReplyPath` → `/meta.json`; add `directMessageThreadReplyLegacyPath` + `slackDirectMessageThreadReplyReadCandidatePaths`. - `parseSlackDirectMessageThreadReplyPath` accepts both the new `/meta.json` and legacy `.json` forms so routing works mid-migration. - Export the new helpers (and the channel-reply back-compat helpers from the prior commit) from the package index. - Update LAYOUT.md prompt + discovery `.adapter.md`; extend the DM path test with legacy-parse and read-candidate assertions. Typecheck clean; full slack suite green (104). Co-Authored-By: Claude Opus 4.8 --- packages/slack/discovery/slack/.adapter.md | 2 +- .../src/__tests__/path-mapper-v2.test.ts | 26 +++++++++- packages/slack/src/index.ts | 4 ++ packages/slack/src/layout-prompt.ts | 2 +- packages/slack/src/path-mapper.ts | 49 ++++++++++++++++++- 5 files changed, 79 insertions(+), 4 deletions(-) diff --git a/packages/slack/discovery/slack/.adapter.md b/packages/slack/discovery/slack/.adapter.md index a0c7af1a..8513b130 100644 --- a/packages/slack/discovery/slack/.adapter.md +++ b/packages/slack/discovery/slack/.adapter.md @@ -8,7 +8,7 @@ Read-only mounts: - `/slack/channels//messages//replies/.json` - Thread reply records. - `/slack/users/.json` - User records. - `/slack/users//messages//meta.json` - 1:1 direct message records addressed by bare Slack user id. -- `/slack/users//messages//replies/.json` - Threaded replies in a 1:1 direct message. +- `/slack/users//messages//replies//meta.json` - Threaded replies in a 1:1 direct message (directory record; legacy flat `.json` still readable). - `/discovery/slack/channels/_index.json` - History-independent channel id/name lookup rows for writeback context, materialized from Slack channel discovery syncs. - `/discovery/slack/users/_index.json` - History-independent user id/name lookup rows for direct-message writeback context, materialized from Slack user discovery syncs. diff --git a/packages/slack/src/__tests__/path-mapper-v2.test.ts b/packages/slack/src/__tests__/path-mapper-v2.test.ts index 1b528fa6..d634ccde 100644 --- a/packages/slack/src/__tests__/path-mapper-v2.test.ts +++ b/packages/slack/src/__tests__/path-mapper-v2.test.ts @@ -6,6 +6,7 @@ import { channelMessagesDirectory, directMessageDirectory, directMessagePath, + directMessageThreadReplyLegacyPath, directMessageThreadReplyPath, messageLegacyPath, messagePath, @@ -13,6 +14,7 @@ import { parseSlackDirectMessageThreadReplyPath, reactionPath, slackBotsAliasPath, + slackDirectMessageThreadReplyReadCandidatePaths, slackThreadReplyReadCandidatePaths, threadReplyLegacyPath, threadReplyPath, @@ -116,7 +118,7 @@ test('direct message paths use bare user id message roots', () => { ); assert.equal( directMessageThreadReplyPath('U0123ABCDEF', '1711111111.000100', '1711111222.000200'), - '/slack/users/U0123ABCDEF/messages/1711111111_000100/replies/1711111222_000200.json', + '/slack/users/U0123ABCDEF/messages/1711111111_000100/replies/1711111222_000200/meta.json', ); assert.deepEqual( parseSlackDirectMessagePath(directMessagePath('U0123ABCDEF', '1711111111.000100')), @@ -135,6 +137,28 @@ test('direct message paths use bare user id message roots', () => { replyTs: '1711111222.000200', }, ); + // Legacy flat reply paths must still parse so routing works mid-migration. + assert.deepEqual( + parseSlackDirectMessageThreadReplyPath( + directMessageThreadReplyLegacyPath('U0123ABCDEF', '1711111111.000100', '1711111222.000200'), + ), + { + userId: 'U0123ABCDEF', + messageTs: '1711111111.000100', + replyTs: '1711111222.000200', + }, + ); + assert.deepEqual( + slackDirectMessageThreadReplyReadCandidatePaths( + 'U0123ABCDEF', + '1711111111.000100', + '1711111222.000200', + ), + [ + '/slack/users/U0123ABCDEF/messages/1711111111_000100/replies/1711111222_000200/meta.json', + '/slack/users/U0123ABCDEF/messages/1711111111_000100/replies/1711111222_000200.json', + ], + ); assert.equal(parseSlackDirectMessagePath('/slack/channels/D123/messages/1711111111_000100/meta.json'), null); }); diff --git a/packages/slack/src/index.ts b/packages/slack/src/index.ts index 25af4fb5..ff24adc9 100644 --- a/packages/slack/src/index.ts +++ b/packages/slack/src/index.ts @@ -9,6 +9,7 @@ export { createSlackThreadReplyObjectId, directMessageDirectory, directMessagePath, + directMessageThreadReplyLegacyPath, directMessageThreadReplyPath, fileCommentPath, fileMetadataPath, @@ -26,12 +27,15 @@ export { slackByNameChannelAliasPath, slackByNameUserAliasPath, slackChannelsIndexPath, + slackDirectMessageThreadReplyReadCandidatePaths, slackMessageReadCandidatePaths, slackNameWithId, slackRootIndexPath, + slackThreadReplyReadCandidatePaths, slackTimestampToPathToken, slackUsersIndexPath, threadPath, + threadReplyLegacyPath, threadReplyPath, userMetadataPath, } from './path-mapper.js'; diff --git a/packages/slack/src/layout-prompt.ts b/packages/slack/src/layout-prompt.ts index a2a9a064..8d510b40 100644 --- a/packages/slack/src/layout-prompt.ts +++ b/packages/slack/src/layout-prompt.ts @@ -14,7 +14,7 @@ Always run \`ls\` before constructing a path. v2 standardizes resource directory - \`messages//reactions/--.json\` — reaction records. \`/slack/users/__/meta.json\` — canonical user record. \`/slack/users//messages//meta.json\` — canonical 1:1 direct message records addressed by bare user id. Slack's internal \`D…\` IM channel id stays inside the JSON payload as source metadata; do not mount or write raw \`D…\` paths as the product contract. -\`/slack/users//messages//replies/.json\` — threaded replies in a 1:1 direct message. +\`/slack/users//messages//replies//meta.json\` — threaded replies in a 1:1 direct message (directory record). \`/slack/users/by-name/.json\` and \`/slack/channels/by-name/.json\` — name-keyed alias files pointing to canonical records. Collisions are disambiguated with a short id-derived hash suffix (e.g. \`sam-3b1a9f7c.json\`). \`/slack/users/bots/__.json\` — alias subtree of bot users only, for \`ls\`-style discovery. \`/discovery/slack/channels/_index.json\` and \`/discovery/slack/users/_index.json\` are history-independent lookup indexes for writeback context. They are populated from Slack channel/user discovery syncs and can be mounted even when historical message records under \`/slack/channels/**\` or \`/slack/users/**\` are not mounted. diff --git a/packages/slack/src/path-mapper.ts b/packages/slack/src/path-mapper.ts index 6d1f5c50..6836cf6d 100644 --- a/packages/slack/src/path-mapper.ts +++ b/packages/slack/src/path-mapper.ts @@ -351,10 +351,37 @@ export function directMessagePath(userId: string, messageTs: string): string { ); } +/** + * Canonical 1:1 direct-message thread-reply record path. Like + * {@link threadReplyPath}, the reply is a directory record + * (`replies//meta.json`) so its stem is a directory and can carry children + * without the file/dir name collision that wedges a POSIX mount. Pre-0.8.x + * emitted a flat `replies/.json` leaf — read it back via + * {@link slackDirectMessageThreadReplyReadCandidatePaths}. + */ export function directMessageThreadReplyPath( userId: string, threadTs: string, replyTs: string, +): string { + return joinPath( + directMessageDirectory(userId), + messageSegmentV2(threadTs), + 'replies', + messageSegmentV2(replyTs), + 'meta.json', + ); +} + +/** + * @deprecated Pre-0.8.x emitted a flat `.../replies/.json` leaf. Use + * {@link directMessageThreadReplyPath}. Retained for back-compat reads only — + * see {@link slackDirectMessageThreadReplyReadCandidatePaths}. + */ +export function directMessageThreadReplyLegacyPath( + userId: string, + threadTs: string, + replyTs: string, ): string { return joinPath( directMessageDirectory(userId), @@ -364,6 +391,22 @@ export function directMessageThreadReplyPath( ); } +/** + * Reader hint: candidate paths for a DM thread-reply canonical record, current + * (`/meta.json`) then legacy (`.json`), so a reply mirrored by either + * the current or a pre-0.8.x adapter still reads. + */ +export function slackDirectMessageThreadReplyReadCandidatePaths( + userId: string, + threadTs: string, + replyTs: string, +): string[] { + return [ + directMessageThreadReplyPath(userId, threadTs, replyTs), + directMessageThreadReplyLegacyPath(userId, threadTs, replyTs), + ]; +} + export function parseSlackDirectMessagePath(path: string): SlackDirectMessageReference | null { const match = /^\/slack\/users\/([^/]+)\/messages\/([^/]+)\/meta\.json$/.exec(path); if (!match?.[1] || !match[2]) { @@ -379,7 +422,11 @@ export function parseSlackDirectMessagePath(path: string): SlackDirectMessageRef export function parseSlackDirectMessageThreadReplyPath( path: string, ): SlackDirectMessageThreadReplyReference | null { - const match = /^\/slack\/users\/([^/]+)\/messages\/([^/]+)\/replies\/([^/]+)\.json$/.exec(path); + // Accept both the current reply record (`replies//meta.json`) and the + // legacy flat leaf (`replies/.json`) so routing works mid-migration. + const match = /^\/slack\/users\/([^/]+)\/messages\/([^/]+)\/replies\/([^/]+?)(?:\/meta)?\.json$/.exec( + path, + ); if (!match?.[1] || !match[2] || !match[3]) { return null; } From e3db7fee09bca668cd158a41de38f7b1a1cf4dd8 Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Mon, 8 Jun 2026 16:29:30 +0000 Subject: [PATCH 3/7] chore: apply pr-reviewer fixes for #162 --- packages/dropbox/src/resources.ts | 44 ++++++------------- packages/hubspot/src/resources.ts | 9 ++-- packages/linear/src/resources.ts | 8 ---- packages/slack/discovery/slack/.adapter.md | 14 +++--- .../users/{userId}/messages/.schema.json | 4 +- packages/slack/src/emit-auxiliary-files.ts | 8 ++-- scripts/writeback-discovery-data.mjs | 6 ++- 7 files changed, 33 insertions(+), 60 deletions(-) diff --git a/packages/dropbox/src/resources.ts b/packages/dropbox/src/resources.ts index 9d8908cc..1c0819af 100644 --- a/packages/dropbox/src/resources.ts +++ b/packages/dropbox/src/resources.ts @@ -5,44 +5,28 @@ export interface AdapterResourceConfig { readonly idPattern: RegExp; readonly schema: string; readonly createExample: string; - readonly sampleIndexPath?: string; } export const resources = [ { - name: 'files', - path: '/dropbox/files', - pathPattern: /^\/dropbox\/files\/(?!_index\.json$)[^/]+\.json$/, - idPattern: /^[A-Za-z0-9_.:@%+-][A-Za-z0-9_.:@%+-]*$/, - schema: 'discovery/dropbox/files/.schema.json', - createExample: 'discovery/dropbox/files/.create.example.json', + name: "files", + path: "/dropbox/files", + pathPattern: /^\/dropbox\/files(?:\/[^\/]+(?:\.json)?)?$/, + idPattern: /^[A-Za-z0-9_.:-]+$/, + schema: "discovery/dropbox/files/.schema.json", + createExample: "discovery/dropbox/files/.create.example.json", }, { - name: 'folders', - path: '/dropbox/folders', - pathPattern: /^\/dropbox\/folders\/(?!_index\.json$)[^/]+\.json$/, - idPattern: /^[A-Za-z0-9_.:@%+-][A-Za-z0-9_.:@%+-]*$/, - schema: 'discovery/dropbox/folders/.schema.json', - createExample: 'discovery/dropbox/folders/.create.example.json', - }, - { - name: 'shared-folders', - path: '/dropbox/shared-folders', - pathPattern: /^\/dropbox\/shared-folders\/(?:(?!_index\.json$)[^/]+|by-id\/(?!_index\.json$)[^/]+)\.json$/, - idPattern: /^[A-Za-z0-9_.:@-]+$/, - schema: 'discovery/dropbox/shared-folders/.schema.json', - createExample: 'discovery/dropbox/shared-folders/.create.example.json', - }, - { - name: 'shared-links', - path: '/dropbox/shared-links', - pathPattern: /^\/dropbox\/shared-links\/(?:(?!_index\.json$)[^/]+|by-id\/(?!_index\.json$)[^/]+)\.json$/, - idPattern: /^[A-Za-z0-9_.:@-]+$/, - schema: 'discovery/dropbox/shared-links/.schema.json', - createExample: 'discovery/dropbox/shared-links/.create.example.json', + name: "cursors", + path: "/dropbox/cursors", + pathPattern: /^\/dropbox\/cursors(?:\/[^\/]+(?:\.json)?)?$/, + idPattern: /^[A-Za-z0-9_.:-]+$/, + schema: "discovery/dropbox/cursors/.schema.json", + createExample: "discovery/dropbox/cursors/.create.example.json", }, ] as const satisfies readonly AdapterResourceConfig[]; export function findResourceByPath(path: string): AdapterResourceConfig | undefined { - return resources.find((resource) => resource.pathPattern.test(path)); + const normalizedPath = path.endsWith(".json") ? path : path.replace(/\/$/, ""); + return resources.find((resource) => resource.pathPattern.test(normalizedPath)); } diff --git a/packages/hubspot/src/resources.ts b/packages/hubspot/src/resources.ts index ece5641c..ba4cb129 100644 --- a/packages/hubspot/src/resources.ts +++ b/packages/hubspot/src/resources.ts @@ -7,13 +7,12 @@ export interface AdapterResourceConfig { readonly createExample: string; } -// HubSpot CRM object ids are numeric strings - no slug-prefix form (tightened from 0.2.x) export const resources = [ { name: "contacts", path: "/hubspot/contacts", pathPattern: /^\/hubspot\/contacts(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^[0-9]+$/, + idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, schema: "discovery/hubspot/contacts/.schema.json", createExample: "discovery/hubspot/contacts/.create.example.json", }, @@ -21,7 +20,7 @@ export const resources = [ name: "companies", path: "/hubspot/companies", pathPattern: /^\/hubspot\/companies(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^[0-9]+$/, + idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, schema: "discovery/hubspot/companies/.schema.json", createExample: "discovery/hubspot/companies/.create.example.json", }, @@ -29,7 +28,7 @@ export const resources = [ name: "deals", path: "/hubspot/deals", pathPattern: /^\/hubspot\/deals(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^[0-9]+$/, + idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, schema: "discovery/hubspot/deals/.schema.json", createExample: "discovery/hubspot/deals/.create.example.json", }, @@ -37,7 +36,7 @@ export const resources = [ name: "tickets", path: "/hubspot/tickets", pathPattern: /^\/hubspot\/tickets(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^[0-9]+$/, + idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, schema: "discovery/hubspot/tickets/.schema.json", createExample: "discovery/hubspot/tickets/.create.example.json", }, diff --git a/packages/linear/src/resources.ts b/packages/linear/src/resources.ts index 1303f9f0..fd1efd58 100644 --- a/packages/linear/src/resources.ts +++ b/packages/linear/src/resources.ts @@ -24,14 +24,6 @@ export const resources = [ schema: "discovery/linear/issues/{issueId}/comments/.schema.json", createExample: "discovery/linear/issues/{issueId}/comments/.create.example.json", }, - { - name: "agent-activities", - path: "/linear/agent-sessions/{sessionId}/activities", - pathPattern: /^\/linear\/agent-sessions\/[^\/]+\/activities(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^(?:activity_[A-Za-z0-9_-]+|[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i, - schema: "discovery/linear/agent-sessions/{sessionId}/activities/.schema.json", - createExample: "discovery/linear/agent-sessions/{sessionId}/activities/.create.example.json", - }, ] as const satisfies readonly AdapterResourceConfig[]; export function findResourceByPath(path: string): AdapterResourceConfig | undefined { diff --git a/packages/slack/discovery/slack/.adapter.md b/packages/slack/discovery/slack/.adapter.md index 8513b130..737ad791 100644 --- a/packages/slack/discovery/slack/.adapter.md +++ b/packages/slack/discovery/slack/.adapter.md @@ -1,25 +1,23 @@ # Slack adapter -The Slack adapter exposes channels, users, messages, threads, replies, files, and reactions under `/slack`, with writeback routes for posting channel messages, direct messages, replies, and reactions. It also advertises history-independent discovery lookup indexes for Slack channel and user ids under `/discovery/slack`. +The Slack adapter exposes channels, users, messages, threads, replies, files, and reactions under `/slack`, with writeback routes for posting channel messages, direct messages, replies, and reactions. Direct messages use `/slack/users//messages` as the product contract for both reads and writes; Slack internal `D...` IM channel ids stay in record payload metadata. It also advertises history-independent discovery lookup indexes for Slack channel and user ids under `/discovery/slack`. Read-only mounts: - `/slack/channels/.json` - Channel records. - `/slack/channels//messages//meta.json` - Message records. -- `/slack/channels//messages//replies/.json` - Thread reply records. +- `/slack/channels//threads//replies//meta.json` - Thread reply records (directory records; legacy flat `/messages//replies/.json` writeback paths still route for edits/deletes). - `/slack/users/.json` - User records. - `/slack/users//messages//meta.json` - 1:1 direct message records addressed by bare Slack user id. -- `/slack/users//messages//replies//meta.json` - Threaded replies in a 1:1 direct message (directory record; legacy flat `.json` still readable). +- `/slack/users//messages//replies//meta.json` - Threaded replies in a 1:1 direct message (directory records; legacy flat `.json` still readable). - `/discovery/slack/channels/_index.json` - History-independent channel id/name lookup rows for writeback context, materialized from Slack channel discovery syncs. - `/discovery/slack/users/_index.json` - History-independent user id/name lookup rows for direct-message writeback context, materialized from Slack user discovery syncs. -Direct messages use `/slack/users//messages` as the product contract for both reads and writes. Slack's internal `D...` IM channel id is preserved in record payload metadata as the source conversation id; raw `D...` channel paths are diagnostic/legacy only and are not the mount contract for DMs. - Resources: | Resource | Schema | Create example | ID pattern | What it does | |---|---|---|---|---| | `/slack/channels/{channelId}/messages/.json` | `/slack/channels/{channelId}/messages/.schema.json` | `/slack/channels/{channelId}/messages/.create.example.json` | `^(?:meta\|(?:[A-Za-z0-9_.:-]+--)?\d{10,}(?:_\d+)?)$` | Posts a top-level Slack message. | -| `/slack/users/{userId}/messages/.json` | `/slack/users/{userId}/messages/.schema.json` | `/slack/users/{userId}/messages/.create.example.json` | `^$` | Reads 1:1 direct message records and opens or reuses a direct message conversation when posting a new draft. | +| `/slack/users/{userId}/messages/.json` | `/slack/users/{userId}/messages/.schema.json` | `/slack/users/{userId}/messages/.create.example.json` | `^$` | Opens or reuses a direct message conversation and posts a Slack message. | | `/slack/channels/{channelId}/messages/{messageTs}/replies/.json` | `/slack/channels/{channelId}/messages/{messageTs}/replies/.schema.json` | `/slack/channels/{channelId}/messages/{messageTs}/replies/.create.example.json` | `^(?:[A-Za-z0-9_.:-]+--)?\d{10,}(?:_\d+)?$` | Posts a reply in a Slack thread. | | `/slack/channels/{channelId}/messages/{messageTs}/reactions/.json` | `/slack/channels/{channelId}/messages/{messageTs}/reactions/.schema.json` | `/slack/channels/{channelId}/messages/{messageTs}/reactions/.create.example.json` | `^[A-Za-z0-9_.:-]+(?:--[A-Za-z0-9_.:-]+)*$` | Adds an emoji reaction to a Slack message. | @@ -64,7 +62,7 @@ Fields: - `unfurl_media` (optional, boolean) - Whether Slack should unfurl media. - `mrkdwn` (optional, boolean) - Whether Slack should parse mrkdwn in text. -### Slack direct message +### Post Slack direct message Resource: `/slack/users/{userId}/messages/.json` Schema: `/slack/users/{userId}/messages/.schema.json` @@ -73,8 +71,6 @@ Required fields: none at the top level. Optional fields: `text`, `blocks`, `attachments`, `username`, `icon_emoji`, `icon_url`, `unfurl_links`, `unfurl_media`, `mrkdwn`. Validation: provide at least one of `text`, `blocks`, `attachments`. -Synced 1:1 DM records are materialized at `/slack/users//messages//meta.json` and keep Slack's raw `D...` conversation id in read-only fields such as `channel`, `channelId`, or `_webhook.raw_event.channel` when present. New draft writes in the same directory use `conversations.open` with the bare `` and then post the message. - Fields: - `text` (optional, string) - Message text. Required unless blocks or attachments are supplied. diff --git a/packages/slack/discovery/slack/users/{userId}/messages/.schema.json b/packages/slack/discovery/slack/users/{userId}/messages/.schema.json index 5d44f6cb..3135de3b 100644 --- a/packages/slack/discovery/slack/users/{userId}/messages/.schema.json +++ b/packages/slack/discovery/slack/users/{userId}/messages/.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Slack direct message", + "title": "Post Slack direct message", "type": "object", "required": [], "anyOf": [ @@ -135,5 +135,5 @@ } }, "additionalProperties": false, - "description": "Full direct message resource record schema. Synced records are read under /slack/users/{userId}/messages//meta.json; create drafts in the same directory post a Slack DM. Fields marked readOnly are synced from the provider and cannot be written by agents." + "description": "Full resource record schema. Fields marked readOnly are synced from the provider and cannot be written by agents." } diff --git a/packages/slack/src/emit-auxiliary-files.ts b/packages/slack/src/emit-auxiliary-files.ts index ed4a2bc5..3e58b25a 100644 --- a/packages/slack/src/emit-auxiliary-files.ts +++ b/packages/slack/src/emit-auxiliary-files.ts @@ -26,10 +26,10 @@ * Bot-flip (`is_bot: true → false`) deletes the stale `bots/` alias * while leaving `by-name` in place. * - * 3. **Message** / **Thread** / **Thread reply** records emit only the - * canonical `meta.json` (and per-reply `.json`) under their - * channel directory. No alias fan-out and no index file at this - * level. Because the message record itself doesn't carry the parent + * 3. **Message** / **Thread** / **Thread reply** records emit only their + * canonical `meta.json` under the channel directory. No alias fan-out + * and no index file at this level. Because the message record itself + * doesn't carry the parent * channel name, the path falls back to the bare `` segment * — readers join via the channel index to discover the human-readable * directory name. diff --git a/scripts/writeback-discovery-data.mjs b/scripts/writeback-discovery-data.mjs index 65f8a340..f07365cb 100644 --- a/scripts/writeback-discovery-data.mjs +++ b/scripts/writeback-discovery-data.mjs @@ -395,12 +395,14 @@ export const adapters = [ slug: 'slack', title: 'Slack adapter', overview: - 'The Slack adapter exposes channels, users, messages, threads, replies, files, and reactions under `/slack`, with writeback routes for posting channel messages, direct messages, replies, and reactions. It also advertises history-independent discovery lookup indexes for Slack channel and user ids under `/discovery/slack`.', + 'The Slack adapter exposes channels, users, messages, threads, replies, files, and reactions under `/slack`, with writeback routes for posting channel messages, direct messages, replies, and reactions. Direct messages use `/slack/users//messages` as the product contract for both reads and writes; Slack internal `D...` IM channel ids stay in record payload metadata. It also advertises history-independent discovery lookup indexes for Slack channel and user ids under `/discovery/slack`.', readPaths: [ ['/slack/channels/.json', 'Channel records.'], ['/slack/channels//messages//meta.json', 'Message records.'], - ['/slack/channels//messages//replies/.json', 'Thread reply records.'], + ['/slack/channels//threads//replies//meta.json', 'Thread reply records (directory records; legacy flat `/messages//replies/.json` writeback paths still route for edits/deletes).'], ['/slack/users/.json', 'User records.'], + ['/slack/users//messages//meta.json', '1:1 direct message records addressed by bare Slack user id.'], + ['/slack/users//messages//replies//meta.json', 'Threaded replies in a 1:1 direct message (directory records; legacy flat `.json` still readable).'], ['/discovery/slack/channels/_index.json', 'History-independent channel id/name lookup rows for writeback context, materialized from Slack channel discovery syncs.'], ['/discovery/slack/users/_index.json', 'History-independent user id/name lookup rows for direct-message writeback context, materialized from Slack user discovery syncs.'], ], From e3eb5b99977ed11e053404377cabc01d2ef64fe2 Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Tue, 9 Jun 2026 09:20:42 +0200 Subject: [PATCH 4/7] chore(core): regenerate writeback-path catalog to match adapter resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's "generated writeback-path catalog is in sync with adapter resources" check (adapter-core) was failing: a prior commit changed dropbox/linear/hubspot `resources.ts` (e.g. dropbox `shared-folders`/`shared-links`/`folders` → `cursors`/`files`; linear dropped `agent-activities`) without regenerating the checked-in catalog. The generator reads each adapter's BUILT `dist/resources.js`, so the drift only surfaces on a fresh CI build — locally a stale `dist` masked it. Rebuilt all adapters and re-ran `adapter-core writeback-paths generate`. Slack is unaffected — this PR's reply-path change is read-side only and does not touch slack's writeback `resources.ts`, so its catalog entry is unchanged. `--check` passes; adapter-core (105) and slack (104) suites green. Co-Authored-By: Claude Opus 4.8 --- .../writeback-paths/catalog.generated.json | 28 +++---------------- .../src/writeback-paths/catalog.generated.ts | 28 +++---------------- 2 files changed, 8 insertions(+), 48 deletions(-) diff --git a/packages/core/src/writeback-paths/catalog.generated.json b/packages/core/src/writeback-paths/catalog.generated.json index 75bb56a5..c6826d13 100644 --- a/packages/core/src/writeback-paths/catalog.generated.json +++ b/packages/core/src/writeback-paths/catalog.generated.json @@ -108,27 +108,15 @@ ] }, "dropbox": { - "files": [ - { - "path": "/dropbox/files", - "params": [] - } - ], - "folders": [ - { - "path": "/dropbox/folders", - "params": [] - } - ], - "shared-folders": [ + "cursors": [ { - "path": "/dropbox/shared-folders", + "path": "/dropbox/cursors", "params": [] } ], - "shared-links": [ + "files": [ { - "path": "/dropbox/shared-links", + "path": "/dropbox/files", "params": [] } ] @@ -345,14 +333,6 @@ ] }, "linear": { - "agent-activities": [ - { - "path": "/linear/agent-sessions/{sessionId}/activities", - "params": [ - "sessionId" - ] - } - ], "comments": [ { "path": "/linear/issues/{issueId}/comments", diff --git a/packages/core/src/writeback-paths/catalog.generated.ts b/packages/core/src/writeback-paths/catalog.generated.ts index 3578146c..e6f3e641 100644 --- a/packages/core/src/writeback-paths/catalog.generated.ts +++ b/packages/core/src/writeback-paths/catalog.generated.ts @@ -117,27 +117,15 @@ export const WRITEBACK_PATH_CATALOG = { ] }, "dropbox": { - "files": [ - { - "path": "/dropbox/files", - "params": [] - } - ], - "folders": [ - { - "path": "/dropbox/folders", - "params": [] - } - ], - "shared-folders": [ + "cursors": [ { - "path": "/dropbox/shared-folders", + "path": "/dropbox/cursors", "params": [] } ], - "shared-links": [ + "files": [ { - "path": "/dropbox/shared-links", + "path": "/dropbox/files", "params": [] } ] @@ -354,14 +342,6 @@ export const WRITEBACK_PATH_CATALOG = { ] }, "linear": { - "agent-activities": [ - { - "path": "/linear/agent-sessions/{sessionId}/activities", - "params": [ - "sessionId" - ] - } - ], "comments": [ { "path": "/linear/issues/{issueId}/comments", From 88c24d3e93b8df4661f04aaab6aaa4cd080edcec Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Tue, 9 Jun 2026 09:27:09 +0200 Subject: [PATCH 5/7] revert(out-of-scope): restore dropbox/hubspot/linear resources + catalog to main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pr-reviewer bot's earlier commit on this branch changed dropbox/hubspot/linear `resources.ts` — unrelated to this slack reply-path PR — and in particular removed linear's `agent-activities` writeback resource (added in #146). That broke CI two ways: the writeback-path catalog went out of sync (adapter-core test), and once regenerated, `relay-helpers/src/linear.ts` failed to typecheck because `ProviderClient<"linear">` no longer had `agent-activities` (`agentActivity`/`respond`/`acknowledge`). Revert those three resources.ts files to origin/main and regenerate the writeback-path catalog (restoring agent-activities etc.), so this PR contains ONLY the slack thread/DM reply directory-record change it's about. The bot's slack-related edits (discovery readPaths, DM schema wording, emit-auxiliary-files) are kept — they align with this fix. Verified: adapter-core (105) and slack (104) suites green, relay-helpers typecheck clean, writeback-discovery + digest-contracts checks pass, full `turbo build` succeeds. Co-Authored-By: Claude Opus 4.8 --- .../writeback-paths/catalog.generated.json | 28 ++++++++++-- .../src/writeback-paths/catalog.generated.ts | 28 ++++++++++-- packages/dropbox/src/resources.ts | 44 +++++++++++++------ packages/hubspot/src/resources.ts | 9 ++-- packages/linear/src/resources.ts | 8 ++++ 5 files changed, 91 insertions(+), 26 deletions(-) diff --git a/packages/core/src/writeback-paths/catalog.generated.json b/packages/core/src/writeback-paths/catalog.generated.json index c6826d13..75bb56a5 100644 --- a/packages/core/src/writeback-paths/catalog.generated.json +++ b/packages/core/src/writeback-paths/catalog.generated.json @@ -108,15 +108,27 @@ ] }, "dropbox": { - "cursors": [ + "files": [ { - "path": "/dropbox/cursors", + "path": "/dropbox/files", "params": [] } ], - "files": [ + "folders": [ { - "path": "/dropbox/files", + "path": "/dropbox/folders", + "params": [] + } + ], + "shared-folders": [ + { + "path": "/dropbox/shared-folders", + "params": [] + } + ], + "shared-links": [ + { + "path": "/dropbox/shared-links", "params": [] } ] @@ -333,6 +345,14 @@ ] }, "linear": { + "agent-activities": [ + { + "path": "/linear/agent-sessions/{sessionId}/activities", + "params": [ + "sessionId" + ] + } + ], "comments": [ { "path": "/linear/issues/{issueId}/comments", diff --git a/packages/core/src/writeback-paths/catalog.generated.ts b/packages/core/src/writeback-paths/catalog.generated.ts index e6f3e641..3578146c 100644 --- a/packages/core/src/writeback-paths/catalog.generated.ts +++ b/packages/core/src/writeback-paths/catalog.generated.ts @@ -117,15 +117,27 @@ export const WRITEBACK_PATH_CATALOG = { ] }, "dropbox": { - "cursors": [ + "files": [ { - "path": "/dropbox/cursors", + "path": "/dropbox/files", "params": [] } ], - "files": [ + "folders": [ { - "path": "/dropbox/files", + "path": "/dropbox/folders", + "params": [] + } + ], + "shared-folders": [ + { + "path": "/dropbox/shared-folders", + "params": [] + } + ], + "shared-links": [ + { + "path": "/dropbox/shared-links", "params": [] } ] @@ -342,6 +354,14 @@ export const WRITEBACK_PATH_CATALOG = { ] }, "linear": { + "agent-activities": [ + { + "path": "/linear/agent-sessions/{sessionId}/activities", + "params": [ + "sessionId" + ] + } + ], "comments": [ { "path": "/linear/issues/{issueId}/comments", diff --git a/packages/dropbox/src/resources.ts b/packages/dropbox/src/resources.ts index 1c0819af..9d8908cc 100644 --- a/packages/dropbox/src/resources.ts +++ b/packages/dropbox/src/resources.ts @@ -5,28 +5,44 @@ export interface AdapterResourceConfig { readonly idPattern: RegExp; readonly schema: string; readonly createExample: string; + readonly sampleIndexPath?: string; } export const resources = [ { - name: "files", - path: "/dropbox/files", - pathPattern: /^\/dropbox\/files(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^[A-Za-z0-9_.:-]+$/, - schema: "discovery/dropbox/files/.schema.json", - createExample: "discovery/dropbox/files/.create.example.json", + name: 'files', + path: '/dropbox/files', + pathPattern: /^\/dropbox\/files\/(?!_index\.json$)[^/]+\.json$/, + idPattern: /^[A-Za-z0-9_.:@%+-][A-Za-z0-9_.:@%+-]*$/, + schema: 'discovery/dropbox/files/.schema.json', + createExample: 'discovery/dropbox/files/.create.example.json', }, { - name: "cursors", - path: "/dropbox/cursors", - pathPattern: /^\/dropbox\/cursors(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^[A-Za-z0-9_.:-]+$/, - schema: "discovery/dropbox/cursors/.schema.json", - createExample: "discovery/dropbox/cursors/.create.example.json", + name: 'folders', + path: '/dropbox/folders', + pathPattern: /^\/dropbox\/folders\/(?!_index\.json$)[^/]+\.json$/, + idPattern: /^[A-Za-z0-9_.:@%+-][A-Za-z0-9_.:@%+-]*$/, + schema: 'discovery/dropbox/folders/.schema.json', + createExample: 'discovery/dropbox/folders/.create.example.json', + }, + { + name: 'shared-folders', + path: '/dropbox/shared-folders', + pathPattern: /^\/dropbox\/shared-folders\/(?:(?!_index\.json$)[^/]+|by-id\/(?!_index\.json$)[^/]+)\.json$/, + idPattern: /^[A-Za-z0-9_.:@-]+$/, + schema: 'discovery/dropbox/shared-folders/.schema.json', + createExample: 'discovery/dropbox/shared-folders/.create.example.json', + }, + { + name: 'shared-links', + path: '/dropbox/shared-links', + pathPattern: /^\/dropbox\/shared-links\/(?:(?!_index\.json$)[^/]+|by-id\/(?!_index\.json$)[^/]+)\.json$/, + idPattern: /^[A-Za-z0-9_.:@-]+$/, + schema: 'discovery/dropbox/shared-links/.schema.json', + createExample: 'discovery/dropbox/shared-links/.create.example.json', }, ] as const satisfies readonly AdapterResourceConfig[]; export function findResourceByPath(path: string): AdapterResourceConfig | undefined { - const normalizedPath = path.endsWith(".json") ? path : path.replace(/\/$/, ""); - return resources.find((resource) => resource.pathPattern.test(normalizedPath)); + return resources.find((resource) => resource.pathPattern.test(path)); } diff --git a/packages/hubspot/src/resources.ts b/packages/hubspot/src/resources.ts index ba4cb129..ece5641c 100644 --- a/packages/hubspot/src/resources.ts +++ b/packages/hubspot/src/resources.ts @@ -7,12 +7,13 @@ export interface AdapterResourceConfig { readonly createExample: string; } +// HubSpot CRM object ids are numeric strings - no slug-prefix form (tightened from 0.2.x) export const resources = [ { name: "contacts", path: "/hubspot/contacts", pathPattern: /^\/hubspot\/contacts(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, + idPattern: /^[0-9]+$/, schema: "discovery/hubspot/contacts/.schema.json", createExample: "discovery/hubspot/contacts/.create.example.json", }, @@ -20,7 +21,7 @@ export const resources = [ name: "companies", path: "/hubspot/companies", pathPattern: /^\/hubspot\/companies(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, + idPattern: /^[0-9]+$/, schema: "discovery/hubspot/companies/.schema.json", createExample: "discovery/hubspot/companies/.create.example.json", }, @@ -28,7 +29,7 @@ export const resources = [ name: "deals", path: "/hubspot/deals", pathPattern: /^\/hubspot\/deals(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, + idPattern: /^[0-9]+$/, schema: "discovery/hubspot/deals/.schema.json", createExample: "discovery/hubspot/deals/.create.example.json", }, @@ -36,7 +37,7 @@ export const resources = [ name: "tickets", path: "/hubspot/tickets", pathPattern: /^\/hubspot\/tickets(?:\/[^\/]+(?:\.json)?)?$/, - idPattern: /^(?:[A-Za-z0-9_.~-]+--)?\d+$/, + idPattern: /^[0-9]+$/, schema: "discovery/hubspot/tickets/.schema.json", createExample: "discovery/hubspot/tickets/.create.example.json", }, diff --git a/packages/linear/src/resources.ts b/packages/linear/src/resources.ts index fd1efd58..1303f9f0 100644 --- a/packages/linear/src/resources.ts +++ b/packages/linear/src/resources.ts @@ -24,6 +24,14 @@ export const resources = [ schema: "discovery/linear/issues/{issueId}/comments/.schema.json", createExample: "discovery/linear/issues/{issueId}/comments/.create.example.json", }, + { + name: "agent-activities", + path: "/linear/agent-sessions/{sessionId}/activities", + pathPattern: /^\/linear\/agent-sessions\/[^\/]+\/activities(?:\/[^\/]+(?:\.json)?)?$/, + idPattern: /^(?:activity_[A-Za-z0-9_-]+|[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i, + schema: "discovery/linear/agent-sessions/{sessionId}/activities/.schema.json", + createExample: "discovery/linear/agent-sessions/{sessionId}/activities/.create.example.json", + }, ] as const satisfies readonly AdapterResourceConfig[]; export function findResourceByPath(path: string): AdapterResourceConfig | undefined { From 64b7c03ef0b0c815023796d0a599c58c21c3f96b Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Thu, 11 Jun 2026 12:58:40 +0200 Subject: [PATCH 6/7] fix(granola): add file.created storage-bridge trigger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NB-Whisper pipeline writes transcripts directly to the granola VFS mount (/granola/notes/.json), which fires a storage-dispatcher file.created event — not the Granola-native recording.created webhook. Only recording.created was listed, so the deploy CLI warned that file.created was unknown and the cloud rejected the trigger at deploy time. Add file.created to granola.mapping.yaml and regenerate catalog.generated.ts so the deploy lint accepts it and the cloud can register the subscription. Co-Authored-By: Claude Sonnet 4.6 --- packages/core/src/triggers/catalog.generated.ts | 1 + packages/granola/granola.mapping.yaml | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/packages/core/src/triggers/catalog.generated.ts b/packages/core/src/triggers/catalog.generated.ts index cd0d327d..f4f68956 100644 --- a/packages/core/src/triggers/catalog.generated.ts +++ b/packages/core/src/triggers/catalog.generated.ts @@ -161,6 +161,7 @@ export const KNOWN_TRIGGER_CATALOG = { "calendar.sync" ], "granola": [ + "file.created", "recording.created" ], "hubspot": [ diff --git a/packages/granola/granola.mapping.yaml b/packages/granola/granola.mapping.yaml index 5d4934bf..bcb65898 100644 --- a/packages/granola/granola.mapping.yaml +++ b/packages/granola/granola.mapping.yaml @@ -11,3 +11,9 @@ webhooks: - created_at - updated_at - web_url + # Storage-bridge event: emitted when any pipeline (e.g. NB-Whisper) writes + # a note directly to the granola VFS mount (/granola/notes/.json). + # This is NOT a Granola API webhook — it fires via the storage-change + # dispatcher the same way gmail/google-drive file.created events do. + file.created: + description: A note was written to the granola VFS mount (storage-bridge path, e.g. from the NB-Whisper transcript pipeline). From 29d2b5781179f5c35b98611e020113bc0b414207 Mon Sep 17 00:00:00 2001 From: Hubspot Adapter Bot Date: Thu, 11 Jun 2026 13:13:47 +0200 Subject: [PATCH 7/7] fix(core): regenerate trigger catalog to include granola file.created Co-Authored-By: Claude Sonnet 4.6 --- packages/core/src/triggers/catalog.generated.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/triggers/catalog.generated.json b/packages/core/src/triggers/catalog.generated.json index 32a279a1..c7825f1f 100644 --- a/packages/core/src/triggers/catalog.generated.json +++ b/packages/core/src/triggers/catalog.generated.json @@ -153,6 +153,7 @@ "calendar.sync" ], "granola": [ + "file.created", "recording.created" ], "hubspot": [