@getmarrow/mcp 3.9.98 → 3.9.100

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.
Files changed (53) hide show
  1. package/README.md +53 -343
  2. package/dist/cli.js +249 -47
  3. package/dist/cli.js.map +1 -1
  4. package/dist/hook-context.d.ts.map +1 -1
  5. package/dist/hook-context.js +142 -16
  6. package/dist/hook-context.js.map +1 -1
  7. package/dist/hook-contract.d.ts +34 -5
  8. package/dist/hook-contract.d.ts.map +1 -1
  9. package/dist/hook-contract.js +223 -15
  10. package/dist/hook-contract.js.map +1 -1
  11. package/dist/hook-pre-action.d.ts +42 -1
  12. package/dist/hook-pre-action.d.ts.map +1 -1
  13. package/dist/hook-pre-action.js +447 -58
  14. package/dist/hook-pre-action.js.map +1 -1
  15. package/dist/hook-session.d.ts.map +1 -1
  16. package/dist/hook-session.js +35 -3
  17. package/dist/hook-session.js.map +1 -1
  18. package/dist/hook.d.ts +12 -0
  19. package/dist/hook.d.ts.map +1 -1
  20. package/dist/hook.js +147 -7
  21. package/dist/hook.js.map +1 -1
  22. package/dist/host-approval-state.d.ts +137 -0
  23. package/dist/host-approval-state.d.ts.map +1 -0
  24. package/dist/host-approval-state.js +510 -0
  25. package/dist/host-approval-state.js.map +1 -0
  26. package/dist/host-approval.d.ts +436 -0
  27. package/dist/host-approval.d.ts.map +1 -0
  28. package/dist/host-approval.js +1417 -0
  29. package/dist/host-approval.js.map +1 -0
  30. package/dist/host-session.d.ts +104 -0
  31. package/dist/host-session.d.ts.map +1 -0
  32. package/dist/host-session.js +728 -0
  33. package/dist/host-session.js.map +1 -0
  34. package/dist/index.d.ts +150 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +748 -33
  37. package/dist/index.js.map +1 -1
  38. package/dist/lifecycle-spool.d.ts +17 -0
  39. package/dist/lifecycle-spool.d.ts.map +1 -1
  40. package/dist/lifecycle-spool.js +39 -9
  41. package/dist/lifecycle-spool.js.map +1 -1
  42. package/dist/normalized-action.d.ts +131 -0
  43. package/dist/normalized-action.d.ts.map +1 -0
  44. package/dist/normalized-action.js +1203 -0
  45. package/dist/normalized-action.js.map +1 -0
  46. package/dist/runtime-contract.d.ts +108 -4
  47. package/dist/runtime-contract.d.ts.map +1 -1
  48. package/dist/runtime-contract.js +178 -35
  49. package/dist/runtime-contract.js.map +1 -1
  50. package/dist/types.d.ts +128 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +19 -25
  53. package/server.json +15 -4
package/dist/cli.js CHANGED
@@ -10,10 +10,12 @@
10
10
  * MARROW_API_KEY=mrw_abc123 npx @getmarrow/mcp
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
+ const node_crypto_1 = require("node:crypto");
13
14
  const model_usage_1 = require("./model-usage");
14
15
  const index_1 = require("./index");
15
16
  const control_state_1 = require("./control-state");
16
17
  const hook_1 = require("./hook");
18
+ const host_approval_1 = require("./host-approval");
17
19
  const hook_contract_1 = require("./hook-contract");
18
20
  const hook_context_1 = require("./hook-context");
19
21
  const hook_session_1 = require("./hook-session");
@@ -32,7 +34,6 @@ const status_cache_1 = require("./status-cache");
32
34
  const habit_loop_copy_1 = require("./habit-loop-copy");
33
35
  const host_capability_1 = require("./host-capability");
34
36
  const session_loop_guard_1 = require("./session-loop-guard");
35
- // Parse CLI args
36
37
  function parseArgs() {
37
38
  const args = process.argv.slice(2);
38
39
  const result = {};
@@ -47,7 +48,10 @@ function parseArgs() {
47
48
  if (['hook', '--hook', 'claude-hook', 'cline-hook', 'codex-hook', 'cursor-hook', 'gemini-hook', 'grok-hook', 'windsurf-hook'].includes(args[i])) {
48
49
  result.hook = true;
49
50
  }
50
- if (['context-hook', '--context-hook', 'claude-context-hook', 'codex-context-hook', 'grok-context-hook'].includes(args[i])) {
51
+ if (['permission-request-hook', 'claude-permission-request-hook'].includes(args[i])) {
52
+ result.permissionRequestHook = true;
53
+ }
54
+ if (['context-hook', '--context-hook', 'claude-context-hook', 'codex-context-hook', 'grok-context-hook', 'cursor-context-hook', 'gemini-context-hook'].includes(args[i])) {
51
55
  result.contextHook = true;
52
56
  }
53
57
  if (['pre-action-hook', '--pre-action-hook', 'claude-pre-action-hook', 'cline-pre-action-hook', 'codex-pre-action-hook', 'cursor-pre-action-hook', 'gemini-pre-action-hook', 'grok-pre-action-hook', 'windsurf-pre-action-hook'].includes(args[i])) {
@@ -306,6 +310,13 @@ ${MARROW_BLOCK_END}`;
306
310
  else {
307
311
  process.stdout.write('PreToolUse hook configuration is present. Activity is client-self-reported and does not certify pre-action control.\n');
308
312
  }
313
+ const permissionHookInstall = (0, hook_1.installPermissionRequestHook)(process.cwd());
314
+ if (permissionHookInstall.installed) {
315
+ process.stdout.write('Configured the pass-through PermissionRequest hook and the PostToolBatch hook (both async). They record when Claude Code shows its own permission dialog for a held action and whether the operator declined it; they never answer the dialog. Activity is client-self-reported.\n');
316
+ }
317
+ else {
318
+ process.stdout.write('PermissionRequest and PostToolBatch hook configuration is present. Activity is client-self-reported.\n');
319
+ }
309
320
  const sessionHookInstall = (0, hook_session_1.installSessionEndHook)(process.cwd());
310
321
  if (sessionHookInstall.installed) {
311
322
  process.stdout.write('Configured Stop hook. Activity is client-self-reported and does not certify session-end coverage.\n');
@@ -430,12 +441,16 @@ if (process.argv[2] !== 'keys') {
430
441
  else if (cliArgs.backgroundNudge) {
431
442
  void runBackgroundNudge();
432
443
  }
444
+ else if (cliArgs.permissionRequestHook) {
445
+ // Pass-through: exits 0 with no output, so it never answers the dialog.
446
+ void (0, hook_1.runPermissionRequestHookCommand)().finally(() => process.exit(0));
447
+ }
433
448
  else if (cliArgs.contextHook) {
434
449
  void (0, hook_context_1.runContextHookCommand)();
435
450
  }
436
451
  else if (cliArgs.preActionHook) {
437
452
  void (0, hook_pre_action_1.runPreActionHookCommand)().catch(() => {
438
- process.stderr.write('Marrow pre-action governance failed closed. Retry after restoring the trusted configuration.\n');
453
+ process.stderr.write('Marrow\'s pre-action check stopped on an internal error, so this action was held. Run `npx -y @getmarrow/install@latest doctor` in this terminal, then retry.\n');
439
454
  process.exitCode = 2;
440
455
  });
441
456
  }
@@ -486,6 +501,92 @@ if (process.argv[2] !== 'keys') {
486
501
  let thinkCallCount = 0;
487
502
  let orientCallCount = 0;
488
503
  let initialized = false;
504
+ // ---------------------------------------------------------------------------
505
+ // MCP elicitation: where the client can ask its user mid-task, a held
506
+ // marrow_auto action is approved in the client's own dialog (one click). The
507
+ // request goes to the client UI, never to the model; the answer is reported to
508
+ // Marrow as client-attested.
509
+ // ---------------------------------------------------------------------------
510
+ const ELICITATION_PROTOCOL_VERSIONS = ['2025-06-18', '2025-11-25'];
511
+ const ELICITATION_TIMEOUT_MS = 120_000;
512
+ let clientElicitation = false;
513
+ let clientHost = 'other';
514
+ let serverRequestSeq = 0;
515
+ const pendingClientRequests = new Map();
516
+ function noteClientCapabilities(params) {
517
+ const record = params && typeof params === 'object' ? params : {};
518
+ const capabilities = record.capabilities && typeof record.capabilities === 'object' ? record.capabilities : {};
519
+ const requested = typeof record.protocolVersion === 'string' ? record.protocolVersion : '';
520
+ const info = record.clientInfo && typeof record.clientInfo === 'object' ? record.clientInfo : {};
521
+ const name = typeof info.name === 'string' ? info.name.toLowerCase() : '';
522
+ clientHost = /claude/.test(name) ? 'claude-code' : /cursor/.test(name) ? 'cursor' : /hermes/.test(name) ? 'hermes'
523
+ : /codex/.test(name) ? 'codex' : /gemini/.test(name) ? 'gemini' : 'other';
524
+ clientElicitation = Boolean(capabilities.elicitation && typeof capabilities.elicitation === 'object')
525
+ && ELICITATION_PROTOCOL_VERSIONS.includes(requested);
526
+ // Elicitation needs the protocol version that defines it; otherwise keep the version this server always spoke.
527
+ return clientElicitation ? requested : '2024-11-05';
528
+ }
529
+ function requestClient(method, params, timeoutMs) {
530
+ serverRequestSeq += 1;
531
+ const id = `marrow-server-${serverRequestSeq}`;
532
+ return new Promise((resolve) => {
533
+ const timer = setTimeout(() => {
534
+ pendingClientRequests.delete(id);
535
+ resolve({ error: { code: 'timeout' } });
536
+ }, timeoutMs);
537
+ timer.unref?.();
538
+ pendingClientRequests.set(id, { resolve, timer });
539
+ send({ jsonrpc: '2.0', id, method, params });
540
+ });
541
+ }
542
+ /** A response from the client to one of this server's requests (elicitation). */
543
+ function settleClientResponse(message) {
544
+ if (typeof message.id !== 'string' || message.method !== undefined)
545
+ return false;
546
+ const pending = pendingClientRequests.get(message.id);
547
+ if (!pending)
548
+ return false;
549
+ pendingClientRequests.delete(message.id);
550
+ clearTimeout(pending.timer);
551
+ pending.resolve({ result: message.result, error: message.error });
552
+ return true;
553
+ }
554
+ /** Asks the user in the client's own dialog; approved, declined, or null (cancelled, timed out, unsupported). */
555
+ async function elicitApproval(request) {
556
+ if (!clientElicitation)
557
+ return null;
558
+ const answer = await requestClient('elicitation/create', {
559
+ message: request.message,
560
+ requestedSchema: {
561
+ type: 'object',
562
+ properties: {
563
+ decision: {
564
+ type: 'string',
565
+ title: 'Your answer',
566
+ description: 'Approve only if you authorize this exact action.',
567
+ enum: ['approve', 'decline'],
568
+ enumNames: ['Approve this action', 'Decline it'],
569
+ },
570
+ },
571
+ required: ['decision'],
572
+ },
573
+ }, ELICITATION_TIMEOUT_MS);
574
+ const result = answer.result && typeof answer.result === 'object' ? answer.result : null;
575
+ if (!result)
576
+ return null;
577
+ if (result.action === 'decline')
578
+ return 'declined';
579
+ if (result.action !== 'accept')
580
+ return null;
581
+ const content = result.content && typeof result.content === 'object' ? result.content : {};
582
+ return content.decision === 'approve' ? 'approved' : content.decision === 'decline' ? 'declined' : null;
583
+ }
584
+ function autoElicitationOptions() {
585
+ if (!clientElicitation)
586
+ return undefined;
587
+ const hostSessionId = typeof SESSION_ID === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(SESSION_ID) ? SESSION_ID : `mcp-${process.pid}`;
588
+ return { elicitApproval, elicitHost: clientHost, elicitHostSessionId: hostSessionId };
589
+ }
489
590
  function formatWarningActionably(w) {
490
591
  const pct = Math.round(w.failureRate * 100);
491
592
  return `⚠️ ${w.type} has ${pct}% failure rate — check what went wrong last time before proceeding`;
@@ -544,6 +645,37 @@ if (process.argv[2] !== 'keys') {
544
645
  ...(isError ? { isError: true } : {}),
545
646
  });
546
647
  }
648
+ /** Proof fields Marrow reported missing per decision or gate receipt, for a plain answer on a retry conflict. */
649
+ const commitProofMissing = new Map();
650
+ function proofFieldsGiven(proof) {
651
+ if (!proof || typeof proof !== 'object' || Array.isArray(proof))
652
+ return [];
653
+ return Object.entries(proof)
654
+ .filter(([, value]) => value !== null && value !== undefined && value !== ''
655
+ && !(Array.isArray(value) && value.length === 0)
656
+ && !(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0))
657
+ .map(([key]) => key);
658
+ }
659
+ /**
660
+ * A retry of marrow_commit that added proof, refused as a conflict with the
661
+ * outcome Marrow already holds (409 MARROW_OUTCOME_OBSERVATION_CONFLICT):
662
+ * say plainly which proof fields are still missing.
663
+ */
664
+ function proofRetryConflict(keys, proof, requiredFromHold) {
665
+ const required = [...new Set([...keys.flatMap((key) => commitProofMissing.get(key) || []), ...requiredFromHold])];
666
+ const given = proofFieldsGiven(proof);
667
+ const stillMissing = required.filter((field) => !given.includes(field));
668
+ return {
669
+ committed: false,
670
+ code: 'MARROW_OUTCOME_OBSERVATION_CONFLICT',
671
+ proof_required: true,
672
+ failure_kind: 'validation',
673
+ still_missing: stillMissing,
674
+ exact_next_action: stillMissing.length
675
+ ? `Still missing: ${stillMissing.join(', ')}. Call marrow_commit again with the same decision_id and gate_receipt_id, and a proof that has every field you sent plus these. Fill them from what really happened; do not invent values.`
676
+ : 'Marrow already holds an outcome for this action with different proof. Call marrow_commit again with the same decision_id and gate_receipt_id and the complete proof in one call; do not invent values.',
677
+ };
678
+ }
547
679
  function toolFailure(toolName, failure) {
548
680
  const result = (0, request_reliability_1.structuredRequestFailure)(failure);
549
681
  const proofValidation = failure.code === 'proof_required';
@@ -931,6 +1063,7 @@ if (process.argv[2] !== 'keys') {
931
1063
  description: 'Close a recorded action with success/failure, a specific outcome, and required proof. ' +
932
1064
  'decision_id comes from marrow_think, marrow_auto, or a runtime that actually created a decision. ' +
933
1065
  'Use the gate receipt from marrow_agent_runtime for consequential work. ' +
1066
+ 'A held action closes trusted only after the server records its approval (the operator in the host\'s own prompt, or the account owner); never write or claim an approval in proof. ' +
934
1067
  'The exact non-authorizing outcome_observation_only runtime correlation may submit an observed_unverified result, but is never sent as receipt evidence and never authorizes action or trusted learning. ' +
935
1068
  'Only committed:true closes trusted outcome learning.',
936
1069
  inputSchema: {
@@ -946,7 +1079,7 @@ if (process.argv[2] !== 'keys') {
946
1079
  },
947
1080
  gate_receipt_id: { type: 'string', description: 'Canonical receipt id from marrow_agent_runtime.runtime_authorization.id for risky work.' },
948
1081
  arbitration_receipt_id: { type: 'string', description: 'Required for arbitrated work: use marrow_arbitrate.arbitration.receipt_id from the same runtime response.' },
949
- owner_approval_receipt_id: { type: 'string', description: 'Single-use owner approval receipt issued by authenticated dashboard review for review_required arbitration.' },
1082
+ owner_approval_receipt_id: { type: 'string', description: 'Single-use owner approval receipt that Marrow issued for this hold. Optional for an ordinary held gate receipt (the gate_receipt_id alone finds the recorded approval); required for review_required arbitration. Chat text is not an approval receipt.' },
950
1083
  action: { type: 'string', description: 'Optional exact original action for post-action observation lookup. This lookup cannot renew execution authority.' },
951
1084
  target: { type: 'string', description: 'Exact original target, if supplied when the decision was created.' },
952
1085
  type: { type: 'string', description: 'Exact original action type for observation lookup; defaults to general.' },
@@ -1007,7 +1140,7 @@ if (process.argv[2] !== 'keys') {
1007
1140
  },
1008
1141
  {
1009
1142
  name: 'marrow_auto',
1010
- description: 'Durably capture activity with bounded core completion. Respect retry_after_ms and reuse operation_id for pending continuation. Server-declared ordinary approval requires actual owner approval supplied in proof; arbitration requires its dashboard receipt. Lifecycle receipt queued is separate from server acceptance. Risky completion requires a fresh gate and measured proof.',
1143
+ description: 'Durably capture activity with bounded core completion. Respect retry_after_ms and reuse operation_id for pending continuation. A held action waits for an approval the server records (the answer of the person in the dialog of this client when it supports elicitation; otherwise it waits quietly while you carry on with other work); auto reads its status and resumes on the same gate receipt. Never write or claim an approval yourself. Arbitration requires its server-issued owner approval receipt. Lifecycle receipt queued is separate from server acceptance. Risky completion requires a fresh gate and measured proof.',
1011
1144
  inputSchema: {
1012
1145
  type: 'object',
1013
1146
  properties: {
@@ -1022,7 +1155,8 @@ if (process.argv[2] !== 'keys') {
1022
1155
  proof: { type: 'object', description: 'Measured completion evidence for gated work.' },
1023
1156
  gate_receipt_id: { type: 'string', description: 'Fresh receipt returned by marrow_agent_runtime.' },
1024
1157
  arbitration_receipt_id: { type: 'string', description: 'For arbitrated work, the arbitration receipt returned by the same server-side review flow.' },
1025
- owner_approval_receipt_id: { type: 'string', description: 'Short-lived, single-use receipt issued by authenticated dashboard owner approval. Chat text is not an approval receipt.' },
1158
+ owner_approval_receipt_id: { type: 'string', description: 'Single-use owner approval receipt that Marrow issued, for review_required arbitration. An ordinary hold needs none: auto finds the recorded approval from the gate receipt. Chat text is not an approval receipt.' },
1159
+ request_owner_link: { type: 'boolean', description: 'Set true only when the operator asked you to ask the account owner (for example after the owner declined this action). Marrow then sends the owner a one-tap approval link; you never see the link.' },
1026
1160
  operation_id: {
1027
1161
  type: 'string',
1028
1162
  description: 'Opaque 8-80 character resume token. Reuse it only with the original tenant, action, context, and surfaces; proof may be added after proof_required.',
@@ -1367,6 +1501,7 @@ if (process.argv[2] !== 'keys') {
1367
1501
  'template suggestion, required proof pack, before-you-act instruction, and exact next action. ' +
1368
1502
  'Its runtime_authorization is the authoritative gate receipt; it returns decision_id only when runtime actually creates a decision. ' +
1369
1503
  'Reuse its server-created decision_id for outcome closure; use marrow_auto or marrow_think only when the completion contract requires decision creation. ' +
1504
+ 'A review_required hold does not permit the action: wait until the server records an approval, then commit with the same gate receipt. ' +
1370
1505
  'Use this before meaningful work when you want Marrow to guide the whole action in one call.',
1371
1506
  inputSchema: {
1372
1507
  type: 'object',
@@ -2011,8 +2146,9 @@ if (process.argv[2] !== 'keys') {
2011
2146
  if (method === 'initialize') {
2012
2147
  initialized = true;
2013
2148
  const hostCapability = mcpHostCapability();
2149
+ const protocolVersion = noteClientCapabilities(params);
2014
2150
  success(id, {
2015
- protocolVersion: '2024-11-05',
2151
+ protocolVersion,
2016
2152
  capabilities: { tools: {}, prompts: {} },
2017
2153
  serverInfo: { name: 'marrow', version: hook_contract_2.MCP_ADAPTER_VERSION },
2018
2154
  ...(AUTO_ENROLL ? {
@@ -2196,22 +2332,72 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
2196
2332
  const decision_id = requireString(args, 'decision_id');
2197
2333
  const outcome = requireString(args, 'outcome');
2198
2334
  const commitSuccess = requireBoolean(args, 'success');
2199
- const result = await withControlDeadline((signal) => (0, index_1.marrowCommit)(API_KEY, BASE_URL, {
2200
- decision_id,
2201
- success: commitSuccess,
2202
- outcome,
2203
- caused_by: args.caused_by,
2204
- proof: args.proof,
2205
- gate_receipt_id: args.gate_receipt_id,
2206
- arbitration_receipt_id: args.arbitration_receipt_id,
2207
- owner_approval_receipt_id: args.owner_approval_receipt_id,
2208
- action: args.action,
2209
- target: args.target,
2210
- type: args.type,
2211
- surfaces: args.surfaces,
2212
- auto_gate: args.auto_gate,
2213
- model_usage: args.model_usage,
2214
- }, SESSION_ID, FLEET_AGENT_ID, signal), { highRisk: true, cacheAware: false, toolName: 'marrow_commit' });
2335
+ // A receipt a host hook held: commit in the session it was issued to,
2336
+ // and send the operator's queued answer first so the commit closes
2337
+ // with that approval.
2338
+ let commitSessionId = SESSION_ID;
2339
+ if (typeof args.gate_receipt_id === 'string' && args.gate_receipt_id) {
2340
+ const holdContext = {
2341
+ apiKey: API_KEY,
2342
+ baseUrl: BASE_URL,
2343
+ sessionId: SESSION_ID || 'mcp-server',
2344
+ agentId: FLEET_AGENT_ID,
2345
+ harness: 'mcp-client',
2346
+ host: 'other',
2347
+ hostSessionId: SESSION_ID || 'mcp-server',
2348
+ };
2349
+ commitSessionId = (0, host_approval_1.holdSessionForReceipt)(holdContext, args.gate_receipt_id) || SESSION_ID;
2350
+ await (0, host_approval_1.deliverQueuedForReceipt)(holdContext, args.gate_receipt_id).catch(() => undefined);
2351
+ }
2352
+ const proofKeys = [`decision:${decision_id}`, ...(typeof args.gate_receipt_id === 'string' && args.gate_receipt_id ? [`receipt:${args.gate_receipt_id}`] : [])];
2353
+ let result;
2354
+ try {
2355
+ result = await withControlDeadline((signal) => (0, index_1.marrowCommit)(API_KEY, BASE_URL, {
2356
+ decision_id,
2357
+ success: commitSuccess,
2358
+ outcome,
2359
+ caused_by: args.caused_by,
2360
+ proof: args.proof,
2361
+ gate_receipt_id: args.gate_receipt_id,
2362
+ arbitration_receipt_id: args.arbitration_receipt_id,
2363
+ owner_approval_receipt_id: args.owner_approval_receipt_id,
2364
+ action: args.action,
2365
+ target: args.target,
2366
+ type: args.type,
2367
+ surfaces: args.surfaces,
2368
+ auto_gate: args.auto_gate,
2369
+ model_usage: args.model_usage,
2370
+ }, commitSessionId, FLEET_AGENT_ID, signal), { highRisk: true, cacheAware: false, toolName: 'marrow_commit' });
2371
+ }
2372
+ catch (err) {
2373
+ if (err instanceof request_reliability_1.MarrowRequestError && err.code === 'proof_required' && err.missingFields.length) {
2374
+ for (const key of proofKeys)
2375
+ commitProofMissing.set(key, [...err.missingFields]);
2376
+ }
2377
+ if (err instanceof request_reliability_1.MarrowRequestError && err.status === 409 && err.backendCode === 'MARROW_OUTCOME_OBSERVATION_CONFLICT') {
2378
+ const fromHold = typeof args.gate_receipt_id === 'string' && args.gate_receipt_id
2379
+ ? (0, host_approval_1.proofFieldsForReceipt)({ apiKey: API_KEY, baseUrl: BASE_URL, sessionId: SESSION_ID || 'mcp-server', agentId: FLEET_AGENT_ID, harness: 'mcp-client', host: 'other', hostSessionId: SESSION_ID || 'mcp-server' }, args.gate_receipt_id)
2380
+ : [];
2381
+ toolSuccess(id, proofRetryConflict(proofKeys, args.proof, fromHold), true);
2382
+ return;
2383
+ }
2384
+ throw err;
2385
+ }
2386
+ if (result.committed) {
2387
+ for (const key of proofKeys)
2388
+ commitProofMissing.delete(key);
2389
+ }
2390
+ else {
2391
+ // A short close is accepted unverified with the missing proof named ("Retry only after proof includes: a, b.").
2392
+ const unverified = result;
2393
+ const listed = unverified.governance_validation?.code === 'MARROW_PROOF_PACK_INCOMPLETE'
2394
+ ? /proof includes: ([A-Za-z0-9_, ]{1,600})\.?\s*$/.exec(String(unverified.exact_next_action || ''))?.[1]
2395
+ : undefined;
2396
+ const missing = listed ? listed.split(',').map((field) => field.trim()).filter(Boolean) : [];
2397
+ if (missing.length)
2398
+ for (const key of proofKeys)
2399
+ commitProofMissing.set(key, missing);
2400
+ }
2215
2401
  const commitResult = { ...result, narrative: result.narrative ?? null };
2216
2402
  lastCommitted = result.committed;
2217
2403
  lastDecisionId = result.committed ? null : decision_id;
@@ -2314,7 +2500,8 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
2314
2500
  // work obtains exactly one canonical runtime authorization inside auto.
2315
2501
  auto_gate: highRisk,
2316
2502
  operation_id: typeof args.operation_id === 'string' ? args.operation_id : undefined,
2317
- }, SESSION_ID, FLEET_AGENT_ID, 8_000);
2503
+ ...(args.request_owner_link === true ? { request_owner_link: true } : {}),
2504
+ }, SESSION_ID, FLEET_AGENT_ID, 8_000, autoElicitationOptions());
2318
2505
  let delivered = null;
2319
2506
  let deliveryFailure = null;
2320
2507
  let failedHttpAttemptTrace = null;
@@ -2337,7 +2524,10 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
2337
2524
  deferDelivery: true,
2338
2525
  event: {
2339
2526
  ...(delivered?.operation_id ? {
2340
- event_id: `auto_${delivered.committed ? 'closed' : 'pending'}_${delivered.operation_id}`,
2527
+ // A closed operation has one record; each pending call is its own attempt.
2528
+ event_id: delivered.committed
2529
+ ? `auto_closed_${delivered.operation_id}`
2530
+ : `auto_pending_${delivered.operation_id}_${(0, node_crypto_1.randomUUID)().replace(/-/g, '').slice(0, 12)}`,
2341
2531
  } : {}),
2342
2532
  event_type: delivered?.committed
2343
2533
  ? 'outcome_committed'
@@ -2369,17 +2559,23 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
2369
2559
  ? 'intent_confirmed'
2370
2560
  : 'durably_queued',
2371
2561
  receipt,
2372
- completion_state: delivered?.committed
2373
- ? 'closed_with_proof'
2374
- : delivered?.phase === 'review_required'
2375
- ? 'review_required_terminal'
2376
- : delivered?.phase === 'owner_approval_required'
2377
- ? 'pending_owner_approval'
2378
- : delivered?.phase === 'proof_required'
2379
- ? 'pending_required_proof'
2380
- : delivered?.phase === 'decision_created' || outcomeSuccess === undefined
2381
- ? 'pending_evidence'
2382
- : 'delivery_pending',
2562
+ completion_state: delivered?.committed && delivered.closure === 'gate_denial'
2563
+ ? 'closed_as_denial'
2564
+ : delivered?.closure === 'already_closed_denial'
2565
+ ? 'closed_earlier_as_denial'
2566
+ : delivered?.closure === 'already_closed'
2567
+ ? 'closed_earlier'
2568
+ : delivered?.committed
2569
+ ? 'closed_with_proof'
2570
+ : delivered?.phase === 'review_required'
2571
+ ? 'review_required_terminal'
2572
+ : delivered?.phase === 'owner_approval_required'
2573
+ ? 'pending_owner_approval'
2574
+ : delivered?.phase === 'proof_required'
2575
+ ? 'pending_required_proof'
2576
+ : delivered?.phase === 'decision_created' || outcomeSuccess === undefined
2577
+ ? 'pending_evidence'
2578
+ : 'delivery_pending',
2383
2579
  decision_id: delivered?.decision_id || null,
2384
2580
  operation_id: delivered?.operation_id || (typeof args.operation_id === 'string' ? args.operation_id : null),
2385
2581
  phase: delivered?.phase || null,
@@ -2390,17 +2586,19 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
2390
2586
  attempts: [],
2391
2587
  dropped_count: 0,
2392
2588
  },
2393
- exact_next_action: delivered?.committed
2394
- ? 'The governed outcome is closed. Reuse this decision_id only for read-only trace inspection.'
2395
- : delivered?.exact_next_action
2396
- ? delivered.exact_next_action
2397
- : delivered?.phase === 'owner_approval_required'
2398
- ? 'Approve this exact arbitration decision in the authenticated Marrow dashboard, then call marrow_auto once with this same operation_id, arbitration_receipt_id, and the server-issued owner_approval_receipt_id. Do not retry proof or use chat approval text.'
2399
- : delivered?.phase === 'proof_required'
2400
- ? 'Attach the required measured proof and retry marrow_auto with this same operation_id and unchanged action, context, and surfaces.'
2401
- : delivered?.resumable
2402
- ? 'Retry marrow_auto with this same operation_id. Do not start a new auto operation.'
2403
- : 'Close this decision after the real outcome is known.',
2589
+ exact_next_action: delivered?.committed && delivered.closure === 'gate_denial' && delivered.exact_next_action
2590
+ ? delivered.exact_next_action
2591
+ : delivered?.committed
2592
+ ? 'The governed outcome is closed. Reuse this decision_id only for read-only trace inspection.'
2593
+ : delivered?.exact_next_action
2594
+ ? delivered.exact_next_action
2595
+ : delivered?.phase === 'owner_approval_required'
2596
+ ? 'The account owner must approve one proposal of this arbitration before it runs; Marrow then issues an owner_approval_receipt_id. Do not run any proposal or write an approval yourself. Once Marrow issues the receipt, call marrow_auto once with this same operation_id, arbitration_receipt_id and owner_approval_receipt_id.'
2597
+ : delivered?.phase === 'proof_required'
2598
+ ? 'Attach the required measured proof and retry marrow_auto with this same operation_id and unchanged action, context, and surfaces.'
2599
+ : delivered?.resumable
2600
+ ? 'Retry marrow_auto with this same operation_id. Do not start a new auto operation.'
2601
+ : 'Close this decision after the real outcome is known.',
2404
2602
  live_delivery: {
2405
2603
  accepted: Boolean(delivered?.decision_id),
2406
2604
  committed: Boolean(delivered?.committed),
@@ -2408,6 +2606,7 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
2408
2606
  },
2409
2607
  host_capability: mcpHostCapability(),
2410
2608
  client_update: (0, request_reliability_1.localClientUpdate)(),
2609
+ ...(delivered?.approval ? { approval: delivered.approval } : {}),
2411
2610
  ...(runtimeGate ? { runtime_gate: runtimeGate } : {}),
2412
2611
  };
2413
2612
  // Measure through response construction; stdout/host consumption occurs
@@ -2982,6 +3181,9 @@ Marrow is not a replacement agent or a standalone memory app. Context and prior
2982
3181
  // MCP notifications (no id) must be silently ignored per spec
2983
3182
  if (msg.id === undefined || msg.id === null)
2984
3183
  continue;
3184
+ // The client's answer to this server's own request (elicitation).
3185
+ if (settleClientResponse(msg))
3186
+ continue;
2985
3187
  pendingRequests++;
2986
3188
  handleRequest(msg)
2987
3189
  .catch((err) => {