@productmaker/mcp 12.2.5 → 12.2.7

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.
@@ -569,6 +569,7 @@
569
569
  forceSizeReport: false,
570
570
  lastReportedHeight: 0,
571
571
  session: null,
572
+ sessionGeneration: 0,
572
573
  toolInput: {},
573
574
  items: [],
574
575
  consumedBridgeIds: new Set(),
@@ -812,6 +813,33 @@
812
813
  return state.abortController?.signal;
813
814
  }
814
815
 
816
+ // An immutable work context prevents old async continuations from
817
+ // acquiring a replacement session's credentials or mutating its UI.
818
+ function captureUploadWork() {
819
+ return state.session
820
+ ? {
821
+ session: state.session,
822
+ generation: state.sessionGeneration,
823
+ signal: activeAbortSignal(),
824
+ }
825
+ : null;
826
+ }
827
+
828
+ function uploadWorkIsCurrent(work) {
829
+ return Boolean(
830
+ work &&
831
+ !state.disposed &&
832
+ !state.workCancelled &&
833
+ !work.signal?.aborted &&
834
+ work.generation === state.sessionGeneration &&
835
+ work.session.sessionId === state.session?.sessionId,
836
+ );
837
+ }
838
+
839
+ function requireCurrentUploadWork(work) {
840
+ if (!uploadWorkIsCurrent(work)) throw new Error("upload_cancelled");
841
+ }
842
+
815
843
  function abortActiveWork() {
816
844
  state.abortController?.abort();
817
845
  for (const request of state.activeXhrs) request.abort();
@@ -1013,7 +1041,12 @@
1013
1041
  }
1014
1042
 
1015
1043
  function handleHostMessage(event) {
1016
- if (event.source !== window.parent || !isRecord(event.data)) return;
1044
+ if (
1045
+ event.source !== window.parent ||
1046
+ !isRecord(event.data) ||
1047
+ event.data.jsonrpc !== "2.0"
1048
+ )
1049
+ return;
1017
1050
  const message = event.data;
1018
1051
  if (message.method === "ui/resource-teardown") {
1019
1052
  cleanup();
@@ -1046,7 +1079,58 @@
1046
1079
  applyHostContainerDimensions();
1047
1080
  return;
1048
1081
  }
1082
+ // Host requests have their own ID namespace: a ping may reuse an
1083
+ // outstanding app request ID without being its response.
1084
+ if (message.method === "ping") {
1085
+ if (
1086
+ (typeof message.id === "string" ||
1087
+ Number.isInteger(message.id)) &&
1088
+ !Object.prototype.hasOwnProperty.call(message, "result") &&
1089
+ !Object.prototype.hasOwnProperty.call(message, "error") &&
1090
+ (message.params === undefined || isRecord(message.params))
1091
+ )
1092
+ window.parent.postMessage(
1093
+ { jsonrpc: "2.0", id: message.id, result: {} },
1094
+ "*",
1095
+ );
1096
+ return;
1097
+ }
1098
+ if (typeof message.method === "string") {
1099
+ if (
1100
+ (typeof message.id === "string" ||
1101
+ Number.isInteger(message.id)) &&
1102
+ !Object.prototype.hasOwnProperty.call(message, "result") &&
1103
+ !Object.prototype.hasOwnProperty.call(message, "error") &&
1104
+ (message.params === undefined ||
1105
+ isRecord(message.params) ||
1106
+ Array.isArray(message.params))
1107
+ ) {
1108
+ window.parent.postMessage(
1109
+ {
1110
+ jsonrpc: "2.0",
1111
+ id: message.id,
1112
+ error: { code: -32601, message: "Method not found" },
1113
+ },
1114
+ "*",
1115
+ );
1116
+ }
1117
+ // Notifications have no id and are handled below.
1118
+ if (Object.prototype.hasOwnProperty.call(message, "id")) return;
1119
+ }
1049
1120
  if (Object.prototype.hasOwnProperty.call(message, "id")) {
1121
+ const successResponse =
1122
+ isRecord(message.result) &&
1123
+ !Object.prototype.hasOwnProperty.call(message, "error");
1124
+ const errorResponse =
1125
+ isRecord(message.error) &&
1126
+ Number.isInteger(message.error.code) &&
1127
+ typeof message.error.message === "string" &&
1128
+ !Object.prototype.hasOwnProperty.call(message, "result");
1129
+ if (
1130
+ Object.prototype.hasOwnProperty.call(message, "method") ||
1131
+ (!successResponse && !errorResponse)
1132
+ )
1133
+ return;
1050
1134
  const pending = state.pendingRpc.get(message.id);
1051
1135
  if (!pending) return;
1052
1136
  window.clearTimeout(pending.timeout);
@@ -1080,6 +1164,7 @@
1080
1164
  const sessionChanged =
1081
1165
  !state.session || state.session.sessionId !== session.sessionId;
1082
1166
  if (sessionChanged) {
1167
+ state.sessionGeneration += 1;
1083
1168
  abortActiveWork();
1084
1169
  for (const item of state.items) revokePreviewUrl(item);
1085
1170
  state.items = [];
@@ -1190,21 +1275,25 @@
1190
1275
  !Array.isArray(state.toolInput.files)
1191
1276
  )
1192
1277
  return;
1278
+ const work = captureUploadWork();
1193
1279
  state.bridgeConsumptionQueued = true;
1194
1280
  void enqueueIngestion(async () => {
1281
+ if (!uploadWorkIsCurrent(work)) return;
1195
1282
  state.bridgeConsumptionQueued = false;
1196
- await consumePendingBridgeFilesNow();
1283
+ await consumePendingBridgeFilesNow(work);
1197
1284
  }).catch(() => {
1198
- state.bridgeConsumptionQueued = false;
1285
+ if (uploadWorkIsCurrent(work))
1286
+ state.bridgeConsumptionQueued = false;
1199
1287
  });
1200
1288
  }
1201
1289
 
1202
- async function consumePendingBridgeFilesNow() {
1290
+ async function consumePendingBridgeFilesNow(work) {
1203
1291
  if (
1204
1292
  state.disposed ||
1205
1293
  !state.session ||
1206
1294
  !Array.isArray(state.toolInput.files) ||
1207
- state.consumingBridgeFiles
1295
+ state.consumingBridgeFiles ||
1296
+ !uploadWorkIsCurrent(work)
1208
1297
  )
1209
1298
  return;
1210
1299
  state.consumingBridgeFiles = true;
@@ -1229,35 +1318,41 @@
1229
1318
  for (const reference of pending.slice(0, availableSlots)) {
1230
1319
  const id = normalizedBridgeReferenceId(reference);
1231
1320
  try {
1232
- const file = await downloadBridgeReference(reference);
1321
+ const file = await downloadBridgeReference(reference, work);
1322
+ requireCurrentUploadWork(work);
1233
1323
  state.consumedBridgeIds.add(id);
1234
1324
  files.push(file);
1235
1325
  } catch {
1326
+ if (!uploadWorkIsCurrent(work)) return;
1236
1327
  state.consumedBridgeIds.add(id);
1237
1328
  addBridgeFailure(reference);
1238
1329
  }
1239
1330
  }
1240
- if (files.length) await processFiles(files);
1331
+ if (files.length) await processFiles(files, work);
1241
1332
  } finally {
1242
- state.consumingBridgeFiles = false;
1243
- const hasUnconsumed =
1244
- Array.isArray(state.toolInput.files) &&
1245
- state.toolInput.files.some((reference) => {
1246
- const id = normalizedBridgeReferenceId(reference);
1247
- return id && !state.consumedBridgeIds.has(id);
1248
- });
1249
- if (
1250
- hasUnconsumed &&
1251
- hasOpenAiFunction("getFileDownloadUrl") &&
1252
- state.items.length < safeSessionLimit()
1253
- ) {
1254
- queueMicrotask(() => void consumePendingBridgeFiles());
1333
+ if (uploadWorkIsCurrent(work)) {
1334
+ state.consumingBridgeFiles = false;
1335
+ const hasUnconsumed =
1336
+ Array.isArray(state.toolInput.files) &&
1337
+ state.toolInput.files.some((reference) => {
1338
+ const id = normalizedBridgeReferenceId(reference);
1339
+ return id && !state.consumedBridgeIds.has(id);
1340
+ });
1341
+ if (
1342
+ hasUnconsumed &&
1343
+ hasOpenAiFunction("getFileDownloadUrl") &&
1344
+ state.items.length < safeSessionLimit()
1345
+ ) {
1346
+ queueMicrotask(() => void consumePendingBridgeFiles());
1347
+ }
1255
1348
  }
1256
1349
  }
1257
1350
  }
1258
1351
 
1259
1352
  function processLocalFiles(files) {
1353
+ const work = captureUploadWork();
1260
1354
  return enqueueIngestion(async () => {
1355
+ if (!uploadWorkIsCurrent(work)) return;
1261
1356
  if (
1262
1357
  state.disposed ||
1263
1358
  !state.session ||
@@ -1271,12 +1366,14 @@
1271
1366
  `Puedes subir hasta ${safeSessionLimit()} archivos por sesión.`,
1272
1367
  );
1273
1368
  }
1274
- await processFiles(files);
1369
+ await processFiles(files, work);
1275
1370
  });
1276
1371
  }
1277
1372
 
1278
1373
  function chooseLibraryFiles() {
1374
+ const work = captureUploadWork();
1279
1375
  return enqueueIngestion(async () => {
1376
+ if (!uploadWorkIsCurrent(work)) return;
1280
1377
  if (
1281
1378
  state.disposed ||
1282
1379
  !state.session ||
@@ -1293,6 +1390,7 @@
1293
1390
  return;
1294
1391
  try {
1295
1392
  const result = await bridge.selectFiles();
1393
+ requireCurrentUploadWork(work);
1296
1394
  const selected = Array.isArray(result) ? result : [];
1297
1395
  const availableSlots = Math.max(
1298
1396
  0,
@@ -1307,18 +1405,23 @@
1307
1405
  if (!fileId || !fileName || !mimeType) continue;
1308
1406
  try {
1309
1407
  files.push(
1310
- await downloadBridgeReference({
1311
- fileId,
1312
- fileName,
1313
- mimeType,
1314
- }),
1408
+ await downloadBridgeReference(
1409
+ {
1410
+ fileId,
1411
+ fileName,
1412
+ mimeType,
1413
+ },
1414
+ work,
1415
+ ),
1315
1416
  );
1316
1417
  } catch {
1418
+ if (!uploadWorkIsCurrent(work)) return;
1317
1419
  addBridgeFailure({ fileId, fileName, mimeType });
1318
1420
  }
1319
1421
  }
1320
- await processFiles(files);
1422
+ await processFiles(files, work);
1321
1423
  } catch {
1424
+ if (!uploadWorkIsCurrent(work)) return;
1322
1425
  setSessionStatus(
1323
1426
  "No se pudo abrir el selector del host. Usa el selector del navegador.",
1324
1427
  "warning",
@@ -1337,7 +1440,8 @@
1337
1440
  );
1338
1441
  }
1339
1442
 
1340
- async function downloadBridgeReference(reference) {
1443
+ async function downloadBridgeReference(reference, work) {
1444
+ requireCurrentUploadWork(work);
1341
1445
  const bridge = openAiBridge();
1342
1446
  if (!bridge || typeof bridge.getFileDownloadUrl !== "function") {
1343
1447
  throw new Error("bridge_download_unavailable");
@@ -1345,6 +1449,7 @@
1345
1449
  const fileId = normalizedBridgeReferenceId(reference);
1346
1450
  if (!fileId) throw new Error("invalid_bridge_file");
1347
1451
  const resolved = await bridge.getFileDownloadUrl({ fileId });
1452
+ requireCurrentUploadWork(work);
1348
1453
  const temporaryUrl = normalizedBridgeDownloadUrl(resolved);
1349
1454
  const parsedUrl = new URL(temporaryUrl);
1350
1455
  if (!safeBridgeDownloadUrl(parsedUrl))
@@ -1353,10 +1458,11 @@
1353
1458
  method: "GET",
1354
1459
  credentials: "omit",
1355
1460
  redirect: "error",
1356
- signal: activeAbortSignal(),
1461
+ signal: work.signal,
1357
1462
  });
1358
1463
  if (!response.ok) throw new Error("bridge_download_failed");
1359
1464
  const blob = await readBoundedBridgeBlob(response);
1465
+ requireCurrentUploadWork(work);
1360
1466
  const preferredType = boundedString(
1361
1467
  reference.mime_type || reference.mimeType || reference.type,
1362
1468
  100,
@@ -1449,13 +1555,14 @@
1449
1555
  });
1450
1556
  }
1451
1557
 
1452
- async function processFiles(fileCandidates) {
1558
+ async function processFiles(fileCandidates, work) {
1453
1559
  if (
1454
1560
  state.disposed ||
1455
1561
  !state.session ||
1456
1562
  sessionExpired() ||
1457
1563
  state.uploadBlocked ||
1458
- state.busyBatch
1564
+ state.busyBatch ||
1565
+ !uploadWorkIsCurrent(work)
1459
1566
  )
1460
1567
  return;
1461
1568
  const files = fileCandidates.filter(isBrowserFile);
@@ -1476,14 +1583,17 @@
1476
1583
  state.busyBatch = true;
1477
1584
  setPickerAvailability();
1478
1585
  for (const file of selected) {
1586
+ if (!uploadWorkIsCurrent(work)) return;
1479
1587
  const mediaKind = mediaKindForType(file.type) || "image";
1480
1588
  const item = createItem(file, mediaKind);
1589
+ item.work = work;
1481
1590
  state.items.push(item);
1482
1591
  renderItems();
1483
1592
  // Thumbnail is cosmetic and must never delay or block the upload.
1484
1593
  void upgradePreviewToDataUrl(item);
1485
- await prepareAndUpload(item);
1594
+ await prepareAndUpload(item, work);
1486
1595
  }
1596
+ if (!uploadWorkIsCurrent(work)) return;
1487
1597
  state.busyBatch = false;
1488
1598
  setPickerAvailability();
1489
1599
  if (state.items.some((item) => item.status === "uploaded")) {
@@ -1510,7 +1620,8 @@
1510
1620
  };
1511
1621
  }
1512
1622
 
1513
- async function prepareAndUpload(item) {
1623
+ async function prepareAndUpload(item, work) {
1624
+ if (!uploadWorkIsCurrent(work)) return;
1514
1625
  if (!item.file) return;
1515
1626
  item.status = "validating";
1516
1627
  item.progress = 0;
@@ -1518,20 +1629,31 @@
1518
1629
  item.failureStage = "";
1519
1630
  renderItems();
1520
1631
  try {
1521
- const inspection = await inspectFile(item.file);
1632
+ const inspection = await inspectFile(item.file, work.session);
1633
+ requireCurrentUploadWork(work);
1522
1634
  item.mediaKind = inspection.mediaKind;
1523
1635
  item.uploadFile =
1524
1636
  inspection.mediaKind === "image"
1525
1637
  ? await compressImage(item.file, inspection)
1526
1638
  : item.file;
1639
+ requireCurrentUploadWork(work);
1527
1640
  emitAnalytics("mcp_media_upload_started", item.mediaKind);
1528
- await uploadWithRetries(item);
1641
+ await uploadWithRetries(item, work);
1642
+ requireCurrentUploadWork(work);
1529
1643
  item.status = "uploaded";
1530
1644
  item.progress = 100;
1531
1645
  item.error = "";
1532
1646
  item.slotReserved = false;
1533
1647
  emitAnalytics("mcp_media_upload_completed", item.mediaKind);
1534
1648
  } catch (error) {
1649
+ if (!uploadWorkIsCurrent(work)) {
1650
+ void releaseUploadSlot(item);
1651
+ emitAnalytics("mcp_media_upload_error", item.mediaKind, {
1652
+ failureStage: error?.safeStage || "upload",
1653
+ errorType: "upload_cancelled",
1654
+ });
1655
+ return;
1656
+ }
1535
1657
  const failure = safeFailure(error);
1536
1658
  item.status = "failed";
1537
1659
  item.error = failure.message;
@@ -1562,7 +1684,7 @@
1562
1684
  renderItems();
1563
1685
  }
1564
1686
 
1565
- async function inspectFile(file) {
1687
+ async function inspectFile(file, session) {
1566
1688
  const mediaKind = mediaKindForType(file.type);
1567
1689
  if (!ACCEPTED_TYPES.has(file.type) || !mediaKind) {
1568
1690
  throw validationError(
@@ -1571,18 +1693,15 @@
1571
1693
  );
1572
1694
  }
1573
1695
  if (
1574
- state.session.mediaKind !== "mixed" &&
1575
- state.session.mediaKind !== mediaKind
1696
+ session.mediaKind !== "mixed" &&
1697
+ session.mediaKind !== mediaKind
1576
1698
  ) {
1577
1699
  throw validationError(
1578
1700
  "wrong_media_kind",
1579
1701
  "El archivo no coincide con el tipo solicitado.",
1580
1702
  );
1581
1703
  }
1582
- if (
1583
- state.session.assetRole === "thumbnail" &&
1584
- mediaKind !== "image"
1585
- ) {
1704
+ if (session.assetRole === "thumbnail" && mediaKind !== "image") {
1586
1705
  throw validationError(
1587
1706
  "thumbnail_requires_image",
1588
1707
  "La miniatura debe ser una imagen.",
@@ -1818,35 +1937,33 @@
1818
1937
  });
1819
1938
  }
1820
1939
 
1821
- async function uploadWithRetries(item) {
1940
+ async function uploadWithRetries(item, work) {
1822
1941
  let lastError = null;
1823
1942
  for (let attempt = 1; attempt <= MAX_UPLOAD_ATTEMPTS; attempt += 1) {
1943
+ requireCurrentUploadWork(work);
1824
1944
  item.attempts = attempt;
1825
1945
  item.status = "uploading";
1826
1946
  item.progress = 0;
1827
1947
  renderItems();
1828
1948
  try {
1829
- const resolved = await resolveUpload(
1830
- item.uploadFile,
1831
- item.uploadId,
1832
- );
1833
- // From here the server holds a slot for this assetId until the
1834
- // object lands or the TTL expires.
1835
- item.slotReserved = true;
1949
+ const resolved = await resolveUpload(item, work);
1950
+ requireCurrentUploadWork(work);
1836
1951
  await putFileWithProgress(
1837
1952
  resolved.uploadUrl,
1838
1953
  item.uploadFile,
1839
1954
  (progress) => {
1955
+ if (!uploadWorkIsCurrent(work)) return;
1840
1956
  item.progress = progress;
1841
1957
  renderItems();
1842
1958
  },
1843
1959
  );
1960
+ requireCurrentUploadWork(work);
1844
1961
  item.assetId = resolved.assetId;
1845
1962
  return;
1846
1963
  } catch (error) {
1847
1964
  if (error && error.message === "session_expired") throw error;
1848
1965
  if (error && error.permanent) throw error;
1849
- if (state.disposed || state.workCancelled) throw error;
1966
+ if (!uploadWorkIsCurrent(work)) throw error;
1850
1967
  lastError = error;
1851
1968
  }
1852
1969
  }
@@ -1863,17 +1980,8 @@
1863
1980
  throw error;
1864
1981
  }
1865
1982
 
1866
- /**
1867
- * Classifies a failed resolve.
1868
- *
1869
- * 410 means the upload link is spent and the whole session is dead; any
1870
- * other 4xx means the API refused this particular file (unsupported type,
1871
- * session already at its file limit). Both are deterministic, and
1872
- * retrying them three times only produced three identical failures before
1873
- * telling the user to "retry" something that could never succeed. Only a
1874
- * server fault or a network blip earns another attempt, so anything below
1875
- * 500 stops at once and carries the server's own reason to the card.
1876
- */
1983
+ // Deterministic API refusals stop immediately. Transport timeouts and
1984
+ // throttling remain retryable within the same bounded upload identity.
1877
1985
  async function resolveFailure(response) {
1878
1986
  const status = response.status;
1879
1987
  const expired = status === 401 || status === 410;
@@ -1882,7 +1990,7 @@
1882
1990
  );
1883
1991
  error.safeCode = expired ? "session_expired" : "resolve_rejected";
1884
1992
  error.safeStage = "resolve";
1885
- error.permanent = status < 500;
1993
+ error.permanent = status < 500 && ![408, 425, 429].includes(status);
1886
1994
  error.safeMessage = await resolveFailureMessage(response);
1887
1995
  return error;
1888
1996
  }
@@ -1928,12 +2036,15 @@
1928
2036
  * still frees the slot on its own, so no error here may reach the user.
1929
2037
  */
1930
2038
  async function releaseUploadSlot(item) {
1931
- if (!item || !item.slotReserved || !state.session) return;
2039
+ if (!item || !item.slotReserved || !item.work) return;
1932
2040
  item.slotReserved = false;
1933
2041
  try {
1934
- const uploadPage = new URL(state.session.uploadPageUrl);
2042
+ const uploadPage = new URL(item.work.session.uploadPageUrl);
1935
2043
  const releaseUrl = new URL(
1936
- state.session.resolveEndpoint.replace(/\/resolve$/, "/release"),
2044
+ item.work.session.resolveEndpoint.replace(
2045
+ /\/resolve$/,
2046
+ "/release",
2047
+ ),
1937
2048
  uploadPage.origin,
1938
2049
  );
1939
2050
  if (!safeReleaseEndpointUrl(releaseUrl)) return;
@@ -1953,10 +2064,11 @@
1953
2064
  }
1954
2065
  }
1955
2066
 
1956
- async function resolveUpload(file, uploadId) {
1957
- const uploadPage = new URL(state.session.uploadPageUrl);
2067
+ async function resolveUpload(item, work) {
2068
+ requireCurrentUploadWork(work);
2069
+ const uploadPage = new URL(work.session.uploadPageUrl);
1958
2070
  const resolveUrl = new URL(
1959
- state.session.resolveEndpoint,
2071
+ work.session.resolveEndpoint,
1960
2072
  uploadPage.origin,
1961
2073
  );
1962
2074
  if (
@@ -1977,26 +2089,41 @@
1977
2089
  headers: { "content-type": "application/json" },
1978
2090
  body: JSON.stringify({
1979
2091
  token,
1980
- uploadId,
1981
- contentType: file.type,
1982
- fileName: file.name,
1983
- fileSize: file.size,
2092
+ uploadId: item.uploadId,
2093
+ contentType: item.uploadFile.type,
2094
+ fileName: item.uploadFile.name,
2095
+ fileSize: item.uploadFile.size,
1984
2096
  }),
1985
2097
  credentials: "omit",
1986
2098
  redirect: "error",
1987
- signal: activeAbortSignal(),
2099
+ signal: work.signal,
1988
2100
  });
1989
2101
  if (!response.ok) {
1990
2102
  throw await resolveFailure(response);
1991
2103
  }
2104
+ // A successful resolver response already owns a server reservation,
2105
+ // even if its body is invalid or this session was replaced meanwhile.
2106
+ // Track it before parsing so terminal cleanup releases the original slot.
2107
+ item.slotReserved = true;
2108
+ requireCurrentUploadWork(work);
1992
2109
  const body = await response.json();
1993
2110
  if (!isRecord(body)) throw new Error("invalid_resolve_response");
1994
- const uploadUrl = boundedString(body.uploadUrl, 3000);
1995
2111
  const assetId = boundedString(body.assetId, 300);
1996
- const parsedUploadUrl = new URL(uploadUrl);
1997
- if (!safeWebProtocol(parsedUploadUrl) || !assetId) {
2112
+ let parsedUploadUrl;
2113
+ try {
2114
+ if (
2115
+ typeof body.uploadUrl !== "string" ||
2116
+ !body.uploadUrl ||
2117
+ body.uploadUrl.length > 3000
2118
+ )
2119
+ throw new Error("invalid_resolve_response");
2120
+ parsedUploadUrl = new URL(body.uploadUrl);
2121
+ if (!safeWebProtocol(parsedUploadUrl) || !assetId)
2122
+ throw new Error("invalid_resolve_response");
2123
+ } catch {
1998
2124
  const error = new Error("invalid_resolve_response");
1999
2125
  error.safeStage = "resolve";
2126
+ error.permanent = true;
2000
2127
  throw error;
2001
2128
  }
2002
2129
  return { uploadUrl: parsedUploadUrl.toString(), assetId };
@@ -2140,6 +2267,9 @@
2140
2267
  request.addEventListener("timeout", () =>
2141
2268
  finish(() => reject(uploadTransportError("upload_timeout"))),
2142
2269
  );
2270
+ request.addEventListener("abort", () =>
2271
+ finish(() => reject(uploadTransportError("upload_cancelled"))),
2272
+ );
2143
2273
  request.send(file);
2144
2274
  });
2145
2275
  }
@@ -2183,8 +2313,10 @@
2183
2313
  item.failureCode === "resolve_rejected"
2184
2314
  )
2185
2315
  return;
2316
+ const work = item.work;
2317
+ if (!uploadWorkIsCurrent(work)) return;
2186
2318
  item.attempts = 0;
2187
- await prepareAndUpload(item);
2319
+ await prepareAndUpload(item, work);
2188
2320
  if (item.status === "uploaded") await refreshSessionStatus();
2189
2321
  }
2190
2322
 
@@ -2245,6 +2377,7 @@
2245
2377
  let source;
2246
2378
  try {
2247
2379
  source = await decodeImageSource(item.file);
2380
+ if (item.work && !uploadWorkIsCurrent(item.work)) return;
2248
2381
  const { width, height } = sourceDimensions(source);
2249
2382
  if (!validDimension(width) || !validDimension(height)) return;
2250
2383
  const scale = Math.min(
@@ -2423,6 +2556,7 @@
2423
2556
  async function refreshSessionStatus({ onMount = false } = {}) {
2424
2557
  if (!state.session) return;
2425
2558
  const requestedSessionId = state.session.sessionId;
2559
+ const requestedGeneration = state.sessionGeneration;
2426
2560
  if (!onMount)
2427
2561
  setSessionStatus("Comprobando el staging privado…", "neutral");
2428
2562
  try {
@@ -2432,7 +2566,8 @@
2432
2566
  if (
2433
2567
  state.disposed ||
2434
2568
  !state.session ||
2435
- state.session.sessionId !== requestedSessionId
2569
+ state.session.sessionId !== requestedSessionId ||
2570
+ state.sessionGeneration !== requestedGeneration
2436
2571
  )
2437
2572
  return;
2438
2573
  const status = parseStatusResult(result);
@@ -2476,7 +2611,13 @@
2476
2611
  partial ? "warning" : "success",
2477
2612
  );
2478
2613
  } catch (error) {
2479
- if (onMount) return;
2614
+ if (
2615
+ onMount ||
2616
+ state.disposed ||
2617
+ state.session?.sessionId !== requestedSessionId ||
2618
+ state.sessionGeneration !== requestedGeneration
2619
+ )
2620
+ return;
2480
2621
  const expired =
2481
2622
  sessionExpired() ||
2482
2623
  (error && /expired|expirad/i.test(String(error.message)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "12.2.5",
3
+ "version": "12.2.7",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "ProductMaker MCP server — cost products, generate landing pages, UGC videos, and ad creatives, and publish to Shopify, Meta Ads, and TikTok Ads.",