@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/README.md +17 -5
- package/dist/cli.js +10 -2
- package/dist/cli.js.map +1 -1
- package/dist/hook-contract.d.ts +2 -2
- package/dist/hook-contract.js +1 -1
- package/dist/index.d.ts +34 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +268 -49
- package/dist/index.js.map +1 -1
- package/dist/request-reliability.d.ts +15 -2
- package/dist/request-reliability.d.ts.map +1 -1
- package/dist/request-reliability.js +60 -8
- package/dist/request-reliability.js.map +1 -1
- package/package.json +1 -1
- package/scripts/control-path-canary.cjs +139 -6
- package/server.json +2 -2
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
|
|
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 (
|
|
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
|
-
? '
|
|
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
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
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
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
&&
|
|
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 (
|
|
571
|
-
if (
|
|
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
|
|
581
|
-
if (!
|
|
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
|
-
|
|
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
|
/**
|