@fabricorg/platform-host 7.1.1 → 7.1.2

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
@@ -282,6 +282,221 @@ function isPromiseLike(value) {
282
282
  );
283
283
  }
284
284
 
285
+ // src/host-internals.ts
286
+ function asAtomicMutationStore(store) {
287
+ const candidate = store;
288
+ return typeof candidate.transactionWithEvents === "function" ? store : void 0;
289
+ }
290
+ function asGovernanceStore(store) {
291
+ const candidate = store;
292
+ return typeof candidate.recordMutationGovernance === "function" && typeof candidate.appendPolicyObligations === "function" && typeof candidate.appendExecutionAttestation === "function" ? store : void 0;
293
+ }
294
+ function withoutPrivateHostFields(data, eventResultFields) {
295
+ return Object.fromEntries(
296
+ Object.entries(data).filter(([key]) => !eventResultFields.includes(key))
297
+ );
298
+ }
299
+ function errorMessage(error) {
300
+ return error instanceof Error ? error.message : String(error);
301
+ }
302
+ function lifecycleId(prefix, invocationId, key) {
303
+ const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
304
+ return `${prefix}_${invocationId}_${safeKey}`;
305
+ }
306
+ function emitInvocationStatusTelemetry(invocation, status, telemetry, occurredAt, previousStatus) {
307
+ if (!status) return;
308
+ const event = status === "completed" ? { name: "invocation.completed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationCompleted } : status === "blocked_by_policy" ? { name: "invocation.policy_blocked", metricName: PLATFORM_HOST_METRIC_NAMES.invocationPolicyBlocked } : status === "validation_failed" ? { name: "invocation.validation_failed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationValidationFailed } : status === "waiting_for_approval" ? { name: "invocation.approval_waited", metricName: PLATFORM_HOST_METRIC_NAMES.invocationApprovalWaited } : status === "reconciliation_required" ? { name: "invocation.reconciliation_required", metricName: PLATFORM_HOST_METRIC_NAMES.invocationReconciliationRequired } : status === "failed" ? { name: "invocation.failed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationFailed } : void 0;
309
+ if (!event) return;
310
+ emitPlatformHostTelemetry(telemetry, {
311
+ kind: "event",
312
+ name: event.name,
313
+ metricName: event.metricName,
314
+ occurredAt,
315
+ tenantId: invocation.tenantId,
316
+ spaceId: invocation.spaceId,
317
+ attributes: {
318
+ ...invocation.actionId ? { actionId: invocation.actionId } : {},
319
+ ...invocation.actionVersion !== void 0 ? { actionVersion: invocation.actionVersion } : {},
320
+ ...previousStatus ? { fromStatus: previousStatus } : {}
321
+ }
322
+ });
323
+ }
324
+
325
+ // src/external-completion.ts
326
+ function createExternalCompletionLifecycle(deps) {
327
+ const { now, actionResolver, appendEvent, persistInvocation, actionResult: actionResult2, withConsistency, eventResultFields } = deps;
328
+ async function completeExternalInvocation(actionInvocationId, tenantId, spaceId, completion) {
329
+ if (typeof completion.externalReference !== "string" || completion.externalReference.trim() === "") {
330
+ throw new ExternalCompletionError("invalid_completion", "External completion must name the external reference it completes.");
331
+ }
332
+ if (completion.outcome !== "completed" && completion.outcome !== "failed") {
333
+ throw new ExternalCompletionError("invalid_completion", `External completion outcome must be "completed" or "failed", received "${String(completion.outcome)}".`);
334
+ }
335
+ const atomicStore = asAtomicMutationStore(deps.store);
336
+ if (atomicStore) {
337
+ return atomicStore.transactionWithEvents(async (transaction) => {
338
+ const read2 = transaction.getActionInvocationForUpdate ? await transaction.getActionInvocationForUpdate(actionInvocationId, tenantId, spaceId) : await deps.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
339
+ return settleExternalCompletion(read2, actionInvocationId, completion, {
340
+ update: (patch) => transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch),
341
+ transaction
342
+ });
343
+ });
344
+ }
345
+ const read = await deps.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
346
+ return settleExternalCompletion(read, actionInvocationId, completion, {
347
+ update: (patch) => deps.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch)
348
+ });
349
+ }
350
+ async function settleExternalCompletion(invocation, actionInvocationId, completion, writer) {
351
+ if (!invocation) throw new ExternalCompletionError("not_found", `ActionInvocation not found: ${actionInvocationId}`);
352
+ const recorded = invocation.externalCompletion;
353
+ const stillExecuting = !recorded && (invocation.status === "pending" || invocation.status === "running" && (invocation.leaseOwner !== void 0 || invocation.pendingCompletion?.parkedAt === void 0));
354
+ if (stillExecuting) {
355
+ throw new ExternalCompletionError("still_executing", `ActionInvocation ${actionInvocationId} is still executing; retry the completion after it parks.`);
356
+ }
357
+ if (recorded) {
358
+ if (recorded.externalReference !== completion.externalReference) {
359
+ throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.externalReference}", not "${completion.externalReference}".`);
360
+ }
361
+ if (completion.provider !== void 0 && recorded.provider !== void 0 && completion.provider !== recorded.provider) {
362
+ throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.provider}", not "${completion.provider}".`);
363
+ }
364
+ const incomingDigest = completionDigest(completion, recorded.provider ?? completion.provider);
365
+ if (recorded.digest === incomingDigest) {
366
+ return withConsistency(actionResult2(invocation), invocation.actionId);
367
+ }
368
+ const message = recorded.outcome === completion.outcome ? `External operation "${completion.externalReference}" reported ${completion.outcome} again with different evidence.` : `External operation "${completion.externalReference}" reported ${completion.outcome} after reporting ${recorded.outcome}.`;
369
+ await recordCompletionReconciliation(invocation, { ...completion, kind: `contradicted:${incomingDigest.slice(0, 16)}` }, message);
370
+ await appendEvent(invocation, {
371
+ eventType: "ExternalOperationContradicted",
372
+ subjectType: "AdapterInvocation",
373
+ subjectId: invocation.pendingCompletion?.adapterInvocationId ?? actionInvocationId,
374
+ payload: { externalReference: completion.externalReference, recorded: recorded.outcome, reported: completion.outcome }
375
+ }, `external:${completion.externalReference}:contradicted:${completion.outcome}`, 1, writer.transaction);
376
+ await writer.update({ status: "reconciliation_required", error: message });
377
+ emitInvocationStatusTelemetry(invocation, "reconciliation_required", deps.telemetry, now(), invocation.status);
378
+ return withConsistency(actionResult2({ ...invocation, status: "reconciliation_required", error: message }), invocation.actionId);
379
+ }
380
+ const pending = invocation.pendingCompletion;
381
+ if (!pending) {
382
+ const action = actionResolver(invocation.actionId);
383
+ if ((action?.execution?.completion ?? "immediate") === "immediate") {
384
+ throw new ExternalCompletionError("immediate_contract", `Action ${invocation.actionId} declares immediate completion; nothing external completes it.`);
385
+ }
386
+ throw new ExternalCompletionError("not_awaiting", `ActionInvocation ${actionInvocationId} is not awaiting an external completion.`);
387
+ }
388
+ if (pending.externalReference !== completion.externalReference) {
389
+ throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} awaits "${pending.externalReference}", not "${completion.externalReference}".`);
390
+ }
391
+ if (completion.provider !== void 0 && completion.provider !== pending.provider) {
392
+ throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} awaits completion from "${pending.provider}", not "${completion.provider}".`);
393
+ }
394
+ const recordedAt = now();
395
+ const externalCompletion = {
396
+ ...completion,
397
+ provider: completion.provider ?? pending.provider,
398
+ observedAt: completion.observedAt instanceof Date ? completion.observedAt.toISOString() : completion.observedAt,
399
+ recordedAt,
400
+ digest: completionDigest(completion, completion.provider ?? pending.provider)
401
+ };
402
+ const subject = { subjectType: "AdapterInvocation", subjectId: pending.adapterInvocationId };
403
+ const settles = invocation.status === "running" && pending.parkedAt !== void 0 || invocation.status === "reconciliation_required" && invocation.error?.includes("did not complete by") === true;
404
+ if (completion.outcome === "completed") {
405
+ const action = actionResolver(invocation.actionId);
406
+ const merged = withoutPrivateHostFields({ ...invocation.result, ...completion.result ?? {} }, eventResultFields);
407
+ if (action?.resultSchema) {
408
+ const parsedResult = action.resultSchema.safeParse(merged);
409
+ if (!parsedResult.success) {
410
+ throw new ExternalCompletionError("result_invalid", `External completion result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
411
+ }
412
+ }
413
+ await deps.store.updateAdapterInvocation(pending.adapterInvocationId, {
414
+ ...completion.result ? { output: withoutPrivateHostFields(completion.result, eventResultFields) } : {},
415
+ updatedAt: recordedAt
416
+ });
417
+ await appendEvent(invocation, {
418
+ eventType: "ExternalOperationCompleted",
419
+ ...subject,
420
+ payload: { externalReference: pending.externalReference, provider: pending.provider, ...completion.result ? { result: withoutPrivateHostFields(completion.result, eventResultFields) } : {} }
421
+ }, `external:${pending.adapterInvocationId}:completed`, 1, writer.transaction);
422
+ const patch2 = settles ? { status: "completed", result: merged, externalCompletion, pendingCompletion: void 0, error: void 0 } : { externalCompletion, pendingCompletion: void 0 };
423
+ await writer.update(patch2);
424
+ if (settles) emitInvocationStatusTelemetry(invocation, "completed", deps.telemetry, now(), invocation.status);
425
+ return withConsistency(actionResult2({ ...invocation, ...patch2, result: settles ? merged : invocation.result, ...settles ? { error: void 0 } : {} }), invocation.actionId);
426
+ }
427
+ const error = completion.error ?? `External operation "${pending.externalReference}" failed.`;
428
+ await deps.store.updateAdapterInvocation(pending.adapterInvocationId, { status: "failed", error, updatedAt: recordedAt });
429
+ await appendEvent(invocation, {
430
+ eventType: "ExternalOperationFailed",
431
+ ...subject,
432
+ payload: { externalReference: pending.externalReference, provider: pending.provider, error }
433
+ }, `external:${pending.adapterInvocationId}:failed`, 1, writer.transaction);
434
+ const patch = settles ? { status: "failed", error, externalCompletion, pendingCompletion: void 0 } : { externalCompletion, pendingCompletion: void 0 };
435
+ await writer.update(patch);
436
+ if (settles) emitInvocationStatusTelemetry(invocation, "failed", deps.telemetry, now(), invocation.status);
437
+ return withConsistency(actionResult2({ ...invocation, ...patch }), invocation.actionId);
438
+ }
439
+ function completionDigest(completion, provider) {
440
+ return createHash("sha256").update(canonicalJson({
441
+ provider: provider ?? null,
442
+ externalReference: completion.externalReference,
443
+ outcome: completion.outcome,
444
+ result: completion.result ?? null,
445
+ error: completion.error ?? null,
446
+ evidenceReferences: completion.evidenceReferences ?? null
447
+ })).digest("hex");
448
+ }
449
+ async function recordCompletionReconciliation(invocation, completion, reason) {
450
+ const governanceStore = asGovernanceStore(deps.store);
451
+ if (!governanceStore) throw new Error("External completion reconciliation requires a governance-capable store.");
452
+ const provider = completion.provider ?? invocation.pendingCompletion?.provider ?? invocation.externalCompletion?.provider ?? "external";
453
+ const seed = createHash("sha256").update(`${provider}\0${completion.externalReference}\0${completion.kind ?? "refused"}`).digest("hex").slice(0, 32);
454
+ await governanceStore.appendExternalReconciliation({
455
+ id: lifecycleId("rec", invocation.id, `ext:${seed}`),
456
+ actionInvocationId: invocation.id,
457
+ tenantId: invocation.tenantId,
458
+ spaceId: invocation.spaceId,
459
+ status: "pending",
460
+ provider,
461
+ externalOperationId: completion.externalReference,
462
+ attempt: 1,
463
+ reason,
464
+ ...completion.evidenceReferences ? { evidenceReferences: completion.evidenceReferences } : {},
465
+ observedAt: now()
466
+ });
467
+ }
468
+ async function reconcileOverdueCompletions(input = {}) {
469
+ const recoverable = deps.store;
470
+ if (!recoverable.listActionInvocations) {
471
+ throw new Error("Overdue completion reconciliation requires a store that can list invocations.");
472
+ }
473
+ if (!asGovernanceStore(deps.store)) {
474
+ throw new Error("Overdue completion reconciliation requires a governance-capable store to record its findings.");
475
+ }
476
+ const current = input.now ?? now();
477
+ const candidates = await recoverable.listActionInvocations({
478
+ statuses: ["running"],
479
+ completionDueBefore: current,
480
+ unleased: true,
481
+ ...input.tenantId ? { tenantId: input.tenantId } : {},
482
+ ...input.spaceId ? { spaceId: input.spaceId } : {},
483
+ limit: Math.max(1, Math.min(input.limit ?? 100, 1e3))
484
+ });
485
+ const overdue = [];
486
+ for (const invocation of candidates) {
487
+ const pending = invocation.pendingCompletion;
488
+ if (!pending?.dueAt || pending.dueAt.getTime() > current.getTime()) continue;
489
+ if (invocation.leaseOwner) continue;
490
+ const message = `External operation "${pending.externalReference}" from ${pending.provider} did not complete by ${pending.dueAt.toISOString()}.`;
491
+ await recordCompletionReconciliation(invocation, { externalReference: pending.externalReference, provider: pending.provider, kind: "overdue" }, message);
492
+ await persistInvocation(invocation, { status: "reconciliation_required", error: message });
493
+ overdue.push(invocation.id);
494
+ }
495
+ return { overdue };
496
+ }
497
+ return { completeExternalInvocation, reconcileOverdueCompletions, recordCompletionReconciliation };
498
+ }
499
+
285
500
  // src/host.ts
286
501
  var DEFAULT_EXTRACT_EVENTS = (data) => {
287
502
  const value = data._events;
@@ -1040,7 +1255,7 @@ function createGovernedActionHost(options) {
1040
1255
  });
1041
1256
  let error = refuse;
1042
1257
  try {
1043
- await recordCompletionReconciliation(invocation, { externalReference: reference, provider: adapter.vendor }, refuse);
1258
+ await externalCompletion.recordCompletionReconciliation(invocation, { externalReference: reference, provider: adapter.vendor }, refuse);
1044
1259
  } catch (logError) {
1045
1260
  error = `${refuse} Reconciliation log unavailable: ${errorMessage(logError)}`;
1046
1261
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { error });
@@ -1121,7 +1336,7 @@ function createGovernedActionHost(options) {
1121
1336
  await persistInvocation(invocation, { status: "reconciliation_required", error: message });
1122
1337
  let error = message;
1123
1338
  try {
1124
- await recordCompletionReconciliation(invocation, { externalReference: pendingHandoff.externalReference, provider: pendingHandoff.provider, kind: "obligations" }, message);
1339
+ await externalCompletion.recordCompletionReconciliation(invocation, { externalReference: pendingHandoff.externalReference, provider: pendingHandoff.provider, kind: "obligations" }, message);
1125
1340
  } catch (logError) {
1126
1341
  error = `${message} Reconciliation log unavailable: ${errorMessage(logError)}`;
1127
1342
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { error });
@@ -1314,175 +1529,6 @@ function createGovernedActionHost(options) {
1314
1529
  spaceId
1315
1530
  });
1316
1531
  }
1317
- async function completeExternalInvocation(actionInvocationId, tenantId, spaceId, completion) {
1318
- if (typeof completion.externalReference !== "string" || completion.externalReference.trim() === "") {
1319
- throw new ExternalCompletionError("invalid_completion", "External completion must name the external reference it completes.");
1320
- }
1321
- if (completion.outcome !== "completed" && completion.outcome !== "failed") {
1322
- throw new ExternalCompletionError("invalid_completion", `External completion outcome must be "completed" or "failed", received "${String(completion.outcome)}".`);
1323
- }
1324
- const atomicStore = asAtomicMutationStore(options.store);
1325
- if (atomicStore) {
1326
- return atomicStore.transactionWithEvents(async (transaction) => {
1327
- const read2 = transaction.getActionInvocationForUpdate ? await transaction.getActionInvocationForUpdate(actionInvocationId, tenantId, spaceId) : await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
1328
- return settleExternalCompletion(read2, actionInvocationId, completion, {
1329
- update: (patch) => transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch),
1330
- transaction
1331
- });
1332
- });
1333
- }
1334
- const read = await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
1335
- return settleExternalCompletion(read, actionInvocationId, completion, {
1336
- update: (patch) => options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch)
1337
- });
1338
- }
1339
- async function settleExternalCompletion(invocation, actionInvocationId, completion, writer) {
1340
- if (!invocation) throw new ExternalCompletionError("not_found", `ActionInvocation not found: ${actionInvocationId}`);
1341
- const recorded = invocation.externalCompletion;
1342
- const stillExecuting = !recorded && (invocation.status === "pending" || invocation.status === "running" && (invocation.leaseOwner !== void 0 || invocation.pendingCompletion?.parkedAt === void 0));
1343
- if (stillExecuting) {
1344
- throw new ExternalCompletionError("still_executing", `ActionInvocation ${actionInvocationId} is still executing; retry the completion after it parks.`);
1345
- }
1346
- if (recorded) {
1347
- if (recorded.externalReference !== completion.externalReference) {
1348
- throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.externalReference}", not "${completion.externalReference}".`);
1349
- }
1350
- if (completion.provider !== void 0 && recorded.provider !== void 0 && completion.provider !== recorded.provider) {
1351
- throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.provider}", not "${completion.provider}".`);
1352
- }
1353
- const incomingDigest = completionDigest(completion, recorded.provider ?? completion.provider);
1354
- if (recorded.digest === incomingDigest) {
1355
- return withConsistency(actionResult(invocation), invocation.actionId);
1356
- }
1357
- const message = recorded.outcome === completion.outcome ? `External operation "${completion.externalReference}" reported ${completion.outcome} again with different evidence.` : `External operation "${completion.externalReference}" reported ${completion.outcome} after reporting ${recorded.outcome}.`;
1358
- await recordCompletionReconciliation(invocation, { ...completion, kind: `contradicted:${incomingDigest.slice(0, 16)}` }, message);
1359
- await appendEvent(invocation, {
1360
- eventType: "ExternalOperationContradicted",
1361
- subjectType: "AdapterInvocation",
1362
- subjectId: invocation.pendingCompletion?.adapterInvocationId ?? actionInvocationId,
1363
- payload: { externalReference: completion.externalReference, recorded: recorded.outcome, reported: completion.outcome }
1364
- }, `external:${completion.externalReference}:contradicted:${completion.outcome}`, 1, writer.transaction);
1365
- await writer.update({ status: "reconciliation_required", error: message });
1366
- emitInvocationStatusTelemetry(invocation, "reconciliation_required", options.telemetry, now(), invocation.status);
1367
- return withConsistency(actionResult({ ...invocation, status: "reconciliation_required", error: message }), invocation.actionId);
1368
- }
1369
- const pending = invocation.pendingCompletion;
1370
- if (!pending) {
1371
- const action = actionResolver(invocation.actionId);
1372
- if ((action?.execution?.completion ?? "immediate") === "immediate") {
1373
- throw new ExternalCompletionError("immediate_contract", `Action ${invocation.actionId} declares immediate completion; nothing external completes it.`);
1374
- }
1375
- throw new ExternalCompletionError("not_awaiting", `ActionInvocation ${actionInvocationId} is not awaiting an external completion.`);
1376
- }
1377
- if (pending.externalReference !== completion.externalReference) {
1378
- throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} awaits "${pending.externalReference}", not "${completion.externalReference}".`);
1379
- }
1380
- if (completion.provider !== void 0 && completion.provider !== pending.provider) {
1381
- throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} awaits completion from "${pending.provider}", not "${completion.provider}".`);
1382
- }
1383
- const recordedAt = now();
1384
- const externalCompletion = {
1385
- ...completion,
1386
- provider: completion.provider ?? pending.provider,
1387
- observedAt: completion.observedAt instanceof Date ? completion.observedAt.toISOString() : completion.observedAt,
1388
- recordedAt,
1389
- digest: completionDigest(completion, completion.provider ?? pending.provider)
1390
- };
1391
- const subject = { subjectType: "AdapterInvocation", subjectId: pending.adapterInvocationId };
1392
- const settles = invocation.status === "running" && pending.parkedAt !== void 0 || invocation.status === "reconciliation_required" && invocation.error?.includes("did not complete by") === true;
1393
- if (completion.outcome === "completed") {
1394
- const action = actionResolver(invocation.actionId);
1395
- const merged = withoutPrivateHostFields({ ...invocation.result, ...completion.result ?? {} }, eventResultFields);
1396
- if (action?.resultSchema) {
1397
- const parsedResult = action.resultSchema.safeParse(merged);
1398
- if (!parsedResult.success) {
1399
- throw new ExternalCompletionError("result_invalid", `External completion result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
1400
- }
1401
- }
1402
- await options.store.updateAdapterInvocation(pending.adapterInvocationId, {
1403
- ...completion.result ? { output: withoutPrivateHostFields(completion.result, eventResultFields) } : {},
1404
- updatedAt: recordedAt
1405
- });
1406
- await appendEvent(invocation, {
1407
- eventType: "ExternalOperationCompleted",
1408
- ...subject,
1409
- payload: { externalReference: pending.externalReference, provider: pending.provider, ...completion.result ? { result: withoutPrivateHostFields(completion.result, eventResultFields) } : {} }
1410
- }, `external:${pending.adapterInvocationId}:completed`, 1, writer.transaction);
1411
- const patch2 = settles ? { status: "completed", result: merged, externalCompletion, pendingCompletion: void 0, error: void 0 } : { externalCompletion, pendingCompletion: void 0 };
1412
- await writer.update(patch2);
1413
- if (settles) emitInvocationStatusTelemetry(invocation, "completed", options.telemetry, now(), invocation.status);
1414
- return withConsistency(actionResult({ ...invocation, ...patch2, result: settles ? merged : invocation.result, ...settles ? { error: void 0 } : {} }), invocation.actionId);
1415
- }
1416
- const error = completion.error ?? `External operation "${pending.externalReference}" failed.`;
1417
- await options.store.updateAdapterInvocation(pending.adapterInvocationId, { status: "failed", error, updatedAt: recordedAt });
1418
- await appendEvent(invocation, {
1419
- eventType: "ExternalOperationFailed",
1420
- ...subject,
1421
- payload: { externalReference: pending.externalReference, provider: pending.provider, error }
1422
- }, `external:${pending.adapterInvocationId}:failed`, 1, writer.transaction);
1423
- const patch = settles ? { status: "failed", error, externalCompletion, pendingCompletion: void 0 } : { externalCompletion, pendingCompletion: void 0 };
1424
- await writer.update(patch);
1425
- if (settles) emitInvocationStatusTelemetry(invocation, "failed", options.telemetry, now(), invocation.status);
1426
- return withConsistency(actionResult({ ...invocation, ...patch }), invocation.actionId);
1427
- }
1428
- function completionDigest(completion, provider) {
1429
- return createHash("sha256").update(canonicalJson({
1430
- provider: provider ?? null,
1431
- externalReference: completion.externalReference,
1432
- outcome: completion.outcome,
1433
- result: completion.result ?? null,
1434
- error: completion.error ?? null,
1435
- evidenceReferences: completion.evidenceReferences ?? null
1436
- })).digest("hex");
1437
- }
1438
- async function recordCompletionReconciliation(invocation, completion, reason) {
1439
- const governanceStore = asGovernanceStore(options.store);
1440
- if (!governanceStore) throw new Error("External completion reconciliation requires a governance-capable store.");
1441
- const provider = completion.provider ?? invocation.pendingCompletion?.provider ?? invocation.externalCompletion?.provider ?? "external";
1442
- const seed = createHash("sha256").update(`${provider}\0${completion.externalReference}\0${completion.kind ?? "refused"}`).digest("hex").slice(0, 32);
1443
- await governanceStore.appendExternalReconciliation({
1444
- id: lifecycleId("rec", invocation.id, `ext:${seed}`),
1445
- actionInvocationId: invocation.id,
1446
- tenantId: invocation.tenantId,
1447
- spaceId: invocation.spaceId,
1448
- status: "pending",
1449
- provider,
1450
- externalOperationId: completion.externalReference,
1451
- attempt: 1,
1452
- reason,
1453
- ...completion.evidenceReferences ? { evidenceReferences: completion.evidenceReferences } : {},
1454
- observedAt: now()
1455
- });
1456
- }
1457
- async function reconcileOverdueCompletions(input = {}) {
1458
- const recoverable = options.store;
1459
- if (!recoverable.listActionInvocations) {
1460
- throw new Error("Overdue completion reconciliation requires a store that can list invocations.");
1461
- }
1462
- if (!asGovernanceStore(options.store)) {
1463
- throw new Error("Overdue completion reconciliation requires a governance-capable store to record its findings.");
1464
- }
1465
- const current = input.now ?? now();
1466
- const candidates = await recoverable.listActionInvocations({
1467
- statuses: ["running"],
1468
- completionDueBefore: current,
1469
- unleased: true,
1470
- ...input.tenantId ? { tenantId: input.tenantId } : {},
1471
- ...input.spaceId ? { spaceId: input.spaceId } : {},
1472
- limit: Math.max(1, Math.min(input.limit ?? 100, 1e3))
1473
- });
1474
- const overdue = [];
1475
- for (const invocation of candidates) {
1476
- const pending = invocation.pendingCompletion;
1477
- if (!pending?.dueAt || pending.dueAt.getTime() > current.getTime()) continue;
1478
- if (invocation.leaseOwner) continue;
1479
- const message = `External operation "${pending.externalReference}" from ${pending.provider} did not complete by ${pending.dueAt.toISOString()}.`;
1480
- await recordCompletionReconciliation(invocation, { externalReference: pending.externalReference, provider: pending.provider, kind: "overdue" }, message);
1481
- await persistInvocation(invocation, { status: "reconciliation_required", error: message });
1482
- overdue.push(invocation.id);
1483
- }
1484
- return { overdue };
1485
- }
1486
1532
  function declaredConsistency(actionId) {
1487
1533
  return actionResolver(actionId)?.execution?.consistency;
1488
1534
  }
@@ -1579,7 +1625,26 @@ function createGovernedActionHost(options) {
1579
1625
  );
1580
1626
  emitInvocationStatusTelemetry(invocation, patch.status, options.telemetry, now(), previousStatus);
1581
1627
  }
1582
- return { submitAction, executeInvocation, resumeApprovedInvocation, recordExecutionAttestation, recordExternalReconciliation, completeExternalInvocation, reconcileOverdueCompletions };
1628
+ const externalCompletion = createExternalCompletionLifecycle({
1629
+ store: options.store,
1630
+ telemetry: options.telemetry,
1631
+ eventResultFields,
1632
+ now,
1633
+ actionResolver,
1634
+ appendEvent,
1635
+ persistInvocation,
1636
+ actionResult,
1637
+ withConsistency
1638
+ });
1639
+ return {
1640
+ submitAction,
1641
+ executeInvocation,
1642
+ resumeApprovedInvocation,
1643
+ recordExecutionAttestation,
1644
+ recordExternalReconciliation,
1645
+ completeExternalInvocation: externalCompletion.completeExternalInvocation,
1646
+ reconcileOverdueCompletions: externalCompletion.reconcileOverdueCompletions
1647
+ };
1583
1648
  }
1584
1649
  function actionResult(invocation) {
1585
1650
  return {
@@ -1598,18 +1663,10 @@ function asApprovalStore(store) {
1598
1663
  const candidate = store;
1599
1664
  return typeof candidate.recordHitlDecision === "function" && typeof candidate.beginApprovalDecision === "function" ? store : void 0;
1600
1665
  }
1601
- function asAtomicMutationStore(store) {
1602
- const candidate = store;
1603
- return typeof candidate.transactionWithEvents === "function" ? store : void 0;
1604
- }
1605
1666
  function asOutboxStore(store) {
1606
1667
  const candidate = store;
1607
1668
  return typeof candidate.appendEventWithOutbox === "function" && typeof candidate.claimOutbox === "function" ? store : void 0;
1608
1669
  }
1609
- function asGovernanceStore(store) {
1610
- const candidate = store;
1611
- return typeof candidate.recordMutationGovernance === "function" && typeof candidate.appendPolicyObligations === "function" && typeof candidate.appendExecutionAttestation === "function" ? store : void 0;
1612
- }
1613
1670
  function numberValue(value) {
1614
1671
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1615
1672
  }
@@ -1645,39 +1702,9 @@ function initialState(machine) {
1645
1702
  function isTerminal(status) {
1646
1703
  return ["completed", "failed", "blocked_by_policy", "reconciliation_required", "validation_failed"].includes(status);
1647
1704
  }
1648
- function emitInvocationStatusTelemetry(invocation, status, telemetry, occurredAt, previousStatus) {
1649
- if (!status) return;
1650
- const event = status === "completed" ? { name: "invocation.completed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationCompleted } : status === "blocked_by_policy" ? { name: "invocation.policy_blocked", metricName: PLATFORM_HOST_METRIC_NAMES.invocationPolicyBlocked } : status === "validation_failed" ? { name: "invocation.validation_failed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationValidationFailed } : status === "waiting_for_approval" ? { name: "invocation.approval_waited", metricName: PLATFORM_HOST_METRIC_NAMES.invocationApprovalWaited } : status === "reconciliation_required" ? { name: "invocation.reconciliation_required", metricName: PLATFORM_HOST_METRIC_NAMES.invocationReconciliationRequired } : status === "failed" ? { name: "invocation.failed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationFailed } : void 0;
1651
- if (!event) return;
1652
- emitPlatformHostTelemetry(telemetry, {
1653
- kind: "event",
1654
- name: event.name,
1655
- metricName: event.metricName,
1656
- occurredAt,
1657
- tenantId: invocation.tenantId,
1658
- spaceId: invocation.spaceId,
1659
- attributes: {
1660
- ...invocation.actionId ? { actionId: invocation.actionId } : {},
1661
- ...invocation.actionVersion !== void 0 ? { actionVersion: invocation.actionVersion } : {},
1662
- ...previousStatus ? { fromStatus: previousStatus } : {}
1663
- }
1664
- });
1665
- }
1666
- function withoutPrivateHostFields(data, eventResultFields) {
1667
- return Object.fromEntries(
1668
- Object.entries(data).filter(([key]) => !eventResultFields.includes(key))
1669
- );
1670
- }
1671
- function errorMessage(error) {
1672
- return error instanceof Error ? error.message : String(error);
1673
- }
1674
1705
  function isRecord(value) {
1675
1706
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
1676
1707
  }
1677
- function lifecycleId(prefix, invocationId, key) {
1678
- const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
1679
- return `${prefix}_${invocationId}_${safeKey}`;
1680
- }
1681
1708
  function validateAuthorizationBinding(input, parameterDigest, authorityMoment, currentTime = /* @__PURE__ */ new Date()) {
1682
1709
  const binding = input.authorizationBinding;
1683
1710
  if (binding.id !== input.authorizationBindingId && input.authorizationBindingId) throw new Error("Authorization binding ID does not match authorizationBindingId.");