opendevbrowser 0.0.25 → 0.0.26

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.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-Z6ENAZUN.js → chunk-AVQL6WAS.js} +9 -2
  3. package/dist/chunk-AVQL6WAS.js.map +1 -0
  4. package/dist/{chunk-7U63PZ4W.js → chunk-GTTYIAI7.js} +369 -175
  5. package/dist/chunk-GTTYIAI7.js.map +1 -0
  6. package/dist/cli/commands/daemon.d.ts +25 -0
  7. package/dist/cli/commands/daemon.d.ts.map +1 -1
  8. package/dist/cli/commands/serve.d.ts +10 -13
  9. package/dist/cli/commands/serve.d.ts.map +1 -1
  10. package/dist/cli/commands/status.d.ts.map +1 -1
  11. package/dist/cli/daemon-client.d.ts.map +1 -1
  12. package/dist/cli/daemon-status-policy.d.ts +6 -0
  13. package/dist/cli/daemon-status-policy.d.ts.map +1 -0
  14. package/dist/cli/daemon-status.d.ts +1 -0
  15. package/dist/cli/daemon-status.d.ts.map +1 -1
  16. package/dist/cli/daemon.d.ts +5 -0
  17. package/dist/cli/daemon.d.ts.map +1 -1
  18. package/dist/cli/index.js +175 -96
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/utils/http.d.ts.map +1 -1
  21. package/dist/daemon-fingerprint.json +3 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +109 -28
  24. package/dist/index.js.map +1 -1
  25. package/dist/opendevbrowser.d.ts.map +1 -1
  26. package/dist/opendevbrowser.js +109 -28
  27. package/dist/opendevbrowser.js.map +1 -1
  28. package/dist/providers/inspiredesign-capture.d.ts.map +1 -1
  29. package/dist/providers/workflows.d.ts.map +1 -1
  30. package/dist/{providers-CYEJZVXB.js → providers-T2FQJCF6.js} +2 -2
  31. package/dist/tools/index.d.ts.map +1 -1
  32. package/dist/tools/status.d.ts.map +1 -1
  33. package/extension/manifest.json +1 -1
  34. package/package.json +1 -1
  35. package/dist/chunk-7U63PZ4W.js.map +0 -1
  36. package/dist/chunk-Z6ENAZUN.js.map +0 -1
  37. /package/dist/{providers-CYEJZVXB.js.map → providers-T2FQJCF6.js.map} +0 -0
@@ -47,7 +47,7 @@ import {
47
47
  runResearchWorkflow,
48
48
  runShoppingWorkflow,
49
49
  toSnippet
50
- } from "./chunk-Z6ENAZUN.js";
50
+ } from "./chunk-AVQL6WAS.js";
51
51
  import {
52
52
  ProviderRuntimeError
53
53
  } from "./chunk-FUSXMW3G.js";
@@ -35991,12 +35991,129 @@ function createOpenDevBrowserCore(options) {
35991
35991
  };
35992
35992
  }
35993
35993
 
35994
+ // src/cli/utils/http.ts
35995
+ var DEFAULT_HTTP_TIMEOUT_MS = 5e3;
35996
+ function isAbortError(error) {
35997
+ if (!error || typeof error !== "object") return false;
35998
+ return "name" in error && error.name === "AbortError";
35999
+ }
36000
+ var resolveTimeoutMs = (timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) => {
36001
+ return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_HTTP_TIMEOUT_MS;
36002
+ };
36003
+ var createTimeoutError = (timeoutMs) => {
36004
+ return new Error(`Request timed out after ${timeoutMs}ms`);
36005
+ };
36006
+ var createTimedSignal = (timeoutMs, upstreamSignal) => {
36007
+ const controller = new AbortController();
36008
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
36009
+ let removeAbortListener;
36010
+ if (upstreamSignal) {
36011
+ if (upstreamSignal.aborted) {
36012
+ controller.abort(upstreamSignal.reason);
36013
+ } else {
36014
+ const onAbort = () => controller.abort(upstreamSignal.reason);
36015
+ upstreamSignal.addEventListener("abort", onAbort, { once: true });
36016
+ removeAbortListener = () => upstreamSignal.removeEventListener("abort", onAbort);
36017
+ }
36018
+ }
36019
+ return {
36020
+ signal: controller.signal,
36021
+ dispose: () => {
36022
+ clearTimeout(timeoutId);
36023
+ removeAbortListener?.();
36024
+ }
36025
+ };
36026
+ };
36027
+ var cancelResponseBody = (response) => {
36028
+ try {
36029
+ const cancelResult = response.body?.cancel?.();
36030
+ if (cancelResult instanceof Promise) {
36031
+ void cancelResult.catch(() => {
36032
+ });
36033
+ }
36034
+ } catch {
36035
+ }
36036
+ };
36037
+ var readResponseBodyWithTimeout = async (response, signal, timeoutMs, reader) => {
36038
+ let bodyCancelled = false;
36039
+ const cancelBody = () => {
36040
+ if (bodyCancelled) {
36041
+ return;
36042
+ }
36043
+ bodyCancelled = true;
36044
+ cancelResponseBody(response);
36045
+ };
36046
+ if (signal.aborted) {
36047
+ cancelBody();
36048
+ throw createTimeoutError(timeoutMs);
36049
+ }
36050
+ let removeAbortListener;
36051
+ const abortPromise = new Promise((_, reject) => {
36052
+ const onAbort = () => {
36053
+ cancelBody();
36054
+ reject(createTimeoutError(timeoutMs));
36055
+ };
36056
+ signal.addEventListener("abort", onAbort, { once: true });
36057
+ removeAbortListener = () => signal.removeEventListener("abort", onAbort);
36058
+ });
36059
+ try {
36060
+ return await Promise.race([reader(), abortPromise]);
36061
+ } catch (error) {
36062
+ if (signal.aborted || isAbortError(error)) {
36063
+ cancelBody();
36064
+ throw createTimeoutError(timeoutMs);
36065
+ }
36066
+ throw error;
36067
+ } finally {
36068
+ removeAbortListener?.();
36069
+ }
36070
+ };
36071
+ async function fetchWithTimeout(input, init = {}, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
36072
+ const resolvedTimeout = resolveTimeoutMs(timeoutMs);
36073
+ const timedSignal = createTimedSignal(resolvedTimeout, init?.signal ?? void 0);
36074
+ try {
36075
+ return await fetch(input, { ...init, signal: timedSignal.signal });
36076
+ } catch (error) {
36077
+ if (isAbortError(error) || timedSignal.signal.aborted) {
36078
+ throw createTimeoutError(resolvedTimeout);
36079
+ }
36080
+ throw error;
36081
+ } finally {
36082
+ timedSignal.dispose();
36083
+ }
36084
+ }
36085
+ async function fetchWithTimeoutContext(input, init = {}, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
36086
+ const resolvedTimeout = resolveTimeoutMs(timeoutMs);
36087
+ const timedSignal = createTimedSignal(resolvedTimeout, init?.signal ?? void 0);
36088
+ try {
36089
+ const response = await fetch(input, { ...init, signal: timedSignal.signal });
36090
+ return {
36091
+ response,
36092
+ signal: timedSignal.signal,
36093
+ timeoutMs: resolvedTimeout,
36094
+ dispose: timedSignal.dispose
36095
+ };
36096
+ } catch (error) {
36097
+ timedSignal.dispose();
36098
+ if (isAbortError(error) || timedSignal.signal.aborted) {
36099
+ throw createTimeoutError(resolvedTimeout);
36100
+ }
36101
+ throw error;
36102
+ }
36103
+ }
36104
+ async function readResponseTextWithTimeout(response, signal, timeoutMs) {
36105
+ return await readResponseBodyWithTimeout(response, signal, timeoutMs, () => response.text());
36106
+ }
36107
+ async function readResponseJsonWithTimeout(response, signal, timeoutMs) {
36108
+ return await readResponseBodyWithTimeout(response, signal, timeoutMs, () => response.json());
36109
+ }
36110
+
35994
36111
  // src/cli/daemon.ts
35995
36112
  import { createServer as createServer2 } from "http";
35996
36113
  import { createHash as createHash6, timingSafeEqual as timingSafeEqual2 } from "crypto";
35997
36114
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3, unlinkSync, existsSync as existsSync6 } from "fs";
35998
36115
  import { homedir as homedir7 } from "os";
35999
- import { join as join16, resolve as resolve7 } from "path";
36116
+ import { basename as basename2, dirname as dirname8, join as join16, resolve as resolve7 } from "path";
36000
36117
  import { fileURLToPath as fileURLToPath2 } from "url";
36001
36118
 
36002
36119
  // src/cli/daemon-commands.ts
@@ -36247,7 +36364,14 @@ var DOM_CAPTURE_EMPTY_MESSAGE = "DOM capture returned empty HTML.";
36247
36364
  var SKIPPED_AFTER_TRANSPORT_TIMEOUT_SUFFIX = "transport timeout.";
36248
36365
  var createRemainingCaptureTimeout = (timeoutMs) => {
36249
36366
  const startedAtMs = Date.now();
36250
- return () => Math.max(1, timeoutMs - Math.max(0, Date.now() - startedAtMs));
36367
+ let firstRead = true;
36368
+ return () => {
36369
+ if (firstRead) {
36370
+ firstRead = false;
36371
+ return timeoutMs;
36372
+ }
36373
+ return Math.max(1, timeoutMs - Math.max(0, Date.now() - startedAtMs));
36374
+ };
36251
36375
  };
36252
36376
  var clampInspiredesignCaptureTimeout = (timeoutMs) => {
36253
36377
  if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) return INSPIREDESIGN_CAPTURE_TIMEOUT_MS;
@@ -37172,119 +37296,6 @@ var getBindingRenewConfig = () => ({
37172
37296
  waitMaxMs: WAIT_MAX_MS
37173
37297
  });
37174
37298
 
37175
- // src/cli/utils/http.ts
37176
- var DEFAULT_HTTP_TIMEOUT_MS = 5e3;
37177
- function isAbortError(error) {
37178
- if (!error || typeof error !== "object") return false;
37179
- return "name" in error && error.name === "AbortError";
37180
- }
37181
- var resolveTimeoutMs = (timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) => {
37182
- return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_HTTP_TIMEOUT_MS;
37183
- };
37184
- var createTimeoutError = (timeoutMs) => {
37185
- return new Error(`Request timed out after ${timeoutMs}ms`);
37186
- };
37187
- var createTimedSignal = (timeoutMs, upstreamSignal) => {
37188
- const controller = new AbortController();
37189
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
37190
- let removeAbortListener;
37191
- if (upstreamSignal) {
37192
- if (upstreamSignal.aborted) {
37193
- controller.abort(upstreamSignal.reason);
37194
- } else {
37195
- const onAbort = () => controller.abort(upstreamSignal.reason);
37196
- upstreamSignal.addEventListener("abort", onAbort, { once: true });
37197
- removeAbortListener = () => upstreamSignal.removeEventListener("abort", onAbort);
37198
- }
37199
- }
37200
- return {
37201
- signal: controller.signal,
37202
- dispose: () => {
37203
- clearTimeout(timeoutId);
37204
- removeAbortListener?.();
37205
- }
37206
- };
37207
- };
37208
- var cancelResponseBody = (response) => {
37209
- try {
37210
- void response.body?.cancel?.();
37211
- } catch {
37212
- }
37213
- };
37214
- var readResponseBodyWithTimeout = async (response, signal, timeoutMs, reader) => {
37215
- let bodyCancelled = false;
37216
- const cancelBody = () => {
37217
- if (bodyCancelled) {
37218
- return;
37219
- }
37220
- bodyCancelled = true;
37221
- cancelResponseBody(response);
37222
- };
37223
- if (signal.aborted) {
37224
- cancelBody();
37225
- throw createTimeoutError(timeoutMs);
37226
- }
37227
- let removeAbortListener;
37228
- const abortPromise = new Promise((_, reject) => {
37229
- const onAbort = () => {
37230
- cancelBody();
37231
- reject(createTimeoutError(timeoutMs));
37232
- };
37233
- signal.addEventListener("abort", onAbort, { once: true });
37234
- removeAbortListener = () => signal.removeEventListener("abort", onAbort);
37235
- });
37236
- try {
37237
- return await Promise.race([reader(), abortPromise]);
37238
- } catch (error) {
37239
- if (signal.aborted || isAbortError(error)) {
37240
- cancelBody();
37241
- throw createTimeoutError(timeoutMs);
37242
- }
37243
- throw error;
37244
- } finally {
37245
- removeAbortListener?.();
37246
- }
37247
- };
37248
- async function fetchWithTimeout(input, init = {}, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
37249
- const resolvedTimeout = resolveTimeoutMs(timeoutMs);
37250
- const timedSignal = createTimedSignal(resolvedTimeout, init?.signal ?? void 0);
37251
- try {
37252
- return await fetch(input, { ...init, signal: timedSignal.signal });
37253
- } catch (error) {
37254
- if (isAbortError(error) || timedSignal.signal.aborted) {
37255
- throw createTimeoutError(resolvedTimeout);
37256
- }
37257
- throw error;
37258
- } finally {
37259
- timedSignal.dispose();
37260
- }
37261
- }
37262
- async function fetchWithTimeoutContext(input, init = {}, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
37263
- const resolvedTimeout = resolveTimeoutMs(timeoutMs);
37264
- const timedSignal = createTimedSignal(resolvedTimeout, init?.signal ?? void 0);
37265
- try {
37266
- const response = await fetch(input, { ...init, signal: timedSignal.signal });
37267
- return {
37268
- response,
37269
- signal: timedSignal.signal,
37270
- timeoutMs: resolvedTimeout,
37271
- dispose: timedSignal.dispose
37272
- };
37273
- } catch (error) {
37274
- timedSignal.dispose();
37275
- if (isAbortError(error) || timedSignal.signal.aborted) {
37276
- throw createTimeoutError(resolvedTimeout);
37277
- }
37278
- throw error;
37279
- }
37280
- }
37281
- async function readResponseTextWithTimeout(response, signal, timeoutMs) {
37282
- return await readResponseBodyWithTimeout(response, signal, timeoutMs, () => response.text());
37283
- }
37284
- async function readResponseJsonWithTimeout(response, signal, timeoutMs) {
37285
- return await readResponseBodyWithTimeout(response, signal, timeoutMs, () => response.json());
37286
- }
37287
-
37288
37299
  // src/cli/daemon-commands.ts
37289
37300
  var createDaemonWorkflowRuntime = (core, options) => resolveBundledProviderRuntime({
37290
37301
  existingRuntime: core.providerRuntime,
@@ -39020,6 +39031,11 @@ async function resolveMacroExpression(options, config, manager, browserFallbackP
39020
39031
 
39021
39032
  // src/cli/daemon.ts
39022
39033
  var DEFAULT_DAEMON_PORT2 = 8788;
39034
+ var DAEMON_STOP_DEBUG_ENV = "OPDEVBROWSER_DEBUG_DAEMON_STOP";
39035
+ var DAEMON_FINGERPRINT_FILE = "daemon-fingerprint.json";
39036
+ var DAEMON_STOP_REASON_HEADER = "x-opendevbrowser-stop-reason";
39037
+ var DAEMON_STOP_CLIENT_PID_HEADER = "x-opendevbrowser-stop-client-pid";
39038
+ var DAEMON_STOP_FINGERPRINT_HEADER = "x-opendevbrowser-stop-fingerprint";
39023
39039
  var RECOVERABLE_PLAYWRIGHT_TRANSPORT_ERRORS = [
39024
39040
  "Cannot find context with specified id",
39025
39041
  "Detached while handling command."
@@ -39076,23 +39092,58 @@ function hashFileContents(entryPath) {
39076
39092
  return "missing";
39077
39093
  }
39078
39094
  }
39095
+ function resolveDaemonFingerprintDistRoot(modulePath) {
39096
+ let currentDir = dirname8(modulePath);
39097
+ while (true) {
39098
+ if (basename2(currentDir) === "dist") {
39099
+ return currentDir;
39100
+ }
39101
+ const parentDir = dirname8(currentDir);
39102
+ if (parentDir === currentDir) {
39103
+ break;
39104
+ }
39105
+ currentDir = parentDir;
39106
+ }
39107
+ return null;
39108
+ }
39109
+ function readDaemonFingerprintArtifact(modulePath) {
39110
+ const distRoot = resolveDaemonFingerprintDistRoot(modulePath);
39111
+ if (distRoot === null) {
39112
+ return null;
39113
+ }
39114
+ try {
39115
+ const content = readFileSync5(join16(distRoot, DAEMON_FINGERPRINT_FILE), "utf-8");
39116
+ const payload = JSON.parse(content);
39117
+ if (typeof payload.fingerprint === "string" && payload.fingerprint.trim().length > 0) {
39118
+ return payload.fingerprint.trim();
39119
+ }
39120
+ } catch {
39121
+ }
39122
+ return null;
39123
+ }
39079
39124
  function getCurrentDaemonFingerprint(options = {}) {
39080
- const entryPath = resolveCurrentDaemonEntrypointPath(options);
39081
39125
  const modulePath = resolve7(fileURLToPath2(options.moduleUrl ?? import.meta.url));
39126
+ const sharedFingerprint = readDaemonFingerprintArtifact(modulePath);
39082
39127
  const fingerprintParts = [
39083
39128
  DAEMON_FINGERPRINT_VERSION,
39084
- process.execPath,
39085
- entryPath,
39086
- hashFileContents(entryPath)
39129
+ sharedFingerprint ?? hashFileContents(modulePath)
39087
39130
  ];
39088
- if (modulePath !== entryPath) {
39089
- fingerprintParts.push(modulePath, hashFileContents(modulePath));
39090
- }
39091
39131
  return createHash6("sha256").update(fingerprintParts.join("\n")).digest("hex");
39092
39132
  }
39093
39133
  function isCurrentDaemonFingerprint(fingerprint) {
39094
39134
  return typeof fingerprint === "string" && fingerprint === getCurrentDaemonFingerprint();
39095
39135
  }
39136
+ function createDaemonStopHeaders(token, reason) {
39137
+ const headers = {
39138
+ Authorization: `Bearer ${token}`,
39139
+ [DAEMON_STOP_FINGERPRINT_HEADER]: getCurrentDaemonFingerprint(),
39140
+ [DAEMON_STOP_REASON_HEADER]: reason
39141
+ };
39142
+ if (process.env[DAEMON_STOP_DEBUG_ENV] === "1") {
39143
+ headers[DAEMON_STOP_CLIENT_PID_HEADER] = String(process.pid);
39144
+ }
39145
+ return headers;
39146
+ }
39096
39147
  function resolveDaemonFingerprint(...candidates) {
39097
39148
  for (const candidate of candidates) {
39098
39149
  if (typeof candidate === "string" && candidate.trim().length > 0) {
@@ -39134,6 +39185,20 @@ function sendJson(response, status, payload) {
39134
39185
  });
39135
39186
  response.end(JSON.stringify(payload));
39136
39187
  }
39188
+ function logDaemonStopDebug(message, details) {
39189
+ if (process.env[DAEMON_STOP_DEBUG_ENV] !== "1") {
39190
+ return;
39191
+ }
39192
+ const suffix = details ? ` ${JSON.stringify(details)}` : "";
39193
+ console.error(`[daemon-stop-debug] ${message}${suffix}`);
39194
+ }
39195
+ function readSingleHeader(request, name) {
39196
+ const value = request.headers[name];
39197
+ if (typeof value === "string") {
39198
+ return value;
39199
+ }
39200
+ return null;
39201
+ }
39137
39202
  var isDaemonCommandRequest = (value) => {
39138
39203
  if (typeof value.name !== "string") {
39139
39204
  return false;
@@ -39185,8 +39250,20 @@ async function startDaemon(options = {}) {
39185
39250
  return;
39186
39251
  }
39187
39252
  if (request.method === "POST" && url.pathname === "/stop") {
39253
+ const stopFingerprint = readSingleHeader(request, DAEMON_STOP_FINGERPRINT_HEADER);
39254
+ logDaemonStopDebug("http.stop", {
39255
+ remoteAddress: request.socket.remoteAddress ?? null,
39256
+ remotePort: request.socket.remotePort ?? null,
39257
+ reason: readSingleHeader(request, DAEMON_STOP_REASON_HEADER),
39258
+ clientPid: readSingleHeader(request, DAEMON_STOP_CLIENT_PID_HEADER),
39259
+ fingerprintMatches: stopFingerprint === fingerprint
39260
+ });
39261
+ if (stopFingerprint !== fingerprint) {
39262
+ sendJson(response, 409, { ok: false, error: "Stale daemon stop request." });
39263
+ return;
39264
+ }
39188
39265
  sendJson(response, 200, { ok: true });
39189
- await stop();
39266
+ await stop("http.stop");
39190
39267
  return;
39191
39268
  }
39192
39269
  if (request.method === "POST" && url.pathname === "/command") {
@@ -39240,7 +39317,7 @@ async function startDaemon(options = {}) {
39240
39317
  return;
39241
39318
  }
39242
39319
  console.error(error);
39243
- void stop().finally(() => {
39320
+ void stop("uncaughtException").finally(() => {
39244
39321
  process.exitCode = 1;
39245
39322
  });
39246
39323
  };
@@ -39249,15 +39326,16 @@ async function startDaemon(options = {}) {
39249
39326
  return;
39250
39327
  }
39251
39328
  console.error(reason);
39252
- void stop().finally(() => {
39329
+ void stop("unhandledRejection").finally(() => {
39253
39330
  process.exitCode = 1;
39254
39331
  });
39255
39332
  };
39256
- const stop = async () => {
39333
+ const stop = async (reason = "unknown") => {
39257
39334
  if (stopping) {
39258
39335
  return;
39259
39336
  }
39260
39337
  stopping = true;
39338
+ logDaemonStopDebug("stop.begin", { reason });
39261
39339
  clearDaemonMetadata();
39262
39340
  clearBinding();
39263
39341
  process.off("SIGINT", sigintHandler);
@@ -39268,13 +39346,14 @@ async function startDaemon(options = {}) {
39268
39346
  await new Promise((resolve9) => {
39269
39347
  server.close(() => resolve9());
39270
39348
  });
39349
+ logDaemonStopDebug("stop.complete", { reason });
39271
39350
  };
39272
39351
  const sigintHandler = () => {
39273
- stop().catch(() => {
39352
+ void stop("SIGINT").catch(() => {
39274
39353
  });
39275
39354
  };
39276
39355
  const sigtermHandler = () => {
39277
- stop().catch(() => {
39356
+ void stop("SIGTERM").catch(() => {
39278
39357
  });
39279
39358
  };
39280
39359
  process.on("SIGINT", sigintHandler);
@@ -39348,7 +39427,15 @@ function resolveExitCode(result) {
39348
39427
  return result.success ? EXIT_SUCCESS : EXIT_EXECUTION;
39349
39428
  }
39350
39429
 
39430
+ // src/cli/daemon-status-policy.ts
39431
+ var DEFAULT_DAEMON_STATUS_FETCH_OPTIONS = {
39432
+ timeoutMs: 5e3,
39433
+ retryAttempts: 5,
39434
+ retryDelayMs: 250
39435
+ };
39436
+
39351
39437
  // src/cli/daemon-status.ts
39438
+ var DEFAULT_DAEMON_STATUS_TIMEOUT_MS = DEFAULT_DAEMON_STATUS_FETCH_OPTIONS.timeoutMs;
39352
39439
  var sleep2 = async (delayMs) => {
39353
39440
  if (!(Number.isFinite(delayMs) && delayMs > 0)) {
39354
39441
  return;
@@ -39361,17 +39448,66 @@ var resolveRetryAttempts = (retryAttempts) => {
39361
39448
  var resolveRetryDelayMs = (retryDelayMs) => {
39362
39449
  return typeof retryDelayMs === "number" && Number.isFinite(retryDelayMs) && retryDelayMs > 0 ? retryDelayMs : 0;
39363
39450
  };
39451
+ var resolveStatusTimeoutMs = (timeoutMs) => {
39452
+ return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_DAEMON_STATUS_TIMEOUT_MS;
39453
+ };
39454
+ var withFingerprintCurrent = (status) => ({
39455
+ ...status,
39456
+ fingerprintCurrent: isCurrentDaemonFingerprint(status.fingerprint)
39457
+ });
39458
+ var readRemainingBudgetMs = (deadlineMs) => {
39459
+ return Math.max(0, deadlineMs - Date.now());
39460
+ };
39461
+ var readSeedTimeoutMs = (remainingBudgetMs, remainingSeedCount) => {
39462
+ if (remainingSeedCount <= 1) {
39463
+ return remainingBudgetMs;
39464
+ }
39465
+ return Math.max(1, Math.floor(remainingBudgetMs / remainingSeedCount));
39466
+ };
39467
+ var resolveDaemonStatusSeeds = (metadata, config) => {
39468
+ const seeds = [];
39469
+ const seen = /* @__PURE__ */ new Set();
39470
+ const addSeed = (seed) => {
39471
+ if (!seed) {
39472
+ return;
39473
+ }
39474
+ const key = `${seed.port}:${seed.token}`;
39475
+ if (seen.has(key)) {
39476
+ return;
39477
+ }
39478
+ seen.add(key);
39479
+ seeds.push(seed);
39480
+ };
39481
+ addSeed(
39482
+ config.daemonPort > 0 && config.daemonToken ? {
39483
+ port: config.daemonPort,
39484
+ token: config.daemonToken,
39485
+ relayPort: config.relayPort
39486
+ } : null
39487
+ );
39488
+ addSeed(metadata);
39489
+ return seeds;
39490
+ };
39364
39491
  async function fetchDaemonStatus(port, token, options = {}) {
39365
39492
  const attempts = resolveRetryAttempts(options.retryAttempts);
39366
39493
  const retryDelayMs = resolveRetryDelayMs(options.retryDelayMs);
39367
39494
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
39368
39495
  try {
39369
- const response = await fetchWithTimeout(`http://127.0.0.1:${port}/status`, {
39496
+ const timedResponse = await fetchWithTimeoutContext(`http://127.0.0.1:${port}/status`, {
39370
39497
  method: "GET",
39371
39498
  headers: { Authorization: `Bearer ${token}` }
39372
39499
  }, options.timeoutMs);
39373
- if (response.ok) {
39374
- return await response.json();
39500
+ try {
39501
+ if (timedResponse.response.ok) {
39502
+ const status = await readResponseJsonWithTimeout(
39503
+ timedResponse.response,
39504
+ timedResponse.signal,
39505
+ timedResponse.timeoutMs
39506
+ );
39507
+ return withFingerprintCurrent(status);
39508
+ }
39509
+ } finally {
39510
+ timedResponse.dispose();
39375
39511
  }
39376
39512
  } catch {
39377
39513
  }
@@ -39385,32 +39521,34 @@ async function fetchDaemonStatusFromMetadata(config, options = {}) {
39385
39521
  const resolvedConfig = config ?? loadGlobalConfig();
39386
39522
  const attempts = resolveRetryAttempts(options.retryAttempts);
39387
39523
  const retryDelayMs = resolveRetryDelayMs(options.retryDelayMs);
39524
+ const deadlineMs = Date.now() + resolveStatusTimeoutMs(options.timeoutMs);
39388
39525
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
39389
39526
  const metadata = readDaemonMetadata();
39390
- if (metadata) {
39391
- const status = await fetchDaemonStatus(metadata.port, metadata.token, { timeoutMs: options.timeoutMs });
39392
- if (status?.ok) {
39393
- persistDaemonStatusMetadata(metadata, status, resolvedConfig);
39394
- return status;
39527
+ const seeds = resolveDaemonStatusSeeds(metadata, resolvedConfig);
39528
+ for (let seedIndex = 0; seedIndex < seeds.length; seedIndex += 1) {
39529
+ const seed = seeds[seedIndex];
39530
+ if (!seed) {
39531
+ continue;
39395
39532
  }
39396
- }
39397
- if (resolvedConfig.daemonPort > 0 && resolvedConfig.daemonToken) {
39398
- const status = await fetchDaemonStatus(resolvedConfig.daemonPort, resolvedConfig.daemonToken, {
39399
- timeoutMs: options.timeoutMs
39533
+ const remainingBudgetMs = readRemainingBudgetMs(deadlineMs);
39534
+ if (remainingBudgetMs <= 0) {
39535
+ return null;
39536
+ }
39537
+ const timeoutMs = readSeedTimeoutMs(remainingBudgetMs, seeds.length - seedIndex);
39538
+ const status = await fetchDaemonStatus(seed.port, seed.token, {
39539
+ timeoutMs
39400
39540
  });
39401
39541
  if (status?.ok) {
39402
- persistDaemonStatusMetadata({
39403
- port: resolvedConfig.daemonPort,
39404
- token: resolvedConfig.daemonToken,
39405
- pid: status.pid,
39406
- relayPort: status.relay.port ?? resolvedConfig.relayPort,
39407
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
39408
- }, status, resolvedConfig);
39542
+ persistDaemonStatusMetadata(seed, status, resolvedConfig);
39409
39543
  return status;
39410
39544
  }
39411
39545
  }
39412
39546
  if (attempt < attempts) {
39413
- await sleep2(retryDelayMs);
39547
+ const remainingBudgetMs = readRemainingBudgetMs(deadlineMs);
39548
+ if (remainingBudgetMs <= 0) {
39549
+ break;
39550
+ }
39551
+ await sleep2(Math.min(retryDelayMs, remainingBudgetMs));
39414
39552
  }
39415
39553
  }
39416
39554
  return null;
@@ -39456,6 +39594,13 @@ var DAEMON_RECOVERY_READY_TIMEOUT_MS = 5e3;
39456
39594
  var DAEMON_RESTART_READY_TIMEOUT_MS = 15e3;
39457
39595
  var DAEMON_RESTART_POLL_DELAY_MS = 250;
39458
39596
  var cachedClientState;
39597
+ var logDaemonStopDebug2 = (message, details) => {
39598
+ if (process.env[DAEMON_STOP_DEBUG_ENV] !== "1") {
39599
+ return;
39600
+ }
39601
+ const suffix = details ? ` ${JSON.stringify(details)}` : "";
39602
+ console.error(`[daemon-stop-debug] ${message}${suffix}`);
39603
+ };
39459
39604
  var getClientStateFilePath = () => {
39460
39605
  const cacheRoot = getCacheRoot();
39461
39606
  return join17(cacheRoot, CLIENT_ID_FILE);
@@ -39579,7 +39724,7 @@ var DaemonClient = class {
39579
39724
  this.maybeTrackLease(name, params2, result);
39580
39725
  return result;
39581
39726
  }
39582
- if (!options.requireBinding && isBindingRequiredError(error)) {
39727
+ if (isBindingRequiredError(error)) {
39583
39728
  if (this.binding) {
39584
39729
  this.clearBinding();
39585
39730
  }
@@ -39703,7 +39848,9 @@ var DaemonClient = class {
39703
39848
  }
39704
39849
  async callRaw(name, params2, timeoutMs) {
39705
39850
  const budget = createTimeoutBudget(timeoutMs);
39706
- const connection = await resolveDaemonConnection(budget);
39851
+ const connection = await resolveDaemonConnection(budget, {
39852
+ preferConfiguredRecovery: requiresConfiguredRecovery(name)
39853
+ });
39707
39854
  let timedResponse;
39708
39855
  try {
39709
39856
  timedResponse = await openDaemonCommand(
@@ -39711,7 +39858,7 @@ var DaemonClient = class {
39711
39858
  connection.token,
39712
39859
  name,
39713
39860
  params2,
39714
- readRemainingBudgetMs(budget)
39861
+ readRemainingBudgetMs2(budget)
39715
39862
  );
39716
39863
  } catch (error) {
39717
39864
  if (isTransportTimeoutError2(error)) {
@@ -39756,7 +39903,7 @@ var createTimeoutBudget = (timeoutMs) => {
39756
39903
  const resolved = asPositiveNumber(timeoutMs);
39757
39904
  return resolved === void 0 ? null : { timeoutMs: resolved, deadlineMs: Date.now() + resolved };
39758
39905
  };
39759
- var readRemainingBudgetMs = (budget) => {
39906
+ var readRemainingBudgetMs2 = (budget) => {
39760
39907
  if (!budget) {
39761
39908
  return void 0;
39762
39909
  }
@@ -39767,7 +39914,7 @@ var readRemainingBudgetMs = (budget) => {
39767
39914
  return remainingMs;
39768
39915
  };
39769
39916
  var capTimeoutToBudget = (timeoutMs, budget) => {
39770
- const remainingMs = readRemainingBudgetMs(budget);
39917
+ const remainingMs = readRemainingBudgetMs2(budget);
39771
39918
  return remainingMs === void 0 ? timeoutMs : Math.max(1, Math.min(timeoutMs, remainingMs));
39772
39919
  };
39773
39920
  var resolveReadyDeadlineMs = (readyTimeoutMs, budget) => {
@@ -39864,7 +40011,7 @@ var fetchCurrentDaemonStatus = async (connection, options, budget = null) => {
39864
40011
  return status;
39865
40012
  }
39866
40013
  if (attempt < attempts) {
39867
- await sleep3(Math.min(retryDelayMs, readRemainingBudgetMs(budget) ?? retryDelayMs));
40014
+ await sleep3(Math.min(retryDelayMs, readRemainingBudgetMs2(budget) ?? retryDelayMs));
39868
40015
  }
39869
40016
  }
39870
40017
  return null;
@@ -39880,7 +40027,7 @@ var fetchAnyDaemonStatus = async (connection, options, budget = null) => {
39880
40027
  return status;
39881
40028
  }
39882
40029
  if (attempt < attempts) {
39883
- await sleep3(Math.min(retryDelayMs, readRemainingBudgetMs(budget) ?? retryDelayMs));
40030
+ await sleep3(Math.min(retryDelayMs, readRemainingBudgetMs2(budget) ?? retryDelayMs));
39884
40031
  }
39885
40032
  }
39886
40033
  return null;
@@ -39891,6 +40038,9 @@ var sleep3 = async (delayMs) => {
39891
40038
  }
39892
40039
  await new Promise((resolve9) => setTimeout(resolve9, delayMs));
39893
40040
  };
40041
+ var requiresConfiguredRecovery = (name) => {
40042
+ return name === "canvas.execute";
40043
+ };
39894
40044
  var getConfiguredDaemonConnection = () => {
39895
40045
  const config = loadGlobalConfig();
39896
40046
  if (!(config.daemonPort > 0 && config.daemonToken)) {
@@ -39912,7 +40062,7 @@ var persistResolvedDaemonStatus = (connection, status) => {
39912
40062
  };
39913
40063
  var persistCurrentConfiguredConnection = async (configuredConnection, status, staleMetadata) => {
39914
40064
  if (staleMetadata && !sameDaemonConnection(staleMetadata.connection, configuredConnection)) {
39915
- void stopDaemonConnection(staleMetadata.connection).catch(() => void 0);
40065
+ void stopDaemonConnection(staleMetadata.connection, null, "persistCurrentConfiguredConnection.staleMetadata").catch(() => void 0);
39916
40066
  }
39917
40067
  persistResolvedDaemonStatus(configuredConnection, status);
39918
40068
  return configuredConnection;
@@ -39921,7 +40071,7 @@ var resolveConfiguredPreferenceOptions = (budget) => {
39921
40071
  if (!budget) {
39922
40072
  return DAEMON_CONFIG_PREFER_OPTIONS;
39923
40073
  }
39924
- const remainingMs = readRemainingBudgetMs(budget);
40074
+ const remainingMs = readRemainingBudgetMs2(budget);
39925
40075
  if (remainingMs === void 0 || remainingMs <= 1) {
39926
40076
  return null;
39927
40077
  }
@@ -39943,14 +40093,27 @@ var resolveConfiguredPreferenceOptions = (budget) => {
39943
40093
  retryDelayMs: retryAttempts > 1 ? retryDelayMs : 0
39944
40094
  };
39945
40095
  };
39946
- var stopDaemonConnection = async (connection, budget = null) => {
40096
+ var stopDaemonConnection = async (connection, budget = null, reason = "unknown") => {
39947
40097
  const stopTimeoutMs = capTimeoutToBudget(DAEMON_RESTART_STATUS_TIMEOUT_MS, budget);
40098
+ logDaemonStopDebug2("client.stop.request", { reason, port: connection.port });
39948
40099
  try {
39949
- await fetchWithTimeout(`http://127.0.0.1:${connection.port}/stop`, {
40100
+ const response = await fetchWithTimeout(`http://127.0.0.1:${connection.port}/stop`, {
39950
40101
  method: "POST",
39951
- headers: { Authorization: `Bearer ${connection.token}` }
40102
+ headers: createDaemonStopHeaders(connection.token, reason)
39952
40103
  }, stopTimeoutMs);
40104
+ if (response.status === 409) {
40105
+ logDaemonStopDebug2("client.stop.fingerprintRejected", { reason, port: connection.port });
40106
+ return "fingerprint_rejected";
40107
+ }
40108
+ if (!response.ok) {
40109
+ logDaemonStopDebug2("client.stop.rejected", { reason, port: connection.port, status: response.status });
40110
+ return "unreachable";
40111
+ }
40112
+ logDaemonStopDebug2("client.stop.complete", { reason, port: connection.port });
40113
+ return "stopped";
39953
40114
  } catch {
40115
+ logDaemonStopDebug2("client.stop.error", { reason, port: connection.port });
40116
+ return "unreachable";
39954
40117
  }
39955
40118
  };
39956
40119
  function resolveDaemonRestartCommand(options = {}) {
@@ -40056,7 +40219,7 @@ var resolveMetadataConnection = async (metadataConnection, configuredConnection,
40056
40219
  }
40057
40220
  return { connection: metadataConnection, status };
40058
40221
  };
40059
- var resolveFreshDaemonConnection = async (budget = null) => {
40222
+ var resolveFreshDaemonConnection = async (budget = null, options = {}) => {
40060
40223
  const configuredConnection = getConfiguredDaemonConnection();
40061
40224
  if (!configuredConnection) {
40062
40225
  throw createDisconnectedError("Daemon not running. Start with `opendevbrowser serve`.");
@@ -40069,9 +40232,22 @@ var resolveFreshDaemonConnection = async (budget = null) => {
40069
40232
  if (currentConfiguredStatus?.ok) {
40070
40233
  return await persistCurrentConfiguredConnection(configuredConnection, currentConfiguredStatus, staleMetadata);
40071
40234
  }
40072
- if (staleMetadata?.status.ok && isCurrentDaemonFingerprint(staleMetadata.status.fingerprint)) {
40235
+ if (options.preferConfiguredRecovery && staleMetadata) {
40236
+ currentConfiguredStatus = await waitForCurrentDaemonStatus(
40237
+ configuredConnection,
40238
+ DAEMON_RECOVERY_READY_TIMEOUT_MS,
40239
+ budget
40240
+ );
40241
+ if (currentConfiguredStatus?.ok) {
40242
+ return await persistCurrentConfiguredConnection(configuredConnection, currentConfiguredStatus, staleMetadata);
40243
+ }
40244
+ if (!configuredStatus?.ok) {
40245
+ throw createDisconnectedError("Daemon not running. Start with `opendevbrowser serve`.");
40246
+ }
40247
+ }
40248
+ if (!options.preferConfiguredRecovery && staleMetadata?.status.ok && isCurrentDaemonFingerprint(staleMetadata.status.fingerprint)) {
40073
40249
  if (configuredStatus?.ok) {
40074
- void stopDaemonConnection(configuredConnection, budget).catch(() => void 0);
40250
+ void stopDaemonConnection(configuredConnection, budget, "resolveFreshDaemonConnection.configuredCurrentMetadataPreferred").catch(() => void 0);
40075
40251
  }
40076
40252
  return staleMetadata.connection;
40077
40253
  }
@@ -40084,13 +40260,11 @@ var resolveFreshDaemonConnection = async (budget = null) => {
40084
40260
  if (currentConfiguredStatus?.ok) {
40085
40261
  return await persistCurrentConfiguredConnection(configuredConnection, currentConfiguredStatus, staleMetadata);
40086
40262
  }
40263
+ throw createDisconnectedError("Daemon not running. Start with `opendevbrowser serve`.");
40087
40264
  }
40088
40265
  const staleConnections = [];
40089
40266
  if (configuredStatus?.ok) {
40090
- staleConnections.push(configuredConnection);
40091
- }
40092
- if (staleMetadata && !sameDaemonConnection(staleMetadata.connection, configuredConnection)) {
40093
- staleConnections.push(staleMetadata.connection);
40267
+ staleConnections.push({ connection: configuredConnection, status: configuredStatus });
40094
40268
  }
40095
40269
  if (staleConnections.length === 0) {
40096
40270
  const recoveringStatus = await waitForCurrentDaemonStatus(
@@ -40105,7 +40279,16 @@ var resolveFreshDaemonConnection = async (budget = null) => {
40105
40279
  throw createDisconnectedError("Daemon not running. Start with `opendevbrowser serve`.");
40106
40280
  }
40107
40281
  for (const staleConnection of staleConnections) {
40108
- await stopDaemonConnection(staleConnection, budget);
40282
+ const stopOutcome = await stopDaemonConnection(
40283
+ staleConnection.connection,
40284
+ budget,
40285
+ "resolveFreshDaemonConnection.staleConnections"
40286
+ );
40287
+ if (stopOutcome === "fingerprint_rejected") {
40288
+ throw createDisconnectedError(
40289
+ `Daemon on 127.0.0.1:${staleConnection.connection.port} pid=${staleConnection.status.pid} is protected by a different opendevbrowser build. Start with \`opendevbrowser serve\`.`
40290
+ );
40291
+ }
40109
40292
  }
40110
40293
  if (configuredStatus?.ok) {
40111
40294
  const shutdownOutcome = await waitForDaemonShutdown(configuredConnection, DAEMON_RECOVERY_READY_TIMEOUT_MS, budget);
@@ -40125,7 +40308,7 @@ var resolveFreshDaemonConnection = async (budget = null) => {
40125
40308
  persistResolvedDaemonStatus(configuredConnection, refreshedStatus);
40126
40309
  return configuredConnection;
40127
40310
  };
40128
- var resolveDaemonConnection = async (budget = null) => {
40311
+ var resolveDaemonConnection = async (budget = null, options = {}) => {
40129
40312
  const metadata = readDaemonMetadata();
40130
40313
  if (metadata && isCurrentDaemonFingerprint(metadata.fingerprint)) {
40131
40314
  const metadataConnection = { port: metadata.port, token: metadata.token };
@@ -40135,6 +40318,9 @@ var resolveDaemonConnection = async (budget = null) => {
40135
40318
  }
40136
40319
  const configuredOptions = resolveConfiguredPreferenceOptions(budget);
40137
40320
  if (!configuredOptions) {
40321
+ if (options.preferConfiguredRecovery) {
40322
+ return await resolveFreshDaemonConnection(budget, options);
40323
+ }
40138
40324
  return metadataConnection;
40139
40325
  }
40140
40326
  const configuredStatus = await fetchCurrentDaemonStatus(configuredConnection, configuredOptions, budget);
@@ -40145,13 +40331,18 @@ var resolveDaemonConnection = async (budget = null) => {
40145
40331
  { connection: metadataConnection }
40146
40332
  );
40147
40333
  }
40334
+ if (options.preferConfiguredRecovery) {
40335
+ return await resolveFreshDaemonConnection(budget, options);
40336
+ }
40148
40337
  return metadataConnection;
40149
40338
  }
40150
- return await resolveFreshDaemonConnection(budget);
40339
+ return await resolveFreshDaemonConnection(budget, options);
40151
40340
  };
40152
40341
  var retryWithRefreshedConnection = async (name, params2, budget) => {
40153
- const connection = await resolveFreshDaemonConnection(budget);
40154
- return await openDaemonCommand(connection.port, connection.token, name, params2, readRemainingBudgetMs(budget));
40342
+ const connection = await resolveFreshDaemonConnection(budget, {
40343
+ preferConfiguredRecovery: requiresConfiguredRecovery(name)
40344
+ });
40345
+ return await openDaemonCommand(connection.port, connection.token, name, params2, readRemainingBudgetMs2(budget));
40155
40346
  };
40156
40347
  var openDaemonCommand = async (port, token, name, params2, timeoutMs) => {
40157
40348
  return await fetchWithTimeoutContext(`http://127.0.0.1:${port}/command`, {
@@ -43320,15 +43511,18 @@ export {
43320
43511
  executeMacroWithRuntime,
43321
43512
  fetchWithTimeout,
43322
43513
  readDaemonMetadata,
43323
- getCurrentDaemonFingerprint,
43514
+ isCurrentDaemonFingerprint,
43515
+ createDaemonStopHeaders,
43324
43516
  startDaemon,
43325
43517
  EXIT_USAGE,
43326
43518
  EXIT_EXECUTION,
43327
43519
  EXIT_DISCONNECTED,
43328
43520
  createUsageError,
43521
+ createDisconnectedError,
43329
43522
  toCliError,
43330
43523
  formatErrorPayload,
43331
43524
  resolveExitCode,
43525
+ DEFAULT_DAEMON_STATUS_FETCH_OPTIONS,
43332
43526
  fetchDaemonStatus,
43333
43527
  fetchDaemonStatusFromMetadata,
43334
43528
  DaemonClient,
@@ -43350,4 +43544,4 @@ export {
43350
43544
  TOOL_SURFACE_ENTRIES
43351
43545
  };
43352
43546
  /* v8 ignore next -- @preserve */
43353
- //# sourceMappingURL=chunk-7U63PZ4W.js.map
43547
+ //# sourceMappingURL=chunk-GTTYIAI7.js.map