agentxchain 2.154.5 → 2.154.8
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
CHANGED
|
@@ -68,18 +68,12 @@ import {
|
|
|
68
68
|
resolvePromptTransport,
|
|
69
69
|
resolveStartupWatchdogMs,
|
|
70
70
|
} from '../src/lib/adapters/local-cli-adapter.js';
|
|
71
|
+
import { CLAUDE_ENV_AUTH_KEYS } from '../src/lib/claude-local-auth.js';
|
|
71
72
|
|
|
72
73
|
const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
|
|
73
74
|
const REPO_ROOT_GUESS = resolve(SCRIPT_DIR, '..', '..');
|
|
74
75
|
|
|
75
76
|
const DIAGNOSTIC_ENV_KEYS = ['PATH', 'HOME', 'PWD', 'SHELL', 'TMPDIR'];
|
|
76
|
-
const AUTH_ENV_KEYS_TO_PROBE = [
|
|
77
|
-
'ANTHROPIC_API_KEY',
|
|
78
|
-
'CLAUDE_API_KEY',
|
|
79
|
-
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
80
|
-
'CLAUDE_CODE_USE_VERTEX',
|
|
81
|
-
'CLAUDE_CODE_USE_BEDROCK',
|
|
82
|
-
];
|
|
83
77
|
|
|
84
78
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
85
79
|
// Argv parsing — kept simple/explicit so tester can read what their flags do.
|
|
@@ -227,7 +221,7 @@ function snapshotEnv(env) {
|
|
|
227
221
|
if (typeof env[k] === 'string' && env[k].length > 0) visible[k] = env[k];
|
|
228
222
|
}
|
|
229
223
|
const authProbe = {};
|
|
230
|
-
for (const k of
|
|
224
|
+
for (const k of CLAUDE_ENV_AUTH_KEYS) {
|
|
231
225
|
authProbe[k] = typeof env[k] === 'string' && env[k].length > 0;
|
|
232
226
|
}
|
|
233
227
|
return { visible, auth_env_present: authProbe };
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
3
3
|
import { approvePhaseTransition } from '../lib/governed-state.js';
|
|
4
|
+
import { getNextPhase } from '../lib/gate-evaluator.js';
|
|
4
5
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
5
6
|
import { resolveGovernedRole } from '../lib/role-resolution.js';
|
|
6
7
|
import { checkCleanBaseline } from '../lib/repo-observer.js';
|
|
7
8
|
|
|
9
|
+
function getStandingPendingHumanExitGate(state, config) {
|
|
10
|
+
const phase = state?.phase || null;
|
|
11
|
+
const gateId = phase ? config?.routing?.[phase]?.exit_gate || null : null;
|
|
12
|
+
if (!phase || !gateId) return null;
|
|
13
|
+
const gate = config?.gates?.[gateId] || null;
|
|
14
|
+
if (!gate?.requires_human_approval) return null;
|
|
15
|
+
if ((state?.phase_gate_status || {})[gateId] !== 'pending') return null;
|
|
16
|
+
return {
|
|
17
|
+
gateId,
|
|
18
|
+
from: phase,
|
|
19
|
+
to: getNextPhase(phase, config?.routing || {}) || null,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
export async function approveTransitionCommand(opts) {
|
|
9
24
|
const context = loadProjectContext();
|
|
10
25
|
if (!context) {
|
|
@@ -25,6 +40,16 @@ export async function approveTransitionCommand(opts) {
|
|
|
25
40
|
if (state?.phase) {
|
|
26
41
|
console.log(chalk.dim(` Current phase: ${state.phase}`));
|
|
27
42
|
}
|
|
43
|
+
const standingGate = getStandingPendingHumanExitGate(state, config);
|
|
44
|
+
if (standingGate) {
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(chalk.yellow(` Gate "${standingGate.gateId}" is pending human approval, but no phase transition object is prepared.`));
|
|
47
|
+
if (standingGate.to) {
|
|
48
|
+
console.log(chalk.dim(` Expected transition: ${standingGate.from} → ${standingGate.to}`));
|
|
49
|
+
}
|
|
50
|
+
console.log(chalk.dim(' If this gate has an open human escalation, run: agentxchain unblock <hesc_id>'));
|
|
51
|
+
console.log(chalk.dim(` To inspect gate evidence, run: agentxchain gate show ${standingGate.gateId} --evaluate`));
|
|
52
|
+
}
|
|
28
53
|
process.exit(1);
|
|
29
54
|
}
|
|
30
55
|
|
package/src/commands/gate.js
CHANGED
|
@@ -312,4 +312,23 @@ function showGate(requestedGateId, { root, config, state, gateIds, opts }) {
|
|
|
312
312
|
}
|
|
313
313
|
console.log('');
|
|
314
314
|
}
|
|
315
|
+
|
|
316
|
+
const standingHint = getStandingRecoveryHint(gate, state);
|
|
317
|
+
if (standingHint) {
|
|
318
|
+
console.log(chalk.dim(' Recovery:'));
|
|
319
|
+
console.log(chalk.dim(` No phase transition is prepared for "${standingHint.gateId}".`));
|
|
320
|
+
console.log(chalk.dim(' If a human escalation is open, resolve with: agentxchain unblock <hesc_id>'));
|
|
321
|
+
console.log(chalk.dim(' After resolution, run: agentxchain approve-transition'));
|
|
322
|
+
console.log('');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function getStandingRecoveryHint(gate, state) {
|
|
327
|
+
if (!gate) return null;
|
|
328
|
+
if (!gate.requires_human_approval) return null;
|
|
329
|
+
if (gate.status !== 'pending') return null;
|
|
330
|
+
const currentPhase = state?.phase || null;
|
|
331
|
+
if (!currentPhase || gate.linked_phase !== currentPhase) return null;
|
|
332
|
+
if (state?.pending_phase_transition) return null;
|
|
333
|
+
return { gateId: gate.id };
|
|
315
334
|
}
|
package/src/commands/resume.js
CHANGED
|
@@ -50,8 +50,50 @@ function hasStandingPendingExitGate(state, config) {
|
|
|
50
50
|
return Boolean(gateId && (state?.phase_gate_status || {})[gateId] === 'pending');
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function
|
|
54
|
-
const
|
|
53
|
+
function getStandingPendingExitGate(state, config) {
|
|
54
|
+
const phase = state?.phase;
|
|
55
|
+
const gateId = phase ? config?.routing?.[phase]?.exit_gate : null;
|
|
56
|
+
if (!gateId || (state?.phase_gate_status || {})[gateId] !== 'pending') {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return config?.gates?.[gateId] || null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function entrySatisfiesSyntheticGateVerification(gate, entry) {
|
|
63
|
+
if (!gate?.requires_verification_pass) return true;
|
|
64
|
+
const verificationStatus = entry?.verification?.status;
|
|
65
|
+
return verificationStatus === 'pass' || verificationStatus === 'attested_pass';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function turnContributedToHumanApprovalGateArtifacts(root, state, config, entry) {
|
|
69
|
+
// Returns true only when the accepted turn itself produced at least one of
|
|
70
|
+
// the phase exit gate's required_files AND all required_files are present
|
|
71
|
+
// on disk. This distinguishes a PM turn that finished phase work and
|
|
72
|
+
// escalated for final sign-off (BUG-52 third variant) from a generic
|
|
73
|
+
// escalation where the agent blocked BEFORE writing gate artifacts
|
|
74
|
+
// (schedule-daemon `needs_decision` fixture).
|
|
75
|
+
const phase = state?.phase;
|
|
76
|
+
const gateId = phase ? config?.routing?.[phase]?.exit_gate : null;
|
|
77
|
+
if (!gateId) return false;
|
|
78
|
+
const gate = config?.gates?.[gateId];
|
|
79
|
+
if (!gate || !Array.isArray(gate.requires_files) || gate.requires_files.length === 0) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (!gate.requires_human_approval) return false;
|
|
83
|
+
const filesChanged = Array.isArray(entry?.files_changed) ? entry.files_changed : [];
|
|
84
|
+
const required = gate.requires_files.filter((p) => typeof p === 'string' && p.trim());
|
|
85
|
+
if (required.length === 0) return false;
|
|
86
|
+
const changedSet = new Set(filesChanged.filter((p) => typeof p === 'string'));
|
|
87
|
+
const contributed = required.some((relPath) => changedSet.has(relPath));
|
|
88
|
+
if (!contributed) return false;
|
|
89
|
+
for (const relPath of required) {
|
|
90
|
+
if (!existsSync(join(root, relPath))) return false;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function latestCompletedTurnWantsPhaseContinuation(root, state, config, opts = {}) {
|
|
96
|
+
const turnId = opts?.turnId || state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null;
|
|
55
97
|
if (!turnId) return false;
|
|
56
98
|
const historyPath = join(root, config?.files?.history || '.agentxchain/history.jsonl');
|
|
57
99
|
if (!existsSync(historyPath)) return false;
|
|
@@ -65,8 +107,34 @@ function latestCompletedTurnWantsPhaseContinuation(root, state, config) {
|
|
|
65
107
|
}
|
|
66
108
|
if (entry?.turn_id !== turnId) continue;
|
|
67
109
|
if (entry.phase_transition_request) return true;
|
|
110
|
+
const standingGate = getStandingPendingExitGate(state, config);
|
|
68
111
|
const proposed = typeof entry.proposed_next_role === 'string' ? entry.proposed_next_role.trim() : '';
|
|
69
|
-
|
|
112
|
+
if (proposed && proposed !== 'human') {
|
|
113
|
+
return entrySatisfiesSyntheticGateVerification(standingGate, entry);
|
|
114
|
+
}
|
|
115
|
+
// Turn 205 extension (refines DEC-BUG52-UNBLOCK-STANDING-GATE-DISCRIMINATOR-001):
|
|
116
|
+
// The realistic BUG-52 third-variant PM shape sets `status: 'needs_human'`,
|
|
117
|
+
// `phase_transition_request: null`, and `proposed_next_role: 'human'` — the
|
|
118
|
+
// PM is escalating specifically for the phase exit gate's human-approval
|
|
119
|
+
// check and has already written the gate's required artifacts to disk.
|
|
120
|
+
// When the current phase's exit gate requires human approval, all of its
|
|
121
|
+
// `requires_files` are present on disk, and any gate-level verification
|
|
122
|
+
// predicate was satisfied by the accepted turn, an `unblock` on that
|
|
123
|
+
// escalation IS the final gate approval and must run the standing-gate
|
|
124
|
+
// reconcile.
|
|
125
|
+
//
|
|
126
|
+
// Distinguished from generic schedule-daemon `needs_decision` escalations
|
|
127
|
+
// (which block BEFORE the agent writes gate artifacts, so `requires_files`
|
|
128
|
+
// are not yet on disk) — those correctly continue to re-dispatch the
|
|
129
|
+
// in-phase role rather than force-advancing the phase.
|
|
130
|
+
if (
|
|
131
|
+
entry.status === 'needs_human'
|
|
132
|
+
&& entrySatisfiesSyntheticGateVerification(standingGate, entry)
|
|
133
|
+
&& turnContributedToHumanApprovalGateArtifacts(root, state, config, entry)
|
|
134
|
+
) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
70
138
|
}
|
|
71
139
|
return false;
|
|
72
140
|
}
|
|
@@ -185,7 +253,7 @@ export async function resumeCommand(opts) {
|
|
|
185
253
|
state.status === 'blocked'
|
|
186
254
|
&& resumeVia === 'operator_unblock'
|
|
187
255
|
&& hasStandingPendingExitGate(state, config)
|
|
188
|
-
&& latestCompletedTurnWantsPhaseContinuation(root, state, config)
|
|
256
|
+
&& latestCompletedTurnWantsPhaseContinuation(root, state, config, { turnId: opts.turn || null })
|
|
189
257
|
) {
|
|
190
258
|
const reactivated = reactivateGovernedRun(root, state, { via: resumeVia, notificationConfig: config });
|
|
191
259
|
if (!reactivated.ok) {
|
|
@@ -204,6 +272,7 @@ export async function resumeCommand(opts) {
|
|
|
204
272
|
const phaseReconciliation = reconcilePhaseAdvanceBeforeDispatch(root, config, state, {
|
|
205
273
|
allow_active_turn_cleanup: true,
|
|
206
274
|
allow_standing_gate: true,
|
|
275
|
+
standing_gate_turn_id: opts.turn || null,
|
|
207
276
|
});
|
|
208
277
|
if (!phaseReconciliation.ok && !phaseReconciliation.state) {
|
|
209
278
|
console.log(chalk.red(`Failed to reconcile phase gate before dispatch: ${phaseReconciliation.error}`));
|
|
@@ -42,6 +42,7 @@ const DIAGNOSTIC_ENV_KEYS = [
|
|
|
42
42
|
];
|
|
43
43
|
const DIAGNOSTIC_STDERR_EXCERPT_LIMIT = 800;
|
|
44
44
|
const DEFAULT_STARTUP_WATCHDOG_MS = 180_000;
|
|
45
|
+
const DEFAULT_STARTUP_WATCHDOG_SIGKILL_GRACE_MS = 10_000;
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Launch a local CLI subprocess for a governed turn.
|
|
@@ -89,6 +90,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
89
90
|
return { ok: false, error: `Runtime "${runtimeId}" not found in config` };
|
|
90
91
|
}
|
|
91
92
|
const startupWatchdogMs = startupWatchdogOverrideMs ?? resolveStartupWatchdogMs(config, runtime);
|
|
93
|
+
const startupWatchdogKillGraceMs = resolveStartupWatchdogKillGraceMs(options.startupWatchdogKillGraceMs);
|
|
92
94
|
|
|
93
95
|
// Read the dispatch bundle prompt
|
|
94
96
|
const promptPath = join(root, getDispatchPromptPath(turn.turn_id));
|
|
@@ -188,6 +190,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
188
190
|
let spawnConfirmedAtMs = null;
|
|
189
191
|
let firstOutputLatencyMs = null;
|
|
190
192
|
let startupWatchdog = null;
|
|
193
|
+
let startupSigkillHandle = null;
|
|
191
194
|
let startupTimedOut = false;
|
|
192
195
|
let startupFailureType = null;
|
|
193
196
|
let stdoutBytes = 0;
|
|
@@ -205,6 +208,10 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
205
208
|
clearTimeout(startupWatchdog);
|
|
206
209
|
startupWatchdog = null;
|
|
207
210
|
}
|
|
211
|
+
if (startupSigkillHandle) {
|
|
212
|
+
clearTimeout(startupSigkillHandle);
|
|
213
|
+
startupSigkillHandle = null;
|
|
214
|
+
}
|
|
208
215
|
};
|
|
209
216
|
|
|
210
217
|
const armStartupWatchdog = () => {
|
|
@@ -218,15 +225,31 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
218
225
|
startupTimedOut = true;
|
|
219
226
|
startupFailureType = 'no_subprocess_output';
|
|
220
227
|
logs.push(`[adapter] Startup watchdog fired after ${Math.round(startupWatchdogMs / 1000)}s with no output.`);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
appendDiagnostic(logs, 'startup_watchdog_fired', {
|
|
229
|
+
startup_watchdog_ms: startupWatchdogMs,
|
|
230
|
+
startup_watchdog_sigkill_grace_ms: startupWatchdogKillGraceMs,
|
|
231
|
+
pid: child.pid ?? null,
|
|
232
|
+
spawn_confirmed_at: spawnConfirmedAt,
|
|
233
|
+
elapsed_since_spawn_ms: spawnConfirmedAtMs == null ? null : Math.max(0, Date.now() - spawnConfirmedAtMs),
|
|
234
|
+
});
|
|
235
|
+
try {
|
|
236
|
+
child.kill('SIGTERM');
|
|
229
237
|
} catch {}
|
|
238
|
+
if (startupWatchdogKillGraceMs > 0) {
|
|
239
|
+
startupSigkillHandle = setTimeout(() => {
|
|
240
|
+
logs.push('[adapter] Startup watchdog grace period expired. Sending SIGKILL.');
|
|
241
|
+
appendDiagnostic(logs, 'startup_watchdog_sigkill', {
|
|
242
|
+
startup_watchdog_ms: startupWatchdogMs,
|
|
243
|
+
startup_watchdog_sigkill_grace_ms: startupWatchdogKillGraceMs,
|
|
244
|
+
pid: child.pid ?? null,
|
|
245
|
+
spawn_confirmed_at: spawnConfirmedAt,
|
|
246
|
+
elapsed_since_spawn_ms: spawnConfirmedAtMs == null ? null : Math.max(0, Date.now() - spawnConfirmedAtMs),
|
|
247
|
+
});
|
|
248
|
+
try {
|
|
249
|
+
child.kill('SIGKILL');
|
|
250
|
+
} catch {}
|
|
251
|
+
}, startupWatchdogKillGraceMs);
|
|
252
|
+
}
|
|
230
253
|
}, startupWatchdogMs);
|
|
231
254
|
};
|
|
232
255
|
|
|
@@ -313,6 +336,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
313
336
|
// Timeout handling per §20.4
|
|
314
337
|
let timeoutHandle;
|
|
315
338
|
let sigkillHandle;
|
|
339
|
+
let abortSigkillHandle;
|
|
316
340
|
|
|
317
341
|
if (timeoutMs > 0 && timeoutMs < Infinity) {
|
|
318
342
|
timeoutHandle = setTimeout(() => {
|
|
@@ -337,11 +361,13 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
337
361
|
clearStartupWatchdog();
|
|
338
362
|
clearTimeout(timeoutHandle);
|
|
339
363
|
clearTimeout(sigkillHandle);
|
|
364
|
+
clearTimeout(abortSigkillHandle);
|
|
340
365
|
try {
|
|
341
366
|
child.kill('SIGTERM');
|
|
342
367
|
} catch {}
|
|
343
368
|
// Give it 5 seconds to exit gracefully
|
|
344
|
-
setTimeout(() => {
|
|
369
|
+
abortSigkillHandle = setTimeout(() => {
|
|
370
|
+
abortSigkillHandle = null;
|
|
345
371
|
try { child.kill('SIGKILL'); } catch {}
|
|
346
372
|
}, 5000);
|
|
347
373
|
};
|
|
@@ -355,6 +381,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
355
381
|
clearStartupWatchdog();
|
|
356
382
|
clearTimeout(timeoutHandle);
|
|
357
383
|
clearTimeout(sigkillHandle);
|
|
384
|
+
clearTimeout(abortSigkillHandle);
|
|
358
385
|
if (signal) signal.removeEventListener('abort', onAbort);
|
|
359
386
|
|
|
360
387
|
if (signal?.aborted) {
|
|
@@ -457,6 +484,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
457
484
|
clearStartupWatchdog();
|
|
458
485
|
clearTimeout(timeoutHandle);
|
|
459
486
|
clearTimeout(sigkillHandle);
|
|
487
|
+
clearTimeout(abortSigkillHandle);
|
|
460
488
|
if (signal) signal.removeEventListener('abort', onAbort);
|
|
461
489
|
// BUG-54 hypothesis #1 fix: explicitly release stdio streams on the
|
|
462
490
|
// error path so Node reclaims pipe handles immediately instead of
|
|
@@ -583,6 +611,13 @@ function resolveStartupWatchdogMs(config, runtime) {
|
|
|
583
611
|
return DEFAULT_STARTUP_WATCHDOG_MS;
|
|
584
612
|
}
|
|
585
613
|
|
|
614
|
+
function resolveStartupWatchdogKillGraceMs(value) {
|
|
615
|
+
if (Number.isInteger(value) && value >= 0) {
|
|
616
|
+
return value;
|
|
617
|
+
}
|
|
618
|
+
return DEFAULT_STARTUP_WATCHDOG_SIGKILL_GRACE_MS;
|
|
619
|
+
}
|
|
620
|
+
|
|
586
621
|
/**
|
|
587
622
|
* Check if the staged result file exists and has meaningful content.
|
|
588
623
|
* Delegates to the shared `hasMeaningfulStagedResult` helper so watchdog,
|
|
@@ -1605,7 +1605,7 @@ function resolvePhaseTransitionSource(historyEntries, gateFailure, fallbackTurnI
|
|
|
1605
1605
|
return requestedSource;
|
|
1606
1606
|
}
|
|
1607
1607
|
|
|
1608
|
-
function buildStandingPhaseTransitionSource(state, config) {
|
|
1608
|
+
function buildStandingPhaseTransitionSource(state, config, opts = {}) {
|
|
1609
1609
|
const phase = state?.phase;
|
|
1610
1610
|
const routing = phase ? config?.routing?.[phase] : null;
|
|
1611
1611
|
const gateId = routing?.exit_gate || null;
|
|
@@ -1617,7 +1617,7 @@ function buildStandingPhaseTransitionSource(state, config) {
|
|
|
1617
1617
|
return null;
|
|
1618
1618
|
}
|
|
1619
1619
|
return {
|
|
1620
|
-
turn_id: state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null,
|
|
1620
|
+
turn_id: opts?.turn_id || state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null,
|
|
1621
1621
|
run_id: state?.run_id || null,
|
|
1622
1622
|
role: null,
|
|
1623
1623
|
assigned_role: null,
|
|
@@ -2746,7 +2746,9 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null,
|
|
|
2746
2746
|
currentState.queued_phase_transition || null,
|
|
2747
2747
|
);
|
|
2748
2748
|
if (!phaseSource?.phase_transition_request && opts?.allow_standing_gate === true) {
|
|
2749
|
-
phaseSource = buildStandingPhaseTransitionSource(currentState, config
|
|
2749
|
+
phaseSource = buildStandingPhaseTransitionSource(currentState, config, {
|
|
2750
|
+
turn_id: opts?.standing_gate_turn_id || null,
|
|
2751
|
+
});
|
|
2750
2752
|
}
|
|
2751
2753
|
if (!phaseSource?.phase_transition_request) {
|
|
2752
2754
|
return {
|
|
@@ -2801,6 +2803,7 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null,
|
|
|
2801
2803
|
const cleanup = cleanupPhaseAdvanceArtifacts(root, nextState, config, prevPhase);
|
|
2802
2804
|
nextState = cleanup.state;
|
|
2803
2805
|
writeState(root, nextState);
|
|
2806
|
+
writeSessionCheckpoint(root, nextState, 'phase_reconciled');
|
|
2804
2807
|
appendJsonl(root, LEDGER_PATH, {
|
|
2805
2808
|
type: 'approval_policy',
|
|
2806
2809
|
gate_type: 'phase_transition',
|
|
@@ -2919,6 +2922,7 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null,
|
|
|
2919
2922
|
nextState = cleanup.state;
|
|
2920
2923
|
|
|
2921
2924
|
writeState(root, nextState);
|
|
2925
|
+
writeSessionCheckpoint(root, nextState, 'phase_reconciled');
|
|
2922
2926
|
const retiredIntentIds = retireApprovedPhaseScopedIntents(root, nextState, config, prevPhase, now);
|
|
2923
2927
|
if (retiredIntentIds.length > 0) {
|
|
2924
2928
|
emitRunEvent(root, 'intent_retired_by_phase_advance', {
|
|
@@ -6152,17 +6156,26 @@ export function approvePhaseTransition(root, config, opts = {}) {
|
|
|
6152
6156
|
run_id: updatedState.run_id,
|
|
6153
6157
|
phase: updatedState.phase,
|
|
6154
6158
|
status: 'active',
|
|
6155
|
-
|
|
6159
|
+
turn: transition.requested_by_turn ? { turn_id: transition.requested_by_turn, role_id: null } : undefined,
|
|
6160
|
+
payload: {
|
|
6161
|
+
gate_id: transition.gate || null,
|
|
6162
|
+
gate_type: 'phase_transition',
|
|
6163
|
+
from: transition.from,
|
|
6164
|
+
to: transition.to,
|
|
6165
|
+
requested_by_turn: transition.requested_by_turn || null,
|
|
6166
|
+
},
|
|
6156
6167
|
});
|
|
6157
6168
|
emitRunEvent(root, 'phase_entered', {
|
|
6158
6169
|
run_id: updatedState.run_id,
|
|
6159
6170
|
phase: updatedState.phase,
|
|
6160
6171
|
status: 'active',
|
|
6172
|
+
turn: transition.requested_by_turn ? { turn_id: transition.requested_by_turn, role_id: null } : undefined,
|
|
6161
6173
|
payload: {
|
|
6162
6174
|
from: transition.from,
|
|
6163
6175
|
to: transition.to,
|
|
6164
6176
|
gate_id: transition.gate || 'no_gate',
|
|
6165
6177
|
trigger: 'human_approved',
|
|
6178
|
+
requested_by_turn: transition.requested_by_turn || null,
|
|
6166
6179
|
},
|
|
6167
6180
|
});
|
|
6168
6181
|
emitRunEvent(root, 'phase_cleanup', {
|