sentinelayer-cli 0.28.2 → 0.28.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.28.2",
3
+ "version": "0.28.4",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -145,6 +145,7 @@ const SESSION_SAY_LOCAL_ONLY_REASONS = new Set([
145
145
  "not_authenticated",
146
146
  "remote_sync_disabled_env",
147
147
  ]);
148
+ const SESSION_READ_AUTO_VIEW_TIMEOUT_MS = 500;
148
149
 
149
150
  function isLocalOnlySessionSayReason(reason) {
150
151
  return SESSION_SAY_LOCAL_ONLY_REASONS.has(normalizeString(reason));
@@ -625,11 +626,76 @@ function sessionReadViewTarget(event = {}) {
625
626
  };
626
627
  }
627
628
 
628
- async function recordSessionReadViews(sessionId, events = [], {
629
+ function normalizeAutoViewTimeoutMs(value) {
630
+ const parsed = Number(value);
631
+ if (!Number.isFinite(parsed) || parsed <= 0) {
632
+ return SESSION_READ_AUTO_VIEW_TIMEOUT_MS;
633
+ }
634
+ return Math.max(50, Math.min(5_000, Math.floor(parsed)));
635
+ }
636
+
637
+ async function writeSessionReadViews(sessionId, writeTargets = [], {
638
+ targetPath,
639
+ agentId,
640
+ timeoutMs = SESSION_READ_AUTO_VIEW_TIMEOUT_MS,
641
+ } = {}) {
642
+ if (!Array.isArray(writeTargets) || writeTargets.length === 0) {
643
+ return;
644
+ }
645
+ const totalTimeoutMs = normalizeAutoViewTimeoutMs(timeoutMs);
646
+ const controller = new AbortController();
647
+ const startedAt = Date.now();
648
+ const timeoutHandle = setTimeout(() => {
649
+ try {
650
+ controller.abort();
651
+ } catch {
652
+ // ignore
653
+ }
654
+ }, totalTimeoutMs);
655
+ if (typeof timeoutHandle.unref === "function") {
656
+ timeoutHandle.unref();
657
+ }
658
+
659
+ try {
660
+ for (const target of writeTargets) {
661
+ if (controller.signal.aborted) {
662
+ break;
663
+ }
664
+ const elapsedMs = Math.max(0, Date.now() - startedAt);
665
+ const remainingMs = Math.max(1, totalTimeoutMs - elapsedMs);
666
+ const result = await createSessionMessageAction(sessionId, {
667
+ actionType: "view",
668
+ targetPath,
669
+ targetSequenceId: target.targetSequenceId,
670
+ targetCursor: target.targetCursor,
671
+ metadata: {
672
+ source: "cli_read",
673
+ agentId,
674
+ },
675
+ idempotencyKey: defaultActionIdempotencyKey({
676
+ actionType: "view",
677
+ targetSequenceId: target.targetSequenceId,
678
+ targetCursor: target.targetCursor,
679
+ agentId,
680
+ }),
681
+ timeoutMs: remainingMs,
682
+ signal: controller.signal,
683
+ });
684
+ if (!result?.ok) {
685
+ break;
686
+ }
687
+ }
688
+ } finally {
689
+ clearTimeout(timeoutHandle);
690
+ }
691
+ }
692
+
693
+ function recordSessionReadViews(sessionId, events = [], {
629
694
  targetPath,
630
695
  agentId,
631
696
  enabled = false,
632
697
  maxTargets = 50,
698
+ timeoutMs = SESSION_READ_AUTO_VIEW_TIMEOUT_MS,
633
699
  } = {}) {
634
700
  const maxAutoViewTargets = Math.max(0, Math.min(200, Number(maxTargets) || 0));
635
701
  const summary = {
@@ -641,6 +707,8 @@ async function recordSessionReadViews(sessionId, events = [], {
641
707
  duplicates: 0,
642
708
  failed: 0,
643
709
  skipped: 0,
710
+ queued: 0,
711
+ background: false,
644
712
  reason: "",
645
713
  };
646
714
  if (!summary.enabled) {
@@ -662,42 +730,31 @@ async function recordSessionReadViews(sessionId, events = [], {
662
730
 
663
731
  const writeTargets = maxAutoViewTargets > 0 ? targets.slice(-maxAutoViewTargets) : [];
664
732
  summary.skipped = Math.max(0, targets.length - writeTargets.length);
733
+ summary.queued = writeTargets.length;
665
734
  if (summary.skipped > 0) {
666
735
  summary.reason = "target_cap_reached";
667
736
  }
668
737
 
669
- for (const target of writeTargets) {
670
- const result = await createSessionMessageAction(sessionId, {
671
- actionType: "view",
672
- targetPath,
673
- targetSequenceId: target.targetSequenceId,
674
- targetCursor: target.targetCursor,
675
- metadata: {
676
- source: "cli_read",
677
- agentId: summary.agentId,
678
- },
679
- idempotencyKey: defaultActionIdempotencyKey({
680
- actionType: "view",
681
- targetSequenceId: target.targetSequenceId,
682
- targetCursor: target.targetCursor,
683
- agentId: summary.agentId,
684
- }),
685
- timeoutMs: 15_000,
686
- });
687
- summary.attempted += 1;
688
- if (result?.ok && result.action) {
689
- summary.recorded += 1;
690
- if (result.duplicate) {
691
- summary.duplicates += 1;
692
- }
693
- continue;
738
+ if (writeTargets.length === 0) {
739
+ if (!summary.reason) {
740
+ summary.reason = "no_targets";
694
741
  }
695
- summary.failed += 1;
696
- summary.reason = normalizeString(result?.reason) || "view_write_failed";
697
- summary.skipped = Math.max(0, targets.length - summary.attempted);
698
- break;
742
+ return summary;
743
+ }
744
+
745
+ summary.background = true;
746
+ if (!summary.reason) {
747
+ summary.reason = "queued_best_effort";
699
748
  }
700
749
 
750
+ void writeSessionReadViews(sessionId, writeTargets, {
751
+ targetPath,
752
+ agentId: summary.agentId,
753
+ timeoutMs,
754
+ }).catch(() => {
755
+ // Best-effort view receipts must never affect transcript rendering.
756
+ });
757
+
701
758
  return summary;
702
759
  }
703
760
 
@@ -4652,11 +4709,12 @@ export function registerSessionCommand(program) {
4652
4709
  : displayEvents.filter((event) => !isSessionControlEvent(event));
4653
4710
  const hiddenControlEventCount = displayEvents.length - transcriptEvents.length;
4654
4711
  const events = mergeSessionActionEvents(transcriptEvents, actionEvents).slice(-tail);
4655
- const autoView = await recordSessionReadViews(normalizedSessionId, events, {
4712
+ const autoView = recordSessionReadViews(normalizedSessionId, events, {
4656
4713
  targetPath,
4657
4714
  agentId: readAgentId,
4658
4715
  enabled: Boolean(options.remote && options.view !== false),
4659
4716
  maxTargets: process.env.SENTINELAYER_SESSION_READ_VIEW_MAX_TARGETS || 50,
4717
+ timeoutMs: process.env.SENTINELAYER_SESSION_READ_VIEW_TIMEOUT_MS || SESSION_READ_AUTO_VIEW_TIMEOUT_MS,
4660
4718
  });
4661
4719
  const remoteVerified = Boolean(
4662
4720
  options.remote &&
@@ -311,6 +311,7 @@ export async function listenSessionEvents({
311
311
  const latest = await _pollLatest(normalizedSessionId, {
312
312
  targetPath,
313
313
  limit: 1,
314
+ forceCircuitProbe: true,
314
315
  });
315
316
  if (!latest?.ok) {
316
317
  throw new Error(`Unable to start listener from the latest event (${latest?.reason || "unknown"}).`);
@@ -172,17 +172,6 @@ function resolveGrantRole(rawRole) {
172
172
  return AUTO_GRANT_DEFAULT_ROLE;
173
173
  }
174
174
 
175
- async function readResponseJsonSafely(response) {
176
- if (!response || typeof response.json !== "function") {
177
- return null;
178
- }
179
- try {
180
- return await response.json();
181
- } catch {
182
- return null;
183
- }
184
- }
185
-
186
175
  function isIdentityForgeryBody(body) {
187
176
  if (!body || typeof body !== "object") return false;
188
177
  const error = body.error;
@@ -412,6 +401,184 @@ function enforceRollingLimit(windowByKey, key, {
412
401
  };
413
402
  }
414
403
 
404
+ function makeSyncTimeoutError(reason) {
405
+ const error = new Error(normalizeString(reason) || "request_timeout");
406
+ error.name = "AbortError";
407
+ error.code = "SYNC_REQUEST_TIMEOUT";
408
+ return error;
409
+ }
410
+
411
+ function combineAbortSignals(signals = []) {
412
+ const activeSignals = signals.filter((signal) => signal && typeof signal === "object");
413
+ if (activeSignals.length === 0) return undefined;
414
+ if (activeSignals.length === 1) return activeSignals[0];
415
+ if (typeof AbortSignal !== "undefined" && typeof AbortSignal.any === "function") {
416
+ return AbortSignal.any(activeSignals);
417
+ }
418
+
419
+ const controller = new AbortController();
420
+ const abort = () => {
421
+ try {
422
+ controller.abort();
423
+ } catch {
424
+ // ignore
425
+ }
426
+ for (const signal of activeSignals) {
427
+ try {
428
+ signal.removeEventListener("abort", abort);
429
+ } catch {
430
+ // ignore
431
+ }
432
+ }
433
+ };
434
+ for (const signal of activeSignals) {
435
+ if (signal.aborted) {
436
+ abort();
437
+ break;
438
+ }
439
+ try {
440
+ signal.addEventListener("abort", abort, { once: true });
441
+ } catch {
442
+ // ignore
443
+ }
444
+ }
445
+ return controller.signal;
446
+ }
447
+
448
+ function abortRequestController(controller, reason) {
449
+ if (!controller || controller.signal?.aborted) return;
450
+ try {
451
+ controller.abort(makeSyncTimeoutError(reason));
452
+ } catch {
453
+ try {
454
+ controller.abort();
455
+ } catch {
456
+ // ignore
457
+ }
458
+ }
459
+ }
460
+
461
+ function cancelResponseBody(response) {
462
+ try {
463
+ if (response?.body && typeof response.body.cancel === "function") {
464
+ void response.body.cancel();
465
+ }
466
+ } catch {
467
+ // Best-effort socket/body cleanup only.
468
+ }
469
+ }
470
+
471
+ function isSyncTimeoutError(error) {
472
+ return error?.code === "SYNC_REQUEST_TIMEOUT";
473
+ }
474
+
475
+ function withTimeout(promise, timeoutMs, { reason = "request_timeout", onTimeout = null } = {}) {
476
+ const resolvedTimeoutMs = normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS);
477
+ return new Promise((resolve, reject) => {
478
+ let settled = false;
479
+ const timeoutHandle = setTimeout(() => {
480
+ if (settled) return;
481
+ settled = true;
482
+ try {
483
+ onTimeout?.();
484
+ } catch {
485
+ // ignore
486
+ }
487
+ reject(makeSyncTimeoutError(reason));
488
+ }, resolvedTimeoutMs);
489
+ const finish = (fn, value) => {
490
+ if (settled) return;
491
+ settled = true;
492
+ clearTimeout(timeoutHandle);
493
+ fn(value);
494
+ };
495
+
496
+ Promise.resolve(promise).then(
497
+ (value) => finish(resolve, value),
498
+ (error) => finish(reject, error),
499
+ );
500
+ });
501
+ }
502
+
503
+ async function readResponseJsonWithTimeout(
504
+ response,
505
+ timeoutMs,
506
+ {
507
+ timeoutReason = "response_body_timeout",
508
+ onTimeout = null,
509
+ fallback = undefined,
510
+ } = {},
511
+ ) {
512
+ if (!response || typeof response.json !== "function") {
513
+ return fallback;
514
+ }
515
+ try {
516
+ return await withTimeout(response.json(), timeoutMs, {
517
+ reason: timeoutReason,
518
+ onTimeout: () => {
519
+ try {
520
+ onTimeout?.();
521
+ } finally {
522
+ cancelResponseBody(response);
523
+ }
524
+ },
525
+ });
526
+ } catch (error) {
527
+ if (isSyncTimeoutError(error)) {
528
+ throw error;
529
+ }
530
+ if (fallback !== undefined) {
531
+ return fallback;
532
+ }
533
+ throw error;
534
+ }
535
+ }
536
+
537
+ async function readResponseJsonSafely(response, timeoutMs = DEFAULT_SYNC_TIMEOUT_MS) {
538
+ try {
539
+ return await readResponseJsonWithTimeout(response, timeoutMs, { fallback: null });
540
+ } catch {
541
+ return null;
542
+ }
543
+ }
544
+
545
+ async function fetchJsonWithFullTimeout(
546
+ url,
547
+ options,
548
+ timeoutMs,
549
+ fetchImpl = fetchWithTimeout,
550
+ { readErrorBody = false } = {},
551
+ ) {
552
+ const resolvedTimeoutMs = normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS);
553
+ const controller = new AbortController();
554
+ const startedAtMs = Date.now();
555
+ let response = null;
556
+ const requestOptions = {
557
+ ...options,
558
+ signal: combineAbortSignals([options?.signal, controller.signal]),
559
+ };
560
+
561
+ response = await withTimeout(
562
+ fetchImpl(url, requestOptions, resolvedTimeoutMs),
563
+ resolvedTimeoutMs,
564
+ {
565
+ reason: "request_timeout",
566
+ onTimeout: () => abortRequestController(controller, "request_timeout"),
567
+ },
568
+ );
569
+
570
+ let payload = {};
571
+ if (response?.ok || readErrorBody) {
572
+ const elapsedMs = Math.max(0, Date.now() - startedAtMs);
573
+ const remainingMs = Math.max(1, resolvedTimeoutMs - elapsedMs);
574
+ payload = await readResponseJsonWithTimeout(response, remainingMs, {
575
+ fallback: {},
576
+ onTimeout: () => abortRequestController(controller, "response_body_timeout"),
577
+ });
578
+ }
579
+ return { response, payload };
580
+ }
581
+
415
582
  async function fetchWithTimeout(url, options, timeoutMs) {
416
583
  const controller = new AbortController();
417
584
  const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
@@ -421,7 +588,7 @@ async function fetchWithTimeout(url, options, timeoutMs) {
421
588
  try {
422
589
  return await fetch(url, {
423
590
  ...options,
424
- signal: controller.signal,
591
+ signal: combineAbortSignals([options?.signal, controller.signal]),
425
592
  });
426
593
  } finally {
427
594
  clearTimeout(timeoutHandle);
@@ -1008,7 +1175,7 @@ export async function pollHumanMessages(
1008
1175
  const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/human-messages?${query.toString()}`;
1009
1176
 
1010
1177
  try {
1011
- const response = await fetchImpl(
1178
+ const { response, payload } = await fetchJsonWithFullTimeout(
1012
1179
  endpoint,
1013
1180
  {
1014
1181
  method: "GET",
@@ -1016,7 +1183,8 @@ export async function pollHumanMessages(
1016
1183
  Authorization: `Bearer ${session.token}`,
1017
1184
  },
1018
1185
  },
1019
- normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS)
1186
+ normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
1187
+ fetchImpl,
1020
1188
  );
1021
1189
  if (!response || !response.ok) {
1022
1190
  recordCircuitFailure(inboundCircuit, normalizedNowMs);
@@ -1027,7 +1195,6 @@ export async function pollHumanMessages(
1027
1195
  cursor: normalizedSince || null,
1028
1196
  };
1029
1197
  }
1030
- const payload = await response.json().catch(() => ({}));
1031
1198
  recordCircuitSuccess(inboundCircuit);
1032
1199
 
1033
1200
  const items = normalizeHumanMessageItems(payload);
@@ -1171,13 +1338,14 @@ export async function pollSessionEvents(
1171
1338
  const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/events?${query.toString()}`;
1172
1339
 
1173
1340
  try {
1174
- const response = await fetchImpl(
1341
+ const { response, payload } = await fetchJsonWithFullTimeout(
1175
1342
  endpoint,
1176
1343
  {
1177
1344
  method: "GET",
1178
1345
  headers: { Authorization: `Bearer ${session.token}` },
1179
1346
  },
1180
- normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS)
1347
+ normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
1348
+ fetchImpl,
1181
1349
  );
1182
1350
  if (!response || !response.ok) {
1183
1351
  recordCircuitFailure(inboundCircuit, normalizedNowMs);
@@ -1188,7 +1356,6 @@ export async function pollSessionEvents(
1188
1356
  cursor: normalizedSince || null,
1189
1357
  };
1190
1358
  }
1191
- const payload = await response.json().catch(() => ({}));
1192
1359
  recordCircuitSuccess(inboundCircuit);
1193
1360
 
1194
1361
  const items = Array.isArray(payload?.events) ? payload.events : [];
@@ -1491,13 +1658,14 @@ export async function pollSessionEventsBefore(
1491
1658
  const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/events/before?${query.toString()}`;
1492
1659
 
1493
1660
  try {
1494
- const response = await fetchImpl(
1661
+ const { response, payload } = await fetchJsonWithFullTimeout(
1495
1662
  endpoint,
1496
1663
  {
1497
1664
  method: "GET",
1498
1665
  headers: { Authorization: `Bearer ${session.token}` },
1499
1666
  },
1500
- normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS)
1667
+ normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
1668
+ fetchImpl,
1501
1669
  );
1502
1670
  if (!response || !response.ok) {
1503
1671
  recordCircuitFailure(inboundCircuit, normalizedNowMs);
@@ -1509,7 +1677,6 @@ export async function pollSessionEventsBefore(
1509
1677
  beforeSequence: Number.isFinite(normalizedBeforeSequence) ? normalizedBeforeSequence : null,
1510
1678
  };
1511
1679
  }
1512
- const payload = await response.json().catch(() => ({}));
1513
1680
  recordCircuitSuccess(inboundCircuit);
1514
1681
 
1515
1682
  const events = chronologicalSessionEvents(payload?.events || []);
@@ -1600,13 +1767,14 @@ export async function listSessionMessageActions(
1600
1767
  const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/actions?${query.toString()}`;
1601
1768
 
1602
1769
  try {
1603
- const response = await fetchImpl(
1770
+ const { response, payload } = await fetchJsonWithFullTimeout(
1604
1771
  endpoint,
1605
1772
  {
1606
1773
  method: "GET",
1607
1774
  headers: { Authorization: `Bearer ${session.token}` },
1608
1775
  },
1609
- normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS)
1776
+ normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
1777
+ fetchImpl,
1610
1778
  );
1611
1779
  if (!response || !response.ok) {
1612
1780
  recordCircuitFailure(inboundCircuit, normalizedNowMs);
@@ -1618,7 +1786,6 @@ export async function listSessionMessageActions(
1618
1786
  projection: null,
1619
1787
  };
1620
1788
  }
1621
- const payload = await response.json().catch(() => ({}));
1622
1789
  recordCircuitSuccess(inboundCircuit);
1623
1790
  const actions = Array.isArray(payload?.actions) ? payload.actions : [];
1624
1791
  return {
@@ -1772,6 +1939,7 @@ export async function createSessionMessageAction(
1772
1939
  metadata = {},
1773
1940
  idempotencyKey = "",
1774
1941
  timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
1942
+ signal = undefined,
1775
1943
  resolveAuthSession = resolveActiveAuthSession,
1776
1944
  fetchImpl = fetchWithTimeout,
1777
1945
  nowMs = Date.now,
@@ -1833,7 +2001,7 @@ export async function createSessionMessageAction(
1833
2001
  }
1834
2002
 
1835
2003
  try {
1836
- const response = await fetchImpl(
2004
+ const { response, payload } = await fetchJsonWithFullTimeout(
1837
2005
  endpoint,
1838
2006
  {
1839
2007
  method: "POST",
@@ -1842,8 +2010,10 @@ export async function createSessionMessageAction(
1842
2010
  Authorization: `Bearer ${session.token}`,
1843
2011
  },
1844
2012
  body: JSON.stringify(body),
2013
+ signal,
1845
2014
  },
1846
- normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS)
2015
+ normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
2016
+ fetchImpl,
1847
2017
  );
1848
2018
  if (!response || !response.ok) {
1849
2019
  recordCircuitFailure(outboundCircuit, normalizedNowMs);
@@ -1853,7 +2023,6 @@ export async function createSessionMessageAction(
1853
2023
  action: null,
1854
2024
  };
1855
2025
  }
1856
- const payload = await response.json().catch(() => ({}));
1857
2026
  recordCircuitSuccess(outboundCircuit);
1858
2027
  return {
1859
2028
  ok: Boolean(payload?.ok ?? true),
@@ -1955,13 +2124,14 @@ export async function searchSessionEvents(
1955
2124
  const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/events/search?${queryParams.toString()}`;
1956
2125
 
1957
2126
  try {
1958
- const response = await fetchImpl(
2127
+ const { response, payload } = await fetchJsonWithFullTimeout(
1959
2128
  endpoint,
1960
2129
  {
1961
2130
  method: "GET",
1962
2131
  headers: { Authorization: `Bearer ${session.token}` },
1963
2132
  },
1964
- normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS)
2133
+ normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
2134
+ fetchImpl,
1965
2135
  );
1966
2136
  if (!response || !response.ok) {
1967
2137
  recordCircuitFailure(inboundCircuit, normalizedNowMs);
@@ -1975,7 +2145,6 @@ export async function searchSessionEvents(
1975
2145
  nextBeforeSequence: null,
1976
2146
  };
1977
2147
  }
1978
- const payload = await response.json().catch(() => ({}));
1979
2148
  recordCircuitSuccess(inboundCircuit);
1980
2149
  const results = Array.isArray(payload?.results) ? payload.results : [];
1981
2150
  return {
@@ -2081,15 +2250,17 @@ export async function listSessionsFromApi({
2081
2250
  const endpoint = `${apiBaseUrl}/api/v1/sessions?${query.toString()}`;
2082
2251
 
2083
2252
  let response;
2253
+ let payload;
2084
2254
  try {
2085
- response = await fetchImpl(
2255
+ ({ response, payload } = await fetchJsonWithFullTimeout(
2086
2256
  endpoint,
2087
2257
  {
2088
2258
  method: "GET",
2089
2259
  headers: { Authorization: `Bearer ${session.token}` },
2090
2260
  },
2091
2261
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
2092
- );
2262
+ fetchImpl,
2263
+ ));
2093
2264
  } catch (err) {
2094
2265
  return {
2095
2266
  ok: false,
@@ -2114,7 +2285,6 @@ export async function listSessionsFromApi({
2114
2285
  warnings: [],
2115
2286
  };
2116
2287
  }
2117
- const payload = await response.json().catch(() => ({}));
2118
2288
  const pageSessions = Array.isArray(payload?.sessions) ? payload.sessions : [];
2119
2289
  for (const item of pageSessions) {
2120
2290
  const sessionId = normalizeString(item?.sessionId || item?.id);
@@ -2200,15 +2370,17 @@ export async function fetchSessionFromApi(
2200
2370
  const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}`;
2201
2371
 
2202
2372
  let response;
2373
+ let body;
2203
2374
  try {
2204
- response = await fetchImpl(
2375
+ ({ response, payload: body } = await fetchJsonWithFullTimeout(
2205
2376
  endpoint,
2206
2377
  {
2207
2378
  method: "GET",
2208
2379
  headers: { Authorization: `Bearer ${session.token}` },
2209
2380
  },
2210
2381
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
2211
- );
2382
+ fetchImpl,
2383
+ ));
2212
2384
  } catch (err) {
2213
2385
  return {
2214
2386
  ok: false,
@@ -2220,7 +2392,6 @@ export async function fetchSessionFromApi(
2220
2392
  return { ok: false, reason: "no_response", session: null };
2221
2393
  }
2222
2394
  if (response.ok) {
2223
- const body = await response.json().catch(() => ({}));
2224
2395
  const sessionPayload = body && body.session && typeof body.session === "object"
2225
2396
  ? body.session
2226
2397
  : body && typeof body === "object"