agentxchain 2.151.0 → 2.152.0
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/package.json +1 -1
- package/src/commands/resume.js +48 -1
- package/src/lib/governed-state.js +171 -9
- package/src/lib/run-events.js +1 -0
package/package.json
CHANGED
package/src/commands/resume.js
CHANGED
|
@@ -94,6 +94,7 @@ export async function resumeCommand(opts) {
|
|
|
94
94
|
const activeTurns = getActiveTurns(state);
|
|
95
95
|
const resumeVia = opts?._via || 'resume';
|
|
96
96
|
const turnResumeVia = opts?._via || 'resume --turn';
|
|
97
|
+
let skipRetainedRedispatch = false;
|
|
97
98
|
|
|
98
99
|
if (state.status === 'active' && activeCount > 0) {
|
|
99
100
|
if (activeCount === 1) {
|
|
@@ -142,7 +143,53 @@ export async function resumeCommand(opts) {
|
|
|
142
143
|
// patched defensively) once the schema citation + migration citation are
|
|
143
144
|
// documented in code and the coverage matrix.
|
|
144
145
|
|
|
145
|
-
if (state.status === 'blocked' && activeCount > 0) {
|
|
146
|
+
if (state.status === 'blocked' && activeCount > 0 && resumeVia === 'operator_unblock') {
|
|
147
|
+
const reactivated = reactivateGovernedRun(root, state, { via: resumeVia, notificationConfig: config });
|
|
148
|
+
if (!reactivated.ok) {
|
|
149
|
+
console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
state = reactivated.state;
|
|
153
|
+
console.log(chalk.green(`Resumed blocked run: ${state.run_id}`));
|
|
154
|
+
if (reactivated.migration_notice) {
|
|
155
|
+
console.log(chalk.yellow(reactivated.migration_notice));
|
|
156
|
+
}
|
|
157
|
+
if (reactivated.phantom_notice) {
|
|
158
|
+
console.log(chalk.yellow(reactivated.phantom_notice));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const phaseReconciliation = reconcilePhaseAdvanceBeforeDispatch(root, config, state, {
|
|
162
|
+
allow_active_turn_cleanup: true,
|
|
163
|
+
allow_standing_gate: true,
|
|
164
|
+
});
|
|
165
|
+
if (!phaseReconciliation.ok && !phaseReconciliation.state) {
|
|
166
|
+
console.log(chalk.red(`Failed to reconcile phase gate before dispatch: ${phaseReconciliation.error}`));
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
state = phaseReconciliation.state || state;
|
|
170
|
+
if (phaseReconciliation.advanced) {
|
|
171
|
+
console.log(chalk.green(`Advanced phase before dispatch: ${phaseReconciliation.from_phase} → ${phaseReconciliation.to_phase}`));
|
|
172
|
+
skipRetainedRedispatch = true;
|
|
173
|
+
} else {
|
|
174
|
+
markRunBlocked(root, {
|
|
175
|
+
category: 'needs_human',
|
|
176
|
+
blockedOn: state.blocked_on || 'human:unblock_reconcile_failed',
|
|
177
|
+
recovery: {
|
|
178
|
+
typed_reason: 'needs_human',
|
|
179
|
+
owner: 'human',
|
|
180
|
+
recovery_action: 'agentxchain approve-transition or agentxchain gate show <gate>',
|
|
181
|
+
turn_retained: true,
|
|
182
|
+
detail: 'Operator unblock resolved the escalation, but no phase transition could be materialized from the current gate state.',
|
|
183
|
+
},
|
|
184
|
+
turnId: opts.turn || null,
|
|
185
|
+
notificationConfig: config,
|
|
186
|
+
});
|
|
187
|
+
console.log(chalk.red('Unblock did not materialize a phase transition; leaving the run blocked for manual recovery.'));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (state.status === 'blocked' && activeCount > 0 && !skipRetainedRedispatch) {
|
|
146
193
|
let retainedTurn = null;
|
|
147
194
|
if (opts.turn) {
|
|
148
195
|
retainedTurn = activeTurns[opts.turn];
|
|
@@ -1600,6 +1600,95 @@ function resolvePhaseTransitionSource(historyEntries, gateFailure, fallbackTurnI
|
|
|
1600
1600
|
return requestedSource;
|
|
1601
1601
|
}
|
|
1602
1602
|
|
|
1603
|
+
function buildStandingPhaseTransitionSource(state, config) {
|
|
1604
|
+
const phase = state?.phase;
|
|
1605
|
+
const routing = phase ? config?.routing?.[phase] : null;
|
|
1606
|
+
const gateId = routing?.exit_gate || null;
|
|
1607
|
+
const nextPhase = getNextPhase(phase, config?.routing || {});
|
|
1608
|
+
if (!phase || !gateId || !nextPhase) {
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
if ((state?.phase_gate_status || {})[gateId] !== 'pending') {
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
return {
|
|
1615
|
+
turn_id: state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null,
|
|
1616
|
+
run_id: state?.run_id || null,
|
|
1617
|
+
role: null,
|
|
1618
|
+
assigned_role: null,
|
|
1619
|
+
phase,
|
|
1620
|
+
status: 'completed',
|
|
1621
|
+
phase_transition_request: nextPhase,
|
|
1622
|
+
summary: `Synthetic ${gateId} transition source for operator-unblocked standing gate.`,
|
|
1623
|
+
verification: { status: 'pass' },
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function getPhaseRoles(config, phase) {
|
|
1628
|
+
const routing = config?.routing?.[phase] || {};
|
|
1629
|
+
const roles = new Set();
|
|
1630
|
+
if (typeof routing.entry_role === 'string' && routing.entry_role) {
|
|
1631
|
+
roles.add(routing.entry_role);
|
|
1632
|
+
}
|
|
1633
|
+
if (Array.isArray(routing.allowed_next_roles)) {
|
|
1634
|
+
for (const role of routing.allowed_next_roles) {
|
|
1635
|
+
if (typeof role === 'string' && role) roles.add(role);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
return roles;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function cleanupPhaseAdvanceArtifacts(root, state, config, fromPhase) {
|
|
1642
|
+
const phaseRoles = getPhaseRoles(config, fromPhase);
|
|
1643
|
+
const activeTurns = getActiveTurns(state);
|
|
1644
|
+
const removedTurnIds = [];
|
|
1645
|
+
const nextActiveTurns = {};
|
|
1646
|
+
for (const [turnId, turn] of Object.entries(activeTurns)) {
|
|
1647
|
+
const role = turn?.assigned_role || turn?.role_id || turn?.role || null;
|
|
1648
|
+
if (phaseRoles.has(role) && turn?.status !== 'accepted' && turn?.status !== 'completed') {
|
|
1649
|
+
removedTurnIds.push(turnId);
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
nextActiveTurns[turnId] = turn;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
const nextReservations = { ...(state?.budget_reservations || {}) };
|
|
1656
|
+
const clearedBudgetTurnIds = [];
|
|
1657
|
+
for (const turnId of removedTurnIds) {
|
|
1658
|
+
if (Object.prototype.hasOwnProperty.call(nextReservations, turnId)) {
|
|
1659
|
+
delete nextReservations[turnId];
|
|
1660
|
+
clearedBudgetTurnIds.push(turnId);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const removedDispatchTurnIds = [];
|
|
1665
|
+
for (const turnId of removedTurnIds) {
|
|
1666
|
+
const dispatchDir = join(root, getDispatchTurnDir(turnId));
|
|
1667
|
+
if (existsSync(dispatchDir)) {
|
|
1668
|
+
try {
|
|
1669
|
+
rmSync(dispatchDir, { recursive: true, force: true });
|
|
1670
|
+
removedDispatchTurnIds.push(turnId);
|
|
1671
|
+
} catch {
|
|
1672
|
+
// Best-effort cleanup; state correctness must not depend on filesystem pruning.
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
return {
|
|
1678
|
+
state: {
|
|
1679
|
+
...state,
|
|
1680
|
+
active_turns: nextActiveTurns,
|
|
1681
|
+
budget_reservations: nextReservations,
|
|
1682
|
+
},
|
|
1683
|
+
payload: {
|
|
1684
|
+
from_phase: fromPhase,
|
|
1685
|
+
removed_turn_ids: removedTurnIds,
|
|
1686
|
+
cleared_budget_turn_ids: clearedBudgetTurnIds,
|
|
1687
|
+
removed_dispatch_turn_ids: removedDispatchTurnIds,
|
|
1688
|
+
},
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1603
1692
|
function buildBlockedReason({ category, recovery, turnId, blockedAt = new Date().toISOString() }) {
|
|
1604
1693
|
return {
|
|
1605
1694
|
category,
|
|
@@ -2355,7 +2444,7 @@ export function markRunBlocked(root, details) {
|
|
|
2355
2444
|
blockedAt,
|
|
2356
2445
|
});
|
|
2357
2446
|
|
|
2358
|
-
|
|
2447
|
+
let updatedState = {
|
|
2359
2448
|
...state,
|
|
2360
2449
|
status: 'blocked',
|
|
2361
2450
|
blocked_on: details.blockedOn,
|
|
@@ -2607,13 +2696,15 @@ export function reactivateGovernedRun(root, state, details = {}) {
|
|
|
2607
2696
|
};
|
|
2608
2697
|
}
|
|
2609
2698
|
|
|
2610
|
-
export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null) {
|
|
2699
|
+
export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null, opts = {}) {
|
|
2611
2700
|
const currentState = state && typeof state === 'object' ? state : readState(root);
|
|
2612
2701
|
if (!currentState) {
|
|
2613
2702
|
return { ok: false, error: 'No governed state.json found' };
|
|
2614
2703
|
}
|
|
2615
2704
|
|
|
2616
|
-
|
|
2705
|
+
const activeTurnCount = getActiveTurnCount(currentState);
|
|
2706
|
+
const allowActiveTurnCleanup = opts?.allow_active_turn_cleanup === true;
|
|
2707
|
+
if (currentState.status !== 'active' || (activeTurnCount > 0 && !allowActiveTurnCleanup)) {
|
|
2617
2708
|
return {
|
|
2618
2709
|
ok: true,
|
|
2619
2710
|
state: attachLegacyCurrentTurnAlias(currentState),
|
|
@@ -2643,12 +2734,15 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
|
|
|
2643
2734
|
}
|
|
2644
2735
|
|
|
2645
2736
|
const historyEntries = readJsonlEntries(root, HISTORY_PATH);
|
|
2646
|
-
|
|
2737
|
+
let phaseSource = resolvePhaseTransitionSource(
|
|
2647
2738
|
historyEntries,
|
|
2648
2739
|
gateFailure,
|
|
2649
2740
|
currentState.last_completed_turn_id || null,
|
|
2650
2741
|
currentState.queued_phase_transition || null,
|
|
2651
2742
|
);
|
|
2743
|
+
if (!phaseSource?.phase_transition_request && opts?.allow_standing_gate === true) {
|
|
2744
|
+
phaseSource = buildStandingPhaseTransitionSource(currentState, config);
|
|
2745
|
+
}
|
|
2652
2746
|
if (!phaseSource?.phase_transition_request) {
|
|
2653
2747
|
return {
|
|
2654
2748
|
ok: true,
|
|
@@ -2685,7 +2779,7 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
|
|
|
2685
2779
|
if (approvalResult.action === 'auto_approve') {
|
|
2686
2780
|
const now = new Date().toISOString();
|
|
2687
2781
|
const prevPhase = currentState.phase;
|
|
2688
|
-
|
|
2782
|
+
let nextState = {
|
|
2689
2783
|
...currentState,
|
|
2690
2784
|
phase: gateResult.next_phase,
|
|
2691
2785
|
phase_entered_at: now,
|
|
@@ -2699,6 +2793,8 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
|
|
|
2699
2793
|
[gateResult.gate_id || 'no_gate']: 'passed',
|
|
2700
2794
|
},
|
|
2701
2795
|
};
|
|
2796
|
+
const cleanup = cleanupPhaseAdvanceArtifacts(root, nextState, config, prevPhase);
|
|
2797
|
+
nextState = cleanup.state;
|
|
2702
2798
|
writeState(root, nextState);
|
|
2703
2799
|
appendJsonl(root, LEDGER_PATH, {
|
|
2704
2800
|
type: 'approval_policy',
|
|
@@ -2738,6 +2834,17 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
|
|
|
2738
2834
|
trigger: 'auto_approved',
|
|
2739
2835
|
},
|
|
2740
2836
|
});
|
|
2837
|
+
emitRunEvent(root, 'phase_cleanup', {
|
|
2838
|
+
run_id: nextState.run_id,
|
|
2839
|
+
phase: nextState.phase,
|
|
2840
|
+
status: nextState.status,
|
|
2841
|
+
payload: {
|
|
2842
|
+
...cleanup.payload,
|
|
2843
|
+
to_phase: gateResult.next_phase,
|
|
2844
|
+
gate_id: gateResult.gate_id || null,
|
|
2845
|
+
trigger: 'auto_approved',
|
|
2846
|
+
},
|
|
2847
|
+
});
|
|
2741
2848
|
return {
|
|
2742
2849
|
ok: true,
|
|
2743
2850
|
state: attachLegacyCurrentTurnAlias(nextState),
|
|
@@ -2789,7 +2896,7 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
|
|
|
2789
2896
|
|
|
2790
2897
|
const now = new Date().toISOString();
|
|
2791
2898
|
const prevPhase = currentState.phase;
|
|
2792
|
-
|
|
2899
|
+
let nextState = {
|
|
2793
2900
|
...currentState,
|
|
2794
2901
|
phase: gateResult.next_phase,
|
|
2795
2902
|
phase_entered_at: now,
|
|
@@ -2803,6 +2910,8 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
|
|
|
2803
2910
|
[gateResult.gate_id || 'no_gate']: 'passed',
|
|
2804
2911
|
},
|
|
2805
2912
|
};
|
|
2913
|
+
const cleanup = cleanupPhaseAdvanceArtifacts(root, nextState, config, prevPhase);
|
|
2914
|
+
nextState = cleanup.state;
|
|
2806
2915
|
|
|
2807
2916
|
writeState(root, nextState);
|
|
2808
2917
|
const retiredIntentIds = retireApprovedPhaseScopedIntents(root, nextState, config, prevPhase, now);
|
|
@@ -2832,6 +2941,18 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
|
|
|
2832
2941
|
trigger: 'reconciled_before_dispatch',
|
|
2833
2942
|
},
|
|
2834
2943
|
});
|
|
2944
|
+
emitRunEvent(root, 'phase_cleanup', {
|
|
2945
|
+
run_id: nextState.run_id,
|
|
2946
|
+
phase: nextState.phase,
|
|
2947
|
+
status: nextState.status,
|
|
2948
|
+
turn: phaseSource.turn_id ? { turn_id: phaseSource.turn_id, role_id: phaseSource.role || phaseSource.assigned_role || null } : undefined,
|
|
2949
|
+
payload: {
|
|
2950
|
+
...cleanup.payload,
|
|
2951
|
+
to_phase: gateResult.next_phase,
|
|
2952
|
+
gate_id: gateResult.gate_id || null,
|
|
2953
|
+
trigger: 'reconciled_before_dispatch',
|
|
2954
|
+
},
|
|
2955
|
+
});
|
|
2835
2956
|
|
|
2836
2957
|
return {
|
|
2837
2958
|
ok: true,
|
|
@@ -2876,7 +2997,7 @@ export function initializeGovernedRun(root, config, options = {}) {
|
|
|
2876
2997
|
const now = new Date().toISOString();
|
|
2877
2998
|
const provenance = buildDefaultRunProvenance(options.provenance);
|
|
2878
2999
|
const repoDecisions = getActiveRepoDecisions(root);
|
|
2879
|
-
|
|
3000
|
+
let updatedState = {
|
|
2880
3001
|
...state,
|
|
2881
3002
|
run_id: runId,
|
|
2882
3003
|
created_at: now,
|
|
@@ -4604,7 +4725,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
4604
4725
|
const remainingReservations = { ...(state.budget_reservations || {}) };
|
|
4605
4726
|
delete remainingReservations[currentTurn.turn_id];
|
|
4606
4727
|
const costUsd = turnResult.cost?.usd || 0;
|
|
4607
|
-
|
|
4728
|
+
let updatedState = {
|
|
4608
4729
|
...state,
|
|
4609
4730
|
turn_sequence: acceptedSequence,
|
|
4610
4731
|
last_completed_turn_id: currentTurn.turn_id,
|
|
@@ -4946,6 +5067,8 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
4946
5067
|
[gateResult.gate_id || 'no_gate']: 'passed',
|
|
4947
5068
|
};
|
|
4948
5069
|
updatedState.queued_phase_transition = null;
|
|
5070
|
+
const cleanup = cleanupPhaseAdvanceArtifacts(root, updatedState, config, prevPhase);
|
|
5071
|
+
updatedState = cleanup.state;
|
|
4949
5072
|
const retiredIntentIds = retireApprovedPhaseScopedIntents(root, updatedState, config, prevPhase, now);
|
|
4950
5073
|
if (retiredIntentIds.length > 0) {
|
|
4951
5074
|
emitRunEvent(root, 'intent_retired_by_phase_advance', {
|
|
@@ -4973,6 +5096,18 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
4973
5096
|
trigger: 'auto',
|
|
4974
5097
|
},
|
|
4975
5098
|
});
|
|
5099
|
+
emitRunEvent(root, 'phase_cleanup', {
|
|
5100
|
+
run_id: updatedState.run_id,
|
|
5101
|
+
phase: updatedState.phase,
|
|
5102
|
+
status: updatedState.status,
|
|
5103
|
+
turn: { turn_id: currentTurn.turn_id, role_id: currentTurn.assigned_role },
|
|
5104
|
+
payload: {
|
|
5105
|
+
...cleanup.payload,
|
|
5106
|
+
to_phase: gateResult.next_phase,
|
|
5107
|
+
gate_id: gateResult.gate_id || null,
|
|
5108
|
+
trigger: 'auto',
|
|
5109
|
+
},
|
|
5110
|
+
});
|
|
4976
5111
|
} else if (gateResult.action === 'awaiting_human_approval') {
|
|
4977
5112
|
// Evaluate approval policy — may auto-approve
|
|
4978
5113
|
const approvalResult = evaluateApprovalPolicy({
|
|
@@ -4992,6 +5127,8 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
4992
5127
|
[gateResult.gate_id || 'no_gate']: 'passed',
|
|
4993
5128
|
};
|
|
4994
5129
|
updatedState.queued_phase_transition = null;
|
|
5130
|
+
const cleanup = cleanupPhaseAdvanceArtifacts(root, updatedState, config, prevPhase);
|
|
5131
|
+
updatedState = cleanup.state;
|
|
4995
5132
|
ledgerEntries.push({
|
|
4996
5133
|
type: 'approval_policy',
|
|
4997
5134
|
gate_type: 'phase_transition',
|
|
@@ -5030,6 +5167,18 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
5030
5167
|
trigger: 'auto_approved',
|
|
5031
5168
|
},
|
|
5032
5169
|
});
|
|
5170
|
+
emitRunEvent(root, 'phase_cleanup', {
|
|
5171
|
+
run_id: updatedState.run_id,
|
|
5172
|
+
phase: updatedState.phase,
|
|
5173
|
+
status: updatedState.status,
|
|
5174
|
+
turn: { turn_id: currentTurn.turn_id, role_id: currentTurn.assigned_role },
|
|
5175
|
+
payload: {
|
|
5176
|
+
...cleanup.payload,
|
|
5177
|
+
to_phase: gateResult.next_phase,
|
|
5178
|
+
gate_id: gateResult.gate_id || null,
|
|
5179
|
+
trigger: 'auto_approved',
|
|
5180
|
+
},
|
|
5181
|
+
});
|
|
5033
5182
|
} else {
|
|
5034
5183
|
updatedState.status = 'paused';
|
|
5035
5184
|
updatedState.blocked_on = `human_approval:${gateResult.gate_id}`;
|
|
@@ -5975,7 +6124,7 @@ export function approvePhaseTransition(root, config, opts = {}) {
|
|
|
5975
6124
|
appendJsonl(root, LEDGER_PATH, entry);
|
|
5976
6125
|
}
|
|
5977
6126
|
|
|
5978
|
-
|
|
6127
|
+
let updatedState = {
|
|
5979
6128
|
...state,
|
|
5980
6129
|
phase: transition.to,
|
|
5981
6130
|
phase_entered_at: new Date().toISOString(),
|
|
@@ -5989,6 +6138,8 @@ export function approvePhaseTransition(root, config, opts = {}) {
|
|
|
5989
6138
|
[transition.gate]: 'passed',
|
|
5990
6139
|
},
|
|
5991
6140
|
};
|
|
6141
|
+
const cleanup = cleanupPhaseAdvanceArtifacts(root, updatedState, config, transition.from);
|
|
6142
|
+
updatedState = cleanup.state;
|
|
5992
6143
|
|
|
5993
6144
|
writeState(root, updatedState);
|
|
5994
6145
|
clearSlaReminders(root, 'pending_phase_transition');
|
|
@@ -6009,6 +6160,17 @@ export function approvePhaseTransition(root, config, opts = {}) {
|
|
|
6009
6160
|
trigger: 'human_approved',
|
|
6010
6161
|
},
|
|
6011
6162
|
});
|
|
6163
|
+
emitRunEvent(root, 'phase_cleanup', {
|
|
6164
|
+
run_id: updatedState.run_id,
|
|
6165
|
+
phase: updatedState.phase,
|
|
6166
|
+
status: 'active',
|
|
6167
|
+
payload: {
|
|
6168
|
+
...cleanup.payload,
|
|
6169
|
+
to_phase: transition.to,
|
|
6170
|
+
gate_id: transition.gate || null,
|
|
6171
|
+
trigger: 'human_approved',
|
|
6172
|
+
},
|
|
6173
|
+
});
|
|
6012
6174
|
|
|
6013
6175
|
// Session checkpoint — non-fatal
|
|
6014
6176
|
writeSessionCheckpoint(root, updatedState, 'phase_approved');
|