agentxchain 2.46.0 → 2.46.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/dashboard/app.js +6 -0
- package/dashboard/components/coordinator-timeouts.js +220 -0
- package/dashboard/components/timeouts.js +201 -0
- package/dashboard/index.html +2 -0
- package/package.json +1 -1
- package/scripts/publish-from-tag.sh +33 -4
- package/src/commands/init.js +1 -0
- package/src/commands/migrate.js +1 -0
- package/src/commands/status.js +49 -0
- package/src/lib/approval-policy.js +139 -0
- package/src/lib/blocked-state.js +11 -0
- package/src/lib/dashboard/bridge-server.js +14 -0
- package/src/lib/dashboard/coordinator-timeout-status.js +139 -0
- package/src/lib/dashboard/timeout-status.js +201 -0
- package/src/lib/governed-state.js +373 -25
- package/src/lib/normalized-config.js +123 -0
- package/src/lib/reference-conformance-adapter.js +1 -0
- package/src/lib/repo-observer.js +132 -1
- package/src/lib/report.js +323 -6
- package/src/lib/schema.js +47 -0
- package/src/lib/timeout-evaluator.js +234 -0
|
@@ -20,11 +20,15 @@ import { join, dirname } from 'path';
|
|
|
20
20
|
import { randomBytes, createHash } from 'crypto';
|
|
21
21
|
import { safeWriteJson } from './safe-write.js';
|
|
22
22
|
import { validateStagedTurnResult } from './turn-result-validator.js';
|
|
23
|
-
import { evaluatePhaseExit, evaluateRunCompletion } from './gate-evaluator.js';
|
|
23
|
+
import { evaluatePhaseExit, evaluateRunCompletion, getNextPhase } from './gate-evaluator.js';
|
|
24
|
+
import { evaluateApprovalPolicy } from './approval-policy.js';
|
|
24
25
|
import { evaluatePolicies } from './policy-evaluator.js';
|
|
26
|
+
import { buildTimeoutBlockedReason, evaluateTimeouts } from './timeout-evaluator.js';
|
|
25
27
|
import {
|
|
26
28
|
captureBaseline,
|
|
27
29
|
observeChanges,
|
|
30
|
+
attributeObservedChangesToTurn,
|
|
31
|
+
buildConflictCandidateFiles,
|
|
28
32
|
classifyObservedChanges,
|
|
29
33
|
buildObservedArtifact,
|
|
30
34
|
normalizeVerification,
|
|
@@ -57,6 +61,50 @@ function generateId(prefix) {
|
|
|
57
61
|
return `${prefix}_${randomBytes(8).toString('hex')}`;
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
function normalizeGateFailure(value) {
|
|
65
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
gate_type: value.gate_type === 'run_completion' ? 'run_completion' : 'phase_transition',
|
|
70
|
+
gate_id: typeof value.gate_id === 'string' && value.gate_id.length > 0 ? value.gate_id : null,
|
|
71
|
+
phase: typeof value.phase === 'string' && value.phase.length > 0 ? value.phase : 'unknown',
|
|
72
|
+
from_phase: typeof value.from_phase === 'string' && value.from_phase.length > 0 ? value.from_phase : null,
|
|
73
|
+
to_phase: typeof value.to_phase === 'string' && value.to_phase.length > 0 ? value.to_phase : null,
|
|
74
|
+
requested_by_turn: typeof value.requested_by_turn === 'string' && value.requested_by_turn.length > 0 ? value.requested_by_turn : null,
|
|
75
|
+
failed_at: typeof value.failed_at === 'string' && value.failed_at.length > 0 ? value.failed_at : null,
|
|
76
|
+
queued_request: value.queued_request === true,
|
|
77
|
+
reasons: Array.isArray(value.reasons) ? value.reasons.filter((reason) => typeof reason === 'string' && reason.length > 0) : [],
|
|
78
|
+
missing_files: Array.isArray(value.missing_files) ? value.missing_files.filter((path) => typeof path === 'string' && path.length > 0) : [],
|
|
79
|
+
missing_verification: value.missing_verification === true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildGateFailureRecord({
|
|
84
|
+
gateType,
|
|
85
|
+
gateResult,
|
|
86
|
+
phase,
|
|
87
|
+
fromPhase = null,
|
|
88
|
+
toPhase = null,
|
|
89
|
+
requestedByTurn = null,
|
|
90
|
+
failedAt,
|
|
91
|
+
queuedRequest,
|
|
92
|
+
}) {
|
|
93
|
+
return normalizeGateFailure({
|
|
94
|
+
gate_type: gateType,
|
|
95
|
+
gate_id: gateResult?.gate_id || null,
|
|
96
|
+
phase,
|
|
97
|
+
from_phase: fromPhase,
|
|
98
|
+
to_phase: toPhase,
|
|
99
|
+
requested_by_turn: requestedByTurn,
|
|
100
|
+
failed_at: failedAt,
|
|
101
|
+
queued_request: queuedRequest,
|
|
102
|
+
reasons: Array.isArray(gateResult?.reasons) ? gateResult.reasons : [],
|
|
103
|
+
missing_files: Array.isArray(gateResult?.missing_files) ? gateResult.missing_files : [],
|
|
104
|
+
missing_verification: gateResult?.missing_verification === true,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
60
108
|
function emitBlockedNotification(root, config, state, details = {}, turn = null) {
|
|
61
109
|
if (!config?.notifications?.webhooks?.length) {
|
|
62
110
|
return;
|
|
@@ -608,6 +656,7 @@ function normalizeV1toV1_1(state) {
|
|
|
608
656
|
: {},
|
|
609
657
|
queued_phase_transition: state.queued_phase_transition ?? null,
|
|
610
658
|
queued_run_completion: state.queued_run_completion ?? null,
|
|
659
|
+
last_gate_failure: normalizeGateFailure(state.last_gate_failure),
|
|
611
660
|
};
|
|
612
661
|
}
|
|
613
662
|
|
|
@@ -880,8 +929,8 @@ function cleanupTurnArtifacts(root, turnId) {
|
|
|
880
929
|
} catch { /* best-effort */ }
|
|
881
930
|
}
|
|
882
931
|
|
|
883
|
-
function detectAcceptanceConflict(targetTurn,
|
|
884
|
-
const observedFiles = [...new Set(
|
|
932
|
+
function detectAcceptanceConflict(targetTurn, conflictFiles, historyEntries) {
|
|
933
|
+
const observedFiles = [...new Set(Array.isArray(conflictFiles) ? conflictFiles : [])];
|
|
885
934
|
if (observedFiles.length === 0) {
|
|
886
935
|
return null;
|
|
887
936
|
}
|
|
@@ -1000,6 +1049,101 @@ function buildBlockedReason({ category, recovery, turnId, blockedAt = new Date()
|
|
|
1000
1049
|
};
|
|
1001
1050
|
}
|
|
1002
1051
|
|
|
1052
|
+
function buildTimeoutLedgerEntry(timeoutResult, timestamp, turnId, phase, type = 'timeout') {
|
|
1053
|
+
return {
|
|
1054
|
+
type,
|
|
1055
|
+
scope: timeoutResult.scope,
|
|
1056
|
+
phase: timeoutResult.phase || phase || null,
|
|
1057
|
+
turn_id: turnId || null,
|
|
1058
|
+
limit_minutes: timeoutResult.limit_minutes,
|
|
1059
|
+
elapsed_minutes: timeoutResult.elapsed_minutes,
|
|
1060
|
+
exceeded_by_minutes: timeoutResult.exceeded_by_minutes,
|
|
1061
|
+
action: timeoutResult.action,
|
|
1062
|
+
timestamp,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function attemptTimeoutPhaseSkip({ root, config, updatedState, nextHistoryEntries, historyEntry, timeoutResult, now }) {
|
|
1067
|
+
const currentPhase = updatedState.phase;
|
|
1068
|
+
const nextPhase = getNextPhase(currentPhase, config.routing || {});
|
|
1069
|
+
if (!nextPhase) {
|
|
1070
|
+
return {
|
|
1071
|
+
ok: false,
|
|
1072
|
+
error: `Phase "${currentPhase}" has no next phase to skip to`,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const syntheticTurn = {
|
|
1077
|
+
...historyEntry,
|
|
1078
|
+
verification: historyEntry.verification || {},
|
|
1079
|
+
phase_transition_request: nextPhase,
|
|
1080
|
+
};
|
|
1081
|
+
const postAcceptanceState = {
|
|
1082
|
+
...updatedState,
|
|
1083
|
+
history: nextHistoryEntries,
|
|
1084
|
+
};
|
|
1085
|
+
const gateResult = evaluatePhaseExit({
|
|
1086
|
+
state: postAcceptanceState,
|
|
1087
|
+
config,
|
|
1088
|
+
acceptedTurn: syntheticTurn,
|
|
1089
|
+
root,
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
if (gateResult.action === 'advance') {
|
|
1093
|
+
return {
|
|
1094
|
+
ok: true,
|
|
1095
|
+
gateResult,
|
|
1096
|
+
updatedState: {
|
|
1097
|
+
...updatedState,
|
|
1098
|
+
phase: gateResult.next_phase,
|
|
1099
|
+
phase_entered_at: now,
|
|
1100
|
+
last_gate_failure: null,
|
|
1101
|
+
phase_gate_status: {
|
|
1102
|
+
...(updatedState.phase_gate_status || {}),
|
|
1103
|
+
[gateResult.gate_id || 'no_gate']: 'passed',
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
ledgerEntry: {
|
|
1107
|
+
...buildTimeoutLedgerEntry(timeoutResult, now, historyEntry.turn_id, currentPhase, 'timeout_skip'),
|
|
1108
|
+
from_phase: currentPhase,
|
|
1109
|
+
to_phase: gateResult.next_phase,
|
|
1110
|
+
gate_id: gateResult.gate_id || null,
|
|
1111
|
+
},
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const reasons = gateResult.action === 'awaiting_human_approval'
|
|
1116
|
+
? [`Gate "${gateResult.gate_id || 'unknown'}" still requires human approval; timeout skip cannot auto-advance`]
|
|
1117
|
+
: Array.isArray(gateResult.reasons) && gateResult.reasons.length > 0
|
|
1118
|
+
? gateResult.reasons
|
|
1119
|
+
: ['Timeout skip failed for an unknown reason'];
|
|
1120
|
+
const gateFailure = buildGateFailureRecord({
|
|
1121
|
+
gateType: 'phase_transition',
|
|
1122
|
+
gateResult: {
|
|
1123
|
+
...gateResult,
|
|
1124
|
+
reasons,
|
|
1125
|
+
},
|
|
1126
|
+
phase: currentPhase,
|
|
1127
|
+
fromPhase: currentPhase,
|
|
1128
|
+
toPhase: nextPhase,
|
|
1129
|
+
requestedByTurn: historyEntry.turn_id,
|
|
1130
|
+
failedAt: now,
|
|
1131
|
+
queuedRequest: false,
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
return {
|
|
1135
|
+
ok: false,
|
|
1136
|
+
gateFailure,
|
|
1137
|
+
ledgerEntry: {
|
|
1138
|
+
...buildTimeoutLedgerEntry(timeoutResult, now, historyEntry.turn_id, currentPhase, 'timeout_skip_failed'),
|
|
1139
|
+
from_phase: currentPhase,
|
|
1140
|
+
to_phase: nextPhase,
|
|
1141
|
+
gate_id: gateResult.gate_id || null,
|
|
1142
|
+
reasons,
|
|
1143
|
+
},
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1003
1147
|
function slugifyEscalationReason(reason) {
|
|
1004
1148
|
if (typeof reason !== 'string') {
|
|
1005
1149
|
return 'operator';
|
|
@@ -1410,6 +1554,15 @@ export function normalizeGovernedStateShape(state) {
|
|
|
1410
1554
|
changed = true;
|
|
1411
1555
|
}
|
|
1412
1556
|
|
|
1557
|
+
const normalizedLastGateFailure = normalizeGateFailure(nextState.last_gate_failure);
|
|
1558
|
+
if (JSON.stringify(nextState.last_gate_failure ?? null) !== JSON.stringify(normalizedLastGateFailure)) {
|
|
1559
|
+
nextState = {
|
|
1560
|
+
...nextState,
|
|
1561
|
+
last_gate_failure: normalizedLastGateFailure,
|
|
1562
|
+
};
|
|
1563
|
+
changed = true;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1413
1566
|
return { state: stripLegacyCurrentTurn(nextState), changed };
|
|
1414
1567
|
}
|
|
1415
1568
|
|
|
@@ -1627,9 +1780,12 @@ export function initializeGovernedRun(root, config) {
|
|
|
1627
1780
|
}
|
|
1628
1781
|
|
|
1629
1782
|
const runId = generateId('run');
|
|
1783
|
+
const now = new Date().toISOString();
|
|
1630
1784
|
const updatedState = {
|
|
1631
1785
|
...state,
|
|
1632
1786
|
run_id: runId,
|
|
1787
|
+
created_at: now,
|
|
1788
|
+
phase_entered_at: now,
|
|
1633
1789
|
status: 'active',
|
|
1634
1790
|
blocked_on: null,
|
|
1635
1791
|
blocked_reason: null,
|
|
@@ -2092,7 +2248,9 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2092
2248
|
const stagingFile = join(root, resolvedStagingPath);
|
|
2093
2249
|
const now = new Date().toISOString();
|
|
2094
2250
|
const baseline = currentTurn.baseline || null;
|
|
2095
|
-
const
|
|
2251
|
+
const rawObservation = observeChanges(root, baseline);
|
|
2252
|
+
const historyEntries = readJsonlEntries(root, HISTORY_PATH);
|
|
2253
|
+
const observation = attributeObservedChangesToTurn(rawObservation, currentTurn, historyEntries);
|
|
2096
2254
|
const role = config.roles?.[turnResult.role];
|
|
2097
2255
|
const runtimeId = turnResult.runtime_id;
|
|
2098
2256
|
const runtime = config.runtimes?.[runtimeId];
|
|
@@ -2125,7 +2283,6 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2125
2283
|
const normalizedVerification = normalizeVerification(turnResult.verification, runtimeType);
|
|
2126
2284
|
const artifactType = turnResult.artifact?.type || 'review';
|
|
2127
2285
|
const derivedRef = deriveAcceptedRef(observation, artifactType, state.accepted_integration_ref);
|
|
2128
|
-
const historyEntries = readJsonlEntries(root, HISTORY_PATH);
|
|
2129
2286
|
|
|
2130
2287
|
// Policy evaluation — declarative governance rules (spec: POLICY_ENGINE_SPEC.md)
|
|
2131
2288
|
const policyResult = evaluatePolicies(config.policies || [], {
|
|
@@ -2203,7 +2360,11 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2203
2360
|
};
|
|
2204
2361
|
}
|
|
2205
2362
|
|
|
2206
|
-
const conflict = detectAcceptanceConflict(
|
|
2363
|
+
const conflict = detectAcceptanceConflict(
|
|
2364
|
+
currentTurn,
|
|
2365
|
+
buildConflictCandidateFiles(rawObservation, observation, turnResult.files_changed || []),
|
|
2366
|
+
historyEntries,
|
|
2367
|
+
);
|
|
2207
2368
|
|
|
2208
2369
|
if (conflict) {
|
|
2209
2370
|
const detectionCount = (currentTurn.conflict_state?.detection_count || 0) + 1;
|
|
@@ -2338,6 +2499,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2338
2499
|
cost: turnResult.cost || {},
|
|
2339
2500
|
accepted_at: now,
|
|
2340
2501
|
};
|
|
2502
|
+
const nextHistoryEntries = [...historyEntries, historyEntry];
|
|
2341
2503
|
// Build ledger entries for the journal
|
|
2342
2504
|
const ledgerEntries = [];
|
|
2343
2505
|
if (turnResult.decisions && turnResult.decisions.length > 0) {
|
|
@@ -2451,6 +2613,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2451
2613
|
|
|
2452
2614
|
let gateResult = null;
|
|
2453
2615
|
let completionResult = null;
|
|
2616
|
+
let timeoutResult = null;
|
|
2454
2617
|
const hasRemainingTurns = Object.keys(remainingTurns).length > 0;
|
|
2455
2618
|
if (turnResult.status !== 'needs_human') {
|
|
2456
2619
|
if (hasRemainingTurns) {
|
|
@@ -2469,7 +2632,6 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2469
2632
|
};
|
|
2470
2633
|
}
|
|
2471
2634
|
} else {
|
|
2472
|
-
const nextHistoryEntries = [...historyEntries, historyEntry];
|
|
2473
2635
|
const postAcceptanceState = {
|
|
2474
2636
|
...state,
|
|
2475
2637
|
active_turns: remainingTurns,
|
|
@@ -2491,6 +2653,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2491
2653
|
if (completionResult.action === 'complete') {
|
|
2492
2654
|
updatedState.status = 'completed';
|
|
2493
2655
|
updatedState.completed_at = now;
|
|
2656
|
+
updatedState.last_gate_failure = null;
|
|
2494
2657
|
if (completionResult.gate_id) {
|
|
2495
2658
|
updatedState.phase_gate_status = {
|
|
2496
2659
|
...(updatedState.phase_gate_status || {}),
|
|
@@ -2500,16 +2663,71 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2500
2663
|
updatedState.queued_run_completion = null;
|
|
2501
2664
|
updatedState.queued_phase_transition = null;
|
|
2502
2665
|
} else if (completionResult.action === 'awaiting_human_approval') {
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2666
|
+
// Evaluate approval policy — may auto-approve
|
|
2667
|
+
const approvalResult = evaluateApprovalPolicy({
|
|
2668
|
+
gateResult: completionResult,
|
|
2669
|
+
gateType: 'run_completion',
|
|
2670
|
+
state: { ...updatedState, history: nextHistoryEntries },
|
|
2671
|
+
config,
|
|
2672
|
+
});
|
|
2673
|
+
|
|
2674
|
+
if (approvalResult.action === 'auto_approve') {
|
|
2675
|
+
updatedState.status = 'completed';
|
|
2676
|
+
updatedState.completed_at = now;
|
|
2677
|
+
updatedState.last_gate_failure = null;
|
|
2678
|
+
if (completionResult.gate_id) {
|
|
2679
|
+
updatedState.phase_gate_status = {
|
|
2680
|
+
...(updatedState.phase_gate_status || {}),
|
|
2681
|
+
[completionResult.gate_id]: 'passed',
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
updatedState.queued_run_completion = null;
|
|
2685
|
+
updatedState.queued_phase_transition = null;
|
|
2686
|
+
ledgerEntries.push({
|
|
2687
|
+
type: 'approval_policy',
|
|
2688
|
+
gate_type: 'run_completion',
|
|
2689
|
+
action: 'auto_approve',
|
|
2690
|
+
matched_rule: approvalResult.matched_rule,
|
|
2691
|
+
reason: approvalResult.reason,
|
|
2692
|
+
gate_id: completionResult.gate_id,
|
|
2693
|
+
timestamp: now,
|
|
2694
|
+
});
|
|
2695
|
+
} else {
|
|
2696
|
+
updatedState.status = 'paused';
|
|
2697
|
+
updatedState.blocked_on = `human_approval:${completionResult.gate_id}`;
|
|
2698
|
+
updatedState.blocked_reason = null;
|
|
2699
|
+
updatedState.last_gate_failure = null;
|
|
2700
|
+
updatedState.pending_run_completion = {
|
|
2701
|
+
gate: completionResult.gate_id,
|
|
2702
|
+
requested_by_turn: completionSource.turn_id,
|
|
2703
|
+
requested_at: now,
|
|
2704
|
+
};
|
|
2705
|
+
updatedState.queued_run_completion = null;
|
|
2706
|
+
updatedState.queued_phase_transition = null;
|
|
2707
|
+
}
|
|
2708
|
+
} else if (completionResult.action === 'gate_failed') {
|
|
2709
|
+
const gateFailure = buildGateFailureRecord({
|
|
2710
|
+
gateType: 'run_completion',
|
|
2711
|
+
gateResult: completionResult,
|
|
2712
|
+
phase: state.phase,
|
|
2713
|
+
fromPhase: state.phase,
|
|
2714
|
+
toPhase: null,
|
|
2715
|
+
requestedByTurn: completionSource?.turn_id || state.queued_run_completion?.requested_by_turn || null,
|
|
2716
|
+
failedAt: now,
|
|
2717
|
+
queuedRequest: Boolean(state.queued_run_completion && !turnResult.run_completion_request),
|
|
2718
|
+
});
|
|
2719
|
+
updatedState.last_gate_failure = gateFailure;
|
|
2720
|
+
if (completionResult.gate_id) {
|
|
2721
|
+
updatedState.phase_gate_status = {
|
|
2722
|
+
...(updatedState.phase_gate_status || {}),
|
|
2723
|
+
[completionResult.gate_id]: 'failed',
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2511
2726
|
updatedState.queued_run_completion = null;
|
|
2512
|
-
|
|
2727
|
+
ledgerEntries.push({
|
|
2728
|
+
type: 'gate_failure',
|
|
2729
|
+
...gateFailure,
|
|
2730
|
+
});
|
|
2513
2731
|
} else if (state.queued_run_completion) {
|
|
2514
2732
|
updatedState.queued_run_completion = null;
|
|
2515
2733
|
}
|
|
@@ -2531,22 +2749,78 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2531
2749
|
|
|
2532
2750
|
if (gateResult.action === 'advance') {
|
|
2533
2751
|
updatedState.phase = gateResult.next_phase;
|
|
2752
|
+
updatedState.phase_entered_at = now;
|
|
2753
|
+
updatedState.last_gate_failure = null;
|
|
2534
2754
|
updatedState.phase_gate_status = {
|
|
2535
2755
|
...(updatedState.phase_gate_status || {}),
|
|
2536
2756
|
[gateResult.gate_id || 'no_gate']: 'passed',
|
|
2537
2757
|
};
|
|
2538
2758
|
updatedState.queued_phase_transition = null;
|
|
2539
2759
|
} else if (gateResult.action === 'awaiting_human_approval') {
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2760
|
+
// Evaluate approval policy — may auto-approve
|
|
2761
|
+
const approvalResult = evaluateApprovalPolicy({
|
|
2762
|
+
gateResult,
|
|
2763
|
+
gateType: 'phase_transition',
|
|
2764
|
+
state: { ...updatedState, history: nextHistoryEntries },
|
|
2765
|
+
config,
|
|
2766
|
+
});
|
|
2767
|
+
|
|
2768
|
+
if (approvalResult.action === 'auto_approve') {
|
|
2769
|
+
updatedState.phase = gateResult.next_phase;
|
|
2770
|
+
updatedState.phase_entered_at = now;
|
|
2771
|
+
updatedState.last_gate_failure = null;
|
|
2772
|
+
updatedState.phase_gate_status = {
|
|
2773
|
+
...(updatedState.phase_gate_status || {}),
|
|
2774
|
+
[gateResult.gate_id || 'no_gate']: 'passed',
|
|
2775
|
+
};
|
|
2776
|
+
updatedState.queued_phase_transition = null;
|
|
2777
|
+
ledgerEntries.push({
|
|
2778
|
+
type: 'approval_policy',
|
|
2779
|
+
gate_type: 'phase_transition',
|
|
2780
|
+
action: 'auto_approve',
|
|
2781
|
+
matched_rule: approvalResult.matched_rule,
|
|
2782
|
+
from_phase: state.phase,
|
|
2783
|
+
to_phase: gateResult.next_phase,
|
|
2784
|
+
reason: approvalResult.reason,
|
|
2785
|
+
gate_id: gateResult.gate_id,
|
|
2786
|
+
timestamp: now,
|
|
2787
|
+
});
|
|
2788
|
+
} else {
|
|
2789
|
+
updatedState.status = 'paused';
|
|
2790
|
+
updatedState.blocked_on = `human_approval:${gateResult.gate_id}`;
|
|
2791
|
+
updatedState.blocked_reason = null;
|
|
2792
|
+
updatedState.last_gate_failure = null;
|
|
2793
|
+
updatedState.pending_phase_transition = {
|
|
2794
|
+
from: state.phase,
|
|
2795
|
+
to: gateResult.next_phase,
|
|
2796
|
+
gate: gateResult.gate_id,
|
|
2797
|
+
requested_by_turn: phaseSource.turn_id,
|
|
2798
|
+
};
|
|
2799
|
+
updatedState.queued_phase_transition = null;
|
|
2800
|
+
}
|
|
2801
|
+
} else if (gateResult.action === 'gate_failed') {
|
|
2802
|
+
const gateFailure = buildGateFailureRecord({
|
|
2803
|
+
gateType: 'phase_transition',
|
|
2804
|
+
gateResult,
|
|
2805
|
+
phase: state.phase,
|
|
2806
|
+
fromPhase: state.phase,
|
|
2807
|
+
toPhase: gateResult.transition_request || state.queued_phase_transition?.to || null,
|
|
2808
|
+
requestedByTurn: phaseSource?.turn_id || state.queued_phase_transition?.requested_by_turn || null,
|
|
2809
|
+
failedAt: now,
|
|
2810
|
+
queuedRequest: Boolean(state.queued_phase_transition && !turnResult.phase_transition_request),
|
|
2811
|
+
});
|
|
2812
|
+
updatedState.last_gate_failure = gateFailure;
|
|
2813
|
+
if (gateResult.gate_id) {
|
|
2814
|
+
updatedState.phase_gate_status = {
|
|
2815
|
+
...(updatedState.phase_gate_status || {}),
|
|
2816
|
+
[gateResult.gate_id]: 'failed',
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2549
2819
|
updatedState.queued_phase_transition = null;
|
|
2820
|
+
ledgerEntries.push({
|
|
2821
|
+
type: 'gate_failure',
|
|
2822
|
+
...gateFailure,
|
|
2823
|
+
});
|
|
2550
2824
|
} else if (state.queued_phase_transition) {
|
|
2551
2825
|
updatedState.queued_phase_transition = null;
|
|
2552
2826
|
}
|
|
@@ -2554,6 +2828,77 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2554
2828
|
}
|
|
2555
2829
|
}
|
|
2556
2830
|
|
|
2831
|
+
const timeoutEvaluation = evaluateTimeouts({
|
|
2832
|
+
config,
|
|
2833
|
+
state: updatedState,
|
|
2834
|
+
turn: currentTurn,
|
|
2835
|
+
turnResult,
|
|
2836
|
+
now,
|
|
2837
|
+
});
|
|
2838
|
+
for (const warning of timeoutEvaluation.warnings) {
|
|
2839
|
+
ledgerEntries.push(buildTimeoutLedgerEntry(warning, now, currentTurn.turn_id, updatedState.phase, 'timeout_warning'));
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
if (updatedState.status === 'active' && timeoutEvaluation.exceeded.length > 0) {
|
|
2843
|
+
timeoutResult = timeoutEvaluation.exceeded[0];
|
|
2844
|
+
|
|
2845
|
+
if (timeoutResult.action === 'skip_phase' && timeoutResult.scope === 'phase') {
|
|
2846
|
+
const skipAttempt = attemptTimeoutPhaseSkip({
|
|
2847
|
+
root,
|
|
2848
|
+
config,
|
|
2849
|
+
updatedState,
|
|
2850
|
+
nextHistoryEntries,
|
|
2851
|
+
historyEntry,
|
|
2852
|
+
timeoutResult,
|
|
2853
|
+
now,
|
|
2854
|
+
});
|
|
2855
|
+
|
|
2856
|
+
if (skipAttempt.ok) {
|
|
2857
|
+
updatedState.phase = skipAttempt.updatedState.phase;
|
|
2858
|
+
updatedState.phase_entered_at = skipAttempt.updatedState.phase_entered_at;
|
|
2859
|
+
updatedState.last_gate_failure = skipAttempt.updatedState.last_gate_failure;
|
|
2860
|
+
updatedState.phase_gate_status = skipAttempt.updatedState.phase_gate_status;
|
|
2861
|
+
ledgerEntries.push(skipAttempt.ledgerEntry);
|
|
2862
|
+
} else {
|
|
2863
|
+
const escalatedTimeout = { ...timeoutResult, action: 'escalate' };
|
|
2864
|
+
timeoutResult = escalatedTimeout;
|
|
2865
|
+
updatedState.status = 'blocked';
|
|
2866
|
+
updatedState.blocked_on = `timeout:${escalatedTimeout.scope}`;
|
|
2867
|
+
updatedState.blocked_reason = buildBlockedReason({
|
|
2868
|
+
...buildTimeoutBlockedReason(escalatedTimeout, {
|
|
2869
|
+
turnRetained: Object.keys(getActiveTurns(updatedState)).length > 0,
|
|
2870
|
+
}),
|
|
2871
|
+
turnId: currentTurn.turn_id,
|
|
2872
|
+
blockedAt: now,
|
|
2873
|
+
});
|
|
2874
|
+
updatedState.last_gate_failure = skipAttempt.gateFailure || null;
|
|
2875
|
+
if (skipAttempt.gateFailure?.gate_id) {
|
|
2876
|
+
updatedState.phase_gate_status = {
|
|
2877
|
+
...(updatedState.phase_gate_status || {}),
|
|
2878
|
+
[skipAttempt.gateFailure.gate_id]: 'failed',
|
|
2879
|
+
};
|
|
2880
|
+
ledgerEntries.push({
|
|
2881
|
+
type: 'gate_failure',
|
|
2882
|
+
...skipAttempt.gateFailure,
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
ledgerEntries.push(skipAttempt.ledgerEntry);
|
|
2886
|
+
ledgerEntries.push(buildTimeoutLedgerEntry(escalatedTimeout, now, currentTurn.turn_id, updatedState.phase));
|
|
2887
|
+
}
|
|
2888
|
+
} else if (timeoutResult.action === 'escalate') {
|
|
2889
|
+
updatedState.status = 'blocked';
|
|
2890
|
+
updatedState.blocked_on = `timeout:${timeoutResult.scope}`;
|
|
2891
|
+
updatedState.blocked_reason = buildBlockedReason({
|
|
2892
|
+
...buildTimeoutBlockedReason(timeoutResult, {
|
|
2893
|
+
turnRetained: Object.keys(getActiveTurns(updatedState)).length > 0,
|
|
2894
|
+
}),
|
|
2895
|
+
turnId: currentTurn.turn_id,
|
|
2896
|
+
blockedAt: now,
|
|
2897
|
+
});
|
|
2898
|
+
ledgerEntries.push(buildTimeoutLedgerEntry(timeoutResult, now, currentTurn.turn_id, updatedState.phase));
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2557
2902
|
// ── Transaction journal: prepare before committing writes ──────────────
|
|
2558
2903
|
const transactionId = generateId('txn');
|
|
2559
2904
|
const journal = {
|
|
@@ -2974,9 +3319,11 @@ export function approvePhaseTransition(root, config) {
|
|
|
2974
3319
|
const updatedState = {
|
|
2975
3320
|
...state,
|
|
2976
3321
|
phase: transition.to,
|
|
3322
|
+
phase_entered_at: new Date().toISOString(),
|
|
2977
3323
|
status: 'active',
|
|
2978
3324
|
blocked_on: null,
|
|
2979
3325
|
blocked_reason: null,
|
|
3326
|
+
last_gate_failure: null,
|
|
2980
3327
|
pending_phase_transition: null,
|
|
2981
3328
|
phase_gate_status: {
|
|
2982
3329
|
...(state.phase_gate_status || {}),
|
|
@@ -3071,6 +3418,7 @@ export function approveRunCompletion(root, config) {
|
|
|
3071
3418
|
completed_at: new Date().toISOString(),
|
|
3072
3419
|
blocked_on: null,
|
|
3073
3420
|
blocked_reason: null,
|
|
3421
|
+
last_gate_failure: null,
|
|
3074
3422
|
pending_run_completion: null,
|
|
3075
3423
|
phase_gate_status: {
|
|
3076
3424
|
...(state.phase_gate_status || {}),
|