agentxchain 2.154.3 → 2.154.5
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 +44 -1
package/package.json
CHANGED
package/src/commands/resume.js
CHANGED
|
@@ -44,6 +44,33 @@ import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
|
44
44
|
import { consumeNextApprovedIntent } from '../lib/intake.js';
|
|
45
45
|
import { reconcileStaleTurns } from '../lib/stale-turn-watchdog.js';
|
|
46
46
|
|
|
47
|
+
function hasStandingPendingExitGate(state, config) {
|
|
48
|
+
const phase = state?.phase;
|
|
49
|
+
const gateId = phase ? config?.routing?.[phase]?.exit_gate : null;
|
|
50
|
+
return Boolean(gateId && (state?.phase_gate_status || {})[gateId] === 'pending');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function latestCompletedTurnWantsPhaseContinuation(root, state, config) {
|
|
54
|
+
const turnId = state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null;
|
|
55
|
+
if (!turnId) return false;
|
|
56
|
+
const historyPath = join(root, config?.files?.history || '.agentxchain/history.jsonl');
|
|
57
|
+
if (!existsSync(historyPath)) return false;
|
|
58
|
+
const lines = readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
59
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
60
|
+
let entry = null;
|
|
61
|
+
try {
|
|
62
|
+
entry = JSON.parse(lines[index]);
|
|
63
|
+
} catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (entry?.turn_id !== turnId) continue;
|
|
67
|
+
if (entry.phase_transition_request) return true;
|
|
68
|
+
const proposed = typeof entry.proposed_next_role === 'string' ? entry.proposed_next_role.trim() : '';
|
|
69
|
+
return Boolean(proposed && proposed !== 'human');
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
export async function resumeCommand(opts) {
|
|
48
75
|
const context = loadProjectContext();
|
|
49
76
|
if (!context) {
|
|
@@ -143,7 +170,23 @@ export async function resumeCommand(opts) {
|
|
|
143
170
|
// patched defensively) once the schema citation + migration citation are
|
|
144
171
|
// documented in code and the coverage matrix.
|
|
145
172
|
|
|
146
|
-
|
|
173
|
+
// BUG-52 third variant (Turn 203/204): operator_unblock must run the
|
|
174
|
+
// standing-gate reconcile path regardless of `activeCount`, but only when the
|
|
175
|
+
// current phase actually has a standing pending exit gate and the blocked
|
|
176
|
+
// turn was trying to continue into a non-human phase role. The tester's
|
|
177
|
+
// v2.151.0 lights-out repro on `tusq.dev` left `active_turns: {}` with
|
|
178
|
+
// `phase_gate_status.planning_signoff: "pending"`, so the old
|
|
179
|
+
// `activeCount > 0` guard skipped the only path that could synthesize the
|
|
180
|
+
// missing transition source. Non-gate human escalations (OAuth, external
|
|
181
|
+
// decisions, schedule recovery with `proposed_next_role: "human"`) must keep
|
|
182
|
+
// the normal unblock/resume behavior instead of being forced through a
|
|
183
|
+
// phase-transition materialization check.
|
|
184
|
+
if (
|
|
185
|
+
state.status === 'blocked'
|
|
186
|
+
&& resumeVia === 'operator_unblock'
|
|
187
|
+
&& hasStandingPendingExitGate(state, config)
|
|
188
|
+
&& latestCompletedTurnWantsPhaseContinuation(root, state, config)
|
|
189
|
+
) {
|
|
147
190
|
const reactivated = reactivateGovernedRun(root, state, { via: resumeVia, notificationConfig: config });
|
|
148
191
|
if (!reactivated.ok) {
|
|
149
192
|
console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
|