agentxchain 2.154.9 → 2.154.10
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 +89 -1
- package/src/lib/continuous-run.js +8 -2
- package/src/lib/governed-state.js +222 -0
package/package.json
CHANGED
package/src/commands/resume.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
getActiveTurnCount,
|
|
28
28
|
reactivateGovernedRun,
|
|
29
29
|
reconcilePhaseAdvanceBeforeDispatch,
|
|
30
|
+
reconcileRunCompletionBeforeDispatch,
|
|
30
31
|
transitionActiveTurnLifecycle,
|
|
31
32
|
STATE_PATH,
|
|
32
33
|
} from '../lib/governed-state.js';
|
|
@@ -59,6 +60,13 @@ function getStandingPendingExitGate(state, config) {
|
|
|
59
60
|
return config?.gates?.[gateId] || null;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
function isFinalPhase(state, config) {
|
|
64
|
+
const phase = state?.phase;
|
|
65
|
+
if (!phase) return false;
|
|
66
|
+
const phases = Object.keys(config?.routing || {});
|
|
67
|
+
return phases.length > 0 && phases[phases.length - 1] === phase;
|
|
68
|
+
}
|
|
69
|
+
|
|
62
70
|
function entrySatisfiesSyntheticGateVerification(gate, entry) {
|
|
63
71
|
if (!gate?.requires_verification_pass) return true;
|
|
64
72
|
const verificationStatus = entry?.verification?.status;
|
|
@@ -122,7 +130,7 @@ function turnCanApproveHumanGateFromEscalation(root, state, config, entry, propo
|
|
|
122
130
|
|
|
123
131
|
function latestCompletedTurnWantsPhaseContinuation(root, state, config, opts = {}) {
|
|
124
132
|
const turnId = opts?.turnId || state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null;
|
|
125
|
-
if (!turnId) return false;
|
|
133
|
+
if (!turnId || isFinalPhase(state, config)) return false;
|
|
126
134
|
const historyPath = join(root, config?.files?.history || '.agentxchain/history.jsonl');
|
|
127
135
|
if (!existsSync(historyPath)) return false;
|
|
128
136
|
const lines = readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
@@ -167,6 +175,35 @@ function latestCompletedTurnWantsPhaseContinuation(root, state, config, opts = {
|
|
|
167
175
|
return false;
|
|
168
176
|
}
|
|
169
177
|
|
|
178
|
+
function latestCompletedTurnWantsRunCompletion(root, state, config, opts = {}) {
|
|
179
|
+
const turnId = opts?.turnId || state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null;
|
|
180
|
+
if (!turnId || !isFinalPhase(state, config)) return false;
|
|
181
|
+
const historyPath = join(root, config?.files?.history || '.agentxchain/history.jsonl');
|
|
182
|
+
if (!existsSync(historyPath)) return false;
|
|
183
|
+
const lines = readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
184
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
185
|
+
let entry = null;
|
|
186
|
+
try {
|
|
187
|
+
entry = JSON.parse(lines[index]);
|
|
188
|
+
} catch {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (entry?.turn_id !== turnId) continue;
|
|
192
|
+
if (entry.run_completion_request === true) return true;
|
|
193
|
+
const standingGate = getStandingPendingExitGate(state, config);
|
|
194
|
+
const proposed = typeof entry.proposed_next_role === 'string' ? entry.proposed_next_role.trim() : '';
|
|
195
|
+
if (
|
|
196
|
+
entry.status === 'needs_human'
|
|
197
|
+
&& entrySatisfiesSyntheticGateVerification(standingGate, entry)
|
|
198
|
+
&& turnCanApproveHumanGateFromEscalation(root, state, config, entry, proposed)
|
|
199
|
+
) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
170
207
|
export async function resumeCommand(opts) {
|
|
171
208
|
const context = loadProjectContext();
|
|
172
209
|
if (!context) {
|
|
@@ -329,6 +366,57 @@ export async function resumeCommand(opts) {
|
|
|
329
366
|
}
|
|
330
367
|
}
|
|
331
368
|
|
|
369
|
+
if (
|
|
370
|
+
state.status === 'blocked'
|
|
371
|
+
&& resumeVia === 'operator_unblock'
|
|
372
|
+
&& hasStandingPendingExitGate(state, config)
|
|
373
|
+
&& latestCompletedTurnWantsRunCompletion(root, state, config, { turnId: opts.turn || null })
|
|
374
|
+
) {
|
|
375
|
+
const reactivated = reactivateGovernedRun(root, state, { via: resumeVia, notificationConfig: config });
|
|
376
|
+
if (!reactivated.ok) {
|
|
377
|
+
console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
state = reactivated.state;
|
|
381
|
+
console.log(chalk.green(`Resumed blocked run: ${state.run_id}`));
|
|
382
|
+
if (reactivated.migration_notice) {
|
|
383
|
+
console.log(chalk.yellow(reactivated.migration_notice));
|
|
384
|
+
}
|
|
385
|
+
if (reactivated.phantom_notice) {
|
|
386
|
+
console.log(chalk.yellow(reactivated.phantom_notice));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const completionReconciliation = reconcileRunCompletionBeforeDispatch(root, config, state, {
|
|
390
|
+
allow_active_turn_cleanup: true,
|
|
391
|
+
allow_standing_gate: true,
|
|
392
|
+
standing_gate_turn_id: opts.turn || null,
|
|
393
|
+
});
|
|
394
|
+
if (!completionReconciliation.ok && !completionReconciliation.state) {
|
|
395
|
+
console.log(chalk.red(`Failed to reconcile run completion before dispatch: ${completionReconciliation.error}`));
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
state = completionReconciliation.state || state;
|
|
399
|
+
if (completionReconciliation.completed) {
|
|
400
|
+
console.log(chalk.green(`Completed run before dispatch: ${state.run_id}`));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
markRunBlocked(root, {
|
|
404
|
+
category: 'needs_human',
|
|
405
|
+
blockedOn: state.blocked_on || 'human:unblock_completion_reconcile_failed',
|
|
406
|
+
recovery: {
|
|
407
|
+
typed_reason: 'needs_human',
|
|
408
|
+
owner: 'human',
|
|
409
|
+
recovery_action: 'agentxchain approve-completion',
|
|
410
|
+
turn_retained: true,
|
|
411
|
+
detail: 'Operator unblock resolved the escalation, but no run completion could be materialized from the current gate state.',
|
|
412
|
+
},
|
|
413
|
+
turnId: opts.turn || null,
|
|
414
|
+
notificationConfig: config,
|
|
415
|
+
});
|
|
416
|
+
console.log(chalk.red('Unblock did not materialize run completion; leaving the run blocked for manual recovery.'));
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
|
|
332
420
|
if (state.status === 'blocked' && activeCount > 0 && !skipRetainedRedispatch) {
|
|
333
421
|
let retainedTurn = null;
|
|
334
422
|
if (opts.turn) {
|
|
@@ -790,11 +790,15 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
790
790
|
}
|
|
791
791
|
|
|
792
792
|
const activeGovernedState = loadProjectState(root, context.config);
|
|
793
|
+
const queuedIntent = session.current_run_id
|
|
794
|
+
? findNextDispatchableIntent(root, { run_id: session.current_run_id })
|
|
795
|
+
: { ok: false };
|
|
793
796
|
if (
|
|
794
797
|
session.current_run_id
|
|
795
798
|
&& activeGovernedState?.status === 'active'
|
|
796
799
|
&& activeGovernedState.run_id === session.current_run_id
|
|
797
|
-
&&
|
|
800
|
+
&& !contOpts.continueFrom
|
|
801
|
+
&& !queuedIntent.ok
|
|
798
802
|
) {
|
|
799
803
|
log('Continuing active governed run.');
|
|
800
804
|
let execution;
|
|
@@ -855,7 +859,9 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
855
859
|
}
|
|
856
860
|
|
|
857
861
|
// Step 1: Check intake queue for pending work (BUG-34: scope to current run)
|
|
858
|
-
const queued =
|
|
862
|
+
const queued = queuedIntent.ok
|
|
863
|
+
? queuedIntent
|
|
864
|
+
: findNextDispatchableIntent(root, { run_id: session.current_run_id });
|
|
859
865
|
let targetIntentId = null;
|
|
860
866
|
let visionObjective = null;
|
|
861
867
|
|
|
@@ -1629,6 +1629,30 @@ function buildStandingPhaseTransitionSource(state, config, opts = {}) {
|
|
|
1629
1629
|
};
|
|
1630
1630
|
}
|
|
1631
1631
|
|
|
1632
|
+
function buildStandingRunCompletionSource(state, config, opts = {}) {
|
|
1633
|
+
const phase = state?.phase;
|
|
1634
|
+
const routing = phase ? config?.routing?.[phase] : null;
|
|
1635
|
+
const gateId = routing?.exit_gate || null;
|
|
1636
|
+
const nextPhase = getNextPhase(phase, config?.routing || {});
|
|
1637
|
+
if (!phase || !gateId || nextPhase) {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
if ((state?.phase_gate_status || {})[gateId] !== 'pending') {
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
return {
|
|
1644
|
+
turn_id: opts?.turn_id || state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null,
|
|
1645
|
+
run_id: state?.run_id || null,
|
|
1646
|
+
role: null,
|
|
1647
|
+
assigned_role: null,
|
|
1648
|
+
phase,
|
|
1649
|
+
status: 'completed',
|
|
1650
|
+
run_completion_request: true,
|
|
1651
|
+
summary: `Synthetic ${gateId} completion source for operator-unblocked standing gate.`,
|
|
1652
|
+
verification: { status: 'pass' },
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1632
1656
|
function getPhaseRoles(config, phase) {
|
|
1633
1657
|
const routing = config?.routing?.[phase] || {};
|
|
1634
1658
|
const roles = new Set();
|
|
@@ -2974,6 +2998,204 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null,
|
|
|
2974
2998
|
};
|
|
2975
2999
|
}
|
|
2976
3000
|
|
|
3001
|
+
export function reconcileRunCompletionBeforeDispatch(root, config, state = null, opts = {}) {
|
|
3002
|
+
const currentState = state && typeof state === 'object' ? state : readState(root);
|
|
3003
|
+
if (!currentState) {
|
|
3004
|
+
return { ok: false, error: 'No governed state.json found' };
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
const activeTurnCount = getActiveTurnCount(currentState);
|
|
3008
|
+
const allowActiveTurnCleanup = opts?.allow_active_turn_cleanup === true;
|
|
3009
|
+
if (currentState.status !== 'active' || (activeTurnCount > 0 && !allowActiveTurnCleanup)) {
|
|
3010
|
+
return {
|
|
3011
|
+
ok: true,
|
|
3012
|
+
state: attachLegacyCurrentTurnAlias(currentState),
|
|
3013
|
+
completed: false,
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
const gateFailure = currentState.last_gate_failure;
|
|
3018
|
+
if (gateFailure && gateFailure.gate_type !== 'run_completion') {
|
|
3019
|
+
return {
|
|
3020
|
+
ok: true,
|
|
3021
|
+
state: attachLegacyCurrentTurnAlias(currentState),
|
|
3022
|
+
completed: false,
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
const historyEntries = readJsonlEntries(root, HISTORY_PATH);
|
|
3027
|
+
const requestedTurnId = gateFailure?.requested_by_turn
|
|
3028
|
+
|| currentState.queued_run_completion?.requested_by_turn
|
|
3029
|
+
|| currentState.last_completed_turn_id
|
|
3030
|
+
|| null;
|
|
3031
|
+
let completionSource = findHistoryTurnRequest(historyEntries, requestedTurnId, 'run_completion');
|
|
3032
|
+
if (!completionSource?.run_completion_request && opts?.allow_standing_gate === true) {
|
|
3033
|
+
completionSource = buildStandingRunCompletionSource(currentState, config, {
|
|
3034
|
+
turn_id: opts?.standing_gate_turn_id || null,
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
if (!completionSource?.run_completion_request) {
|
|
3038
|
+
return {
|
|
3039
|
+
ok: true,
|
|
3040
|
+
state: attachLegacyCurrentTurnAlias(currentState),
|
|
3041
|
+
completed: false,
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
const completionResult = evaluateRunCompletion({
|
|
3046
|
+
state: { ...currentState, history: historyEntries },
|
|
3047
|
+
config,
|
|
3048
|
+
acceptedTurn: completionSource,
|
|
3049
|
+
root,
|
|
3050
|
+
});
|
|
3051
|
+
|
|
3052
|
+
if (completionResult.action === 'awaiting_human_approval') {
|
|
3053
|
+
const approvalResult = evaluateApprovalPolicy({
|
|
3054
|
+
gateResult: completionResult,
|
|
3055
|
+
gateType: 'run_completion',
|
|
3056
|
+
state: { ...currentState, history: historyEntries },
|
|
3057
|
+
config,
|
|
3058
|
+
});
|
|
3059
|
+
|
|
3060
|
+
if (approvalResult.action === 'auto_approve') {
|
|
3061
|
+
const now = new Date().toISOString();
|
|
3062
|
+
const updatedState = {
|
|
3063
|
+
...currentState,
|
|
3064
|
+
status: 'completed',
|
|
3065
|
+
completed_at: now,
|
|
3066
|
+
blocked_on: null,
|
|
3067
|
+
blocked_reason: null,
|
|
3068
|
+
last_gate_failure: null,
|
|
3069
|
+
pending_run_completion: null,
|
|
3070
|
+
queued_run_completion: null,
|
|
3071
|
+
queued_phase_transition: null,
|
|
3072
|
+
phase_gate_status: {
|
|
3073
|
+
...(currentState.phase_gate_status || {}),
|
|
3074
|
+
[completionResult.gate_id || 'no_gate']: 'passed',
|
|
3075
|
+
},
|
|
3076
|
+
};
|
|
3077
|
+
writeState(root, updatedState);
|
|
3078
|
+
clearSlaReminders(root, 'pending_run_completion');
|
|
3079
|
+
appendJsonl(root, LEDGER_PATH, {
|
|
3080
|
+
type: 'approval_policy',
|
|
3081
|
+
gate_type: 'run_completion',
|
|
3082
|
+
action: 'auto_approve',
|
|
3083
|
+
matched_rule: approvalResult.matched_rule,
|
|
3084
|
+
reason: approvalResult.reason,
|
|
3085
|
+
gate_id: completionResult.gate_id || null,
|
|
3086
|
+
timestamp: now,
|
|
3087
|
+
});
|
|
3088
|
+
emitPendingLifecycleNotification(root, config, updatedState, 'run_completed', {
|
|
3089
|
+
completed_at: updatedState.completed_at,
|
|
3090
|
+
completed_via: 'approval_policy_reconciled_before_dispatch',
|
|
3091
|
+
gate: completionResult.gate_id || null,
|
|
3092
|
+
requested_by_turn: completionSource.turn_id || null,
|
|
3093
|
+
}, null);
|
|
3094
|
+
emitRunEvent(root, 'gate_approved', {
|
|
3095
|
+
run_id: updatedState.run_id,
|
|
3096
|
+
phase: updatedState.phase,
|
|
3097
|
+
status: 'completed',
|
|
3098
|
+
turn: completionSource.turn_id ? { turn_id: completionSource.turn_id, role_id: completionSource.role || completionSource.assigned_role || null } : undefined,
|
|
3099
|
+
payload: {
|
|
3100
|
+
gate_type: 'run_completion',
|
|
3101
|
+
gate_id: completionResult.gate_id || null,
|
|
3102
|
+
requested_by_turn: completionSource.turn_id || null,
|
|
3103
|
+
trigger: 'auto_approved',
|
|
3104
|
+
},
|
|
3105
|
+
});
|
|
3106
|
+
emitRunEvent(root, 'run_completed', {
|
|
3107
|
+
run_id: updatedState.run_id,
|
|
3108
|
+
phase: updatedState.phase,
|
|
3109
|
+
status: 'completed',
|
|
3110
|
+
payload: { completed_at: updatedState.completed_at },
|
|
3111
|
+
});
|
|
3112
|
+
writeSessionCheckpoint(root, updatedState, 'run_completed');
|
|
3113
|
+
recordRunHistory(root, updatedState, config, 'completed');
|
|
3114
|
+
return {
|
|
3115
|
+
ok: true,
|
|
3116
|
+
state: attachLegacyCurrentTurnAlias(updatedState),
|
|
3117
|
+
completed: true,
|
|
3118
|
+
gate_id: completionResult.gate_id || null,
|
|
3119
|
+
completionResult,
|
|
3120
|
+
approval_policy: approvalResult,
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
const pausedState = {
|
|
3125
|
+
...currentState,
|
|
3126
|
+
status: 'paused',
|
|
3127
|
+
blocked_on: `human_approval:${completionResult.gate_id}`,
|
|
3128
|
+
blocked_reason: null,
|
|
3129
|
+
pending_run_completion: {
|
|
3130
|
+
gate: completionResult.gate_id,
|
|
3131
|
+
requested_by_turn: completionSource.turn_id,
|
|
3132
|
+
requested_at: new Date().toISOString(),
|
|
3133
|
+
},
|
|
3134
|
+
queued_run_completion: null,
|
|
3135
|
+
queued_phase_transition: null,
|
|
3136
|
+
};
|
|
3137
|
+
writeState(root, pausedState);
|
|
3138
|
+
const approved = approveRunCompletion(root, config);
|
|
3139
|
+
return {
|
|
3140
|
+
ok: approved.ok,
|
|
3141
|
+
error: approved.error,
|
|
3142
|
+
state: approved.state || attachLegacyCurrentTurnAlias(readState(root)),
|
|
3143
|
+
completed: approved.ok,
|
|
3144
|
+
gate_id: completionResult.gate_id || null,
|
|
3145
|
+
completionResult,
|
|
3146
|
+
approval_policy: approvalResult,
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
if (completionResult.action !== 'complete') {
|
|
3151
|
+
return {
|
|
3152
|
+
ok: true,
|
|
3153
|
+
state: attachLegacyCurrentTurnAlias(currentState),
|
|
3154
|
+
completed: false,
|
|
3155
|
+
completionResult,
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
const now = new Date().toISOString();
|
|
3160
|
+
const updatedState = {
|
|
3161
|
+
...currentState,
|
|
3162
|
+
status: 'completed',
|
|
3163
|
+
completed_at: now,
|
|
3164
|
+
blocked_on: null,
|
|
3165
|
+
blocked_reason: null,
|
|
3166
|
+
last_gate_failure: null,
|
|
3167
|
+
pending_run_completion: null,
|
|
3168
|
+
queued_run_completion: null,
|
|
3169
|
+
queued_phase_transition: null,
|
|
3170
|
+
phase_gate_status: {
|
|
3171
|
+
...(currentState.phase_gate_status || {}),
|
|
3172
|
+
[completionResult.gate_id || 'no_gate']: 'passed',
|
|
3173
|
+
},
|
|
3174
|
+
};
|
|
3175
|
+
writeState(root, updatedState);
|
|
3176
|
+
emitPendingLifecycleNotification(root, config, updatedState, 'run_completed', {
|
|
3177
|
+
completed_at: updatedState.completed_at,
|
|
3178
|
+
completed_via: 'reconciled_before_dispatch',
|
|
3179
|
+
gate: completionResult.gate_id || null,
|
|
3180
|
+
requested_by_turn: completionSource.turn_id || null,
|
|
3181
|
+
}, null);
|
|
3182
|
+
emitRunEvent(root, 'run_completed', {
|
|
3183
|
+
run_id: updatedState.run_id,
|
|
3184
|
+
phase: updatedState.phase,
|
|
3185
|
+
status: 'completed',
|
|
3186
|
+
payload: { completed_at: updatedState.completed_at },
|
|
3187
|
+
});
|
|
3188
|
+
writeSessionCheckpoint(root, updatedState, 'run_completed');
|
|
3189
|
+
recordRunHistory(root, updatedState, config, 'completed');
|
|
3190
|
+
return {
|
|
3191
|
+
ok: true,
|
|
3192
|
+
state: attachLegacyCurrentTurnAlias(updatedState),
|
|
3193
|
+
completed: true,
|
|
3194
|
+
gate_id: completionResult.gate_id || null,
|
|
3195
|
+
completionResult,
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
|
|
2977
3199
|
// ── Core Operations ──────────────────────────────────────────────────────────
|
|
2978
3200
|
|
|
2979
3201
|
/**
|