agentxchain 2.155.62 → 2.155.64
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/lib/continuous-run.js +60 -0
- package/src/lib/turn-checkpoint.js +91 -7
package/package.json
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
formatLegacyIntentMigrationNotice,
|
|
53
53
|
formatPhantomIntentSupersessionNotice,
|
|
54
54
|
} from './intent-startup-migration.js';
|
|
55
|
+
import { checkpointAcceptedTurn } from './turn-checkpoint.js';
|
|
55
56
|
|
|
56
57
|
const CONTINUOUS_SESSION_PATH = '.agentxchain/continuous-session.json';
|
|
57
58
|
const PRODUCTIVE_TIMEOUT_RETRY_MAX_PER_RUN = 1;
|
|
@@ -574,6 +575,59 @@ async function maybeAutoRetryContinuousBlocker(context, session, contOpts, block
|
|
|
574
575
|
|| await maybeAutoRetryGhostBlocker(context, session, contOpts, blockedState, log);
|
|
575
576
|
}
|
|
576
577
|
|
|
578
|
+
function extractCheckpointTurnIdFromExecution(execution) {
|
|
579
|
+
const errors = Array.isArray(execution?.result?.errors) ? execution.result.errors : [];
|
|
580
|
+
for (const error of errors) {
|
|
581
|
+
const text = String(error || '');
|
|
582
|
+
const match = text.match(/\bcheckpoint-turn\s+--turn\s+(turn_[A-Za-z0-9_-]+)/);
|
|
583
|
+
if (match) return match[1];
|
|
584
|
+
}
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function maybeAutoCheckpointBlockedExecution(context, session, contOpts, execution, log = console.log) {
|
|
589
|
+
if (!contOpts.autoCheckpoint) return null;
|
|
590
|
+
const turnId = extractCheckpointTurnIdFromExecution(execution);
|
|
591
|
+
if (!turnId) return null;
|
|
592
|
+
|
|
593
|
+
const checkpoint = checkpointAcceptedTurn(context.root, { turnId });
|
|
594
|
+
if (!checkpoint.ok) {
|
|
595
|
+
log(`Auto-checkpoint skipped for ${turnId}: ${checkpoint.error || 'checkpoint failed'}`);
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
if (checkpoint.already_checkpointed || checkpoint.skipped) {
|
|
599
|
+
log(`Auto-checkpoint skipped for ${turnId}: ${checkpoint.reason || 'no checkpoint changes were created'}`);
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
session.status = 'running';
|
|
604
|
+
session.current_run_id = session.current_run_id || execution?.result?.state?.run_id || null;
|
|
605
|
+
writeContinuousSession(context.root, session);
|
|
606
|
+
|
|
607
|
+
emitRunEvent(context.root, 'continuous_auto_checkpoint_recovered', {
|
|
608
|
+
run_id: session.current_run_id || execution?.result?.state?.run_id || null,
|
|
609
|
+
phase: execution?.result?.state?.phase || null,
|
|
610
|
+
status: 'active',
|
|
611
|
+
turn: { turn_id: turnId, role_id: null },
|
|
612
|
+
payload: {
|
|
613
|
+
session_id: session.session_id,
|
|
614
|
+
checkpoint_sha: checkpoint.checkpoint_sha || null,
|
|
615
|
+
already_checkpointed: Boolean(checkpoint.already_checkpointed),
|
|
616
|
+
recovered_files_changed: checkpoint.recovered_files_changed || checkpoint.files_changed || null,
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
log(`Auto-checkpoint recovered accepted turn ${turnId}; continuing active run.`);
|
|
621
|
+
return {
|
|
622
|
+
ok: true,
|
|
623
|
+
status: 'running',
|
|
624
|
+
action: 'auto_checkpoint_recovered',
|
|
625
|
+
run_id: session.current_run_id,
|
|
626
|
+
turn_id: turnId,
|
|
627
|
+
checkpoint_sha: checkpoint.checkpoint_sha || null,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
577
631
|
async function maybeAutoRetryGhostBlocker(context, session, contOpts, blockedState, log = console.log) {
|
|
578
632
|
const { root, config } = context;
|
|
579
633
|
const decision = classifyGhostRetryDecision({
|
|
@@ -1784,6 +1838,8 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
1784
1838
|
const resumeStopReason = execution.result?.stop_reason;
|
|
1785
1839
|
|
|
1786
1840
|
if (isBlockedContinuousExecution(execution)) {
|
|
1841
|
+
const checkpointed = maybeAutoCheckpointBlockedExecution(context, session, contOpts, execution, log);
|
|
1842
|
+
if (checkpointed) return checkpointed;
|
|
1787
1843
|
const blockedState = execution?.result?.state || loadProjectState(root, context.config);
|
|
1788
1844
|
const retried = await maybeAutoRetryContinuousBlocker(context, session, contOpts, blockedState, log);
|
|
1789
1845
|
if (retried) return retried;
|
|
@@ -1852,6 +1908,8 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
1852
1908
|
const resumeStopReason = execution.result?.stop_reason;
|
|
1853
1909
|
|
|
1854
1910
|
if (isBlockedContinuousExecution(execution)) {
|
|
1911
|
+
const checkpointed = maybeAutoCheckpointBlockedExecution(context, session, contOpts, execution, log);
|
|
1912
|
+
if (checkpointed) return checkpointed;
|
|
1855
1913
|
const blockedState = execution?.result?.state || loadProjectState(root, context.config);
|
|
1856
1914
|
const retried = await maybeAutoRetryContinuousBlocker(context, session, contOpts, blockedState, log);
|
|
1857
1915
|
if (retried) return retried;
|
|
@@ -2044,6 +2102,8 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
2044
2102
|
}
|
|
2045
2103
|
|
|
2046
2104
|
if (isBlockedContinuousExecution(execution)) {
|
|
2105
|
+
const checkpointed = maybeAutoCheckpointBlockedExecution(context, session, contOpts, execution, log);
|
|
2106
|
+
if (checkpointed) return checkpointed;
|
|
2047
2107
|
const blockedState = execution?.result?.state || loadProjectState(root, context.config);
|
|
2048
2108
|
const retried = await maybeAutoRetryContinuousBlocker(context, session, contOpts, blockedState, log);
|
|
2049
2109
|
if (retried) return retried;
|
|
@@ -96,7 +96,69 @@ function recoverLegacyCheckpointFiles(root, entry, opts = {}) {
|
|
|
96
96
|
return getActorDirtyFiles(root, opts.dirtyFiles);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
function
|
|
99
|
+
function extractObservedDiffSummaryPaths(entry) {
|
|
100
|
+
const summary = entry?.observed_artifact?.diff_summary;
|
|
101
|
+
if (typeof summary !== 'string' || summary.trim().length === 0) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const paths = [];
|
|
106
|
+
for (const line of summary.split('\n')) {
|
|
107
|
+
const tableMatch = line.match(/^\s*(.+?)\s+\|\s+/);
|
|
108
|
+
if (tableMatch?.[1]) {
|
|
109
|
+
paths.push(tableMatch[1].trim());
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const untrackedMatch = line.match(/^\s*-\s+(.+)$/);
|
|
114
|
+
if (untrackedMatch?.[1]) {
|
|
115
|
+
paths.push(untrackedMatch[1].trim());
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return normalizeFilesChanged(paths);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function recoverSupplementalCheckpointFiles(root, entry, opts = {}) {
|
|
123
|
+
if (!supportsLegacyFilesChangedRecovery(entry) || !entry?.checkpoint_sha) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const state = readState(root);
|
|
128
|
+
if (state && Object.keys(state.active_turns || {}).length > 0) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const acceptedHistory = queryAcceptedTurnHistory(root);
|
|
133
|
+
if (acceptedHistory[0]?.turn_id !== entry?.turn_id) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const actorDirtyFiles = getActorDirtyFiles(root, opts.dirtyFiles);
|
|
138
|
+
if (actorDirtyFiles.length === 0) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const observedSummaryFiles = new Set(extractObservedDiffSummaryPaths(entry));
|
|
143
|
+
if (observedSummaryFiles.size === 0) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const declared = new Set(normalizeFilesChanged(entry.files_changed));
|
|
148
|
+
const recoverable = actorDirtyFiles.filter((filePath) => (
|
|
149
|
+
!declared.has(filePath) && observedSummaryFiles.has(filePath)
|
|
150
|
+
));
|
|
151
|
+
if (recoverable.length === 0) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const unrecoverable = actorDirtyFiles.filter((filePath) => (
|
|
156
|
+
!declared.has(filePath) && !observedSummaryFiles.has(filePath)
|
|
157
|
+
));
|
|
158
|
+
return unrecoverable.length === 0 ? recoverable : [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function persistRecoveredFilesChanged(root, turnId, recoveredFiles, source = 'legacy_dirty_worktree') {
|
|
100
162
|
const normalizedRecoveredFiles = normalizeFilesChanged(recoveredFiles);
|
|
101
163
|
if (normalizedRecoveredFiles.length === 0) return;
|
|
102
164
|
|
|
@@ -110,15 +172,21 @@ function persistRecoveredFilesChanged(root, turnId, recoveredFiles) {
|
|
|
110
172
|
? historyEntry.observed_artifact
|
|
111
173
|
: null;
|
|
112
174
|
|
|
175
|
+
const existingFiles = normalizeFilesChanged(historyEntry.files_changed);
|
|
176
|
+
const mergedFiles = normalizeFilesChanged([...existingFiles, ...normalizedRecoveredFiles]);
|
|
177
|
+
|
|
113
178
|
return {
|
|
114
179
|
...historyEntry,
|
|
115
|
-
files_changed:
|
|
180
|
+
files_changed: mergedFiles,
|
|
116
181
|
files_changed_recovered_at: recoveredAt,
|
|
117
|
-
files_changed_recovery_source:
|
|
182
|
+
files_changed_recovery_source: source,
|
|
118
183
|
observed_artifact: observedArtifact
|
|
119
184
|
? {
|
|
120
185
|
...observedArtifact,
|
|
121
|
-
files_changed:
|
|
186
|
+
files_changed: normalizeFilesChanged([
|
|
187
|
+
...(Array.isArray(observedArtifact.files_changed) ? observedArtifact.files_changed : []),
|
|
188
|
+
...normalizedRecoveredFiles,
|
|
189
|
+
]),
|
|
122
190
|
}
|
|
123
191
|
: observedArtifact,
|
|
124
192
|
};
|
|
@@ -234,7 +302,17 @@ export function detectPendingCheckpoint(root, dirtyFiles = []) {
|
|
|
234
302
|
if (!resolved.ok || !resolved.entry) return { required: false };
|
|
235
303
|
|
|
236
304
|
const entry = resolved.entry;
|
|
237
|
-
if (entry.checkpoint_sha)
|
|
305
|
+
if (entry.checkpoint_sha) {
|
|
306
|
+
const supplementalFiles = recoverSupplementalCheckpointFiles(root, entry, { dirtyFiles: actorDirtyFiles });
|
|
307
|
+
if (supplementalFiles.length === 0) return { required: false };
|
|
308
|
+
return {
|
|
309
|
+
required: true,
|
|
310
|
+
turn_id: entry.turn_id,
|
|
311
|
+
recovered_files_changed: supplementalFiles,
|
|
312
|
+
supplemental: true,
|
|
313
|
+
message: `Accepted turn ${entry.turn_id} is checkpointed but still owns ${supplementalFiles.length} dirty actor file(s) from its observed diff summary. Run agentxchain checkpoint-turn --turn ${entry.turn_id} to create a supplemental checkpoint before assigning the next code-writing turn.`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
238
316
|
|
|
239
317
|
const turnFiles = normalizeFilesChanged(entry.files_changed);
|
|
240
318
|
const recoveredFiles = turnFiles.length === 0
|
|
@@ -267,7 +345,10 @@ export function checkpointAcceptedTurn(root, opts = {}) {
|
|
|
267
345
|
}
|
|
268
346
|
|
|
269
347
|
const entry = resolved.entry;
|
|
270
|
-
|
|
348
|
+
const supplementalFilesChanged = entry.checkpoint_sha
|
|
349
|
+
? recoverSupplementalCheckpointFiles(root, entry)
|
|
350
|
+
: [];
|
|
351
|
+
if (entry.checkpoint_sha && supplementalFilesChanged.length === 0) {
|
|
271
352
|
return {
|
|
272
353
|
ok: true,
|
|
273
354
|
already_checkpointed: true,
|
|
@@ -281,11 +362,14 @@ export function checkpointAcceptedTurn(root, opts = {}) {
|
|
|
281
362
|
? recoverLegacyCheckpointFiles(root, entry)
|
|
282
363
|
: [];
|
|
283
364
|
const filesChanged = declaredFilesChanged.length > 0
|
|
284
|
-
? declaredFilesChanged
|
|
365
|
+
? normalizeFilesChanged([...declaredFilesChanged, ...supplementalFilesChanged])
|
|
285
366
|
: recoveredFilesChanged;
|
|
286
367
|
if (recoveredFilesChanged.length > 0) {
|
|
287
368
|
persistRecoveredFilesChanged(root, entry.turn_id, recoveredFilesChanged);
|
|
288
369
|
}
|
|
370
|
+
if (supplementalFilesChanged.length > 0) {
|
|
371
|
+
persistRecoveredFilesChanged(root, entry.turn_id, supplementalFilesChanged, 'supplemental_dirty_worktree');
|
|
372
|
+
}
|
|
289
373
|
|
|
290
374
|
if (filesChanged.length === 0) {
|
|
291
375
|
return {
|