@getmarrow/mcp 3.9.83 → 3.9.85

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/dist/index.js CHANGED
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.MARROW_AUTO_RESPONSE_BUDGET_MAX_MS = void 0;
7
7
  exports.validatePathParam = validatePathParam;
8
8
  exports.validateBaseUrl = validateBaseUrl;
9
+ exports.marrowAutoHttpTraceFromError = marrowAutoHttpTraceFromError;
9
10
  exports.marrowCreateKey = marrowCreateKey;
10
11
  exports.marrowListKeys = marrowListKeys;
11
12
  exports.marrowGetKey = marrowGetKey;
@@ -73,10 +74,12 @@ const SAFE_ARBITRATION_EVIDENCE_KIND = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,39}$/;
73
74
  const SAFE_ARBITRATION_EVIDENCE_REFERENCE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/;
74
75
  const SAFE_OUTCOME_OBSERVATION_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/;
75
76
  const SAFE_INSTRUCTION_REFERENCE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
76
- const SAFE_IDEMPOTENCY_KEY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
77
77
  const AGENT_WRITE_RECONCILIATION_ATTEMPTS = 3;
78
78
  const AGENT_WRITE_RECONCILIATION_DELAY_MS = 1_000;
79
79
  const AUTO_MANAGED_WRITE = Symbol('marrow-auto-managed-write');
80
+ const AUTO_HTTP_TRACE = Symbol('marrow-auto-http-trace');
81
+ const AUTO_HTTP_TRACE_ERROR = Symbol('marrow-auto-http-trace-error');
82
+ const AUTO_HTTP_TRACE_LIMIT = 12;
80
83
  const INSTRUCTION_REFERENCE_LONG_DIGIT_RUN = /\d{7,}/;
81
84
  const INSTRUCTION_REFERENCE_DATE = /(?:^|[._:-])(?:(?:19|20)\d{2}[._:-](?:0?[1-9]|1[0-2])[._:-](?:0?[1-9]|[12]\d|3[01])|(?:0?[1-9]|1[0-2])[._:-](?:0?[1-9]|[12]\d|3[01])[._:-](?:19|20)\d{2})(?:$|[._:-])/;
82
85
  const INSTRUCTION_REFERENCE_PROVIDER_ID = /^(?:telegram|tg|discord|slack|signal|whatsapp|wa|matrix|imessage|message|msg|chat|user|thread|channel|room|dm|sms|device)[_:.-]?[A-Za-z]*\d+[A-Za-z0-9_-]*$/i;
@@ -300,14 +303,22 @@ async function safeJsonResponse(res) {
300
303
  if (contentType.includes('json'))
301
304
  detail = await res.json();
302
305
  }
303
- catch { /* ignore malformed or non-JSON error bodies */ }
306
+ catch (error) {
307
+ const failure = (0, request_reliability_1.normalizeRequestError)(error);
308
+ if (failure.code === 'request_timeout')
309
+ throw failure;
310
+ // Malformed error bodies do not hide the authoritative HTTP status.
311
+ }
304
312
  throw (0, request_reliability_1.requestErrorFromResponse)(res, detail);
305
313
  }
306
314
  let json;
307
315
  try {
308
316
  json = await res.json();
309
317
  }
310
- catch {
318
+ catch (error) {
319
+ const failure = (0, request_reliability_1.normalizeRequestError)(error);
320
+ if (failure.code === 'request_timeout')
321
+ throw failure;
311
322
  throw (0, request_reliability_1.invalidResponseError)();
312
323
  }
313
324
  if (!json || typeof json !== 'object' || Array.isArray(json) || json.error) {
@@ -453,14 +464,14 @@ function isDurableObservedOutcome(value) {
453
464
  }
454
465
  function invocationIdempotencyKey(kind, supplied) {
455
466
  if (supplied !== undefined) {
456
- if (supplied !== supplied.trim() || !SAFE_IDEMPOTENCY_KEY.test(supplied)) {
467
+ if (!(0, request_reliability_1.privacySafeIdempotencyKey)(supplied)) {
457
468
  throw new TypeError('Idempotency key must be a bounded privacy-safe identifier.');
458
469
  }
459
470
  return supplied;
460
471
  }
461
472
  return `mcp-${kind}:${(0, node_crypto_1.randomUUID)()}`;
462
473
  }
463
- function reconciliationError(exhausted) {
474
+ function reconciliationError(exhausted, pendingReceipt) {
464
475
  return new request_reliability_1.MarrowRequestError({
465
476
  code: 'invalid_response',
466
477
  backendCode: exhausted ? 'MCP_RECONCILIATION_EXHAUSTED' : 'MCP_RECONCILIATION_INVALID',
@@ -469,8 +480,10 @@ function reconciliationError(exhausted) {
469
480
  : 'Marrow returned an invalid write reconciliation response',
470
481
  status: 202,
471
482
  retryable: exhausted,
483
+ retryAfterMs: exhausted ? 1000 : null,
484
+ pendingReceipt,
472
485
  exactFix: exhausted
473
- ? 'Retry the same operation explicitly after checking Marrow status; do not assume the decision or outcome closed.'
486
+ ? 'Resume with the pending receipt key and request_hash, unchanged arguments, credentials, agent and session. Do not act, create another decision, or assume closure.'
474
487
  : 'Check Marrow status and retry explicitly; do not accept or act on the malformed pending response.',
475
488
  });
476
489
  }
@@ -481,27 +494,41 @@ function validCorrelationId(value) {
481
494
  && value === value.trim()
482
495
  && !/[\u0000-\u001f\u007f-\u009f]/.test(value);
483
496
  }
484
- function pendingDecisionId(kind, value, idempotencyKey, expectedDecisionId) {
485
- if (value.retryable !== true
486
- || value.committed !== false
487
- || value.idempotency_key !== idempotencyKey
488
- || !validCorrelationId(value.decision_id)) {
497
+ function pendingWriteReconciliation(kind, value, idempotencyKey, expectedDecisionId) {
498
+ if (value.retryable !== true || value.committed !== false || value.idempotency_key !== idempotencyKey) {
489
499
  return null;
490
500
  }
491
501
  if (kind === 'think') {
492
- return value.reconciliation_state === 'runtime_continuation_persistence_pending'
493
- && value.decision_state === 'created'
494
- ? value.decision_id
495
- : null;
502
+ const decisionId = value.decision_id === undefined ? null
503
+ : validCorrelationId(value.decision_id) ? value.decision_id : undefined;
504
+ if (decisionId === undefined)
505
+ return null;
506
+ const typed = value.reconciliation_contract !== undefined;
507
+ if (typed && (value.reconciliation_contract !== 'agent_write_reconciliation.v1'
508
+ || value.reconciliation_operation !== 'think' || value.safe_to_continue !== false))
509
+ return null;
510
+ const canonicalPending = value.phase === 'think_pending' && value.resumable === true
511
+ && value.safe_to_continue !== true
512
+ && value.retry_after_ms === AGENT_WRITE_RECONCILIATION_DELAY_MS;
513
+ const state = value.reconciliation_state;
514
+ const validState = state === 'runtime_continuation_persistence_pending' && value.decision_state === 'created'
515
+ || state === 'runtime_decision_authority_pending' && value.decision_state === 'pending' && decisionId === null
516
+ || state === 'pending' && (value.decision_state === 'pending' && decisionId === null
517
+ || value.decision_state === 'created');
518
+ if (canonicalPending && validState)
519
+ return { decisionId };
520
+ // Keep the earlier documented identifier-bearing continuation contract compatible.
521
+ if (!typed && decisionId && state === 'runtime_continuation_persistence_pending'
522
+ && value.decision_state === 'created' && value.safe_to_continue !== true)
523
+ return { decisionId };
524
+ return null;
496
525
  }
497
- if (value.decision_id !== expectedDecisionId)
526
+ if (!validCorrelationId(value.decision_id) || value.decision_id !== expectedDecisionId)
498
527
  return null;
499
528
  if (value.reconciliation_state === 'pending')
500
- return value.decision_id;
529
+ return { decisionId: value.decision_id };
501
530
  return value.reconciliation_state === 'runtime_continuation_invalidation_pending'
502
- && value.outcome_persisted === true
503
- ? value.decision_id
504
- : null;
531
+ && value.outcome_persisted === true ? { decisionId: value.decision_id } : null;
505
532
  }
506
533
  async function waitForWriteReconciliation(signal) {
507
534
  await new Promise((resolve, reject) => {
@@ -523,19 +550,177 @@ async function waitForWriteReconciliation(signal) {
523
550
  signal?.addEventListener('abort', abort, { once: true });
524
551
  });
525
552
  }
526
- async function fetchAgentWrite(url, init, kind, idempotencyKey, expectedDecisionId, autoManaged = false) {
553
+ const AUTO_HTTP_STATE_CODES = new Set([
554
+ 'runtime_pending', 'think_pending', 'commit_pending', 'pending',
555
+ 'created', 'committed', 'closed', 'replayed', 'idempotent_replay',
556
+ 'lost_ack_recovered', 'duplicate', 'original', 'new',
557
+ ]);
558
+ function traceMs(value) {
559
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
560
+ ? Math.min(60_000, Math.round(value))
561
+ : null;
562
+ }
563
+ function traceCode(value) {
564
+ const code = typeof value === 'string' ? value.toLowerCase() : '';
565
+ return AUTO_HTTP_STATE_CODES.has(code) ? code : null;
566
+ }
567
+ function serverTimingEvidence(data) {
568
+ const timings = {};
569
+ const add = (name, value) => {
570
+ const duration = traceMs(value);
571
+ if (duration !== null && timings[name] === undefined)
572
+ timings[name] = duration;
573
+ };
574
+ const performanceShape = data?.performance;
575
+ if (performanceShape && typeof performanceShape === 'object' && !Array.isArray(performanceShape)) {
576
+ const performanceData = performanceShape;
577
+ add('auth_ms', performanceData.auth_ms);
578
+ add('parse_ms', performanceData.parse_ms);
579
+ const nestedTimings = performanceData.timings;
580
+ if (nestedTimings && typeof nestedTimings === 'object' && !Array.isArray(nestedTimings)) {
581
+ const nestedData = nestedTimings;
582
+ add('auth_ms', nestedData.auth_ms);
583
+ add('parse_ms', nestedData.parse_ms);
584
+ }
585
+ }
586
+ return {
587
+ server_timings_ms: timings,
588
+ // The backend's detailed think/commit DB and DO stage timings are deferred
589
+ // query-performance records, not response fields. These response spans can
590
+ // therefore be partial at best.
591
+ server_timing_coverage: Object.keys(timings).length ? 'partial' : 'unavailable',
592
+ };
593
+ }
594
+ function appendAutoHttpAttempt(trace, input) {
595
+ if (!trace)
596
+ return null;
597
+ if (trace.attempts.length >= AUTO_HTTP_TRACE_LIMIT) {
598
+ trace.dropped_count += 1;
599
+ return null;
600
+ }
601
+ const attempt = { ...input, actual_wait_ms: 0 };
602
+ trace.attempts.push(attempt);
603
+ return attempt;
604
+ }
605
+ function snapshotAutoHttpTrace(trace) {
606
+ return {
607
+ attempts: trace.attempts.map((attempt) => ({
608
+ ...attempt,
609
+ server_timings_ms: { ...attempt.server_timings_ms },
610
+ })),
611
+ dropped_count: trace.dropped_count,
612
+ };
613
+ }
614
+ function attachAutoHttpTrace(error, trace) {
615
+ if (error instanceof Error) {
616
+ Object.defineProperty(error, AUTO_HTTP_TRACE_ERROR, {
617
+ value: snapshotAutoHttpTrace(trace),
618
+ configurable: true,
619
+ });
620
+ }
621
+ return error;
622
+ }
623
+ function marrowAutoHttpTraceFromError(error) {
624
+ if (!(error instanceof Error))
625
+ return null;
626
+ const trace = error[AUTO_HTTP_TRACE_ERROR];
627
+ return trace ? snapshotAutoHttpTrace(trace) : null;
628
+ }
629
+ async function fetchAgentWrite(url, init, kind, idempotencyKey, expectedDecisionId, autoManaged = false, expectedRequestHash, autoHttpTrace) {
630
+ const headers = new Headers(init.headers);
631
+ // Bind the canonical wire body and authenticated scope without exposing either.
632
+ const requestHash = (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(canonicalAutoBindingValue({
633
+ url, key: idempotencyKey, body: JSON.parse(String(init.body)),
634
+ authorization: headers.get('Authorization'),
635
+ session: headers.get('X-Marrow-Session-Id'), agent: headers.get('X-Marrow-Agent-Id'),
636
+ }))).digest('hex');
637
+ if (expectedRequestHash !== undefined && (!/^[a-f0-9]{64}$/.test(expectedRequestHash)
638
+ || expectedRequestHash !== requestHash)) {
639
+ throw new TypeError('Pending request hash does not match this exact operation and authenticated scope.');
640
+ }
641
+ const pendingReceipt = {
642
+ contract: 'mcp_write_pending.v1', operation: kind, committed: false, safe_to_continue: false,
643
+ idempotency_key: idempotencyKey, request_hash: requestHash,
644
+ };
527
645
  let reconciledDecisionId = null;
646
+ // A manual receipt resume must validate its very first terminal response.
647
+ let reconciling = expectedRequestHash !== undefined;
528
648
  for (let attempt = 0; attempt < AGENT_WRITE_RECONCILIATION_ATTEMPTS; attempt += 1) {
529
649
  // Automatic writes have one retry owner and leave room for exact replay
530
650
  // after a lost ACK inside marrowAuto's unchanged total response deadline.
531
- const response = await (0, request_reliability_1.reliableFetch)(url, init, autoManaged
532
- ? { retryOwner: 'caller', timeoutMs: 4_000 }
533
- : {});
534
- const json = await safeJsonResponse(response);
651
+ const requestStarted = performance.now();
652
+ let receivedStatus = null;
653
+ let response;
654
+ let json;
655
+ try {
656
+ const consumed = await (0, request_reliability_1.reliableFetch)(url, init, autoManaged
657
+ ? {
658
+ retryOwner: 'caller',
659
+ timeoutMs: 4_000,
660
+ consumeResponse: async (currentResponse) => {
661
+ receivedStatus = currentResponse.status;
662
+ return { response: currentResponse, json: await safeJsonResponse(currentResponse) };
663
+ },
664
+ }
665
+ : {
666
+ consumeResponse: async (currentResponse) => {
667
+ receivedStatus = currentResponse.status;
668
+ return { response: currentResponse, json: await safeJsonResponse(currentResponse) };
669
+ },
670
+ });
671
+ response = consumed.response;
672
+ json = consumed.json;
673
+ }
674
+ catch (error) {
675
+ const failure = (0, request_reliability_1.normalizeRequestError)(error);
676
+ appendAutoHttpAttempt(autoHttpTrace, {
677
+ route_phase: kind,
678
+ duration_ms: traceMs(performance.now() - requestStarted) || 0,
679
+ status: receivedStatus ?? failure.status,
680
+ error_category: failure.code,
681
+ typed_timeout: failure.code === 'request_timeout',
682
+ pending_code: null,
683
+ replay_code: null,
684
+ ...serverTimingEvidence(),
685
+ requested_wait_ms: traceMs(failure.retryAfterMs),
686
+ });
687
+ throw error;
688
+ }
535
689
  if (!json.data || typeof json.data !== 'object' || Array.isArray(json.data)) {
690
+ appendAutoHttpAttempt(autoHttpTrace, {
691
+ route_phase: kind,
692
+ duration_ms: traceMs(performance.now() - requestStarted) || 0,
693
+ status: response.status,
694
+ error_category: 'invalid_response',
695
+ typed_timeout: false,
696
+ pending_code: null,
697
+ replay_code: null,
698
+ ...serverTimingEvidence(),
699
+ requested_wait_ms: null,
700
+ });
536
701
  throw (0, request_reliability_1.invalidResponseError)();
537
702
  }
538
703
  const data = json.data;
704
+ const headerWait = (0, request_reliability_1.responseRetryAfter)(response);
705
+ const bodyWait = traceMs(data.retry_after_ms);
706
+ const hasRequestedWait = bodyWait !== null || headerWait.delayMs !== null;
707
+ const requestedWait = headerWait.valid && hasRequestedWait
708
+ ? Math.max(bodyWait ?? 0, headerWait.delayMs ?? 0)
709
+ : null;
710
+ const traceAttempt = appendAutoHttpAttempt(autoHttpTrace, {
711
+ route_phase: kind,
712
+ duration_ms: traceMs(performance.now() - requestStarted) || 0,
713
+ status: response.status,
714
+ error_category: null,
715
+ typed_timeout: false,
716
+ pending_code: response.status === 202
717
+ ? traceCode(data.pending_code) || traceCode(data.phase) || `${kind}_pending`
718
+ : null,
719
+ replay_code: traceCode(data.replay_code) || traceCode(data.replay_state)
720
+ || (data.replayed === true ? 'replayed' : null),
721
+ ...serverTimingEvidence(data),
722
+ requested_wait_ms: requestedWait,
723
+ });
539
724
  if (autoManaged) {
540
725
  const retryGuidance = (0, request_reliability_1.responseRetryAfter)(response);
541
726
  if (!retryGuidance.valid) {
@@ -556,7 +741,7 @@ async function fetchAgentWrite(url, init, kind, idempotencyKey, expectedDecision
556
741
  // Canonical write reconciliation predates auto's phase/resumable fields.
557
742
  // Adapt only a validated exact-operation pending response.
558
743
  if (response.status === 202 && retryGuidance.valid && data.resumable !== false
559
- && pendingDecisionId(kind, data, idempotencyKey, expectedDecisionId)) {
744
+ && pendingWriteReconciliation(kind, data, idempotencyKey, expectedDecisionId)) {
560
745
  data.phase = `${kind}_pending`;
561
746
  data.resumable = true;
562
747
  }
@@ -567,8 +752,12 @@ async function fetchAgentWrite(url, init, kind, idempotencyKey, expectedDecision
567
752
  return { response, data };
568
753
  }
569
754
  if (response.status !== 202 || (kind === 'commit' && data.outcome_state === 'observed_unverified')) {
570
- if (reconciledDecisionId) {
571
- if (kind === 'think' && data.decision_id !== reconciledDecisionId) {
755
+ if (reconciling) {
756
+ if (data.idempotency_key !== undefined && data.idempotency_key !== idempotencyKey) {
757
+ throw reconciliationError(false);
758
+ }
759
+ if (kind === 'think' && (!validCorrelationId(data.decision_id)
760
+ || (reconciledDecisionId && data.decision_id !== reconciledDecisionId))) {
572
761
  throw reconciliationError(false);
573
762
  }
574
763
  if (kind === 'commit' && data.decision_id !== undefined && data.decision_id !== expectedDecisionId) {
@@ -577,17 +766,22 @@ async function fetchAgentWrite(url, init, kind, idempotencyKey, expectedDecision
577
766
  }
578
767
  return { response, data };
579
768
  }
580
- const currentDecisionId = pendingDecisionId(kind, data, idempotencyKey, expectedDecisionId);
581
- if (!currentDecisionId || (reconciledDecisionId && currentDecisionId !== reconciledDecisionId)) {
769
+ const pending = pendingWriteReconciliation(kind, data, idempotencyKey, expectedDecisionId);
770
+ if (!pending || (reconciledDecisionId && pending.decisionId && pending.decisionId !== reconciledDecisionId)) {
582
771
  throw reconciliationError(false);
583
772
  }
584
- reconciledDecisionId = currentDecisionId;
773
+ reconciling = true;
774
+ if (pending.decisionId)
775
+ reconciledDecisionId = pending.decisionId;
585
776
  if (attempt + 1 >= AGENT_WRITE_RECONCILIATION_ATTEMPTS) {
586
- throw reconciliationError(true);
777
+ throw reconciliationError(true, pendingReceipt);
587
778
  }
779
+ const waitStarted = performance.now();
588
780
  await waitForWriteReconciliation(init.signal || undefined);
781
+ if (traceAttempt)
782
+ traceAttempt.actual_wait_ms = traceMs(performance.now() - waitStarted) || 0;
589
783
  }
590
- throw reconciliationError(true);
784
+ throw reconciliationError(true, pendingReceipt);
591
785
  }
592
786
  function clampPeriodDays(value, defaultDays = 7) {
593
787
  const parsed = typeof value === 'number' ? value : parseInt(String(value || defaultDays), 10);
@@ -653,6 +847,8 @@ async function marrowThink(apiKey, baseUrl, params, sessionId, agentId, signal,
653
847
  body.previous_outcome = (0, redact_1.redactSensitiveText)(params.previous_outcome ?? '');
654
848
  }
655
849
  const thinkUrl = `${baseUrl}/v1/agent/think${options?.responseMode === 'ack' ? '?response=ack' : ''}`;
850
+ if (options?.requestHash !== undefined && !options.idempotencyKey)
851
+ throw new TypeError('Pending resume requires the original idempotency key.');
656
852
  const idempotencyKey = invocationIdempotencyKey('think', options?.idempotencyKey);
657
853
  const thinkInit = {
658
854
  method: 'POST',
@@ -663,7 +859,7 @@ async function marrowThink(apiKey, baseUrl, params, sessionId, agentId, signal,
663
859
  body: JSON.stringify(body),
664
860
  signal,
665
861
  };
666
- const { response, data } = await fetchAgentWrite(thinkUrl, thinkInit, 'think', idempotencyKey, undefined, options?.[AUTO_MANAGED_WRITE] === true);
862
+ const { response, data } = await fetchAgentWrite(thinkUrl, thinkInit, 'think', idempotencyKey, undefined, options?.[AUTO_MANAGED_WRITE] === true, options?.requestHash, options?.[AUTO_HTTP_TRACE]);
667
863
  return markAutoResponseStatus(data, response.status);
668
864
  }
669
865
  /**
@@ -759,7 +955,7 @@ async function marrowCommit(apiKey, baseUrl, params, sessionId, agentId, signal,
759
955
  body: JSON.stringify(body),
760
956
  signal,
761
957
  };
762
- const { response, data } = await fetchAgentWrite(`${baseUrl}/v1/agent/commit`, commitInit, 'commit', resolvedIdempotencyKey, params.decision_id, params[AUTO_MANAGED_WRITE] === true);
958
+ const { response, data } = await fetchAgentWrite(`${baseUrl}/v1/agent/commit`, commitInit, 'commit', resolvedIdempotencyKey, params.decision_id, params[AUTO_MANAGED_WRITE] === true, undefined, params[AUTO_HTTP_TRACE]);
763
959
  if (data.outcome_state === 'observed_unverified') {
764
960
  if (!isDurableObservedOutcome(data))
765
961
  throw (0, request_reliability_1.invalidResponseError)();
@@ -913,7 +1109,7 @@ function autoContinuationDelay(pending) {
913
1109
  ? Math.max(25, Math.ceil(requested))
914
1110
  : 50;
915
1111
  }
916
- async function waitForAutoContinuation(pending, startedAt, responseBudgetMs) {
1112
+ async function waitForAutoContinuation(pending, startedAt, responseBudgetMs, autoHttpTrace) {
917
1113
  const remaining = responseBudgetMs - (Date.now() - startedAt) - AUTO_RESPONSE_DEADLINE_MARGIN_MS;
918
1114
  if (remaining <= 0)
919
1115
  return false;
@@ -922,9 +1118,14 @@ async function waitForAutoContinuation(pending, startedAt, responseBudgetMs) {
922
1118
  if (delayMs >= remaining)
923
1119
  return false;
924
1120
  const resumeAt = performance.now() + delayMs;
1121
+ const waitStarted = performance.now();
925
1122
  do {
926
1123
  await new Promise((resolve) => setTimeout(resolve, Math.ceil(resumeAt - performance.now())));
927
1124
  } while (performance.now() < resumeAt);
1125
+ const latestAttempt = autoHttpTrace?.attempts[autoHttpTrace.attempts.length - 1];
1126
+ if (latestAttempt) {
1127
+ latestAttempt.actual_wait_ms = Math.min(60_000, latestAttempt.actual_wait_ms + Math.max(0, Math.round(performance.now() - waitStarted)));
1128
+ }
928
1129
  return responseBudgetMs - (Date.now() - startedAt) > AUTO_RESPONSE_DEADLINE_MARGIN_MS;
929
1130
  }
930
1131
  function autoPartial(input) {
@@ -947,6 +1148,7 @@ function autoPartial(input) {
947
1148
  ...input.timings,
948
1149
  total: Date.now() - input.startedAt,
949
1150
  },
1151
+ http_attempt_trace: snapshotAutoHttpTrace(input.autoHttpTrace),
950
1152
  };
951
1153
  }
952
1154
  /**
@@ -957,6 +1159,15 @@ function autoPartial(input) {
957
1159
  * another decision.
958
1160
  */
959
1161
  async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs) {
1162
+ const autoHttpTrace = { attempts: [], dropped_count: 0 };
1163
+ try {
1164
+ return await marrowAutoWithTrace(apiKey, baseUrl, params, sessionId, agentId, timeoutMs, autoHttpTrace);
1165
+ }
1166
+ catch (error) {
1167
+ throw attachAutoHttpTrace(error, autoHttpTrace);
1168
+ }
1169
+ }
1170
+ async function marrowAutoWithTrace(apiKey, baseUrl, params, sessionId, agentId, timeoutMs, autoHttpTrace) {
960
1171
  const startedAt = Date.now();
961
1172
  const responseBudgetMs = autoResponseBudget(timeoutMs);
962
1173
  const operationId = resolveAutoOperationId(params.operation_id);
@@ -1005,10 +1216,10 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
1005
1216
  }
1006
1217
  catch (error) {
1007
1218
  if ((0, request_reliability_1.normalizeRequestError)(error).code !== 'request_timeout'
1008
- || !await waitForAutoContinuation({}, startedAt, responseBudgetMs)) {
1219
+ || !await waitForAutoContinuation({}, startedAt, responseBudgetMs, autoHttpTrace)) {
1009
1220
  timings.runtime = Date.now() - phaseStarted;
1010
1221
  if ((0, request_reliability_1.normalizeRequestError)(error).code === 'request_timeout') {
1011
- return autoPartial({ operationId, phase: 'runtime_pending', timings, startedAt });
1222
+ return autoPartial({ operationId, phase: 'runtime_pending', timings, startedAt, autoHttpTrace });
1012
1223
  }
1013
1224
  throw error;
1014
1225
  }
@@ -1091,16 +1302,17 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
1091
1302
  idempotencyKey: autoIdempotencyKey(operationId, 'think'),
1092
1303
  responseMode: 'ack',
1093
1304
  [AUTO_MANAGED_WRITE]: true,
1305
+ [AUTO_HTTP_TRACE]: autoHttpTrace,
1094
1306
  });
1095
1307
  }
1096
1308
  catch (error) {
1097
1309
  const failure = (0, request_reliability_1.normalizeRequestError)(error);
1098
1310
  const recoverable = error instanceof request_reliability_1.MarrowRequestError && failure.retryable && failure.code !== 'invalid_response';
1099
1311
  if (!recoverable
1100
- || !await waitForAutoContinuation({ retry_after_ms: failure.retryAfterMs }, startedAt, responseBudgetMs)) {
1312
+ || !await waitForAutoContinuation({ retry_after_ms: failure.retryAfterMs }, startedAt, responseBudgetMs, autoHttpTrace)) {
1101
1313
  timings.think = Date.now() - thinkStarted;
1102
1314
  if (recoverable) {
1103
- return autoPartial({ operationId, decisionId: pendingThinkDecisionId, phase: 'think_pending', runtimeGate, timings, startedAt,
1315
+ return autoPartial({ operationId, decisionId: pendingThinkDecisionId, phase: 'think_pending', runtimeGate, timings, startedAt, autoHttpTrace,
1104
1316
  retryAfterMs: failure.retryAfterMs ?? undefined });
1105
1317
  }
1106
1318
  throw error;
@@ -1126,9 +1338,9 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
1126
1338
  pendingThinkDecisionId = receivedDecisionId;
1127
1339
  operationBinding.pendingThinkDecisionId = receivedDecisionId;
1128
1340
  }
1129
- if (!await waitForAutoContinuation(thinkResult, startedAt, responseBudgetMs)) {
1341
+ if (!await waitForAutoContinuation(thinkResult, startedAt, responseBudgetMs, autoHttpTrace)) {
1130
1342
  timings.think = Date.now() - thinkStarted;
1131
- return autoPartial({ operationId, decisionId: pendingThinkDecisionId, phase: 'think_pending', runtimeGate, timings, startedAt,
1343
+ return autoPartial({ operationId, decisionId: pendingThinkDecisionId, phase: 'think_pending', runtimeGate, timings, startedAt, autoHttpTrace,
1132
1344
  retryAfterMs: autoContinuationDelay(thinkResult) });
1133
1345
  }
1134
1346
  }
@@ -1141,6 +1353,7 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
1141
1353
  runtimeGate,
1142
1354
  timings,
1143
1355
  startedAt,
1356
+ autoHttpTrace,
1144
1357
  retryAfterMs: null,
1145
1358
  });
1146
1359
  }
@@ -1164,7 +1377,7 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
1164
1377
  if (genericReviewRequired && ordinaryApprovalDeclared && runtimeGate) {
1165
1378
  if (!(0, runtime_contract_1.hasOrdinaryOwnerApprovalProof)(params.proof)) {
1166
1379
  return autoPartial({
1167
- operationId, decisionId, phase: 'owner_approval_required', runtimeGate, timings, startedAt,
1380
+ operationId, decisionId, phase: 'owner_approval_required', runtimeGate, timings, startedAt, autoHttpTrace,
1168
1381
  resumable: false,
1169
1382
  exactNextAction: 'Wait for explicit owner approval of this exact work. Then supply the server-supported proof.owner_approval object with the required measured proof and call marrow_auto with this same operation_id. Preserve the original decision and gate receipt. Do not infer or manufacture approval.',
1170
1383
  });
@@ -1172,7 +1385,7 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
1172
1385
  proofCanClose = (0, runtime_contract_1.ordinaryOwnerApprovalCanAttemptCommit)(runtimeGate, params.proof, gateReceiptId);
1173
1386
  if (!proofCanClose) {
1174
1387
  return autoPartial({
1175
- operationId, decisionId, phase: 'review_required', runtimeGate, timings, startedAt,
1388
+ operationId, decisionId, phase: 'review_required', runtimeGate, timings, startedAt, autoHttpTrace,
1176
1389
  resumable: false,
1177
1390
  exactNextAction: 'The original ordinary approval receipt is expired or its enforced completion contract is not valid. Stop and reconcile fresh server guidance for this exact operation before attempting commit.',
1178
1391
  });
@@ -1186,6 +1399,7 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
1186
1399
  runtimeGate,
1187
1400
  timings,
1188
1401
  startedAt,
1402
+ autoHttpTrace,
1189
1403
  resumable: false,
1190
1404
  exactNextAction: 'Stop this operation and obtain the server-supported completion contract for this exact review. No supported ordinary approval path was declared. Preserve the operation and receipt references; do not infer approval or automatically retry.',
1191
1405
  });
@@ -1198,6 +1412,7 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
1198
1412
  runtimeGate,
1199
1413
  timings,
1200
1414
  startedAt,
1415
+ autoHttpTrace,
1201
1416
  resumable: false,
1202
1417
  });
1203
1418
  }
@@ -1222,6 +1437,7 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
1222
1437
  runtimeGate,
1223
1438
  timings,
1224
1439
  startedAt,
1440
+ autoHttpTrace,
1225
1441
  retryAfterMs: null,
1226
1442
  });
1227
1443
  }
@@ -1233,6 +1449,7 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
1233
1449
  runtimeGate,
1234
1450
  timings,
1235
1451
  startedAt,
1452
+ autoHttpTrace,
1236
1453
  });
1237
1454
  }
1238
1455
  const commitStarted = Date.now();
@@ -1253,16 +1470,17 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
1253
1470
  surfaces: params.surfaces,
1254
1471
  auto_gate: false,
1255
1472
  [AUTO_MANAGED_WRITE]: true,
1473
+ [AUTO_HTTP_TRACE]: autoHttpTrace,
1256
1474
  }, sessionId, agentId, commitTimeout.signal, autoIdempotencyKey(operationId, 'commit'));
1257
1475
  }
1258
1476
  catch (error) {
1259
1477
  const failure = (0, request_reliability_1.normalizeRequestError)(error);
1260
1478
  const recoverable = error instanceof request_reliability_1.MarrowRequestError && failure.retryable && failure.code !== 'invalid_response';
1261
1479
  if (!recoverable
1262
- || !await waitForAutoContinuation({ retry_after_ms: failure.retryAfterMs }, startedAt, responseBudgetMs)) {
1480
+ || !await waitForAutoContinuation({ retry_after_ms: failure.retryAfterMs }, startedAt, responseBudgetMs, autoHttpTrace)) {
1263
1481
  timings.commit = Date.now() - commitStarted;
1264
1482
  if (recoverable) {
1265
- return autoPartial({ operationId, decisionId, phase: 'commit_pending', runtimeGate, timings, startedAt,
1483
+ return autoPartial({ operationId, decisionId, phase: 'commit_pending', runtimeGate, timings, startedAt, autoHttpTrace,
1266
1484
  retryAfterMs: failure.retryAfterMs ?? undefined });
1267
1485
  }
1268
1486
  throw error;
@@ -1277,12 +1495,12 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
1277
1495
  const canResumeCommit = commitResult.resumable !== false && commitResult.retryable !== false;
1278
1496
  if (!isAutoPendingResponse(commitResult, 'commit_pending')) {
1279
1497
  timings.commit = Date.now() - commitStarted;
1280
- return autoPartial({ operationId, decisionId, phase: 'commit_pending', runtimeGate, timings, startedAt,
1498
+ return autoPartial({ operationId, decisionId, phase: 'commit_pending', runtimeGate, timings, startedAt, autoHttpTrace,
1281
1499
  resumable: canResumeCommit });
1282
1500
  }
1283
- if (!await waitForAutoContinuation(commitResult, startedAt, responseBudgetMs)) {
1501
+ if (!await waitForAutoContinuation(commitResult, startedAt, responseBudgetMs, autoHttpTrace)) {
1284
1502
  timings.commit = Date.now() - commitStarted;
1285
- return autoPartial({ operationId, decisionId, phase: 'commit_pending', runtimeGate, timings, startedAt,
1503
+ return autoPartial({ operationId, decisionId, phase: 'commit_pending', runtimeGate, timings, startedAt, autoHttpTrace,
1286
1504
  retryAfterMs: autoContinuationDelay(commitResult) });
1287
1505
  }
1288
1506
  }
@@ -1299,6 +1517,7 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
1299
1517
  ...timings,
1300
1518
  total: Date.now() - startedAt,
1301
1519
  },
1520
+ http_attempt_trace: snapshotAutoHttpTrace(autoHttpTrace),
1302
1521
  };
1303
1522
  }
1304
1523
  /**