diff --git a/.trajectories/active/traj_y5jru5dh9ku6/trajectory.json b/.trajectories/active/traj_y5jru5dh9ku6/trajectory.json new file mode 100644 index 00000000..755ae350 --- /dev/null +++ b/.trajectories/active/traj_y5jru5dh9ku6/trajectory.json @@ -0,0 +1,19 @@ +{ + "id": "traj_y5jru5dh9ku6", + "version": 1, + "task": { + "title": "Review and fix PR #243" + }, + "status": "active", + "startedAt": "2026-06-06T00:15:50.055Z", + "agents": [], + "chapters": [], + "commits": [], + "filesChanged": [], + "projectId": "/home/daytona/workspace", + "tags": [], + "_trace": { + "startRef": "cf7fd174c9071e7c600793bfc4a9286354a61a79", + "endRef": "cf7fd174c9071e7c600793bfc4a9286354a61a79" + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-04/traj_7x9nltybo08h.compaction.json b/.trajectories/completed/2026-04/traj_7x9nltybo08h.compaction.json new file mode 100644 index 00000000..56a08d64 --- /dev/null +++ b/.trajectories/completed/2026-04/traj_7x9nltybo08h.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_7x9nltybo08h", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-04/traj_82lywlk9dcnc.compaction.json b/.trajectories/completed/2026-04/traj_82lywlk9dcnc.compaction.json new file mode 100644 index 00000000..c001bd64 --- /dev/null +++ b/.trajectories/completed/2026-04/traj_82lywlk9dcnc.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_82lywlk9dcnc", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.951Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-04/traj_cdist8i8vdmd.compaction.json b/.trajectories/completed/2026-04/traj_cdist8i8vdmd.compaction.json new file mode 100644 index 00000000..041b5240 --- /dev/null +++ b/.trajectories/completed/2026-04/traj_cdist8i8vdmd.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_cdist8i8vdmd", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-04/traj_dmoc4slub7ox.compaction.json b/.trajectories/completed/2026-04/traj_dmoc4slub7ox.compaction.json new file mode 100644 index 00000000..49d18ea1 --- /dev/null +++ b/.trajectories/completed/2026-04/traj_dmoc4slub7ox.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_dmoc4slub7ox", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-04/traj_em3hvzpg1xmx.compaction.json b/.trajectories/completed/2026-04/traj_em3hvzpg1xmx.compaction.json new file mode 100644 index 00000000..121d58c2 --- /dev/null +++ b/.trajectories/completed/2026-04/traj_em3hvzpg1xmx.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_em3hvzpg1xmx", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-04/traj_i1f02867dkxn.compaction.json b/.trajectories/completed/2026-04/traj_i1f02867dkxn.compaction.json new file mode 100644 index 00000000..ee15700f --- /dev/null +++ b/.trajectories/completed/2026-04/traj_i1f02867dkxn.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_i1f02867dkxn", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-04/traj_iuzm83ogm43k.compaction.json b/.trajectories/completed/2026-04/traj_iuzm83ogm43k.compaction.json new file mode 100644 index 00000000..a3963552 --- /dev/null +++ b/.trajectories/completed/2026-04/traj_iuzm83ogm43k.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_iuzm83ogm43k", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.951Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-04/traj_nixaonkglri1.compaction.json b/.trajectories/completed/2026-04/traj_nixaonkglri1.compaction.json new file mode 100644 index 00000000..2b9d3fd5 --- /dev/null +++ b/.trajectories/completed/2026-04/traj_nixaonkglri1.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_nixaonkglri1", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.951Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-04/traj_qi3qmy5oveab.compaction.json b/.trajectories/completed/2026-04/traj_qi3qmy5oveab.compaction.json new file mode 100644 index 00000000..ba228819 --- /dev/null +++ b/.trajectories/completed/2026-04/traj_qi3qmy5oveab.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_qi3qmy5oveab", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-04/traj_wez7rl7pkfpn.compaction.json b/.trajectories/completed/2026-04/traj_wez7rl7pkfpn.compaction.json new file mode 100644 index 00000000..83272ae3 --- /dev/null +++ b/.trajectories/completed/2026-04/traj_wez7rl7pkfpn.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_wez7rl7pkfpn", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_6fjv0fnvrc5e.compaction.json b/.trajectories/completed/2026-05/traj_6fjv0fnvrc5e.compaction.json new file mode 100644 index 00000000..8f5cf2b3 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_6fjv0fnvrc5e.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_6fjv0fnvrc5e", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_6lyjg41p6a28.compaction.json b/.trajectories/completed/2026-05/traj_6lyjg41p6a28.compaction.json new file mode 100644 index 00000000..ce916ef8 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_6lyjg41p6a28.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_6lyjg41p6a28", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_9khc36ax639i.compaction.json b/.trajectories/completed/2026-05/traj_9khc36ax639i.compaction.json new file mode 100644 index 00000000..82e46896 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_9khc36ax639i.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_9khc36ax639i", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_a6rfc30zag40.compaction.json b/.trajectories/completed/2026-05/traj_a6rfc30zag40.compaction.json new file mode 100644 index 00000000..1f34dd2f --- /dev/null +++ b/.trajectories/completed/2026-05/traj_a6rfc30zag40.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_a6rfc30zag40", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_ailh4waboewf.compaction.json b/.trajectories/completed/2026-05/traj_ailh4waboewf.compaction.json new file mode 100644 index 00000000..37a2d7bc --- /dev/null +++ b/.trajectories/completed/2026-05/traj_ailh4waboewf.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_ailh4waboewf", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_d3drzvodqpn7.compaction.json b/.trajectories/completed/2026-05/traj_d3drzvodqpn7.compaction.json new file mode 100644 index 00000000..3fc26843 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_d3drzvodqpn7.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_d3drzvodqpn7", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_hyqnsfininh5.compaction.json b/.trajectories/completed/2026-05/traj_hyqnsfininh5.compaction.json new file mode 100644 index 00000000..e454598a --- /dev/null +++ b/.trajectories/completed/2026-05/traj_hyqnsfininh5.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_hyqnsfininh5", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_v1un6n66y38i.compaction.json b/.trajectories/completed/2026-05/traj_v1un6n66y38i.compaction.json new file mode 100644 index 00000000..7a028e1b --- /dev/null +++ b/.trajectories/completed/2026-05/traj_v1un6n66y38i.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_v1un6n66y38i", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_xf18gkmtr3ib.compaction.json b/.trajectories/completed/2026-05/traj_xf18gkmtr3ib.compaction.json new file mode 100644 index 00000000..5b6b7579 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_xf18gkmtr3ib.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_xf18gkmtr3ib", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_z2klijcrwqed.compaction.json b/.trajectories/completed/2026-05/traj_z2klijcrwqed.compaction.json new file mode 100644 index 00000000..f14fab25 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_z2klijcrwqed.compaction.json @@ -0,0 +1,5 @@ +{ + "trajectoryId": "traj_z2klijcrwqed", + "compactedInto": "compact_xl96yexa79wg", + "compactedAt": "2026-06-06T00:15:49.952Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-06/traj_cf89ajbo2ast.json b/.trajectories/completed/2026-06/traj_cf89ajbo2ast.json new file mode 100644 index 00000000..7c8b1408 --- /dev/null +++ b/.trajectories/completed/2026-06/traj_cf89ajbo2ast.json @@ -0,0 +1,53 @@ +{ + "id": "traj_cf89ajbo2ast", + "version": 1, + "task": { + "title": "Review and fix PR #243" + }, + "status": "completed", + "startedAt": "2026-06-06T00:23:06.923Z", + "completedAt": "2026-06-06T00:23:16.097Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-06-06T00:23:07.249Z" + } + ], + "chapters": [ + { + "id": "chap_aybw5w15o4tm", + "title": "Work", + "agentName": "default", + "startedAt": "2026-06-06T00:23:07.249Z", + "endedAt": "2026-06-06T00:23:16.097Z", + "events": [ + { + "ts": 1780705387250, + "type": "decision", + "content": "Disable mount websocket path for write-only sync: Disable mount websocket path for write-only sync", + "raw": { + "question": "Disable mount websocket path for write-only sync", + "chosen": "Disable mount websocket path for write-only sync", + "alternatives": [], + "reasoning": "write-only mode is documented and implemented to push local changes without mirroring provider history, so the daemon ticker must not maintain websocket pull connections and should keep normal reconcile cadence" + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Reviewed PR #243 mount layout/sync-mode changes, validated stale bot comments against current checkout, fixed write-only daemon websocket behavior, and ran focused Go and TypeScript tests.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "/home/daytona/workspace", + "tags": [], + "_trace": { + "startRef": "cf7fd174c9071e7c600793bfc4a9286354a61a79", + "endRef": "cf7fd174c9071e7c600793bfc4a9286354a61a79" + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-06/traj_cf89ajbo2ast.md b/.trajectories/completed/2026-06/traj_cf89ajbo2ast.md new file mode 100644 index 00000000..98ce4dd6 --- /dev/null +++ b/.trajectories/completed/2026-06/traj_cf89ajbo2ast.md @@ -0,0 +1,31 @@ +# Trajectory: Review and fix PR #243 + +> **Status:** ✅ Completed +> **Confidence:** 90% +> **Started:** June 6, 2026 at 12:23 AM +> **Completed:** June 6, 2026 at 12:23 AM + +--- + +## Summary + +Reviewed PR #243 mount layout/sync-mode changes, validated stale bot comments against current checkout, fixed write-only daemon websocket behavior, and ran focused Go and TypeScript tests. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Disable mount websocket path for write-only sync +- **Chose:** Disable mount websocket path for write-only sync +- **Reasoning:** write-only mode is documented and implemented to push local changes without mirroring provider history, so the daemon ticker must not maintain websocket pull connections and should keep normal reconcile cadence + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Disable mount websocket path for write-only sync: Disable mount websocket path for write-only sync diff --git a/.trajectories/index.json b/.trajectories/index.json index da175fdd..92d91136 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,289 +1,181 @@ { "version": 1, - "lastUpdated": "2026-05-28T07:49:59.646Z", + "lastUpdated": "2026-06-06T00:23:16.149Z", "trajectories": { - "traj_mfyus7zfgxt2": { - "title": "062-sdk-setup-client-workflow", - "status": "active", - "startedAt": "2026-04-30T16:53:07.629Z", - "path": ".trajectories/active/traj_mfyus7zfgxt2.json" + "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": ".trajectories/completed/2026-04/traj_82lywlk9dcnc.json", - "compactedInto": "compact_xl96yexa79wg" + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_82lywlk9dcnc.json" }, - "traj_iuzm83ogm43k": { - "title": "Replace chokidar with @parcel/watcher in local-mount", + "traj_cdist8i8vdmd": { + "title": "Address PR 65 feedback", "status": "completed", - "startedAt": "2026-04-20T20:35:15.759Z", - "completedAt": "2026-04-20T20:58:15.412Z", - "path": ".trajectories/completed/2026-04/traj_iuzm83ogm43k.json", - "compactedInto": "compact_xl96yexa79wg" + "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_nixaonkglri1": { - "title": "Migrate relayfile e2e and conformance scripts to RS256 local JWKS", + "traj_dmoc4slub7ox": { + "title": "Fix SDK setup workflow evidence path", "status": "completed", - "startedAt": "2026-04-24T09:06:31.046Z", - "completedAt": "2026-04-24T09:10:42.425Z", - "path": ".trajectories/completed/2026-04/traj_nixaonkglri1.json", - "compactedInto": "compact_xl96yexa79wg" + "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": ".trajectories/completed/2026-04/traj_i1f02867dkxn.json", - "compactedInto": "compact_xl96yexa79wg" - }, - "traj_ubq95azheqpt": { - "title": "062-sdk-setup-client-workflow", - "status": "active", - "startedAt": "2026-04-30T17:12:22.684Z", - "path": ".trajectories/active/traj_ubq95azheqpt.json" + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_i1f02867dkxn.json" }, - "traj_dmoc4slub7ox": { - "title": "Fix SDK setup workflow evidence path", + "traj_iuzm83ogm43k": { + "title": "Replace chokidar with @parcel/watcher in local-mount", "status": "completed", - "startedAt": "2026-04-30T17:33:11.390Z", - "completedAt": "2026-04-30T17:34:29.265Z", - "path": ".trajectories/completed/2026-04/traj_dmoc4slub7ox.json", - "compactedInto": "compact_xl96yexa79wg" + "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_cojnals16pf9": { + "traj_mfyus7zfgxt2": { "title": "062-sdk-setup-client-workflow", - "status": "active", - "startedAt": "2026-04-30T17:35:02.672Z", - "path": ".trajectories/active/traj_cojnals16pf9.json" + "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": ".trajectories/completed/2026-04/traj_qi3qmy5oveab.json", - "compactedInto": "compact_xl96yexa79wg" + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_qi3qmy5oveab.json" }, - "traj_em3hvzpg1xmx": { + "traj_ubq95azheqpt": { "title": "062-sdk-setup-client-workflow", "status": "completed", - "startedAt": "2026-04-30T17:43:56.110Z", - "completedAt": "2026-04-30T17:43:59.041Z", - "path": ".trajectories/completed/2026-04/traj_em3hvzpg1xmx.json", - "compactedInto": "compact_xl96yexa79wg" - }, - "traj_cdist8i8vdmd": { - "title": "Address PR 65 feedback", - "status": "completed", - "startedAt": "2026-04-30T20:10:45.916Z", - "completedAt": "2026-04-30T20:11:29.126Z", - "path": ".trajectories/completed/2026-04/traj_cdist8i8vdmd.json", - "compactedInto": "compact_xl96yexa79wg" + "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": ".trajectories/completed/2026-04/traj_wez7rl7pkfpn.json", - "compactedInto": "compact_xl96yexa79wg" + "path": "/home/daytona/workspace/.trajectories/completed/2026-04/traj_wez7rl7pkfpn.json" }, - "traj_7x9nltybo08h": { - "title": "Update PR 65 usage instructions", + "traj_6fjv0fnvrc5e": { + "title": "Relayfile follow-up PRs: cloud conventions, cloud sdk/core bump, adapters release pipeline investigation", "status": "completed", - "startedAt": "2026-04-30T21:07:25.621Z", - "completedAt": "2026-04-30T21:08:20.135Z", - "path": ".trajectories/completed/2026-04/traj_7x9nltybo08h.json", - "compactedInto": "compact_xl96yexa79wg" + "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_z2klijcrwqed": { - "title": "Design simple agent workspace connect flow", + "traj_6lyjg41p6a28": { + "title": "Address PR comments on cloud#504 Linear conventions", "status": "completed", - "startedAt": "2026-05-01T14:58:15.412Z", - "completedAt": "2026-05-01T15:06:23.351Z", - "path": ".trajectories/completed/2026-05/traj_z2klijcrwqed.json", - "compactedInto": "compact_xl96yexa79wg" + "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": ".trajectories/completed/2026-05/traj_a6rfc30zag40.json", - "compactedInto": "compact_xl96yexa79wg" - }, - "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": ".trajectories/completed/2026-05/traj_hyqnsfininh5.json", - "compactedInto": "compact_xl96yexa79wg" + "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": ".trajectories/completed/2026-05/traj_ailh4waboewf.json", - "compactedInto": "compact_xl96yexa79wg" - }, - "traj_ozxb98dy7hk5": { - "title": "Research relayfile positioning vs MCP and CLI", - "status": "completed", - "startedAt": "2026-05-04T10:00:09.507Z", - "completedAt": "2026-05-04T10:01:10.778Z", - "path": ".trajectories/completed/2026-05/traj_ozxb98dy7hk5.json" - }, - "traj_rnldje4mr6nw": { - "title": "Research relayfile positioning vs MCP and CLI", - "status": "completed", - "startedAt": "2026-05-04T10:01:57.596Z", - "completedAt": "2026-05-04T10:01:57.776Z", - "path": ".trajectories/completed/2026-05/traj_rnldje4mr6nw.json" - }, - "traj_78fn1fyq1m4d": { - "title": "Explain relayfile relayauth governance model", - "status": "completed", - "startedAt": "2026-05-04T10:16:51.523Z", - "completedAt": "2026-05-04T10:17:37.753Z", - "path": ".trajectories/completed/2026-05/traj_78fn1fyq1m4d.json" - }, - "traj_aomtlnb3g5sa": { - "title": "Identify relayfile relayauth changes for per-user governance", - "status": "completed", - "startedAt": "2026-05-04T10:22:29.278Z", - "completedAt": "2026-05-04T10:22:29.396Z", - "path": ".trajectories/completed/2026-05/traj_aomtlnb3g5sa.json" - }, - "traj_5uxheavo6d3q": { - "title": "Write per-user relayfile governance spec", - "status": "completed", - "startedAt": "2026-05-04T10:25:24.905Z", - "completedAt": "2026-05-04T10:27:40.891Z", - "path": ".trajectories/completed/2026-05/traj_5uxheavo6d3q.json" - }, - "traj_2nl6e3gqkvki": { - "title": "Document programmatic Notion connect interface", - "status": "completed", - "startedAt": "2026-05-04T10:38:33.363Z", - "completedAt": "2026-05-04T10:40:25.800Z", - "path": ".trajectories/completed/2026-05/traj_2nl6e3gqkvki.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": ".trajectories/completed/2026-05/traj_v1un6n66y38i.json", - "compactedInto": "compact_xl96yexa79wg" - }, - "traj_9khc36ax639i": { - "title": "Add relayfile eval harness", - "status": "completed", - "startedAt": "2026-05-08T23:08:09.607Z", - "completedAt": "2026-05-08T23:18:24.282Z", - "path": ".trajectories/completed/2026-05/traj_9khc36ax639i.json", - "compactedInto": "compact_xl96yexa79wg" + "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": ".trajectories/completed/2026-05/traj_d3drzvodqpn7.json", - "compactedInto": "compact_xl96yexa79wg" + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_d3drzvodqpn7.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": ".trajectories/completed/2026-05/traj_6fjv0fnvrc5e.json", - "compactedInto": "compact_xl96yexa79wg" - }, - "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": ".trajectories/completed/2026-05/traj_xf18gkmtr3ib.json", - "compactedInto": "compact_xl96yexa79wg" - }, - "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": ".trajectories/completed/2026-05/traj_6lyjg41p6a28.json", - "compactedInto": "compact_xl96yexa79wg" - }, - "traj_a8szuw8b3yt8": { - "title": "Add proactive runtime contract markers for relayfile M1", - "status": "completed", - "startedAt": "2026-05-11T21:47:36.074Z", - "completedAt": "2026-05-11T21:49:35.589Z", - "path": ".trajectories/completed/2026-05/traj_a8szuw8b3yt8.json" - }, - "traj_62i8dhll2w9n": { - "title": "Implement relayfile subscribe, event lookup, retention, and contract updates", + "traj_hyqnsfininh5": { + "title": "Write agent workspace implementation workflow", "status": "completed", - "startedAt": "2026-05-12T01:29:17.382Z", - "completedAt": "2026-05-12T01:43:48.965Z", - "path": ".trajectories/completed/2026-05/traj_62i8dhll2w9n.json" + "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": ".trajectories/completed/2026-05/traj_nxbsptr6c5q0.json" + "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": ".trajectories/completed/2026-05/traj_qrt7hh3ht8nk.json" + "path": "/home/daytona/workspace/.trajectories/completed/2026-05/traj_qrt7hh3ht8nk.json" }, - "traj_onpf37fwj1mj": { - "title": "Implement resumable bootstrap + decoupled timeouts for large-workspace mount sync", - "status": "completed", - "startedAt": "2026-05-18T18:45:52.924Z", - "completedAt": "2026-05-18T19:14:56.067Z", - "path": ".trajectories/completed/2026-05/traj_onpf37fwj1mj.json" - }, - "traj_wzwo03w1kb1x": { - "title": "autofix-swarm-Agentworkforce-relayfile-workflow", + "traj_v1un6n66y38i": { + "title": "Async createMount — issue #104", "status": "completed", - "startedAt": "2026-05-21T08:52:34.142Z", - "completedAt": "2026-05-21T09:02:24.642Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/.msd-autofix-b09c607b/.trajectories/completed/2026-05/traj_wzwo03w1kb1x.json" + "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_5310ok4zat2n": { - "title": "Fix relayfile CLI rehome legacy LocalDir and pid verification", + "traj_xf18gkmtr3ib": { + "title": "Address PR comments on relayfile-adapters#59", "status": "completed", - "startedAt": "2026-05-21T08:54:30.035Z", - "completedAt": "2026-05-21T08:54:30.166Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/.msd-autofix-b09c607b/.trajectories/completed/2026-05/traj_5310ok4zat2n.json" + "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_eud9obtnr1br": { - "title": "Fix relayfile auth refresh and status warnings for issues 188 and 189", + "traj_z2klijcrwqed": { + "title": "Design simple agent workspace connect flow", "status": "completed", - "startedAt": "2026-05-21T13:14:23.504Z", - "completedAt": "2026-05-21T13:20:34.729Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relayfile-issues-188-189/.trajectories/completed/2026-05/traj_eud9obtnr1br.json" + "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_0meitjjmvpf2": { - "title": "Address relayfile PR 215 feedback", + "traj_cf89ajbo2ast": { + "title": "Review and fix PR #243", "status": "completed", - "startedAt": "2026-05-28T07:48:35.401Z", - "completedAt": "2026-05-28T07:49:59.552Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relayfile-issue-214/.trajectories/completed/2026-05/traj_0meitjjmvpf2.json" + "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" } } } \ No newline at end of file diff --git a/cmd/relayfile-mount/main.go b/cmd/relayfile-mount/main.go index 136ed1c1..a222e02c 100644 --- a/cmd/relayfile-mount/main.go +++ b/cmd/relayfile-mount/main.go @@ -25,6 +25,10 @@ import ( const ( mountModePoll = "poll" mountModeFuse = "fuse" + localLayoutExact = "exact" + localLayoutScoped = "scoped" + syncModeMirror = "mirror" + syncModeWriteOnly = "write-only" websocketReconcileEvery = 10 minMountPollInterval = 5 * time.Second ) @@ -39,9 +43,11 @@ type mountConfig struct { remotePaths []string eventProvider string localDir string + localLayout string stateFile string stateDir string mountKind string + syncMode string interval time.Duration intervalJitter float64 timeout time.Duration @@ -75,9 +81,11 @@ func main() { pathsFile := flag.String("paths-file", strings.TrimSpace(os.Getenv("RELAYFILE_MOUNT_PATHS_FILE")), "file containing remote root paths, as JSON array or newline-separated list") eventProvider := flag.String("provider", strings.TrimSpace(os.Getenv("RELAYFILE_MOUNT_PROVIDER")), "event provider filter") localDir := flag.String("local-dir", strings.TrimSpace(os.Getenv("RELAYFILE_LOCAL_DIR")), "local mirror directory") + localLayout := flag.String("local-layout", envOrDefault("RELAYFILE_MOUNT_LOCAL_LAYOUT", localLayoutExact), "local directory layout: exact (local-dir is mirror root) or scoped (remote path is appended under local-dir)") stateFile := flag.String("state-file", strings.TrimSpace(os.Getenv("RELAYFILE_MOUNT_STATE_FILE")), "state file path") stateDir := flag.String("state-dir", envOrDefault("RELAYFILE_MOUNT_STATE_DIR", mountsync.DefaultMountStateDir()), "directory for private mount state") mountKind := flag.String("mount-kind", envOrDefault("RELAYFILE_MOUNT_KIND", mountsync.MountKindDaemon), "private state identity kind: daemon, flush, or initial-sync") + syncModeFlag := flag.String("sync-mode", envOrDefault("RELAYFILE_MOUNT_SYNC_MODE", syncModeMirror), "sync behavior: mirror (pull and push) or write-only (push local changes without mirroring provider history)") interval := flag.Duration("interval", durationEnv("RELAYFILE_MOUNT_INTERVAL", 30*time.Second), "sync interval") intervalJitter := flag.Float64("interval-jitter", floatEnv("RELAYFILE_MOUNT_INTERVAL_JITTER", 0.2), "sync interval jitter ratio (0.0-1.0)") timeout := flag.Duration("timeout", durationEnv("RELAYFILE_MOUNT_TIMEOUT", 15*time.Second), "per-sync timeout") @@ -121,6 +129,14 @@ func main() { if err != nil { log.Fatalf("invalid mount mode: %v", err) } + resolvedLocalLayout, err := resolveLocalLayout(*localLayout) + if err != nil { + log.Fatalf("invalid local layout: %v", err) + } + resolvedSyncMode, err := resolveSyncMode(*syncModeFlag) + if err != nil { + log.Fatalf("invalid sync mode: %v", err) + } rootCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() @@ -133,9 +149,11 @@ func main() { remotePaths: normalizeRemotePaths(allRemotePaths, envOrDefault("RELAYFILE_REMOTE_PATH", "/")), eventProvider: strings.TrimSpace(*eventProvider), localDir: *localDir, + localLayout: resolvedLocalLayout, stateFile: *stateFile, stateDir: *stateDir, mountKind: *mountKind, + syncMode: resolvedSyncMode, interval: *interval, intervalJitter: *intervalJitter, timeout: *timeout, @@ -177,6 +195,32 @@ func resolveMountMode(mode string, fuse bool) (string, error) { } } +func resolveLocalLayout(layout string) (string, error) { + normalized := strings.ToLower(strings.TrimSpace(layout)) + if normalized == "" { + return localLayoutExact, nil + } + switch normalized { + case localLayoutExact, localLayoutScoped: + return normalized, nil + default: + return "", fmt.Errorf("%q (supported: %s, %s)", layout, localLayoutExact, localLayoutScoped) + } +} + +func resolveSyncMode(mode string) (string, error) { + normalized := strings.ToLower(strings.TrimSpace(mode)) + if normalized == "" { + return syncModeMirror, nil + } + switch normalized { + case syncModeMirror, syncModeWriteOnly: + return normalized, nil + default: + return "", fmt.Errorf("%q (supported: %s, %s)", mode, syncModeMirror, syncModeWriteOnly) + } +} + func executeMount(rootCtx context.Context, cfg mountConfig, runPoll pollRunner, runFuse fuseRunner) error { switch cfg.mode { case mountModePoll: @@ -189,14 +233,23 @@ func executeMount(rootCtx context.Context, cfg mountConfig, runPoll pollRunner, } func runPollingMount(rootCtx context.Context, cfg mountConfig) error { + return runPollingMountWithRunner(rootCtx, cfg, runSinglePollingMount) +} + +func runPollingMountWithRunner(rootCtx context.Context, cfg mountConfig, run pollRunner) error { remotePaths := cfg.remotePaths if len(remotePaths) == 0 { remotePaths = []string{cfg.remotePath} } - if len(remotePaths) > 1 || (len(remotePaths) == 1 && normalizeMountRemotePath(remotePaths[0]) != "/") { - return runScopedPollingMounts(rootCtx, cfg, remotePaths) + if cfg.localLayout == localLayoutScoped { + return runScopedPollingMountsWithRunner(rootCtx, cfg, remotePaths, run) } - return runSinglePollingMount(rootCtx, cfg) + if len(remotePaths) > 1 { + return fmt.Errorf("multiple --remote-path values require --local-layout=%s", localLayoutScoped) + } + cfg.remotePath = normalizeMountRemotePath(remotePaths[0]) + cfg.remotePaths = nil + return run(rootCtx, cfg) } func runScopedPollingMounts(rootCtx context.Context, cfg mountConfig, remotePaths []string) error { @@ -323,6 +376,7 @@ func runSinglePollingMount(rootCtx context.Context, cfg mountConfig) error { BootstrapTimeout: cfg.bootstrapTimeout, CursorTimeout: cfg.cursorTimeout, ForceFullReconcile: boolPtr(cfg.forceFullRecon), + SyncMode: cfg.syncMode, }) if err != nil { return fmt.Errorf("initialize mount syncer: %w", err) @@ -330,6 +384,7 @@ func runSinglePollingMount(rootCtx context.Context, cfg mountConfig) error { if _, err := mountsync.StartDiagnostics(rootCtx, cfg.pprofAddr, cfg.memlogInterval, log.Default()); err != nil { return fmt.Errorf("start diagnostics: %w", err) } + log.Printf("%s", mountStartupLogLine(cfg)) log.Printf("Mirror started at %s. Sync interval %s +/- %.0f%%. Public state: %s", cfg.localDir, cfg.interval.Round(time.Second), cfg.intervalJitter*100, filepath.Join(cfg.localDir, ".relay", "state.json")) run := func(reconcile bool) { @@ -386,7 +441,7 @@ func runSinglePollingMount(rootCtx context.Context, cfg mountConfig) error { log.Printf("mount sync stopping: %v", rootCtx.Err()) return nil case <-wsTicker.C: - if cfg.websocketEnabled { + if mountWebSocketEnabled(cfg) { ctx, cancel := context.WithTimeout(rootCtx, cfg.timeout) if err := syncer.MaintainWebSocket(ctx); err != nil { log.Printf("websocket unavailable; using polling sync: %v", err) @@ -395,7 +450,7 @@ func runSinglePollingMount(rootCtx context.Context, cfg mountConfig) error { } case <-timer.C: cycle++ - reconcile := shouldReconcileMountCycle(cfg.websocketEnabled, cycle) + reconcile := shouldReconcileMountCycle(mountWebSocketEnabled(cfg), cycle) if reconcile { run(true) } @@ -475,6 +530,26 @@ func scopedLocalDir(localRoot, remotePath string) string { return filepath.Join(localRoot, filepath.FromSlash(strings.TrimPrefix(remotePath, "/"))) } +func mountStartupLogLine(cfg mountConfig) string { + layout := cfg.localLayout + if layout == "" { + layout = localLayoutExact + } + syncMode := cfg.syncMode + if syncMode == "" { + syncMode = syncModeMirror + } + return fmt.Sprintf( + "mount layout=%s remote=%s local=%s sync=%s mode=%s state=%s", + layout, + normalizeMountRemotePath(cfg.remotePath), + cfg.localDir, + syncMode, + cfg.mode, + filepath.Join(cfg.localDir, ".relay", "state.json"), + ) +} + // readBootstrapProgress reads the in-progress bootstrap block from the // mountsync public state file. ok is false when there is no bootstrap in // progress (or the file is missing/unparseable). @@ -641,6 +716,10 @@ func shouldReconcileMountCycle(websocketEnabled bool, cycle int) bool { return !websocketEnabled || cycle%websocketReconcileEvery == 0 } +func mountWebSocketEnabled(cfg mountConfig) bool { + return cfg.websocketEnabled && cfg.syncMode != syncModeWriteOnly +} + func clampJitterRatio(value float64) float64 { if value < 0 { return 0 diff --git a/cmd/relayfile-mount/main_test.go b/cmd/relayfile-mount/main_test.go index f331c7b8..c3e50bf9 100644 --- a/cmd/relayfile-mount/main_test.go +++ b/cmd/relayfile-mount/main_test.go @@ -114,6 +114,16 @@ func TestWebSocketMaintenanceDoesNotLowerReconcileCadence(t *testing.T) { } } +func TestWriteOnlyMountDisablesWebSocketCadence(t *testing.T) { + cfg := mountConfig{websocketEnabled: true, syncMode: syncModeWriteOnly} + if mountWebSocketEnabled(cfg) { + t.Fatal("write-only mount should not maintain websocket connections") + } + if !shouldReconcileMountCycle(mountWebSocketEnabled(cfg), 1) { + t.Fatal("write-only mount should keep regular reconcile cadence") + } +} + func TestResolveMountMode(t *testing.T) { tests := []struct { name string @@ -149,6 +159,72 @@ func TestResolveMountMode(t *testing.T) { } } +func TestResolveLocalLayout(t *testing.T) { + tests := []struct { + name string + layout string + want string + wantErr bool + }{ + {name: "default empty layout uses exact", want: localLayoutExact}, + {name: "explicit exact", layout: "exact", want: localLayoutExact}, + {name: "explicit scoped", layout: "scoped", want: localLayoutScoped}, + {name: "case and whitespace normalized", layout: " SCOPED ", want: localLayoutScoped}, + {name: "invalid layout errors", layout: "auto", wantErr: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := resolveLocalLayout(tc.layout) + if tc.wantErr { + if err == nil { + t.Fatalf("expected error, got layout %q", got) + } + return + } + if err != nil { + t.Fatalf("resolveLocalLayout returned error: %v", err) + } + if got != tc.want { + t.Fatalf("expected layout %q, got %q", tc.want, got) + } + }) + } +} + +func TestResolveSyncMode(t *testing.T) { + tests := []struct { + name string + mode string + want string + wantErr bool + }{ + {name: "default empty sync mode uses mirror", want: syncModeMirror}, + {name: "explicit mirror", mode: "mirror", want: syncModeMirror}, + {name: "explicit write-only", mode: "write-only", want: syncModeWriteOnly}, + {name: "case and whitespace normalized", mode: " WRITE-ONLY ", want: syncModeWriteOnly}, + {name: "invalid sync mode errors", mode: "push", wantErr: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := resolveSyncMode(tc.mode) + if tc.wantErr { + if err == nil { + t.Fatalf("expected error, got sync mode %q", got) + } + return + } + if err != nil { + t.Fatalf("resolveSyncMode returned error: %v", err) + } + if got != tc.want { + t.Fatalf("expected sync mode %q, got %q", tc.want, got) + } + }) + } +} + func TestExecuteMountDispatchesPollMode(t *testing.T) { cfg := mountConfig{mode: mountModePoll} pollCalled := false @@ -280,6 +356,109 @@ func TestRunScopedPollingMountsKeepsSharedStateDirForHashResolver(t *testing.T) } } +func TestRunPollingMountSingleNonRootDefaultsToExactLocalDir(t *testing.T) { + localDir := t.TempDir() + var got []mountConfig + + err := runPollingMountWithRunner( + context.Background(), + mountConfig{ + localDir: localDir, + stateDir: t.TempDir(), + remotePath: "/slack/channels/C123", + remotePaths: []string{"/slack/channels/C123"}, + }, + func(_ context.Context, cfg mountConfig) error { + got = append(got, cfg) + return nil + }, + ) + if err != nil { + t.Fatalf("runPollingMountWithRunner returned error: %v", err) + } + if len(got) != 1 { + t.Fatalf("expected one mount, got %d", len(got)) + } + if got[0].localDir != localDir { + t.Fatalf("expected exact local dir %q, got %q", localDir, got[0].localDir) + } +} + +func TestRunPollingMountScopedLayoutAppendsRemotePath(t *testing.T) { + localRoot := t.TempDir() + var got []mountConfig + + err := runPollingMountWithRunner( + context.Background(), + mountConfig{ + localDir: localRoot, + localLayout: localLayoutScoped, + stateDir: t.TempDir(), + remotePath: "/slack/channels/C123", + remotePaths: []string{"/slack/channels/C123"}, + }, + func(_ context.Context, cfg mountConfig) error { + got = append(got, cfg) + return nil + }, + ) + if err != nil { + t.Fatalf("runPollingMountWithRunner returned error: %v", err) + } + if len(got) != 1 { + t.Fatalf("expected one mount, got %d", len(got)) + } + want := filepath.Join(localRoot, "slack", "channels", "C123") + if got[0].localDir != want { + t.Fatalf("expected scoped local dir %q, got %q", want, got[0].localDir) + } +} + +func TestRunPollingMountMultiPathRequiresExplicitScopedLayout(t *testing.T) { + err := runPollingMountWithRunner( + context.Background(), + mountConfig{ + localDir: t.TempDir(), + stateDir: t.TempDir(), + remotePaths: []string{"/github", "/slack"}, + }, + func(_ context.Context, cfg mountConfig) error { + t.Fatalf("runner should not start with implicit multi-path layout: %+v", cfg) + return nil + }, + ) + if err == nil { + t.Fatal("expected multi-path exact layout to fail") + } + if !strings.Contains(err.Error(), "--local-layout=scoped") { + t.Fatalf("expected scoped-layout guidance, got %v", err) + } +} + +func TestMountStartupLogLineIncludesResolvedLayoutAndSyncContract(t *testing.T) { + localDir := t.TempDir() + got := mountStartupLogLine(mountConfig{ + localDir: localDir, + localLayout: localLayoutExact, + remotePath: "/slack/channels/C123", + syncMode: syncModeWriteOnly, + mode: mountModePoll, + }) + + for _, want := range []string{ + "layout=exact", + "remote=/slack/channels/C123", + "local=" + localDir, + "sync=write-only", + "mode=poll", + "state=" + filepath.Join(localDir, ".relay", "state.json"), + } { + if !strings.Contains(got, want) { + t.Fatalf("startup log %q missing %q", got, want) + } + } +} + func TestRunScopedPollingMountsRejectsSharedExactStateFileOverride(t *testing.T) { stateFile := filepath.Join(t.TempDir(), "state.json") diff --git a/internal/mountsync/syncer.go b/internal/mountsync/syncer.go index 26b69771..133cf1fd 100644 --- a/internal/mountsync/syncer.go +++ b/internal/mountsync/syncer.go @@ -681,6 +681,7 @@ type SyncerOptions struct { RootCtx context.Context Logger Logger Mode string + SyncMode string Interval time.Duration // FullPullEvery controls how often the incremental pull path forces a // full tree pull as a "trust but verify" safety net against cloud-side @@ -779,6 +780,15 @@ type noopLogger struct{} func (noopLogger) Printf(string, ...any) {} +func normalizeSyncMode(mode string) string { + switch strings.ToLower(strings.TrimSpace(mode)) { + case "write-only": + return "write-only" + default: + return "mirror" + } +} + func logMemoryStats(ctx context.Context, interval time.Duration, logger Logger) { ticker := time.NewTicker(interval) defer ticker.Stop() @@ -861,6 +871,7 @@ type Syncer struct { oversizedLogged map[string]struct{} lazyRepos bool lowMemory bool + writeOnly bool layoutRegistrar ProviderLayoutRegistrar githubWorkingTree *githubWorkingTreeMount closeScheduler *CloseScheduler @@ -985,6 +996,7 @@ type publicState struct { RemoteRoot string `json:"remoteRoot"` LocalRoot string `json:"localRoot"` Mode string `json:"mode"` + SyncMode string `json:"syncMode,omitempty"` IntervalMs int64 `json:"intervalMs"` LastReconcileAt string `json:"lastReconcileAt,omitempty"` LastSuccessfulReconcileAt string `json:"lastSuccessfulReconcileAt,omitempty"` @@ -1248,6 +1260,7 @@ func NewSyncer(client RemoteClient, opts SyncerOptions) (*Syncer, error) { denialLogPath: filepath.Join(localRoot, ".relay", "permissions-denied.log"), bulkFlushThreshold: bulkFlushThreshold, mode: strings.TrimSpace(opts.Mode), + writeOnly: normalizeSyncMode(opts.SyncMode) == "write-only", interval: opts.Interval, fullPullEvery: fullPullEvery, cursorTimeout: cursorTimeout, @@ -1979,8 +1992,10 @@ func (s *Syncer) sync(ctx context.Context, forcePoll bool) error { s.mu.Unlock() - if err := s.MaintainWebSocket(ctx); err != nil { - s.logf("websocket unavailable; using polling sync: %v", err) + if !s.writeOnly { + if err := s.MaintainWebSocket(ctx); err != nil { + s.logf("websocket unavailable; using polling sync: %v", err) + } } // Re-acquire lock for the remainder of the sync operation. @@ -2000,7 +2015,11 @@ func (s *Syncer) sync(ctx context.Context, forcePoll bool) error { conflicted := map[string]struct{}{} didPoll := false - if !s.state.BootstrapComplete || s.forceFullReconcile { + if s.writeOnly { + if !s.state.BootstrapComplete { + s.markBootstrapComplete() + } + } else if !s.state.BootstrapComplete || s.forceFullReconcile { if err := s.pullRemote(ctx, conflicted); err != nil { s.markSyncError(err) _ = s.saveState() @@ -2018,7 +2037,7 @@ func (s *Syncer) sync(ctx context.Context, forcePoll bool) error { } shouldPoll := !didPoll && (forcePoll || !s.bootstrapped || s.wsConn == nil) - if shouldPoll { + if shouldPoll && !s.writeOnly { if err := s.pullRemote(ctx, conflicted); err != nil { s.markSyncError(err) _ = s.saveState() @@ -4916,11 +4935,16 @@ func (s *Syncer) savePublicState() error { if mode == "" { mode = "poll" } + syncMode := "mirror" + if s.writeOnly { + syncMode = "write-only" + } public := publicState{ WorkspaceID: s.workspace, RemoteRoot: s.remoteRoot, LocalRoot: s.localRoot, Mode: mode, + SyncMode: syncMode, IntervalMs: s.interval.Milliseconds(), LastReconcileAt: s.state.LastReconcileAt, LastSuccessfulReconcileAt: s.state.LastSuccessfulReconcileAt, diff --git a/internal/mountsync/syncer_test.go b/internal/mountsync/syncer_test.go index b65ed4e7..a934bdc6 100644 --- a/internal/mountsync/syncer_test.go +++ b/internal/mountsync/syncer_test.go @@ -225,6 +225,69 @@ func TestSyncOncePullsRemoteAndPushesLocalEdits(t *testing.T) { } } +func TestSyncOnceWriteOnlySkipsRemotePullButPushesLocalFiles(t *testing.T) { + client := &fakeClient{ + files: map[string]RemoteFile{ + "/slack/channels/C123/messages/history.json": { + Path: "/slack/channels/C123/messages/history.json", + Revision: "rev_1", + ContentType: "application/json", + Content: `{"text":"history"}`, + }, + }, + revisionCounter: 1, + } + localDir := t.TempDir() + syncer, err := NewSyncer(client, SyncerOptions{ + WorkspaceID: "ws_write_only", + RemoteRoot: "/slack/channels/C123/messages", + LocalRoot: localDir, + SyncMode: "write-only", + }) + if err != nil { + t.Fatalf("new syncer failed: %v", err) + } + + localDraft := filepath.Join(localDir, "wb-pear-ack.json") + if err := os.WriteFile(localDraft, []byte(`{"text":"ack"}`), 0o644); err != nil { + t.Fatalf("write local draft failed: %v", err) + } + + if err := syncer.SyncOnce(context.Background()); err != nil { + t.Fatalf("write-only sync failed: %v", err) + } + + if client.listTreeCalls != 0 || client.listEventsCalls != 0 || client.readFileCalls != 0 { + t.Fatalf("write-only sync should not pull remote records; tree=%d events=%d read=%d", client.listTreeCalls, client.listEventsCalls, client.readFileCalls) + } + if got := len(client.bulkWriteBatches); got != 1 { + t.Fatalf("expected one bulk write batch, got %d", got) + } + if got, want := client.bulkWriteBatches[0][0].Path, "/slack/channels/C123/messages/wb-pear-ack.json"; got != want { + t.Fatalf("expected canonical write path %q, got %q", want, got) + } + if _, err := os.Stat(filepath.Join(localDir, ".relay", "dead-letter")); err != nil { + t.Fatalf("expected write-only mount to keep dead-letter feedback dir: %v", err) + } + var public struct { + Mode string `json:"mode"` + SyncMode string `json:"syncMode"` + } + data, err := os.ReadFile(filepath.Join(localDir, ".relay", "state.json")) + if err != nil { + t.Fatalf("read public state: %v", err) + } + if err := json.Unmarshal(data, &public); err != nil { + t.Fatalf("decode public state: %v", err) + } + if public.Mode != "poll" || public.SyncMode != "write-only" { + t.Fatalf("expected public state mode poll/write-only, got %+v", public) + } + if _, err := os.Stat(filepath.Join(localDir, "history.json")); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("write-only sync mirrored provider history, stat err=%v", err) + } +} + func TestHandleLocalChangeIgnoresAlreadyTrackedContent(t *testing.T) { client := &fakeClient{ files: map[string]RemoteFile{ diff --git a/packages/sdk/typescript/src/index.ts b/packages/sdk/typescript/src/index.ts index 94560166..dbaf5a6d 100644 --- a/packages/sdk/typescript/src/index.ts +++ b/packages/sdk/typescript/src/index.ts @@ -50,10 +50,12 @@ export { type MountLauncherEvent, type MountLauncherInstance, type MountLauncherStart, + type MountLocalLayout, type MountMode, type MountSessionRequest, type MountSessionResponse, type MountSessionResult, + type MountSyncMode, type MountedWorkspaceHandle, type MountedWorkspaceStatus, type MountWorkspaceInput, diff --git a/packages/sdk/typescript/src/mount-launcher.test.ts b/packages/sdk/typescript/src/mount-launcher.test.ts index 8f8178c9..9bd4f810 100644 --- a/packages/sdk/typescript/src/mount-launcher.test.ts +++ b/packages/sdk/typescript/src/mount-launcher.test.ts @@ -218,4 +218,52 @@ describe("default mount launcher", () => { await rm(tempRoot, { recursive: true, force: true }) } }) + + it("reads scoped-layout state from the resolved mount root", async () => { + const tempRoot = await mkdtemp( + path.join(os.tmpdir(), "relayfile-default-launcher-scoped-status-") + ) + const localDir = path.join(tempRoot, "mirror") + const scopedDir = path.join(localDir, "slack", "channels", "C123") + const fetchMock = vi.fn() + vi.stubGlobal("fetch", fetchMock) + + try { + const lastReconcileAt = new Date(Date.now() - 1_000).toISOString() + await mkdir(path.join(scopedDir, ".relay"), { recursive: true }) + await writeFile( + path.join(scopedDir, ".relay", "state.json"), + JSON.stringify({ + mode: "poll", + intervalMs: 30_000, + lastReconcileAt, + daemon: { pid: 8888 }, + providers: [{ status: "ready" }] + }), + "utf8" + ) + + const status = await readMountedWorkspaceStatus({ + localDir, + workspaceId: "ws_123", + remotePath: "/slack/channels/C123", + mode: "poll", + localLayout: "scoped", + relayfileBaseUrl: "https://relayfile.mount.test", + relayfileToken: "rf_mount_token", + expiresAt: null, + suggestedRefreshAt: null + }) + + expect(status).toMatchObject({ + ready: true, + mode: "poll", + pid: 8888, + lastReconcileAt + }) + expect(fetchMock).not.toHaveBeenCalled() + } finally { + await rm(tempRoot, { recursive: true, force: true }) + } + }) }) diff --git a/packages/sdk/typescript/src/mount-launcher.ts b/packages/sdk/typescript/src/mount-launcher.ts index 0f35dbf7..3ca4ad96 100644 --- a/packages/sdk/typescript/src/mount-launcher.ts +++ b/packages/sdk/typescript/src/mount-launcher.ts @@ -20,10 +20,12 @@ import { RelayfileSetupError } from "./setup-errors.js" import type { + MountLocalLayout, MountLauncher, MountLauncherInstance, MountLauncherStart, MountMode, + MountSyncMode, MountedWorkspaceStatus, ReadMountedWorkspaceStatusInput } from "./setup-types.js" @@ -71,7 +73,9 @@ export function createDefaultMountLauncher( export async function readMountedWorkspaceStatus( input: ReadMountedWorkspaceStatusInput ): Promise { - const state = await readMountStateFile(input.localDir) + const state = await readMountStateFile( + resolveMountLocalDir(input.localDir, input.remotePath, input.localLayout) + ) if (state && !isMountStateStale(state)) { return { ready: isMountStateReady(state), @@ -102,7 +106,12 @@ async function startRelayfileMount( options: DefaultMountLauncherOptions ): Promise { const localDir = path.resolve(input.env.RELAYFILE_LOCAL_DIR ?? process.cwd()) - const relayDir = path.join(localDir, ".relay") + const mountLocalDir = resolveMountLocalDir( + localDir, + input.env.RELAYFILE_REMOTE_PATH, + input.env.RELAYFILE_MOUNT_LOCAL_LAYOUT + ) + const relayDir = path.join(mountLocalDir, ".relay") const logPath = path.join(relayDir, "mount.log") const pidPath = path.join(relayDir, "mount.pid") await mkdir(relayDir, { recursive: true }) @@ -111,7 +120,7 @@ async function startRelayfileMount( const command = await resolveRelayfileMountCommand() const args = input.background === false ? ["--once"] : [] const child = (options.spawnImpl ?? spawn)(command, args, { - cwd: input.cwd ?? localDir, + cwd: input.cwd ?? mountLocalDir, env: { ...process.env, ...input.env @@ -133,7 +142,7 @@ async function startRelayfileMount( pidPath, outputBuffer, input, - localDir, + localDir: mountLocalDir, now: options.now ?? Date.now, readyPollIntervalMs: options.readyPollIntervalMs ?? DEFAULT_READY_POLL_INTERVAL_MS @@ -190,6 +199,8 @@ class RelayfileMountProcessInstance implements MountLauncherInstance { workspaceId: this.input.env.RELAYFILE_WORKSPACE ?? "", remotePath: this.input.env.RELAYFILE_REMOTE_PATH ?? "/", mode: normalizeMountMode(this.input.env.RELAYFILE_MOUNT_MODE) ?? "poll", + localLayout: normalizeMountLocalLayout(this.input.env.RELAYFILE_MOUNT_LOCAL_LAYOUT), + syncMode: normalizeMountSyncMode(this.input.env.RELAYFILE_MOUNT_SYNC_MODE), relayfileBaseUrl: this.input.env.RELAYFILE_BASE_URL ?? "", relayfileToken: this.input.env.RELAYFILE_TOKEN ?? "", expiresAt: null, @@ -360,6 +371,42 @@ function normalizeMountMode(mode?: string): MountMode | undefined { return mode === "fuse" ? "fuse" : mode === "poll" ? "poll" : undefined } +function normalizeMountLocalLayout(layout?: string): MountLocalLayout { + return layout === "scoped" ? "scoped" : "exact" +} + +function normalizeMountSyncMode(mode?: string): MountSyncMode { + return mode === "write-only" ? "write-only" : "mirror" +} + +function resolveMountLocalDir( + localDir: string, + remotePath?: string, + localLayout?: string +): string { + const root = path.resolve(localDir) + if (normalizeMountLocalLayout(localLayout) !== "scoped") { + return root + } + const normalizedRemote = normalizeRemotePath(remotePath) + if (normalizedRemote === "/") { + return root + } + return path.join(root, ...normalizedRemote.split("/").filter(Boolean)) +} + +function normalizeRemotePath(remotePath?: string): string { + const trimmed = typeof remotePath === "string" ? remotePath.trim() : "" + if (!trimmed || trimmed === "/") { + return "/" + } + const slashNormalized = trimmed.replace(/\\/g, "/") + const normalized = path.posix.normalize( + slashNormalized.startsWith("/") ? slashNormalized : `/${slashNormalized}` + ) + return normalized === "/" ? "/" : normalized.replace(/\/+$/, "") +} + function normalizeIsoString(value: unknown): string | undefined { if (typeof value !== "string" || value.trim() === "") { return undefined diff --git a/packages/sdk/typescript/src/setup-types.ts b/packages/sdk/typescript/src/setup-types.ts index 69a95946..83914088 100644 --- a/packages/sdk/typescript/src/setup-types.ts +++ b/packages/sdk/typescript/src/setup-types.ts @@ -89,6 +89,8 @@ export interface WorkspaceMountEnvOptions { export type WorkspaceMountEnv = Record export type MountMode = "poll" | "fuse" +export type MountLocalLayout = "exact" | "scoped" +export type MountSyncMode = "mirror" | "write-only" export interface MountSessionRequest { localDir: string @@ -123,6 +125,8 @@ export interface MountSessionResult { remotePath: string localDir: string mode: MountMode + localLayout: MountLocalLayout + syncMode: MountSyncMode scopes: string[] tokenIssuedAt: string | null expiresAt: string | null @@ -149,6 +153,8 @@ export interface ReadMountedWorkspaceStatusInput { workspaceId: string remotePath: string mode: MountMode + localLayout?: MountLocalLayout + syncMode?: MountSyncMode relayfileBaseUrl: string relayfileToken: string expiresAt: string | null @@ -201,6 +207,8 @@ export interface MountWorkspaceInput { localDir: string remotePath?: string mode?: MountMode + localLayout?: MountLocalLayout + syncMode?: MountSyncMode background?: boolean agentName?: string scopes?: string[] diff --git a/packages/sdk/typescript/src/setup.test.ts b/packages/sdk/typescript/src/setup.test.ts index bc16c41b..df748736 100644 --- a/packages/sdk/typescript/src/setup.test.ts +++ b/packages/sdk/typescript/src/setup.test.ts @@ -1203,6 +1203,8 @@ describe("RelayfileSetup", () => { RELAYFILE_REMOTE_PATH: "/notion", RELAYFILE_LOCAL_DIR: localDir, RELAYFILE_MOUNT_MODE: "poll", + RELAYFILE_MOUNT_LOCAL_LAYOUT: "exact", + RELAYFILE_MOUNT_SYNC_MODE: "mirror", RELAYCAST_API_KEY: "rc_mount", RELAY_API_KEY: "rc_mount", RELAYCAST_BASE_URL: "https://relaycast.mount.test", @@ -1234,7 +1236,9 @@ describe("RelayfileSetup", () => { RELAYFILE_WORKSPACE: "ws_123", RELAYFILE_REMOTE_PATH: "/notion", RELAYFILE_LOCAL_DIR: localDir, - RELAYFILE_MOUNT_MODE: "poll" + RELAYFILE_MOUNT_MODE: "poll", + RELAYFILE_MOUNT_LOCAL_LAYOUT: "exact", + RELAYFILE_MOUNT_SYNC_MODE: "mirror" }) await handle.stop() @@ -1244,6 +1248,51 @@ describe("RelayfileSetup", () => { } }) + it("passes explicit local layout and sync mode only to the local mount launcher", async () => { + const tempRoot = await mkdtemp( + path.join(os.tmpdir(), "relayfile-sdk-mount-workspace-contract-") + ) + const localDir = path.join(tempRoot, "mirror") + const fetchMock = queueFetch( + makeJoinResponse("rf_jwt_joined"), + makeMountSessionResponse({ + remotePath: "/slack/channels/C123/messages" + }) + ) + const { launcher, readyControl } = createLauncherStub() + + try { + const setup = new RelayfileSetup() + const workspace = await setup.joinWorkspace("ws_123") + readyControl.resolve() + await setup.mountWorkspace({ + workspace, + localDir, + remotePath: "/slack/channels/C123/messages", + mode: "poll", + localLayout: "scoped", + syncMode: "write-only", + launcher + }) + + const launcherStart = (launcher.start as ReturnType).mock.calls[0][0] + expect(launcherStart.env).toMatchObject({ + RELAYFILE_REMOTE_PATH: "/slack/channels/C123/messages", + RELAYFILE_LOCAL_DIR: localDir, + RELAYFILE_MOUNT_LOCAL_LAYOUT: "scoped", + RELAYFILE_MOUNT_SYNC_MODE: "write-only" + }) + expect(readRequestBody(fetchMock, 1)).toEqual({ + localDir, + remotePath: "/slack/channels/C123/messages", + mode: "poll", + agentName: "relayfile-mount" + }) + } finally { + await rm(tempRoot, { recursive: true, force: true }) + } + }) + it("mountWorkspace joins by workspaceId before mounting", async () => { const tempRoot = await mkdtemp( path.join(os.tmpdir(), "relayfile-sdk-mount-by-id-") diff --git a/packages/sdk/typescript/src/setup.ts b/packages/sdk/typescript/src/setup.ts index 2163db1b..ecfed1ac 100644 --- a/packages/sdk/typescript/src/setup.ts +++ b/packages/sdk/typescript/src/setup.ts @@ -36,10 +36,12 @@ import { type JoinWorkspaceOptions, type MountLauncher, type MountLauncherInstance, + type MountLocalLayout, type MountMode, type MountSessionRequest, type MountSessionResponse, type MountSessionResult, + type MountSyncMode, type MountedWorkspaceHandle, type MountedWorkspaceStatus, type MountWorkspaceInput, @@ -68,6 +70,8 @@ const DEFAULT_WAIT_INTERVAL_MS = 2_000 const DEFAULT_WAIT_TIMEOUT_MS = 300_000 const DEFAULT_MOUNT_READY_TIMEOUT_MS = 60_000 const DEFAULT_MOUNT_AGENT_NAME = "relayfile-mount" +const DEFAULT_MOUNT_LOCAL_LAYOUT: MountLocalLayout = "exact" +const DEFAULT_MOUNT_SYNC_MODE: MountSyncMode = "mirror" const TOKEN_REFRESH_AGE_MS = 55 * 60 * 1000 const nodeOnlyMountLauncher: MountLauncher = { @@ -137,6 +141,8 @@ interface NormalizedMountWorkspaceInput { localDir: string remotePath: string mode: MountMode + localLayout: MountLocalLayout + syncMode: MountSyncMode background: boolean agentName?: string scopes?: string[] @@ -341,6 +347,8 @@ export class RelayfileSetup { localDir: normalized.localDir, remotePath: normalized.remotePath, mode: normalized.mode, + localLayout: normalized.localLayout, + syncMode: normalized.syncMode, background: normalized.background, agentName: normalized.agentName, scopes: normalized.scopes, @@ -488,7 +496,7 @@ export class RelayfileSetup { }) try { - return validateMountSessionResponse( + const session = validateMountSessionResponse( await workspace.requestJson({ operation: "mountWorkspace", method: "POST", @@ -498,6 +506,11 @@ export class RelayfileSetup { }), input.localDir ) + return { + ...session, + localLayout: input.localLayout, + syncMode: input.syncMode + } } catch (error) { throw mapMountSessionError(error, request) } @@ -1128,6 +1141,8 @@ class MountedWorkspaceHandleImpl implements MountedWorkspaceHandle { workspaceId: this.workspaceId, remotePath: this.remotePath, mode: this.mode, + localLayout: this.mountSession.localLayout, + syncMode: this.mountSession.syncMode, relayfileBaseUrl: this.mountSession.relayfileBaseUrl, relayfileToken: this.mountSession.relayfileToken, expiresAt: this.expiresAt, @@ -1275,6 +1290,8 @@ function validateMountSessionResponse( remotePath: requireStringField(payload, "remotePath"), localDir, mode: requireMountModeField(payload, "mode"), + localLayout: DEFAULT_MOUNT_LOCAL_LAYOUT, + syncMode: DEFAULT_MOUNT_SYNC_MODE, scopes: requireStringArrayField(payload, "scopes"), tokenIssuedAt: readNullableStringField(payload, "tokenIssuedAt"), expiresAt: readNullableStringField(payload, "expiresAt"), @@ -1387,6 +1404,8 @@ function normalizeMountWorkspaceInput( localDir: resolveLocalDir(localDir), remotePath: normalizeMountRemotePath(input.remotePath), mode: normalizeMountModeInput(input.mode), + localLayout: normalizeMountLocalLayoutInput(input.localLayout), + syncMode: normalizeMountSyncModeInput(input.syncMode), background: input.background !== false, agentName: normalizeNonEmptyString(input.agentName), scopes: @@ -1423,6 +1442,26 @@ function normalizeMountModeInput(mode?: string): MountMode { return normalized } +function normalizeMountLocalLayoutInput(layout?: string): MountLocalLayout { + const normalized = normalizeNonEmptyString(layout) ?? DEFAULT_MOUNT_LOCAL_LAYOUT + if (normalized !== "exact" && normalized !== "scoped") { + throw new MountSessionInputError( + `Invalid localLayout "${normalized}" for mount session.` + ) + } + return normalized +} + +function normalizeMountSyncModeInput(mode?: string): MountSyncMode { + const normalized = normalizeNonEmptyString(mode) ?? DEFAULT_MOUNT_SYNC_MODE + if (normalized !== "mirror" && normalized !== "write-only") { + throw new MountSessionInputError( + `Invalid syncMode "${normalized}" for mount session.` + ) + } + return normalized +} + function normalizeMountRemotePath(remotePath?: string): string { const normalized = normalizeNonEmptyString(remotePath) ?? "/" if (normalized.includes("\u0000")) { @@ -1545,6 +1584,8 @@ function buildMountedWorkspaceEnv( RELAYFILE_REMOTE_PATH: mountSession.remotePath, RELAYFILE_LOCAL_DIR: mountSession.localDir, RELAYFILE_MOUNT_MODE: mountSession.mode, + RELAYFILE_MOUNT_LOCAL_LAYOUT: mountSession.localLayout, + RELAYFILE_MOUNT_SYNC_MODE: mountSession.syncMode, RELAYCAST_API_KEY: mountSession.relaycastApiKey, RELAY_API_KEY: mountSession.relaycastApiKey, RELAYCAST_BASE_URL: relaycastBaseUrl,