diff --git a/plugins/reporting-governance/src/core/runtime-integrated.mjs b/plugins/reporting-governance/src/core/runtime-integrated.mjs index 8d52b7d..80b84c6 100644 --- a/plugins/reporting-governance/src/core/runtime-integrated.mjs +++ b/plugins/reporting-governance/src/core/runtime-integrated.mjs @@ -70,13 +70,23 @@ function resolveRuntimeRoute({ governance, runtime, repoRootOverride }) { return createNotAttemptedResult('runtime execution not attempted: no adapter_action matched an adapter runner route'); } +function canPromoteAckedFromSupervisor(supervisor) { + const ackedCount = Number(supervisor?.ackedCount ?? 0); + const blockedCount = Number(supervisor?.blockedCount ?? 0); + const pendingCount = Number(supervisor?.pendingCount ?? 0); + + // Current runtime contract is a single notice settlement path. + // Only promote the overall truth state when the observed terminal set is fully acked. + return ackedCount > 0 && blockedCount === 0 && pendingCount === 0; +} + function promoteTruthStateFromRuntime(governance, routeResult) { const supervisor = routeResult.runtimeExecution?.result?.supervisor ?? null; if (!routeResult.attempted || !supervisor) { return governance; } - if ((supervisor.ackedCount ?? 0) > 0) { + if (canPromoteAckedFromSupervisor(supervisor)) { return { ...governance, planning: { @@ -87,7 +97,7 @@ function promoteTruthStateFromRuntime(governance, routeResult) { delivery_state: 'acked', notes: [ ...(Array.isArray(governance.planning?.receipt?.notes) ? governance.planning.receipt.notes : []), - 'Runtime execution produced sender-backed ack evidence; truth state promoted to acked.' + 'Runtime execution produced sender-backed ack evidence; truth state promoted to acked only after all observed terminal outcomes were fully acked.' ], }, }, @@ -148,5 +158,6 @@ export const __testables = { ADAPTER_ACTION_RUNNER_ROUTES, createNotAttemptedResult, resolveRuntimeRoute, + canPromoteAckedFromSupervisor, promoteTruthStateFromRuntime, }; diff --git a/plugins/reporting-governance/test/runtime-integrated.integration.test.mjs b/plugins/reporting-governance/test/runtime-integrated.integration.test.mjs index 81c442e..ff1e693 100644 --- a/plugins/reporting-governance/test/runtime-integrated.integration.test.mjs +++ b/plugins/reporting-governance/test/runtime-integrated.integration.test.mjs @@ -5,6 +5,7 @@ import path from 'node:path'; import os from 'node:os'; import { executeRuntimeIntegratedGovernance } from '../src/index.mjs'; +import { __testables as runtimeIntegratedTestables } from '../src/core/runtime-integrated.mjs'; import capabilityDescriptor from '../capabilities/openclaw-watchdog-reference.json' with { type: 'json' }; const packageRoot = path.resolve(import.meta.dirname, '..'); @@ -267,6 +268,34 @@ function assertAckedTruthState(result) { ); } +test('truth-state promotion guardrail: mixed terminal outcomes must not promote overall state to acked', () => { + assert.equal(runtimeIntegratedTestables.canPromoteAckedFromSupervisor({ + ackedCount: 1, + pendingCount: 1, + blockedCount: 0, + }), false); + + assert.equal(runtimeIntegratedTestables.canPromoteAckedFromSupervisor({ + ackedCount: 1, + pendingCount: 0, + blockedCount: 1, + }), false); +}); + +test('truth-state promotion guardrail: only fully acked observed terminal set may promote to acked', () => { + assert.equal(runtimeIntegratedTestables.canPromoteAckedFromSupervisor({ + ackedCount: 0, + pendingCount: 0, + blockedCount: 0, + }), false); + + assert.equal(runtimeIntegratedTestables.canPromoteAckedFromSupervisor({ + ackedCount: 1, + pendingCount: 0, + blockedCount: 0, + }), true); +}); + const futureTruthStateMatrix = Object.freeze({ deferred: { contractDeliveryState: 'pending_external_send',