agentxchain 2.155.18 → 2.155.20
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 +45 -31
- package/src/commands/step.js +54 -0
- package/src/lib/governed-state.js +8 -3
package/package.json
CHANGED
package/src/commands/resume.js
CHANGED
|
@@ -21,6 +21,7 @@ import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
|
21
21
|
import {
|
|
22
22
|
initializeGovernedRun,
|
|
23
23
|
assignGovernedTurn,
|
|
24
|
+
reissueTurn,
|
|
24
25
|
deriveAfterDispatchHookRecoveryAction,
|
|
25
26
|
markRunBlocked,
|
|
26
27
|
getActiveTurns,
|
|
@@ -44,6 +45,7 @@ import { runHooks } from '../lib/hook-runner.js';
|
|
|
44
45
|
import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
45
46
|
import { consumeNextApprovedIntent } from '../lib/intake.js';
|
|
46
47
|
import { reconcileStaleTurns } from '../lib/stale-turn-watchdog.js';
|
|
48
|
+
import { resolveGovernedRole } from '../lib/role-resolution.js';
|
|
47
49
|
|
|
48
50
|
function hasStandingPendingExitGate(state, config) {
|
|
49
51
|
const phase = state?.phase;
|
|
@@ -442,12 +444,6 @@ export async function resumeCommand(opts) {
|
|
|
442
444
|
process.exit(1);
|
|
443
445
|
}
|
|
444
446
|
|
|
445
|
-
printResumeRunContext({ root, state, config });
|
|
446
|
-
console.log(chalk.yellow(`Re-dispatching blocked turn: ${retainedTurn.turn_id}`));
|
|
447
|
-
console.log(` Role: ${retainedTurn.assigned_role}`);
|
|
448
|
-
console.log(` Attempt: ${retainedTurn.attempt}`);
|
|
449
|
-
console.log('');
|
|
450
|
-
|
|
451
447
|
const reactivated = reactivateGovernedRun(root, state, { via: turnResumeVia, notificationConfig: config });
|
|
452
448
|
if (!reactivated.ok) {
|
|
453
449
|
console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
|
|
@@ -461,6 +457,38 @@ export async function resumeCommand(opts) {
|
|
|
461
457
|
console.log(chalk.yellow(reactivated.phantom_notice));
|
|
462
458
|
}
|
|
463
459
|
|
|
460
|
+
const materializationResolution = resolveGovernedRole({ state, config });
|
|
461
|
+
if (
|
|
462
|
+
state.charter_materialization_pending
|
|
463
|
+
&& state.phase === 'planning'
|
|
464
|
+
&& materializationResolution.roleId
|
|
465
|
+
&& materializationResolution.roleId !== retainedTurn.assigned_role
|
|
466
|
+
&& !opts.turn
|
|
467
|
+
) {
|
|
468
|
+
const reason = `charter materialization pending superseded stale retained ${retainedTurn.assigned_role} turn`;
|
|
469
|
+
const reissued = reissueTurn(root, config, {
|
|
470
|
+
turnId: retainedTurn.turn_id,
|
|
471
|
+
roleId: materializationResolution.roleId,
|
|
472
|
+
reason,
|
|
473
|
+
});
|
|
474
|
+
if (!reissued.ok) {
|
|
475
|
+
console.log(chalk.red(`Failed to reissue retained turn for materialization: ${reissued.error}`));
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
state = reissued.state;
|
|
479
|
+
retainedTurn = reissued.newTurn;
|
|
480
|
+
console.log(chalk.yellow(`Reissued retained turn for charter materialization: ${retainedTurn.turn_id}`));
|
|
481
|
+
console.log(` Role: ${retainedTurn.assigned_role}`);
|
|
482
|
+
console.log(` Reason: ${reason}`);
|
|
483
|
+
console.log('');
|
|
484
|
+
} else {
|
|
485
|
+
printResumeRunContext({ root, state, config });
|
|
486
|
+
console.log(chalk.yellow(`Re-dispatching blocked turn: ${retainedTurn.turn_id}`));
|
|
487
|
+
console.log(` Role: ${retainedTurn.assigned_role}`);
|
|
488
|
+
console.log(` Attempt: ${retainedTurn.attempt}`);
|
|
489
|
+
console.log('');
|
|
490
|
+
}
|
|
491
|
+
|
|
464
492
|
const bundleResult = writeDispatchBundle(root, state, config, { turnId: retainedTurn.turn_id });
|
|
465
493
|
if (!bundleResult.ok) {
|
|
466
494
|
console.log(chalk.red(`Failed to write dispatch bundle: ${bundleResult.error}`));
|
|
@@ -709,38 +737,24 @@ function printResumeRunContext({ root, state, config }) {
|
|
|
709
737
|
}
|
|
710
738
|
|
|
711
739
|
function resolveTargetRole(opts, state, config) {
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
if (!config.roles?.[opts.role]) {
|
|
718
|
-
console.log(chalk.red(`Unknown role: "${opts.role}"`));
|
|
719
|
-
console.log(chalk.dim(`Available roles: ${Object.keys(config.roles || {}).join(', ')}`));
|
|
720
|
-
return null;
|
|
721
|
-
}
|
|
722
|
-
if (routing?.allowed_next_roles && !routing.allowed_next_roles.includes(opts.role) && opts.role !== 'human') {
|
|
723
|
-
console.log(chalk.yellow(`Warning: role "${opts.role}" is not in allowed_next_roles for phase "${phase}".`));
|
|
724
|
-
console.log(chalk.dim(`Allowed: ${routing.allowed_next_roles.join(', ')}`));
|
|
725
|
-
// Allow it as an override, but warn
|
|
740
|
+
const resolved = resolveGovernedRole({ override: opts.role || null, state, config });
|
|
741
|
+
if (resolved.error) {
|
|
742
|
+
console.log(chalk.red(resolved.error));
|
|
743
|
+
if (resolved.availableRoles.length) {
|
|
744
|
+
console.log(chalk.dim(`Available roles: ${resolved.availableRoles.join(', ')}`));
|
|
726
745
|
}
|
|
727
|
-
return
|
|
746
|
+
return null;
|
|
728
747
|
}
|
|
729
748
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
return routing.entry_role;
|
|
749
|
+
if (!opts.role && state.next_recommended_role && resolved.roleId === state.next_recommended_role) {
|
|
750
|
+
console.log(chalk.dim(`Using recommended role: ${resolved.roleId} (from previous turn)`));
|
|
733
751
|
}
|
|
734
752
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if (roles.length > 0) {
|
|
738
|
-
console.log(chalk.yellow(`No entry_role for phase "${phase}". Defaulting to "${roles[0]}".`));
|
|
739
|
-
return roles[0];
|
|
753
|
+
for (const warning of resolved.warnings) {
|
|
754
|
+
console.log(chalk.yellow(`Warning: ${warning}`));
|
|
740
755
|
}
|
|
741
756
|
|
|
742
|
-
|
|
743
|
-
return null;
|
|
757
|
+
return resolved.roleId;
|
|
744
758
|
}
|
|
745
759
|
|
|
746
760
|
function runAfterDispatchHooks(root, hooksConfig, state, turn, config) {
|
package/src/commands/step.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
assignGovernedTurn,
|
|
31
31
|
acceptGovernedTurn,
|
|
32
32
|
deriveAfterDispatchHookRecoveryAction,
|
|
33
|
+
reissueTurn,
|
|
33
34
|
rejectGovernedTurn,
|
|
34
35
|
markRunBlocked,
|
|
35
36
|
getActiveTurnCount,
|
|
@@ -99,6 +100,35 @@ export async function stepCommand(opts) {
|
|
|
99
100
|
process.exit(1);
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
if (opts.resume && !opts.turn) {
|
|
104
|
+
const activeTurnsBeforeStaleCheck = getActiveTurns(state);
|
|
105
|
+
const activeTurnListBeforeStaleCheck = Object.values(activeTurnsBeforeStaleCheck);
|
|
106
|
+
const retainedTurn = activeTurnListBeforeStaleCheck.length === 1 ? activeTurnListBeforeStaleCheck[0] : null;
|
|
107
|
+
const materializationResolution = resolveGovernedRole({ state, config });
|
|
108
|
+
if (
|
|
109
|
+
retainedTurn
|
|
110
|
+
&& state.status === 'active'
|
|
111
|
+
&& state.charter_materialization_pending
|
|
112
|
+
&& state.phase === 'planning'
|
|
113
|
+
&& materializationResolution.roleId
|
|
114
|
+
&& materializationResolution.roleId !== retainedTurn.assigned_role
|
|
115
|
+
) {
|
|
116
|
+
const reason = `charter materialization pending superseded stale active ${retainedTurn.assigned_role} turn`;
|
|
117
|
+
const reissued = reissueTurn(root, config, {
|
|
118
|
+
turnId: retainedTurn.turn_id,
|
|
119
|
+
roleId: materializationResolution.roleId,
|
|
120
|
+
reason,
|
|
121
|
+
});
|
|
122
|
+
if (!reissued.ok) {
|
|
123
|
+
console.log(chalk.red(`Failed to reissue active turn for materialization: ${reissued.error}`));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
state = reissued.state;
|
|
127
|
+
console.log(chalk.yellow(`Reissued active turn for charter materialization: ${reissued.newTurn.turn_id}`));
|
|
128
|
+
console.log(` Role: ${reissued.newTurn.assigned_role}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
102
132
|
const staleReconciliation = reconcileStaleTurns(root, state, config);
|
|
103
133
|
state = staleReconciliation.state || state;
|
|
104
134
|
if (staleReconciliation.ghost_turns.length > 0) {
|
|
@@ -251,6 +281,30 @@ export async function stepCommand(opts) {
|
|
|
251
281
|
state = reactivated.state;
|
|
252
282
|
skipAssignment = true;
|
|
253
283
|
|
|
284
|
+
const materializationResolution = resolveGovernedRole({ state, config });
|
|
285
|
+
if (
|
|
286
|
+
state.charter_materialization_pending
|
|
287
|
+
&& state.phase === 'planning'
|
|
288
|
+
&& materializationResolution.roleId
|
|
289
|
+
&& materializationResolution.roleId !== targetTurn.assigned_role
|
|
290
|
+
&& !opts.turn
|
|
291
|
+
) {
|
|
292
|
+
const reason = `charter materialization pending superseded stale retained ${targetTurn.assigned_role} turn`;
|
|
293
|
+
const reissued = reissueTurn(root, config, {
|
|
294
|
+
turnId: targetTurn.turn_id,
|
|
295
|
+
roleId: materializationResolution.roleId,
|
|
296
|
+
reason,
|
|
297
|
+
});
|
|
298
|
+
if (!reissued.ok) {
|
|
299
|
+
console.log(chalk.red(`Failed to reissue retained turn for materialization: ${reissued.error}`));
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
state = reissued.state;
|
|
303
|
+
targetTurn = reissued.newTurn;
|
|
304
|
+
console.log(chalk.yellow(`Reissued retained turn for charter materialization: ${targetTurn.turn_id}`));
|
|
305
|
+
console.log(` Role: ${targetTurn.assigned_role}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
254
308
|
// BUG-1 fix: refresh baseline snapshot to capture files dirtied between assignment and dispatch
|
|
255
309
|
refreshTurnBaselineSnapshot(root, targetTurn.turn_id);
|
|
256
310
|
state = JSON.parse(readFileSync(join(root, '.agentxchain/state.json'), 'utf8'));
|
|
@@ -3712,6 +3712,7 @@ export function refreshTurnBaselineSnapshot(root, turnId) {
|
|
|
3712
3712
|
* @param {object} config - normalized config
|
|
3713
3713
|
* @param {object} opts
|
|
3714
3714
|
* @param {string} [opts.turnId] - specific turn to reissue
|
|
3715
|
+
* @param {string} [opts.roleId] - replacement role; defaults to original turn role
|
|
3715
3716
|
* @param {string} [opts.reason] - reason for reissue
|
|
3716
3717
|
* @returns {{ ok: boolean, state?: object, newTurn?: object, baselineDelta?: object, error?: string }}
|
|
3717
3718
|
*/
|
|
@@ -3769,7 +3770,8 @@ export function reissueTurn(root, config, opts = {}) {
|
|
|
3769
3770
|
const oldTurn = activeTurns[turnId];
|
|
3770
3771
|
if (!oldTurn) return { ok: false, error: `Turn ${turnId} not found in active turns` };
|
|
3771
3772
|
|
|
3772
|
-
const
|
|
3773
|
+
const oldRoleId = oldTurn.assigned_role;
|
|
3774
|
+
const roleId = opts.roleId || oldRoleId;
|
|
3773
3775
|
const role = config.roles?.[roleId];
|
|
3774
3776
|
if (!role) return { ok: false, error: `Role "${roleId}" not found in config` };
|
|
3775
3777
|
|
|
@@ -3795,7 +3797,7 @@ export function reissueTurn(root, config, opts = {}) {
|
|
|
3795
3797
|
appendJsonl(root, HISTORY_PATH, {
|
|
3796
3798
|
turn_id: oldTurn.turn_id,
|
|
3797
3799
|
run_id: state.run_id,
|
|
3798
|
-
role:
|
|
3800
|
+
role: oldRoleId,
|
|
3799
3801
|
phase: state.phase,
|
|
3800
3802
|
status: 'reissued',
|
|
3801
3803
|
summary: `Turn reissued: ${reason}`,
|
|
@@ -3811,7 +3813,8 @@ export function reissueTurn(root, config, opts = {}) {
|
|
|
3811
3813
|
timestamp: now,
|
|
3812
3814
|
decision: 'turn_reissued',
|
|
3813
3815
|
turn_id: oldTurn.turn_id,
|
|
3814
|
-
role:
|
|
3816
|
+
role: oldRoleId,
|
|
3817
|
+
new_role: roleId,
|
|
3815
3818
|
phase: state.phase,
|
|
3816
3819
|
reason,
|
|
3817
3820
|
old_baseline: {
|
|
@@ -3895,6 +3898,8 @@ export function reissueTurn(root, config, opts = {}) {
|
|
|
3895
3898
|
new_head: newBaseline.head_ref,
|
|
3896
3899
|
old_runtime: oldRuntimeId,
|
|
3897
3900
|
new_runtime: currentRuntimeId,
|
|
3901
|
+
old_role: oldRoleId,
|
|
3902
|
+
new_role: roleId,
|
|
3898
3903
|
},
|
|
3899
3904
|
});
|
|
3900
3905
|
|