@sendmux/cli 1.6.0 → 1.7.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"agent-auth.d.ts","sourceRoot":"","sources":["../src/agent-auth.ts"],"names":[],"mappings":"AAGA,OAAO,EAKL,KAAK,qBAAqB,EAE1B,KAAK,SAAS,EAGf,MAAM,eAAe,CAAC;AAUvB,UAAU,kBAAkB;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB;AAWD,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB,CAAC,EAAE,SAAS,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,QAAQ,CAAC;CAClB;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAyGnG;AAED,wBAAsB,gBAAgB,CAAC,EACrC,SAAS,EACT,KAAK,EACL,WAAW,GACZ,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC,CAiChD;AAED,wBAAsB,wBAAwB,CAAC,EAC7C,MAAM,EACN,SAAS,EACT,OAAO,EACP,WAAW,GACZ,EAAE;IACD,MAAM,EAAE,SAAS,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,qBAAqB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2ClB;AA6DD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYxD;AAaD,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,EAC9B,SAAS,SAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
1
+ {"version":3,"file":"agent-auth.d.ts","sourceRoot":"","sources":["../src/agent-auth.ts"],"names":[],"mappings":"AAGA,OAAO,EAKL,KAAK,qBAAqB,EAE1B,KAAK,SAAS,EAGf,MAAM,eAAe,CAAC;AAYvB,UAAU,kBAAkB;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB;AAWD,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB,CAAC,EAAE,SAAS,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,QAAQ,CAAC;CAClB;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAyGnG;AAED,wBAAsB,gBAAgB,CAAC,EACrC,SAAS,EACT,KAAK,EACL,WAAW,GACZ,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC,CAiChD;AAED,wBAAsB,wBAAwB,CAAC,EAC7C,MAAM,EACN,SAAS,EACT,OAAO,EACP,WAAW,GACZ,EAAE;IACD,MAAM,EAAE,SAAS,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,qBAAqB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6ClB;AA6DD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYxD;AAaD,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,EAC9B,SAAS,SAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAsCf"}
@@ -7,6 +7,8 @@ const SENDING_API_RESOURCE = "https://smtp.sendmux.ai/api/v1";
7
7
  const TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
8
8
  const ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
9
9
  const READINESS_TIMEOUT_MS = 10 * 60 * 1_000;
10
+ const AUTH_REQUEST_TIMEOUT_MS = 15_000;
11
+ const MAX_AUTH_RESPONSE_BYTES = 256 * 1024;
10
12
  const SENDING_TOKEN_SKEW_MS = 60 * 1_000;
11
13
  export async function registerAgent(input) {
12
14
  const initialConfig = await readCliConfig(input.configDir);
@@ -155,6 +157,8 @@ export async function resolveAgentSendingToken({ config, configDir, profile, pro
155
157
  }),
156
158
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
157
159
  method: "POST",
160
+ redirect: "error",
161
+ signal: AbortSignal.timeout(AUTH_REQUEST_TIMEOUT_MS),
158
162
  });
159
163
  const body = await responseJson(response);
160
164
  if (!response.ok) {
@@ -257,22 +261,30 @@ export async function waitForMailbox(profile, timeoutMs = READINESS_TIMEOUT_MS)
257
261
  const remainingBeforeFetch = deadline - Date.now();
258
262
  if (remainingBeforeFetch <= 0)
259
263
  throw mailboxReadinessTimeoutError();
264
+ const signal = AbortSignal.timeout(remainingBeforeFetch);
260
265
  let response;
266
+ let body;
261
267
  try {
262
268
  response = await fetch(`${profile.appApiBaseUrl}/mailbox/me`, {
263
269
  headers: { Authorization: `Bearer ${profile.accessToken}` },
264
- signal: AbortSignal.timeout(remainingBeforeFetch),
270
+ redirect: "error",
271
+ signal,
265
272
  });
273
+ if (response.ok) {
274
+ await response.body?.cancel();
275
+ return;
276
+ }
277
+ body = await responseJson(response);
266
278
  }
267
279
  catch (error) {
268
- if (isAbortError(error) || Date.now() >= deadline)
280
+ if (signal.aborted || isAbortError(error) || Date.now() >= deadline)
269
281
  throw mailboxReadinessTimeoutError();
270
282
  throw error;
271
283
  }
272
- if (response.ok)
273
- return;
274
- const body = await responseJson(response);
275
- const provisioningUnavailable = response.status === 503 && (body.error === "service_unavailable" || body.error === "temporarily_unavailable");
284
+ const errorCode = typeof body.error === "object" && body.error !== null && !Array.isArray(body.error) && "code" in body.error
285
+ ? body.error.code
286
+ : body.error;
287
+ const provisioningUnavailable = response.status === 503 && (errorCode === "service_unavailable" || errorCode === "temporarily_unavailable");
276
288
  if (!provisioningUnavailable) {
277
289
  throw new Error(agentAuthFailureMessage(response.status, body));
278
290
  }
@@ -304,6 +316,8 @@ async function postJson(url, options) {
304
316
  body: JSON.stringify(options.body),
305
317
  headers: { "Content-Type": "application/json", ...options.headers },
306
318
  method: "POST",
319
+ redirect: "error",
320
+ signal: AbortSignal.timeout(AUTH_REQUEST_TIMEOUT_MS),
307
321
  });
308
322
  const body = await responseJson(response);
309
323
  if (!options.expectedStatuses.includes(response.status)) {
@@ -312,7 +326,37 @@ async function postJson(url, options) {
312
326
  return body;
313
327
  }
314
328
  async function responseJson(response) {
315
- const body = await response.json().catch(() => null);
329
+ let bytes = 0;
330
+ let body;
331
+ try {
332
+ const reader = response.body?.getReader();
333
+ const chunks = [];
334
+ if (reader) {
335
+ try {
336
+ while (true) {
337
+ const chunk = await reader.read();
338
+ if (chunk.done)
339
+ break;
340
+ bytes += chunk.value.byteLength;
341
+ if (bytes > MAX_AUTH_RESPONSE_BYTES)
342
+ break;
343
+ chunks.push(chunk.value);
344
+ }
345
+ }
346
+ finally {
347
+ await reader.cancel();
348
+ }
349
+ }
350
+ if (bytes <= MAX_AUTH_RESPONSE_BYTES) {
351
+ body = JSON.parse(new TextDecoder().decode(Buffer.concat(chunks)));
352
+ }
353
+ }
354
+ catch {
355
+ body = null;
356
+ }
357
+ if (bytes > MAX_AUTH_RESPONSE_BYTES) {
358
+ throw new Error("Sendmux agent authentication response exceeds the size limit.");
359
+ }
316
360
  if (!body || typeof body !== "object" || Array.isArray(body)) {
317
361
  throw new Error(`Sendmux agent authentication returned HTTP ${response.status} without a JSON object.`);
318
362
  }
@@ -1 +1 @@
1
- {"version":3,"file":"operation-runner.d.ts","sourceRoot":"","sources":["../src/operation-runner.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAmBhE,wBAAsB,eAAe,CACnC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,OAAO,CAAC,CAwElB"}
1
+ {"version":3,"file":"operation-runner.d.ts","sourceRoot":"","sources":["../src/operation-runner.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAuBhE,wBAAsB,eAAe,CACnC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,OAAO,CAAC,CAwElB"}
@@ -1,7 +1,12 @@
1
1
  import * as sdk from "@sendmux/sdk";
2
+ import { createHash } from "node:crypto";
3
+ import { closeSync, createReadStream, fsyncSync, openSync, writeSync } from "node:fs";
2
4
  import { readFile, stat } from "node:fs/promises";
3
5
  import { basename, extname } from "node:path";
4
6
  import { parseOperationOptions, } from "./operation-flags.js";
7
+ const MAX_SENDING_ATTACHMENTS = 10;
8
+ // Absolute Sending service ceiling; deployment policy may impose a lower limit.
9
+ const MAX_SENDING_ATTACHMENT_BYTES = 18 * 1024 * 1024;
5
10
  const surfaceModules = {
6
11
  mailbox: sdk.mailbox,
7
12
  management: sdk.management,
@@ -160,13 +165,18 @@ async function uploadMailboxAttachmentFromFile(command, client, operationOptions
160
165
  async function withAttachedFiles(command, operation, client, operationOptions, flags) {
161
166
  const body = jsonObjectBody(command, operationOptions.body);
162
167
  const existingAttachments = attachmentArray(command, body.attachments);
168
+ if (operation.operationId === "sendingSendEmail"
169
+ && existingAttachments.length + (flags.attach?.length ?? 0) > MAX_SENDING_ATTACHMENTS) {
170
+ command.error(`Sending email supports at most ${MAX_SENDING_ATTACHMENTS} attachments, including existing attachments and files.`, { exit: 2 });
171
+ }
163
172
  const files = [];
173
+ const maxBytes = operation.operationId === "sendingSendEmail" ? MAX_SENDING_ATTACHMENT_BYTES : undefined;
164
174
  for (const path of flags.attach ?? []) {
165
- files.push(await readAttachmentFile(command, path, flags["content-type"]));
175
+ files.push(await readAttachmentFile(command, path, flags["content-type"], maxBytes));
166
176
  }
167
177
  if (operation.operationId === "mailboxSendMessage") {
168
178
  const uploaded = [];
169
- for (const file of files) {
179
+ for (const [ordinal, file] of files.entries()) {
170
180
  const uploadResponse = await sdk.mailbox.mailboxUploadAttachment({
171
181
  client: client,
172
182
  body: blobFor(file),
@@ -179,8 +189,10 @@ async function withAttachedFiles(command, operation, client, operationOptions, f
179
189
  },
180
190
  });
181
191
  const result = envelopeData(uploadResponse, "mailboxUploadAttachment");
192
+ const blobId = stringField(result, "blob_id", "mailboxUploadAttachment");
193
+ journalNestedAttachment({ file, id: blobId, nestedOperationId: "mailboxUploadAttachment", operationId: operation.operationId, ordinal });
182
194
  uploaded.push({
183
- blob_id: stringField(result, "blob_id", "mailboxUploadAttachment"),
195
+ blob_id: blobId,
184
196
  content_type: stringField(result, "content_type", "mailboxUploadAttachment"),
185
197
  filename: stringField(result, "filename", "mailboxUploadAttachment"),
186
198
  });
@@ -195,11 +207,19 @@ async function withAttachedFiles(command, operation, client, operationOptions, f
195
207
  }
196
208
  if (operation.operationId === "sendingSendEmail") {
197
209
  const uploaded = [];
198
- for (const file of files) {
210
+ const outerKey = operationOptions.headers?.["Idempotency-Key"];
211
+ const idempotencyKey = typeof outerKey === "string" ? outerKey.trim() : undefined;
212
+ for (const [index, file] of files.entries()) {
199
213
  const uploadResponse = await sdk.sending.sendingUploadAttachment({
200
214
  client: client,
201
215
  body: blobFor(file),
202
216
  headers: {
217
+ ...(idempotencyKey ? {
218
+ // Match the TS/Python helpers; content must not change the key and evade conflict detection.
219
+ "Idempotency-Key": createHash("sha256")
220
+ .update(`sendmux:sending:attachment:${index}:${idempotencyKey}`)
221
+ .digest("hex"),
222
+ } : {}),
203
223
  "Content-Length": file.sizeBytes,
204
224
  "Content-Type": file.contentType,
205
225
  },
@@ -209,8 +229,10 @@ async function withAttachedFiles(command, operation, client, operationOptions, f
209
229
  },
210
230
  });
211
231
  const result = envelopeData(uploadResponse, "sendingUploadAttachment");
232
+ const attachmentId = stringField(result, "attachment_id", "sendingUploadAttachment");
233
+ journalNestedAttachment({ file, id: attachmentId, nestedOperationId: "sendingUploadAttachment", operationId: operation.operationId, ordinal: index });
212
234
  uploaded.push({
213
- attachment_id: stringField(result, "attachment_id", "sendingUploadAttachment"),
235
+ attachment_id: attachmentId,
214
236
  });
215
237
  }
216
238
  return {
@@ -237,7 +259,30 @@ async function withAttachedFiles(command, operation, client, operationOptions, f
237
259
  },
238
260
  };
239
261
  }
240
- async function readAttachmentFile(command, filePath, contentTypeOverride) {
262
+ function journalNestedAttachment({ file, id, nestedOperationId, operationId, ordinal }) {
263
+ const path = process.env.SENDMUX_LIVE_E2E_ATTACHMENT_JOURNAL;
264
+ if (!path)
265
+ return;
266
+ const record = {
267
+ adapter: "cli",
268
+ filename: file.filename,
269
+ id,
270
+ operationId,
271
+ nestedOperationId,
272
+ ordinal,
273
+ sha256: createHash("sha256").update(file.bytes).digest("hex"),
274
+ size_bytes: file.sizeBytes,
275
+ };
276
+ const descriptor = openSync(path, "a", 0o600);
277
+ try {
278
+ writeSync(descriptor, `${JSON.stringify(record)}\n`, undefined, "utf8");
279
+ fsyncSync(descriptor);
280
+ }
281
+ finally {
282
+ closeSync(descriptor);
283
+ }
284
+ }
285
+ async function readAttachmentFile(command, filePath, contentTypeOverride, maxBytes) {
241
286
  const info = await stat(filePath).catch((error) => {
242
287
  const message = error instanceof Error ? error.message : String(error);
243
288
  command.error(`Could not read attachment file ${filePath}: ${message}`, { exit: 2 });
@@ -248,7 +293,26 @@ async function readAttachmentFile(command, filePath, contentTypeOverride) {
248
293
  if (info.size === 0) {
249
294
  command.error(`Attachment file is empty: ${filePath}`, { exit: 2 });
250
295
  }
251
- const bytes = await readFile(filePath);
296
+ if (maxBytes !== undefined && info.size > maxBytes) {
297
+ command.error(`Attachment file exceeds ${maxBytes} bytes: ${filePath}`, { exit: 2 });
298
+ }
299
+ let bytes;
300
+ if (maxBytes === undefined) {
301
+ bytes = await readFile(filePath);
302
+ }
303
+ else {
304
+ const chunks = [];
305
+ // Include one excess byte so a growing file is rejected instead of truncated and uploaded.
306
+ for await (const chunk of createReadStream(filePath, { end: maxBytes }))
307
+ chunks.push(chunk);
308
+ bytes = Buffer.concat(chunks);
309
+ if (bytes.length > maxBytes) {
310
+ command.error(`Attachment file exceeds ${maxBytes} bytes: ${filePath}`, { exit: 2 });
311
+ }
312
+ }
313
+ if (bytes.length === 0) {
314
+ command.error(`Attachment file is empty: ${filePath}`, { exit: 2 });
315
+ }
252
316
  return {
253
317
  bytes,
254
318
  contentType: contentTypeOverride ?? inferContentType(filePath),
@@ -1 +1 @@
1
- {"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"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"}
package/dist/profiles.js CHANGED
@@ -5,6 +5,8 @@ const CONFIG_FILE = "config.json";
5
5
  const CONFIG_LOCK_RETRY_MS = 25;
6
6
  const CONFIG_LOCK_STALE_MS = 5_000;
7
7
  const CONFIG_LOCK_TIMEOUT_MS = 10_000;
8
+ const CONFIG_REPLACE_TIMEOUT_MS = CONFIG_LOCK_STALE_MS / 2;
9
+ const CONFIG_REPLACE_MAX_WAITS = CONFIG_REPLACE_TIMEOUT_MS / CONFIG_LOCK_RETRY_MS;
8
10
  export async function readCliConfig(configDir) {
9
11
  const path = configPath(configDir);
10
12
  try {
@@ -25,10 +27,11 @@ export async function readCliConfig(configDir) {
25
27
  export async function updateCliConfig(configDir, update) {
26
28
  await mkdir(configDir, { mode: 0o700, recursive: true });
27
29
  const releaseLock = await acquireConfigWriteLock(configDir);
30
+ const replaceDeadline = Date.now() + CONFIG_REPLACE_TIMEOUT_MS;
28
31
  try {
29
32
  const config = await readCliConfig(configDir);
30
33
  const result = update(config);
31
- await writeCliConfigFile(configDir, config);
34
+ await writeCliConfigFile(configDir, config, replaceDeadline);
32
35
  return result;
33
36
  }
34
37
  finally {
@@ -86,12 +89,30 @@ export async function clearAgentRegistrationIntent(configDir, profileName) {
86
89
  export function configPath(configDir) {
87
90
  return join(configDir, CONFIG_FILE);
88
91
  }
89
- async function writeCliConfigFile(configDir, config) {
92
+ async function writeCliConfigFile(configDir, config, replaceDeadline) {
90
93
  const path = configPath(configDir);
91
94
  const tempPath = `${path}.${process.pid}.tmp`;
92
95
  await writeFile(tempPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
93
96
  await chmod(tempPath, 0o600);
94
- await rename(tempPath, path);
97
+ let waits = 0;
98
+ while (true) {
99
+ try {
100
+ await rename(tempPath, path);
101
+ break;
102
+ }
103
+ catch (error) {
104
+ if (!isRetryableWindowsProfileFsError(error))
105
+ throw error;
106
+ if (waits >= CONFIG_REPLACE_MAX_WAITS ||
107
+ Date.now() + CONFIG_LOCK_RETRY_MS > replaceDeadline) {
108
+ throw error;
109
+ }
110
+ waits += 1;
111
+ await new Promise((resolve) => setTimeout(resolve, CONFIG_LOCK_RETRY_MS));
112
+ if (Date.now() >= replaceDeadline)
113
+ throw error;
114
+ }
115
+ }
95
116
  await chmod(path, 0o600);
96
117
  }
97
118
  async function acquireConfigWriteLock(configDir) {
@@ -99,7 +120,14 @@ async function acquireConfigWriteLock(configDir) {
99
120
  }
100
121
  async function acquireFileLock(lockPath, timeoutMessage) {
101
122
  const deadline = Date.now() + CONFIG_LOCK_TIMEOUT_MS;
123
+ let lastOpenError;
102
124
  while (true) {
125
+ if (Date.now() >= deadline) {
126
+ if (lastOpenError && isRetryableWindowsProfileFsError(lastOpenError)) {
127
+ throw lastOpenError;
128
+ }
129
+ throw new Error(timeoutMessage);
130
+ }
103
131
  try {
104
132
  const handle = await open(lockPath, "wx", 0o600);
105
133
  return async () => {
@@ -118,8 +146,19 @@ async function acquireFileLock(lockPath, timeoutMessage) {
118
146
  };
119
147
  }
120
148
  catch (error) {
121
- if (!isNodeError(error) || error.code !== "EEXIST")
149
+ if (isNodeError(error) && error.code === "EEXIST") {
150
+ lastOpenError = error;
151
+ }
152
+ else if (isRetryableWindowsProfileFsError(error)) {
153
+ lastOpenError = error;
154
+ if (Date.now() + CONFIG_LOCK_RETRY_MS > deadline)
155
+ throw error;
156
+ await new Promise((resolve) => setTimeout(resolve, CONFIG_LOCK_RETRY_MS));
157
+ continue;
158
+ }
159
+ else {
122
160
  throw error;
161
+ }
123
162
  }
124
163
  try {
125
164
  const lock = await stat(lockPath);
@@ -141,9 +180,8 @@ async function acquireFileLock(lockPath, timeoutMessage) {
141
180
  continue;
142
181
  throw error;
143
182
  }
144
- if (Date.now() >= deadline) {
183
+ if (Date.now() + CONFIG_LOCK_RETRY_MS > deadline)
145
184
  throw new Error(timeoutMessage);
146
- }
147
185
  await new Promise((resolve) => setTimeout(resolve, CONFIG_LOCK_RETRY_MS));
148
186
  }
149
187
  }
@@ -179,3 +217,8 @@ export function isActiveAgentProfile(profile) {
179
217
  function isNodeError(error) {
180
218
  return Boolean(error && typeof error === "object" && "code" in error);
181
219
  }
220
+ function isRetryableWindowsProfileFsError(error) {
221
+ return (process.platform === "win32" &&
222
+ isNodeError(error) &&
223
+ (error.code === "EPERM" || error.code === "EACCES" || error.code === "EBUSY"));
224
+ }