@relayfile/sdk 0.10.31 → 0.10.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export type { RelayFileReadCacheOptions } from "./types.js";
3
3
  export { RelayfileSetup, RELAYFILE_SDK_VERSION, WorkspaceHandle } from "./setup.js";
4
4
  export { type RelayfileCloudLoginOptions, type RelayfileCloudTokenSet, type RelayfileCloudTokenSetupOptions } from "./cloud-login.js";
5
5
  export { CloudAbortError, CloudApiError, CloudTimeoutError, InvalidLocalDirError, InvalidMountModeError, InvalidRemotePathError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, MountModeUnavailableError, MountReadyTimeoutError, MountSessionInputError, ProviderNotConnectedError, ProviderNotReadyError, RelayfileSetupError, UnknownProviderError } from "./setup-errors.js";
6
- export { type EnsureMountedWorkspaceInput, WORKSPACE_INTEGRATION_PROVIDERS, type AgentWorkspaceInvite, type AgentWorkspaceInviteOptions, type AgentWorkspaceScopedInviteOptions, type ConnectIntegrationOptions, type ConnectIntegrationResult, type CreateWorkspaceOptions, type JoinWorkspaceOptions, type MountLauncher, type MountLauncherEvent, type MountLauncherInstance, type MountLauncherStart, type MountLocalLayout, type MountMode, type MountSessionRequest, type MountSessionResponse, type MountSessionResult, type MountSyncMode, type MountedWorkspaceHandle, type MountedWorkspaceStatus, type MountWorkspaceInput, type ReadMountedWorkspaceStatusInput, type RelayfileSetupOptions, type RelayfileSetupRetryOptions, type WaitForConnectionOptions, type WorkspaceInfo, type WorkspaceIntegrationProvider, type WorkspaceMountEnv, type WorkspaceMountEnvOptions, type WorkspacePermissions } from "./setup-types.js";
6
+ export { type EnsureMountedWorkspaceInput, WORKSPACE_INTEGRATION_PROVIDERS, type AgentWorkspaceInvite, type AgentWorkspaceInviteOptions, type AgentWorkspaceScopedInviteOptions, type ConnectIntegrationOptions, type ConnectIntegrationResult, type CreateWorkspaceOptions, type JoinWorkspaceOptions, type MountLauncher, type MountLauncherEvent, type MountLauncherInstance, type MountLauncherStart, type MountLocalLayout, type MountMode, type MountSessionRequest, type MountSessionResponse, type MountSessionResult, type MountSyncMode, type MountSupervisorEvent, type MountedWorkspaceHandle, type MountedWorkspaceStatus, type MountWorkspaceInput, type ReadMountedWorkspaceStatusInput, type RelayfileSetupOptions, type RelayfileSetupRetryOptions, type WaitForConnectionOptions, type WorkspaceInfo, type WorkspaceIntegrationProvider, type WorkspaceMountEnv, type WorkspaceMountEnvOptions, type WorkspacePermissions } from "./setup-types.js";
7
7
  export { RelayFileSync, type RelayFileSyncOptions, type RelayFileSyncPong, type RelayFileSyncReconnectOptions, type RelayFileSyncSocket, type RelayFileSyncStart, type RelayFileSyncState, type RelayFileSyncTokenProvider } from "./sync.js";
8
8
  export { onWrite, pathMatches, type OnWriteClient, type OnWriteHandler, type OnWriteHandlerError, type OnWriteOptions } from "./onWrite.js";
9
9
  export { InvalidStateError, PayloadTooLargeError, QueueFullError, RelayFileApiError, RevisionConflictError } from "./errors.js";
@@ -109,7 +109,7 @@ class RelayfileMountProcessInstance {
109
109
  this.ready = this.waitForReady();
110
110
  }
111
111
  async status() {
112
- return readMountedWorkspaceStatus({
112
+ const status = await readMountedWorkspaceStatus({
113
113
  localDir: this.localDir,
114
114
  workspaceId: this.input.env.RELAYFILE_WORKSPACE ?? "",
115
115
  remotePath: this.input.env.RELAYFILE_REMOTE_PATH ?? "/",
@@ -122,6 +122,7 @@ class RelayfileMountProcessInstance {
122
122
  suggestedRefreshAt: null,
123
123
  pid: this.pid
124
124
  });
125
+ return this.exited ? { ...status, ready: false } : status;
125
126
  }
126
127
  async stop() {
127
128
  if (!this.stopping) {
@@ -146,6 +146,14 @@ export interface MountedWorkspaceHandle {
146
146
  status(): Promise<MountedWorkspaceStatus>;
147
147
  stop(): Promise<void>;
148
148
  }
149
+ export interface MountSupervisorEvent {
150
+ type: "mount.refreshed" | "mount.refresh_failed";
151
+ workspaceId: string;
152
+ localDir: string;
153
+ attempt: number;
154
+ retryInMs?: number;
155
+ errorCode?: string;
156
+ }
149
157
  export interface MountLauncherEvent {
150
158
  type: string;
151
159
  [key: string]: unknown;
@@ -186,6 +194,15 @@ export interface EnsureMountedWorkspaceInput extends MountWorkspaceInput {
186
194
  provider?: WorkspaceIntegrationProvider;
187
195
  verifyProvider?: boolean;
188
196
  providerReadyTimeoutMs?: number;
197
+ /** Keep a background mount healthy and renew its short-lived session. Defaults to true. */
198
+ supervise?: boolean;
199
+ /** Health-check cadence for supervised background mounts. */
200
+ healthCheckIntervalMs?: number;
201
+ /** Initial retry delay after a supervised refresh failure. */
202
+ refreshRetryBaseMs?: number;
203
+ /** Maximum retry delay after repeated supervised refresh failures. */
204
+ refreshRetryMaxMs?: number;
205
+ onSupervisorEvent?: (event: MountSupervisorEvent) => void | Promise<void>;
189
206
  }
190
207
  export interface AgentWorkspaceInviteOptions {
191
208
  agentName?: string;
package/dist/setup.js CHANGED
@@ -18,6 +18,9 @@ const DEFAULT_MOUNT_READY_TIMEOUT_MS = 60_000;
18
18
  const DEFAULT_MOUNT_AGENT_NAME = "relayfile-mount";
19
19
  const DEFAULT_MOUNT_LOCAL_LAYOUT = "exact";
20
20
  const DEFAULT_MOUNT_SYNC_MODE = "mirror";
21
+ const DEFAULT_MOUNT_HEALTH_INTERVAL_MS = 30_000;
22
+ const DEFAULT_MOUNT_REFRESH_RETRY_BASE_MS = 1_000;
23
+ const DEFAULT_MOUNT_REFRESH_RETRY_MAX_MS = 60_000;
21
24
  const TOKEN_REFRESH_AGE_MS = 55 * 60 * 1000;
22
25
  const nodeOnlyMountLauncher = {
23
26
  async start() {
@@ -139,7 +142,7 @@ export class RelayfileSetup {
139
142
  }
140
143
  await verifyWorkspaceProviderReady(workspace, normalized.provider, normalized.providerReadyTimeoutMs, normalized.signal);
141
144
  }
142
- return this.mountWorkspace({
145
+ const mountInput = {
143
146
  workspace,
144
147
  localDir: normalized.localDir,
145
148
  remotePath: normalized.remotePath,
@@ -152,6 +155,19 @@ export class RelayfileSetup {
152
155
  signal: normalized.signal,
153
156
  launcher: normalized.launcher,
154
157
  readyTimeoutMs: normalized.readyTimeoutMs
158
+ };
159
+ const mounted = await this.mountWorkspace(mountInput);
160
+ if (!normalized.supervise || !normalized.background) {
161
+ return mounted;
162
+ }
163
+ return new SupervisedMountedWorkspaceHandle({
164
+ mounted,
165
+ launch: () => this.mountWorkspace(mountInput),
166
+ healthCheckIntervalMs: normalized.healthCheckIntervalMs,
167
+ refreshRetryBaseMs: normalized.refreshRetryBaseMs,
168
+ refreshRetryMaxMs: normalized.refreshRetryMaxMs,
169
+ onEvent: normalized.onSupervisorEvent,
170
+ signal: normalized.signal
155
171
  });
156
172
  }
157
173
  async joinWorkspaceResponse(workspaceId, options, overrides = {}) {
@@ -758,12 +774,176 @@ class MountedWorkspaceHandleImpl {
758
774
  await this.stopPromise;
759
775
  }
760
776
  async performStop() {
777
+ this.readySnapshot = false;
761
778
  if (this.probeOnly || !this.launcherInstance) {
762
779
  return;
763
780
  }
764
781
  await safeStopLauncher(this.launcherInstance);
765
782
  }
766
783
  }
784
+ class SupervisedMountedWorkspaceHandle {
785
+ mounted;
786
+ launch;
787
+ healthCheckIntervalMs;
788
+ refreshRetryBaseMs;
789
+ refreshRetryMaxMs;
790
+ onEvent;
791
+ signal;
792
+ timer;
793
+ refreshPromise;
794
+ stopPromise;
795
+ stopped = false;
796
+ supervisorReady = true;
797
+ refreshAttempts = 0;
798
+ constructor(input) {
799
+ this.mounted = input.mounted;
800
+ this.launch = input.launch;
801
+ this.healthCheckIntervalMs = input.healthCheckIntervalMs;
802
+ this.refreshRetryBaseMs = input.refreshRetryBaseMs;
803
+ this.refreshRetryMaxMs = input.refreshRetryMaxMs;
804
+ this.onEvent = input.onEvent;
805
+ this.signal = input.signal;
806
+ if (this.signal?.aborted) {
807
+ this.handleAbort();
808
+ }
809
+ else {
810
+ this.signal?.addEventListener("abort", this.handleAbort, { once: true });
811
+ this.schedule();
812
+ }
813
+ }
814
+ get workspaceId() {
815
+ return this.mounted.workspaceId;
816
+ }
817
+ get localDir() {
818
+ return this.mounted.localDir;
819
+ }
820
+ get remotePath() {
821
+ return this.mounted.remotePath;
822
+ }
823
+ get mode() {
824
+ return this.mounted.mode;
825
+ }
826
+ get ready() {
827
+ return !this.stopped && this.supervisorReady && this.mounted.ready;
828
+ }
829
+ get expiresAt() {
830
+ return this.mounted.expiresAt;
831
+ }
832
+ get suggestedRefreshAt() {
833
+ return this.mounted.suggestedRefreshAt;
834
+ }
835
+ env() {
836
+ return this.mounted.env();
837
+ }
838
+ async status() {
839
+ const status = await this.mounted.status();
840
+ return {
841
+ ...status,
842
+ ready: !this.stopped && this.supervisorReady && status.ready,
843
+ expiresAt: this.expiresAt,
844
+ suggestedRefreshAt: this.suggestedRefreshAt
845
+ };
846
+ }
847
+ async stop() {
848
+ if (!this.stopPromise)
849
+ this.stopPromise = this.performStop();
850
+ await this.stopPromise;
851
+ }
852
+ async performStop() {
853
+ this.stopped = true;
854
+ this.supervisorReady = false;
855
+ this.signal?.removeEventListener("abort", this.handleAbort);
856
+ if (this.timer)
857
+ clearTimeout(this.timer);
858
+ this.timer = undefined;
859
+ await this.refreshPromise?.catch(() => { });
860
+ await this.mounted.stop();
861
+ }
862
+ handleAbort = () => {
863
+ void this.stop();
864
+ };
865
+ schedule(delayOverrideMs) {
866
+ if (this.stopped || this.timer)
867
+ return;
868
+ const suggestedRefreshAtMs = Date.parse(this.mounted.suggestedRefreshAt ?? "");
869
+ const untilRefresh = Number.isFinite(suggestedRefreshAtMs)
870
+ ? Math.max(0, suggestedRefreshAtMs - Date.now())
871
+ : this.healthCheckIntervalMs;
872
+ const delayMs = delayOverrideMs ?? Math.min(this.healthCheckIntervalMs, untilRefresh);
873
+ this.timer = setTimeout(() => {
874
+ this.timer = undefined;
875
+ this.refreshPromise = this.checkAndRefresh().finally(() => {
876
+ this.refreshPromise = undefined;
877
+ });
878
+ }, Math.max(1_000, delayMs));
879
+ this.timer.unref?.();
880
+ }
881
+ async checkAndRefresh() {
882
+ if (this.stopped)
883
+ return;
884
+ try {
885
+ const suggestedRefreshAtMs = Date.parse(this.mounted.suggestedRefreshAt ?? "");
886
+ const refreshDue = Number.isFinite(suggestedRefreshAtMs) && Date.now() >= suggestedRefreshAtMs;
887
+ if (!refreshDue) {
888
+ const status = await this.mounted.status();
889
+ if (status.ready) {
890
+ this.supervisorReady = true;
891
+ this.refreshAttempts = 0;
892
+ this.schedule();
893
+ return;
894
+ }
895
+ }
896
+ await this.replaceMount();
897
+ if (this.stopped)
898
+ return;
899
+ this.supervisorReady = true;
900
+ this.refreshAttempts = 0;
901
+ this.emit({
902
+ type: "mount.refreshed",
903
+ workspaceId: this.workspaceId,
904
+ localDir: this.localDir,
905
+ attempt: 0
906
+ });
907
+ this.schedule();
908
+ }
909
+ catch (error) {
910
+ if (this.stopped)
911
+ return;
912
+ this.supervisorReady = false;
913
+ this.refreshAttempts += 1;
914
+ const retryInMs = Math.min(this.refreshRetryMaxMs, this.refreshRetryBaseMs * 2 ** (this.refreshAttempts - 1));
915
+ this.emit({
916
+ type: "mount.refresh_failed",
917
+ workspaceId: this.workspaceId,
918
+ localDir: this.localDir,
919
+ attempt: this.refreshAttempts,
920
+ retryInMs,
921
+ errorCode: setupErrorCode(error)
922
+ });
923
+ this.schedule(retryInMs);
924
+ }
925
+ }
926
+ async replaceMount() {
927
+ const previous = this.mounted;
928
+ if (this.stopped)
929
+ return;
930
+ await previous.stop().catch(() => { });
931
+ if (this.stopped)
932
+ return;
933
+ const replacement = await this.launch();
934
+ if (this.stopped) {
935
+ await replacement.stop().catch(() => { });
936
+ return;
937
+ }
938
+ this.mounted = replacement;
939
+ }
940
+ emit(event) {
941
+ void Promise.resolve(this.onEvent?.(event)).catch(() => { });
942
+ }
943
+ }
944
+ function setupErrorCode(error) {
945
+ return error instanceof RelayfileSetupError ? error.code : "mount_refresh_failed";
946
+ }
767
947
  // The metadata + accessible-resources verbs accept `WorkspaceIntegrationProvider | string`
768
948
  // so operators can target dynamically-discovered providers (Composio toolkit
769
949
  // slugs, future Atlassian-family additions) without an SDK release. We still
@@ -959,9 +1139,19 @@ function normalizeEnsureMountedWorkspaceInput(input) {
959
1139
  ...normalized,
960
1140
  provider: input.provider,
961
1141
  verifyProvider: input.verifyProvider !== false,
962
- providerReadyTimeoutMs: Math.max(0, Math.floor(input.providerReadyTimeoutMs ?? 0))
1142
+ providerReadyTimeoutMs: Math.max(0, Math.floor(input.providerReadyTimeoutMs ?? 0)),
1143
+ supervise: input.supervise !== false && normalized.background,
1144
+ healthCheckIntervalMs: positiveInterval(input.healthCheckIntervalMs, DEFAULT_MOUNT_HEALTH_INTERVAL_MS),
1145
+ refreshRetryBaseMs: positiveInterval(input.refreshRetryBaseMs, DEFAULT_MOUNT_REFRESH_RETRY_BASE_MS),
1146
+ refreshRetryMaxMs: positiveInterval(input.refreshRetryMaxMs, DEFAULT_MOUNT_REFRESH_RETRY_MAX_MS),
1147
+ onSupervisorEvent: input.onSupervisorEvent
963
1148
  };
964
1149
  }
1150
+ function positiveInterval(value, fallback) {
1151
+ if (value === undefined || !Number.isFinite(value) || value <= 0)
1152
+ return fallback;
1153
+ return Math.max(1_000, Math.floor(value));
1154
+ }
965
1155
  function normalizeMountModeInput(mode) {
966
1156
  const normalized = normalizeNonEmptyString(mode) ?? "poll";
967
1157
  if (normalized !== "poll" && normalized !== "fuse") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayfile/sdk",
3
- "version": "0.10.31",
3
+ "version": "0.10.32",
4
4
  "description": "TypeScript SDK for relayfile — real-time filesystem for humans and agents",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -59,15 +59,15 @@
59
59
  "prepublishOnly": "npm run build"
60
60
  },
61
61
  "dependencies": {
62
- "@relayfile/core": "0.10.31",
62
+ "@relayfile/core": "0.10.32",
63
63
  "ignore": "^7.0.5",
64
64
  "tar": "^7.5.10"
65
65
  },
66
66
  "optionalDependencies": {
67
- "@relayfile/mount-darwin-arm64": "0.10.31",
68
- "@relayfile/mount-darwin-x64": "0.10.31",
69
- "@relayfile/mount-linux-arm64": "0.10.31",
70
- "@relayfile/mount-linux-x64": "0.10.31"
67
+ "@relayfile/mount-darwin-arm64": "0.10.32",
68
+ "@relayfile/mount-darwin-x64": "0.10.32",
69
+ "@relayfile/mount-linux-arm64": "0.10.32",
70
+ "@relayfile/mount-linux-x64": "0.10.32"
71
71
  },
72
72
  "devDependencies": {
73
73
  "typescript": "^5.7.3",