@getmarrow/mcp 3.9.73 → 3.9.74

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
@@ -3,6 +3,7 @@
3
3
  * @getmarrow/mcp — API Functions
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MARROW_AUTO_RESPONSE_BUDGET_MAX_MS = void 0;
6
7
  exports.validatePathParam = validatePathParam;
7
8
  exports.validateBaseUrl = validateBaseUrl;
8
9
  exports.marrowCreateKey = marrowCreateKey;
@@ -407,7 +408,7 @@ async function marrowThink(apiKey, baseUrl, params, sessionId, agentId, signal,
407
408
  ? await fetch(thinkUrl, thinkInit)
408
409
  : await fetchWithRetryQueue(thinkUrl, thinkInit, true);
409
410
  const json = await safeJsonResponse(res);
410
- return json.data;
411
+ return markAutoResponseStatus(json.data, res.status);
411
412
  }
412
413
  /**
413
414
  * Explicitly commit the result of an action to Marrow.
@@ -478,13 +479,20 @@ async function marrowCommit(apiKey, baseUrl, params, sessionId, agentId, signal,
478
479
  ? await fetch(`${baseUrl}/v1/agent/commit`, commitInit)
479
480
  : await fetchWithRetryQueue(`${baseUrl}/v1/agent/commit`, commitInit, true);
480
481
  const json = await safeJsonResponse(res);
482
+ if (res.status === 202
483
+ && json.data
484
+ && typeof json.data === 'object'
485
+ && !Array.isArray(json.data)
486
+ && (json.data.phase === undefined || json.data.phase === 'commit_pending' || json.data.resumable === true)) {
487
+ return markAutoResponseStatus({ ...json.data, committed: false }, res.status);
488
+ }
481
489
  if (!json.data
482
490
  || typeof json.data !== 'object'
483
491
  || Array.isArray(json.data)
484
492
  || typeof json.data.committed !== 'boolean') {
485
493
  throw (0, request_reliability_1.invalidResponseError)();
486
494
  }
487
- return { ...json.data, committed: json.data.committed, runtime_gate: runtimeGate };
495
+ return markAutoResponseStatus({ ...json.data, committed: json.data.committed, runtime_gate: runtimeGate }, res.status);
488
496
  }
489
497
  async function marrowModelUsage(apiKey, baseUrl, input, sessionId, agentId) {
490
498
  const body = normalizeModelUsage({
@@ -520,6 +528,10 @@ function createTimeoutSignal(timeoutMs, startedAt) {
520
528
  const SAFE_AUTO_OPERATION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,79}$/;
521
529
  const AUTO_OPERATION_BINDING_TTL_MS = 30 * 60 * 1_000;
522
530
  const AUTO_OPERATION_BINDING_LIMIT = 256;
531
+ const AUTO_RESPONSE_BUDGET_DEFAULT_MS = 8_000;
532
+ exports.MARROW_AUTO_RESPONSE_BUDGET_MAX_MS = 8_000;
533
+ const AUTO_RESPONSE_DEADLINE_MARGIN_MS = 75;
534
+ const AUTO_RESPONSE_STATUS = Symbol('marrowAutoResponseStatus');
523
535
  const autoOperationBindings = new Map();
524
536
  function canonicalAutoBindingValue(value) {
525
537
  if (Array.isArray(value))
@@ -589,6 +601,36 @@ function resolveAutoOperationId(value) {
589
601
  function autoIdempotencyKey(operationId, phase) {
590
602
  return `mcp-auto:${operationId}:${phase}`;
591
603
  }
604
+ function autoResponseBudget(timeoutMs) {
605
+ const requested = Number(timeoutMs);
606
+ return Number.isFinite(requested) && requested > 0
607
+ ? Math.min(exports.MARROW_AUTO_RESPONSE_BUDGET_MAX_MS, Math.max(500, Math.floor(requested)))
608
+ : AUTO_RESPONSE_BUDGET_DEFAULT_MS;
609
+ }
610
+ function markAutoResponseStatus(value, status) {
611
+ if (value && typeof value === 'object') {
612
+ Object.defineProperty(value, AUTO_RESPONSE_STATUS, { value: status });
613
+ }
614
+ return value;
615
+ }
616
+ function isAutoPendingResponse(value, phase) {
617
+ if (!value || typeof value !== 'object' || Array.isArray(value))
618
+ return false;
619
+ const pending = value;
620
+ return pending[AUTO_RESPONSE_STATUS] === 202
621
+ && (pending.phase === phase || (pending.phase === undefined && pending.resumable === true));
622
+ }
623
+ async function waitForAutoContinuation(pending, startedAt, responseBudgetMs) {
624
+ const remaining = responseBudgetMs - (Date.now() - startedAt) - AUTO_RESPONSE_DEADLINE_MARGIN_MS;
625
+ if (remaining <= 0)
626
+ return false;
627
+ const requestedDelay = Number(pending.retry_after_ms);
628
+ const delayMs = Number.isFinite(requestedDelay)
629
+ ? Math.min(500, Math.max(25, Math.floor(requestedDelay)))
630
+ : 50;
631
+ await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, remaining)));
632
+ return responseBudgetMs - (Date.now() - startedAt) > AUTO_RESPONSE_DEADLINE_MARGIN_MS;
633
+ }
592
634
  function autoPartial(input) {
593
635
  return {
594
636
  operation_id: input.operationId,
@@ -605,12 +647,15 @@ function autoPartial(input) {
605
647
  };
606
648
  }
607
649
  /**
608
- * Fire-and-forget style logging helper for tool hooks and simple integrations.
609
- * Logs intent, and when outcome is supplied, immediately commits it.
650
+ * Bounded outcome logging helper for tool hooks and simple integrations.
651
+ * One outer invocation logs intent and, when an outcome is supplied, continues
652
+ * resumable server phases so the outcome normally closes in-band. If the
653
+ * caller's deadline is reached, the same operation ID resumes without opening
654
+ * another decision.
610
655
  */
611
656
  async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs) {
612
657
  const startedAt = Date.now();
613
- const responseBudgetMs = Math.min(3_200, Math.max(500, Math.floor(timeoutMs || 3_200)));
658
+ const responseBudgetMs = autoResponseBudget(timeoutMs);
614
659
  const operationId = resolveAutoOperationId(params.operation_id);
615
660
  const operationBinding = boundAutoOperation(operationId, autoOperationSignature({
616
661
  apiKey,
@@ -638,36 +683,36 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
638
683
  timings.runtime = 0;
639
684
  }
640
685
  else {
641
- const runtimeTimeout = createTimeoutSignal(responseBudgetMs, startedAt);
642
- try {
643
- runtimeGate = await marrowAgentRuntime(apiKey, baseUrl, {
644
- action: (0, redact_1.redactSensitiveText)(params.action_for_gate || params.action),
645
- type: params.type || 'general',
646
- agent_id: agentId,
647
- session_id: sessionId,
648
- surfaces: params.surfaces,
649
- // Proof is commit evidence, not part of immutable runtime authorization.
650
- // Keeping it out makes missing -> supplied proof a monotonic continuation.
651
- context: { source: 'mcp_auto_risk_upgrade', operation_id: operationId },
652
- }, sessionId, agentId, runtimeTimeout.signal, autoIdempotencyKey(operationId, 'runtime'));
653
- operationBinding.runtimeGate = runtimeGate;
654
- timings.runtime = Date.now() - phaseStarted;
655
- }
656
- catch (error) {
657
- timings.runtime = Date.now() - phaseStarted;
658
- if ((0, request_reliability_1.normalizeRequestError)(error).code === 'request_timeout') {
659
- return autoPartial({
660
- operationId,
661
- phase: 'runtime_pending',
662
- timings,
663
- startedAt,
664
- });
686
+ while (!runtimeGate) {
687
+ const runtimeTimeout = createTimeoutSignal(responseBudgetMs, startedAt);
688
+ try {
689
+ runtimeGate = await marrowAgentRuntime(apiKey, baseUrl, {
690
+ action: (0, redact_1.redactSensitiveText)(params.action_for_gate || params.action),
691
+ type: params.type || 'general',
692
+ agent_id: agentId,
693
+ session_id: sessionId,
694
+ surfaces: params.surfaces,
695
+ // Proof is commit evidence, not part of immutable runtime authorization.
696
+ // Keeping it out makes missing -> supplied proof a monotonic continuation.
697
+ context: { source: 'mcp_auto_risk_upgrade', operation_id: operationId },
698
+ }, sessionId, agentId, runtimeTimeout.signal, autoIdempotencyKey(operationId, 'runtime'));
699
+ operationBinding.runtimeGate = runtimeGate;
700
+ }
701
+ catch (error) {
702
+ if ((0, request_reliability_1.normalizeRequestError)(error).code !== 'request_timeout'
703
+ || !await waitForAutoContinuation({}, startedAt, responseBudgetMs)) {
704
+ timings.runtime = Date.now() - phaseStarted;
705
+ if ((0, request_reliability_1.normalizeRequestError)(error).code === 'request_timeout') {
706
+ return autoPartial({ operationId, phase: 'runtime_pending', timings, startedAt });
707
+ }
708
+ throw error;
709
+ }
710
+ }
711
+ finally {
712
+ runtimeTimeout.cancel();
665
713
  }
666
- throw error;
667
- }
668
- finally {
669
- runtimeTimeout.cancel();
670
714
  }
715
+ timings.runtime = Date.now() - phaseStarted;
671
716
  }
672
717
  gateReceiptId = (0, runtime_contract_1.runtimeAuthorizationReceiptId)(runtimeGate) || gateReceiptId;
673
718
  if (!gateReceiptId) {
@@ -677,56 +722,59 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
677
722
  || (0, runtime_contract_1.highRiskRuntimeCanContinueWithProof)(runtimeGate, params.proof, gateReceiptId);
678
723
  }
679
724
  const thinkStarted = Date.now();
680
- const thinkTimeout = createTimeoutSignal(responseBudgetMs, startedAt);
681
- let thinkResult;
682
- try {
683
- thinkResult = await marrowThink(apiKey, baseUrl, {
684
- action: params.action,
685
- type: params.type || 'general',
686
- context: params.context,
687
- source_kind: 'agent_autonomous',
688
- source_confidence: 0.9,
689
- human_directed: false,
690
- source_meta: {
691
- channel: 'mcp',
692
- client: defaultSourceClient(),
693
- user_intent: 'operate',
694
- ...(params.source_meta || {}),
695
- },
696
- }, sessionId, agentId, thinkTimeout.signal, {
697
- idempotencyKey: autoIdempotencyKey(operationId, 'think'),
698
- responseMode: 'ack',
699
- });
700
- timings.think = Date.now() - thinkStarted;
701
- }
702
- catch (error) {
703
- timings.think = Date.now() - thinkStarted;
704
- if ((0, request_reliability_1.normalizeRequestError)(error).code === 'request_timeout') {
705
- return autoPartial({
706
- operationId,
707
- phase: 'think_pending',
708
- runtimeGate,
709
- timings,
710
- startedAt,
725
+ let decisionId = operationBinding.decisionId || null;
726
+ const reusedDecision = Boolean(decisionId);
727
+ while (!decisionId) {
728
+ const thinkTimeout = createTimeoutSignal(responseBudgetMs, startedAt);
729
+ let thinkResult;
730
+ try {
731
+ thinkResult = await marrowThink(apiKey, baseUrl, {
732
+ action: params.action,
733
+ type: params.type || 'general',
734
+ context: params.context,
735
+ source_kind: 'agent_autonomous',
736
+ source_confidence: 0.9,
737
+ human_directed: false,
738
+ source_meta: {
739
+ channel: 'mcp',
740
+ client: defaultSourceClient(),
741
+ user_intent: 'operate',
742
+ ...(params.source_meta || {}),
743
+ },
744
+ }, sessionId, agentId, thinkTimeout.signal, {
745
+ idempotencyKey: autoIdempotencyKey(operationId, 'think'),
746
+ responseMode: 'ack',
711
747
  });
712
748
  }
713
- throw error;
714
- }
715
- finally {
716
- thinkTimeout.cancel();
717
- }
718
- const decisionId = typeof thinkResult.decision_id === 'string' && thinkResult.decision_id.trim()
719
- ? thinkResult.decision_id.trim()
720
- : null;
721
- if (!decisionId) {
722
- return autoPartial({
723
- operationId,
724
- phase: 'think_pending',
725
- runtimeGate,
726
- timings,
727
- startedAt,
728
- });
749
+ catch (error) {
750
+ if ((0, request_reliability_1.normalizeRequestError)(error).code !== 'request_timeout'
751
+ || !await waitForAutoContinuation({}, startedAt, responseBudgetMs)) {
752
+ timings.think = Date.now() - thinkStarted;
753
+ if ((0, request_reliability_1.normalizeRequestError)(error).code === 'request_timeout') {
754
+ return autoPartial({ operationId, phase: 'think_pending', runtimeGate, timings, startedAt });
755
+ }
756
+ throw error;
757
+ }
758
+ continue;
759
+ }
760
+ finally {
761
+ thinkTimeout.cancel();
762
+ }
763
+ decisionId = typeof thinkResult.decision_id === 'string' && thinkResult.decision_id.trim()
764
+ ? thinkResult.decision_id.trim()
765
+ : null;
766
+ if (decisionId) {
767
+ operationBinding.decisionId = decisionId;
768
+ break;
769
+ }
770
+ if (!isAutoPendingResponse(thinkResult, 'think_pending'))
771
+ throw (0, request_reliability_1.invalidResponseError)();
772
+ if (!await waitForAutoContinuation(thinkResult, startedAt, responseBudgetMs)) {
773
+ timings.think = Date.now() - thinkStarted;
774
+ return autoPartial({ operationId, phase: 'think_pending', runtimeGate, timings, startedAt });
775
+ }
729
776
  }
777
+ timings.think = reusedDecision ? 0 : Date.now() - thinkStarted;
730
778
  if (params.outcome === undefined || typeof params.success !== 'boolean') {
731
779
  return autoPartial({
732
780
  operationId,
@@ -760,49 +808,48 @@ async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs
760
808
  });
761
809
  }
762
810
  const commitStarted = Date.now();
763
- const commitTimeout = createTimeoutSignal(responseBudgetMs, startedAt);
764
811
  let commitResult;
765
- try {
766
- commitResult = await marrowCommit(apiKey, baseUrl, {
767
- decision_id: decisionId,
768
- success: params.success,
769
- outcome: params.outcome,
770
- proof: params.proof,
771
- gate_receipt_id: gateReceiptId,
772
- action: params.action_for_gate || params.action,
773
- type: params.type || 'general',
774
- surfaces: params.surfaces,
775
- auto_gate: false,
776
- }, sessionId, agentId, commitTimeout.signal, autoIdempotencyKey(operationId, 'commit'));
777
- timings.commit = Date.now() - commitStarted;
778
- }
779
- catch (error) {
780
- timings.commit = Date.now() - commitStarted;
781
- if ((0, request_reliability_1.normalizeRequestError)(error).code === 'request_timeout') {
782
- return autoPartial({
783
- operationId,
784
- decisionId,
785
- phase: 'commit_pending',
786
- runtimeGate,
787
- timings,
788
- startedAt,
789
- });
812
+ while (true) {
813
+ const commitTimeout = createTimeoutSignal(responseBudgetMs, startedAt);
814
+ try {
815
+ commitResult = await marrowCommit(apiKey, baseUrl, {
816
+ decision_id: decisionId,
817
+ success: params.success,
818
+ outcome: params.outcome,
819
+ proof: params.proof,
820
+ gate_receipt_id: gateReceiptId,
821
+ action: params.action_for_gate || params.action,
822
+ type: params.type || 'general',
823
+ surfaces: params.surfaces,
824
+ auto_gate: false,
825
+ }, sessionId, agentId, commitTimeout.signal, autoIdempotencyKey(operationId, 'commit'));
826
+ }
827
+ catch (error) {
828
+ if ((0, request_reliability_1.normalizeRequestError)(error).code !== 'request_timeout'
829
+ || !await waitForAutoContinuation({}, startedAt, responseBudgetMs)) {
830
+ timings.commit = Date.now() - commitStarted;
831
+ if ((0, request_reliability_1.normalizeRequestError)(error).code === 'request_timeout') {
832
+ return autoPartial({ operationId, decisionId, phase: 'commit_pending', runtimeGate, timings, startedAt });
833
+ }
834
+ throw error;
835
+ }
836
+ continue;
837
+ }
838
+ finally {
839
+ commitTimeout.cancel();
840
+ }
841
+ if (commitResult.committed)
842
+ break;
843
+ if (!isAutoPendingResponse(commitResult, 'commit_pending')) {
844
+ timings.commit = Date.now() - commitStarted;
845
+ return autoPartial({ operationId, decisionId, phase: 'commit_pending', runtimeGate, timings, startedAt });
846
+ }
847
+ if (!await waitForAutoContinuation(commitResult, startedAt, responseBudgetMs)) {
848
+ timings.commit = Date.now() - commitStarted;
849
+ return autoPartial({ operationId, decisionId, phase: 'commit_pending', runtimeGate, timings, startedAt });
790
850
  }
791
- throw error;
792
- }
793
- finally {
794
- commitTimeout.cancel();
795
- }
796
- if (!commitResult.committed) {
797
- return autoPartial({
798
- operationId,
799
- decisionId,
800
- phase: 'commit_pending',
801
- runtimeGate,
802
- timings,
803
- startedAt,
804
- });
805
851
  }
852
+ timings.commit = Date.now() - commitStarted;
806
853
  return {
807
854
  operation_id: operationId,
808
855
  decision_id: decisionId,