From cceba924a704606fd671dcd48f4e89b8f2460d9a Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 8 Jun 2026 17:23:59 +0200 Subject: [PATCH] fix(integration-mounts): mirror Slack message/DM roots so inbound reads down MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Slack `messages` (top-level) and `users//messages` (DM) mounts were launched `sync=write-only`, so the daemon pushed our send-writeback command files up but did ZERO remote->local pulls — inbound top-level messages and DMs never materialized on disk, and the event-preview targeted read (which reads the local mounted file) returned "unavailable". Only the `threads` context root was `mirror`, which is why thread replies read down but top-level/DMs did not. Root cause: mountSpecsFor set syncMode via `isSlackWritebackCommandRoot(mountPath) ? 'write-only' : 'mirror'`. The `messages`/`users` roots are DUAL-PURPOSE (writeback command root AND inbound read surface), so write-only structurally disabled their read-down leg. Fix: mount all Slack roots `mirror`. Mirror is a strict superset of write-only — the local push/dispatch path (pushLocal) is byte-identical; it only ADDS the inbound pull — so send-writebacks still dispatch while inbound now reads down, matching the proven `threads` mirror mount. A fresh mirror full pull also backfills the messages/DMs missed while write-only. Durable cloud path-correctness for channel messages ships in cloud#2010 (deployed); DMs use the cloud#1997 D->U mapping (committed path is /slack/users//messages//meta.json, confirmed readable). Co-Authored-By: Claude Opus 4.8 --- src/main/integration-mounts.test.ts | 13 ++++++++----- src/main/integration-mounts.ts | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/main/integration-mounts.test.ts b/src/main/integration-mounts.test.ts index be096c42..f4cac09a 100644 --- a/src/main/integration-mounts.test.ts +++ b/src/main/integration-mounts.test.ts @@ -221,7 +221,7 @@ describe('IntegrationMountManager', () => { ]) }) - it('mounts canonical Slack command roots exactly once in write-only mode', async () => { + it('mounts Slack channel message roots in mirror mode (read-down + writeback)', async () => { const manager = new IntegrationMountManager() await manager.ensureMounted([ @@ -236,7 +236,10 @@ describe('IntegrationMountManager', () => { localDir: '/tmp/pear-home/.agentworkforce/pear/relayfile/workspaces/account-workspace-id/slack/channels/C123/messages', remotePath: '/slack/channels/C123/messages', localLayout: 'exact', - syncMode: 'write-only', + // Dual-purpose root: reads inbound top-level messages down AND accepts + // send-writebacks. Mirror is a superset of write-only, so it does both; + // write-only disabled inbound read-down entirely. + syncMode: 'mirror', scopes: ['relayfile:fs:read:/slack/channels/C123/messages/**', 'relayfile:fs:write:/slack/channels/C123/messages/**'] }) expect(mock.mountInputs[0]?.localDir).not.toContain('messages/slack/channels/C123/messages') @@ -253,13 +256,13 @@ describe('IntegrationMountManager', () => { expect(mock.startMount).toHaveBeenCalledWith(expect.objectContaining({ env: expect.objectContaining({ RELAYFILE_MOUNT_LOCAL_LAYOUT: 'exact', - RELAYFILE_MOUNT_SYNC_MODE: 'write-only', + RELAYFILE_MOUNT_SYNC_MODE: 'mirror', RELAYFILE_MOUNT_TIMEOUT: '180s' }) })) }) - it('uses the shared Slack command-root grammar for write-only mode', async () => { + it('mounts Slack user DM roots in mirror mode so inbound DMs read down', async () => { const manager = new IntegrationMountManager() await manager.ensureMounted([ @@ -273,7 +276,7 @@ describe('IntegrationMountManager', () => { localDir: '/tmp/pear-home/.agentworkforce/pear/relayfile/workspaces/account-workspace-id/slack/users/U123/messages', remotePath: '/slack/users/U123/messages', localLayout: 'exact', - syncMode: 'write-only' + syncMode: 'mirror' }) }) diff --git a/src/main/integration-mounts.ts b/src/main/integration-mounts.ts index 85eaeed4..7dcaaabe 100644 --- a/src/main/integration-mounts.ts +++ b/src/main/integration-mounts.ts @@ -9,7 +9,6 @@ import { } from '@relayfile/sdk' import { accountWorkspaceReadyRetryOptions, getAccountWorkspaceId, refreshCloudAuth, resolveCloudAuth } from './auth' import { createPearMountLauncher } from './relayfile-mount-launcher' -import { isSlackWritebackCommandRoot } from './slack-writeback-command-roots' const MOUNT_READY_TIMEOUT_MS = 60_000 const MOUNT_SYNC_TIMEOUT = '180s' @@ -519,7 +518,18 @@ export class IntegrationMountManager { remotePath: mountPath, localDir: join(mountRoot, ...remotePathSegments(mountPath)), localLayout: 'exact', - syncMode: isSlackWritebackCommandRoot(mountPath) ? 'write-only' : 'mirror', + // Slack message/DM roots (`/slack/channels//messages`, + // `/slack/users//messages`) are DUAL-PURPOSE: they are writeback + // command roots AND the inbound read-down surface for top-level messages + // and DMs. Mounting them `write-only` pushed our send-writeback command + // files up but did ZERO remote->local pulls, so inbound never + // materialized (the DM/top-level read-down gap; only `threads`, which is + // mirror, read down). `mirror` is a strict superset of `write-only` — the + // local push/dispatch path is identical, it only ADDS the inbound pull — + // so use it for every Slack mount: inbound reads down AND send-writebacks + // still dispatch. (Durable cloud path-correctness for channel messages + // ships in cloud#2010; DMs use the cloud#1997 D->U mapping.) + syncMode: 'mirror', agentName: `pear-integrations-${agentSegment}`, scopes: [ `relayfile:fs:read:${mountPath}/**`,