@relayfile/sdk 0.10.56 → 0.10.57

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.
@@ -14,6 +14,9 @@ const LOG_ROTATION_FILES = 3;
14
14
  const DEFAULT_CHECKPOINT_TIMEOUT_MS = 30_000;
15
15
  const MAX_CHECKPOINT_OUTPUT_BYTES = 1024 * 1024;
16
16
  const FUSE_UNAVAILABLE_SIGNATURE = "fuse mode is not available in this build";
17
+ // relayfile-mount uses EX_TEMPFAIL only for the typed, resumable --once
18
+ // bootstrap outcome. All other nonzero exits remain fatal.
19
+ const INITIAL_BOOTSTRAP_INCOMPLETE_EXIT_CODE = 75;
17
20
  export const defaultMountLauncher = createDefaultMountLauncher();
18
21
  export function createDefaultMountLauncher(options = {}) {
19
22
  return {
@@ -92,7 +95,6 @@ async function startRelayfileMount(input, options) {
92
95
  });
93
96
  }
94
97
  class RelayfileMountProcessInstance {
95
- pid;
96
98
  ready;
97
99
  child;
98
100
  logStream;
@@ -107,6 +109,7 @@ class RelayfileMountProcessInstance {
107
109
  cwd;
108
110
  spawnImpl;
109
111
  exited = false;
112
+ exitCode = null;
110
113
  stopping;
111
114
  readyResolved = false;
112
115
  checkpointPromise;
@@ -124,14 +127,14 @@ class RelayfileMountProcessInstance {
124
127
  this.effectiveEnv = input.effectiveEnv;
125
128
  this.cwd = input.cwd;
126
129
  this.spawnImpl = input.spawnImpl;
127
- this.pid = input.child.pid ?? undefined;
128
130
  this.now = input.now;
129
131
  this.readyPollIntervalMs = input.readyPollIntervalMs;
130
- this.child.once("exit", () => {
131
- this.exited = true;
132
- });
132
+ this.attachExitListener(input.child);
133
133
  this.ready = this.waitForReady();
134
134
  }
135
+ get pid() {
136
+ return this.child.pid ?? undefined;
137
+ }
135
138
  async status() {
136
139
  const status = await readMountedWorkspaceStatus({
137
140
  localDir: this.localDir,
@@ -146,7 +149,13 @@ class RelayfileMountProcessInstance {
146
149
  suggestedRefreshAt: null,
147
150
  pid: this.pid
148
151
  });
149
- return this.exited ? { ...status, ready: false } : status;
152
+ // A foreground --once child is expected to exit after publishing its
153
+ // final state. Preserve that ready state; daemon exits still make an
154
+ // otherwise-stale state unready.
155
+ return this.exited &&
156
+ (this.input.background !== false || this.exitCode !== 0)
157
+ ? { ...status, ready: false }
158
+ : status;
150
159
  }
151
160
  async stop() {
152
161
  if (!this.stopping) {
@@ -203,14 +212,43 @@ class RelayfileMountProcessInstance {
203
212
  throw new CloudAbortError("mountWorkspace");
204
213
  }
205
214
  const status = await this.status();
206
- if (status.ready) {
215
+ // Foreground --once is authoritative only after the child exits 0. A
216
+ // prior run may have left a fresh-looking public state file behind, so
217
+ // accepting it while this attempt is still running (or after exit 75)
218
+ // would skip the resumable checkpoint retry contract.
219
+ if (!this.stopping &&
220
+ status.ready &&
221
+ (this.input.background !== false || (this.exited && this.exitCode === 0))) {
207
222
  this.readyResolved = true;
208
223
  return;
209
224
  }
210
225
  if (this.isFuseUnavailable()) {
211
226
  throw new MountModeUnavailableError("fuse");
212
227
  }
228
+ // A typed foreground yield is retryable only inside the caller's
229
+ // readiness budget. Once that budget is exhausted, report the public
230
+ // timeout contract and run normal shutdown cleanup instead of falling
231
+ // through to the generic early-exit error.
232
+ if (!this.stopping &&
233
+ this.isResumableOnceExit() &&
234
+ this.now() >= timeoutAt) {
235
+ const error = new MountReadyTimeoutError(this.localDir, this.input.readyTimeoutMs);
236
+ await this.stop();
237
+ throw error;
238
+ }
213
239
  if (this.exited) {
240
+ if (this.isResumableOnceExit() &&
241
+ !this.stopping &&
242
+ this.now() < timeoutAt) {
243
+ await delay(this.readyPollIntervalMs);
244
+ if (this.stopping ||
245
+ this.input.signal?.aborted ||
246
+ this.now() >= timeoutAt) {
247
+ continue;
248
+ }
249
+ await this.restartOnceMount();
250
+ continue;
251
+ }
214
252
  throw this.buildEarlyExitError();
215
253
  }
216
254
  if (this.now() >= timeoutAt) {
@@ -231,6 +269,39 @@ class RelayfileMountProcessInstance {
231
269
  return (normalizeMountMode(this.input.env.RELAYFILE_MOUNT_MODE) === "fuse" &&
232
270
  this.outputBuffer.join("").includes(FUSE_UNAVAILABLE_SIGNATURE));
233
271
  }
272
+ isResumableOnceExit() {
273
+ return (this.input.background === false &&
274
+ this.exitCode === INITIAL_BOOTSTRAP_INCOMPLETE_EXIT_CODE);
275
+ }
276
+ async restartOnceMount() {
277
+ const child = this.spawnImpl(this.command, ["--once"], {
278
+ cwd: this.cwd,
279
+ env: this.effectiveEnv,
280
+ stdio: ["ignore", "pipe", "pipe"]
281
+ });
282
+ pipeChildOutput(child, this.logStream, this.outputBuffer, this.input);
283
+ this.child = child;
284
+ this.exited = child.exitCode !== null;
285
+ this.exitCode = child.exitCode;
286
+ this.attachExitListener(child);
287
+ if (typeof child.pid === "number" && child.pid > 0) {
288
+ await writeAtomicFile(this.pidPath, `${child.pid}\n`);
289
+ }
290
+ }
291
+ attachExitListener(child) {
292
+ if (child.exitCode !== null) {
293
+ this.exited = true;
294
+ this.exitCode = child.exitCode;
295
+ }
296
+ child.once("exit", (code) => {
297
+ // A late event from a previous resumable attempt must not mark its
298
+ // replacement as exited.
299
+ if (this.child !== child)
300
+ return;
301
+ this.exited = true;
302
+ this.exitCode = code;
303
+ });
304
+ }
234
305
  async performStop() {
235
306
  if (!this.exited && typeof this.child.pid === "number") {
236
307
  this.child.kill("SIGTERM");
package/dist/sync.js CHANGED
@@ -743,7 +743,7 @@ export class RelayFileSync {
743
743
  this.emitFilesystemEvent(normalized);
744
744
  }
745
745
  emitFilesystemEvent(event) {
746
- if (!pathMatchesAnyFilter(this.paths, event.path)) {
746
+ if (event.type !== "sync.reconcile" && !pathMatchesAnyFilter(this.paths, event.path)) {
747
747
  return;
748
748
  }
749
749
  this.emit("event", event);
package/dist/types.d.ts CHANGED
@@ -284,7 +284,7 @@ export interface WriteQueuedResponse {
284
284
  state?: WritebackState;
285
285
  };
286
286
  }
287
- export type FilesystemEventType = "file.created" | "file.updated" | "file.deleted" | "dir.created" | "dir.deleted" | "sync.error" | "sync.ignored" | "sync.suppressed" | "sync.stale" | "writeback.failed" | "writeback.succeeded";
287
+ export type FilesystemEventType = "file.created" | "file.updated" | "file.deleted" | "dir.created" | "dir.deleted" | "sync.error" | "sync.ignored" | "sync.suppressed" | "sync.stale" | "sync.reconcile" | "writeback.failed" | "writeback.succeeded";
288
288
  export type EventOrigin = "provider_sync" | "agent_write" | "system";
289
289
  export interface FilesystemEvent {
290
290
  eventId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayfile/sdk",
3
- "version": "0.10.56",
3
+ "version": "0.10.57",
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.56",
62
+ "@relayfile/core": "0.10.57",
63
63
  "ignore": "^7.0.5",
64
64
  "tar": "^7.5.10"
65
65
  },
66
66
  "optionalDependencies": {
67
- "@relayfile/mount-darwin-arm64": "0.10.56",
68
- "@relayfile/mount-darwin-x64": "0.10.56",
69
- "@relayfile/mount-linux-arm64": "0.10.56",
70
- "@relayfile/mount-linux-x64": "0.10.56"
67
+ "@relayfile/mount-darwin-arm64": "0.10.57",
68
+ "@relayfile/mount-darwin-x64": "0.10.57",
69
+ "@relayfile/mount-linux-arm64": "0.10.57",
70
+ "@relayfile/mount-linux-x64": "0.10.57"
71
71
  },
72
72
  "devDependencies": {
73
73
  "typescript": "^5.7.3",