different-ai/openwork · error

transferId is already active.

Error message

transferId is already active.

What it means

runDesktopTransfer() keys in-flight transfers by `${event.sender.id}:${transferId}` in activeDesktopTransfers. If a transfer with the same key is already running, it throws this error to prevent concurrent duplicate work on the same sender/transfer pair. The entry is removed in the finally block when the transfer finishes.

Source

Thrown at apps/desktop/electron/main.mjs:1085

  app,
  defaultDenBaseUrl: DEFAULT_DEN_BASE_URL,
  defaultRequireSignin: DEFAULT_DESKTOP_REQUIRE_SIGNIN,
  forceRequireSignin: FORCE_DESKTOP_REQUIRE_SIGNIN,
});

const activeDesktopTransfers = new Map();

function desktopTransferKey(event, transferId) {
  const normalizedId = typeof transferId === "string" ? transferId.trim() : "";
  if (!normalizedId || normalizedId.length > 128 || !/^[a-zA-Z0-9._-]+$/.test(normalizedId)) {
    throw new Error("A valid transferId is required.");
  }
  return `${event.sender.id}:${normalizedId}`;
}

async function runDesktopTransfer(event, input, operation) {
  const key = desktopTransferKey(event, input?.transferId);
  if (activeDesktopTransfers.has(key)) throw new Error("transferId is already active.");
  const controller = new AbortController();
  const abort = () => controller.abort();
  activeDesktopTransfers.set(key, controller);
  event.sender.once("destroyed", abort);
  try {
    // Both authorities come from app-owned state in userData; workspace-
    // writable configuration must never widen where a transfer may write.
    const [authorizedRoots, allowedUrlPrefixes] = await Promise.all([
      workspaceStore.listLocalWorkspacePaths(),
      workspaceStore.listRemoteWorkspaceUrlPrefixes(),
    ]);
    return await operation(input, {
      authorizedRoots,
      allowedUrlPrefixes,
      // App-owned staging keeps in-flight downloads outside every authorized
      // workspace root until they complete.
      stagingDir: path.join(app.getPath("userData"), "binary-transfers"),
      fetcher: electronNet.fetch,

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Use a fresh unique transferId for each new operation instead of reusing one.
  2. Wait for the first transfer's completion before retrying; disable the triggering button while pending.
  3. If a transfer is stuck, restart the renderer/webContents (sender destruction aborts and clears the key).
  4. Add renderer-side state so an already-active transfer shows progress instead of re-invoking.

Example fix

// before
await invoke('desktop:transfer', { transferId: 'export' }); // second call throws
// after
const transferId = crypto.randomUUID(); // unique per attempt
await invoke('desktop:transfer', { transferId });
Defensive patterns

Strategy: retry

Validate before calling

// track in-flight ids renderer-side before re-invoking
const inFlight = new Set();
if (inFlight.has(transferId)) return; // skip duplicate invocation

Try / catch

try {
  await invoke('desktop:transfer', { transferId });
} catch (err) {
  if (String(err.message) === 'transferId is already active.') {
    await new Promise(r => setTimeout(r, 250)); // then retry with a NEW transferId
  } else throw err;
}

Prevention

When it happens

Trigger: An IPC transfer call arrives while a previous call with the same sender id and transferId is still pending — e.g. double-click retry, renderer re-issuing after a slow response, or a stale transfer that never completed (aborts only fire on sender destruction).

Common situations: Users clicking a download/export button twice; the renderer not awaiting the first result before retrying; a hung operation never settling so the key stays in the map; React StrictMode double-invocation of effects.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/3782d66616f8a2fd. Report an issue: GitHub.