agentxchain 2.107.0 → 2.108.0
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/dashboard/app.js +2 -1
- package/dashboard/components/timeline.js +100 -1
- package/package.json +1 -1
- package/src/commands/events.js +33 -1
- package/src/lib/governed-state.js +21 -0
- package/src/lib/report.js +84 -3
- package/src/lib/run-events.js +1 -0
- package/src/lib/run-loop.js +47 -2
package/dashboard/app.js
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from './live-observer.js';
|
|
26
26
|
|
|
27
27
|
const VIEWS = {
|
|
28
|
-
timeline: { fetch: ['state', 'continuity', 'history', 'audit', 'annotations', 'connectors', 'coordinatorAudit', 'coordinatorAnnotations'], render: renderTimeline },
|
|
28
|
+
timeline: { fetch: ['state', 'continuity', 'history', 'events', 'audit', 'annotations', 'connectors', 'coordinatorAudit', 'coordinatorAnnotations'], render: renderTimeline },
|
|
29
29
|
delegations: { fetch: ['state', 'history'], render: renderDelegations },
|
|
30
30
|
ledger: { fetch: ['state', 'ledger', 'coordinatorState', 'coordinatorLedger', 'repoDecisionsSummary'], render: renderLedger },
|
|
31
31
|
hooks: { fetch: ['audit', 'annotations', 'coordinatorAudit', 'coordinatorAnnotations'], render: renderHooks },
|
|
@@ -62,6 +62,7 @@ const API_MAP = {
|
|
|
62
62
|
runHistory: '/api/run-history',
|
|
63
63
|
timeouts: '/api/timeouts',
|
|
64
64
|
coordinatorTimeouts: '/api/coordinator/timeouts',
|
|
65
|
+
events: '/api/events?type=turn_conflicted&limit=10',
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
const viewState = {
|
|
@@ -90,6 +90,11 @@ function formatTimestamp(iso) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
function formatPercent(value) {
|
|
94
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return null;
|
|
95
|
+
return `${Math.round(value * 100)}%`;
|
|
96
|
+
}
|
|
97
|
+
|
|
93
98
|
function statusBadge(status) {
|
|
94
99
|
const colors = {
|
|
95
100
|
running: 'var(--green)',
|
|
@@ -206,6 +211,99 @@ function renderDelegationReview(review) {
|
|
|
206
211
|
return `<div class="turn-detail"><span class="detail-label">Delegation Review:</span> <span class="mono">${esc(review.parent_turn_id || 'unknown')}</span> with ${esc(resultCount)} result${resultCount === 1 ? '' : 's'}</div>`;
|
|
207
212
|
}
|
|
208
213
|
|
|
214
|
+
function collectConflictCards(state, events) {
|
|
215
|
+
const latestByTurn = new Map();
|
|
216
|
+
|
|
217
|
+
if (Array.isArray(events)) {
|
|
218
|
+
for (const event of [...events].reverse()) {
|
|
219
|
+
if (event?.event_type !== 'turn_conflicted') continue;
|
|
220
|
+
const turnId = event?.turn?.turn_id;
|
|
221
|
+
if (!turnId || latestByTurn.has(turnId)) continue;
|
|
222
|
+
latestByTurn.set(turnId, {
|
|
223
|
+
turn_id: turnId,
|
|
224
|
+
role_id: event?.turn?.role_id || 'unknown',
|
|
225
|
+
detected_at: event?.timestamp || null,
|
|
226
|
+
state_label: event?.status === 'blocked' ? 'conflict loop blocked run' : 'recent conflict',
|
|
227
|
+
detection_count: typeof event?.payload?.detection_count === 'number' ? event.payload.detection_count : null,
|
|
228
|
+
conflicting_files: Array.isArray(event?.payload?.conflicting_files) ? event.payload.conflicting_files : [],
|
|
229
|
+
accepted_since_turn_ids: Array.isArray(event?.payload?.accepted_since_turn_ids) ? event.payload.accepted_since_turn_ids : [],
|
|
230
|
+
overlap_ratio: typeof event?.payload?.overlap_ratio === 'number' ? event.payload.overlap_ratio : null,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const activeTurns = state?.active_turns ? Object.values(state.active_turns) : [];
|
|
236
|
+
for (const turn of activeTurns) {
|
|
237
|
+
if (turn?.status !== 'conflicted') continue;
|
|
238
|
+
if (latestByTurn.has(turn.turn_id)) {
|
|
239
|
+
const existing = latestByTurn.get(turn.turn_id);
|
|
240
|
+
latestByTurn.set(turn.turn_id, {
|
|
241
|
+
...existing,
|
|
242
|
+
state_label: state?.blocked_reason?.category === 'conflict_loop' && state?.blocked_on?.includes(turn.turn_id)
|
|
243
|
+
? 'conflict loop blocked run'
|
|
244
|
+
: 'active conflict',
|
|
245
|
+
});
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const conflictError = turn?.conflict_state?.conflict_error || {};
|
|
250
|
+
latestByTurn.set(turn.turn_id, {
|
|
251
|
+
turn_id: turn.turn_id,
|
|
252
|
+
role_id: getRole(turn),
|
|
253
|
+
detected_at: turn?.conflict_state?.detected_at || null,
|
|
254
|
+
state_label: state?.blocked_reason?.category === 'conflict_loop' && state?.blocked_on?.includes(turn.turn_id)
|
|
255
|
+
? 'conflict loop blocked run'
|
|
256
|
+
: 'active conflict',
|
|
257
|
+
detection_count: typeof turn?.conflict_state?.detection_count === 'number' ? turn.conflict_state.detection_count : null,
|
|
258
|
+
conflicting_files: Array.isArray(conflictError.conflicting_files) ? conflictError.conflicting_files : [],
|
|
259
|
+
accepted_since_turn_ids: Array.isArray(conflictError.accepted_since)
|
|
260
|
+
? conflictError.accepted_since.map((entry) => entry?.turn_id).filter(Boolean)
|
|
261
|
+
: [],
|
|
262
|
+
overlap_ratio: typeof conflictError.overlap_ratio === 'number' ? conflictError.overlap_ratio : null,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return [...latestByTurn.values()].slice(0, 5);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function renderConflictPanel(state, events) {
|
|
270
|
+
const conflicts = collectConflictCards(state, events);
|
|
271
|
+
if (conflicts.length === 0) return '';
|
|
272
|
+
|
|
273
|
+
let html = `<div class="section"><h3>Conflicts</h3><div class="turn-list">`;
|
|
274
|
+
for (const conflict of conflicts) {
|
|
275
|
+
const detectedAt = formatTimestamp(conflict.detected_at);
|
|
276
|
+
const overlap = formatPercent(conflict.overlap_ratio);
|
|
277
|
+
html += `<div class="turn-card">
|
|
278
|
+
<div class="turn-header">
|
|
279
|
+
${roleBadge(conflict.role_id)}
|
|
280
|
+
<span class="mono">${esc(conflict.turn_id)}</span>
|
|
281
|
+
${statusBadge('conflicted')}
|
|
282
|
+
</div>
|
|
283
|
+
<div class="turn-detail"><span class="detail-label">Scope:</span> ${esc(conflict.state_label)}</div>`;
|
|
284
|
+
|
|
285
|
+
if (detectedAt) {
|
|
286
|
+
html += `<div class="turn-detail"><span class="detail-label">Detected:</span> ${esc(detectedAt)}</div>`;
|
|
287
|
+
}
|
|
288
|
+
if (conflict.conflicting_files.length > 0) {
|
|
289
|
+
html += `<div class="turn-detail"><span class="detail-label">Files:</span> <span class="mono">${conflict.conflicting_files.map((file) => esc(file)).join(', ')}</span></div>`;
|
|
290
|
+
}
|
|
291
|
+
if (conflict.accepted_since_turn_ids.length > 0) {
|
|
292
|
+
html += `<div class="turn-detail"><span class="detail-label">Accepted since:</span> <span class="mono">${conflict.accepted_since_turn_ids.map((turnId) => esc(turnId)).join(', ')}</span></div>`;
|
|
293
|
+
}
|
|
294
|
+
if (overlap) {
|
|
295
|
+
html += `<div class="turn-detail"><span class="detail-label">Overlap:</span> ${esc(overlap)}</div>`;
|
|
296
|
+
}
|
|
297
|
+
if (conflict.detection_count != null) {
|
|
298
|
+
html += `<div class="turn-detail"><span class="detail-label">Detection count:</span> ${esc(conflict.detection_count)}</div>`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
html += `</div>`;
|
|
302
|
+
}
|
|
303
|
+
html += `</div></div>`;
|
|
304
|
+
return html;
|
|
305
|
+
}
|
|
306
|
+
|
|
209
307
|
function renderContinuityPanel(continuity) {
|
|
210
308
|
if (!continuity) return '';
|
|
211
309
|
|
|
@@ -306,7 +404,7 @@ function renderConnectorHealthPanel(connectorsPayload) {
|
|
|
306
404
|
|
|
307
405
|
export { formatDuration, computeElapsed, formatTimestamp };
|
|
308
406
|
|
|
309
|
-
export function render({ state, continuity, history, annotations, audit, connectors, coordinatorAudit = null, coordinatorAnnotations = null, liveMeta = null }) {
|
|
407
|
+
export function render({ state, continuity, history, events = null, annotations, audit, connectors, coordinatorAudit = null, coordinatorAnnotations = null, liveMeta = null }) {
|
|
310
408
|
if (!state) {
|
|
311
409
|
return `<div class="placeholder"><h2>No Run</h2><p>No governed run found. Start one with <code class="mono">agentxchain init --governed</code></p></div>`;
|
|
312
410
|
}
|
|
@@ -330,6 +428,7 @@ export function render({ state, continuity, history, annotations, audit, connect
|
|
|
330
428
|
|
|
331
429
|
html += renderContinuityPanel(continuity);
|
|
332
430
|
html += renderConnectorHealthPanel(connectors);
|
|
431
|
+
html += renderConflictPanel(state, events);
|
|
333
432
|
|
|
334
433
|
// Active turns
|
|
335
434
|
if (activeTurns.length > 0) {
|
package/package.json
CHANGED
package/src/commands/events.js
CHANGED
|
@@ -61,6 +61,9 @@ function printEvent(evt) {
|
|
|
61
61
|
const runId = evt.run_id ? evt.run_id.slice(0, 12) : '—';
|
|
62
62
|
const phase = evt.phase || '—';
|
|
63
63
|
const turnInfo = evt.turn?.role_id ? ` [${evt.turn.role_id}]` : '';
|
|
64
|
+
const conflictDetail = evt.event_type === 'turn_conflicted'
|
|
65
|
+
? ` — ${formatConflictDetail(evt)}`
|
|
66
|
+
: '';
|
|
64
67
|
const rejectionDetail = evt.event_type === 'turn_rejected' && evt.payload?.reason
|
|
65
68
|
? ` — ${evt.payload.reason}${evt.payload.failed_stage ? ` (${evt.payload.failed_stage})` : ''}`
|
|
66
69
|
: '';
|
|
@@ -70,7 +73,34 @@ function printEvent(evt) {
|
|
|
70
73
|
const gateFailedDetail = evt.event_type === 'gate_failed' && evt.payload?.from_phase
|
|
71
74
|
? ` ${evt.payload.from_phase} → ${evt.payload.to_phase || '?'}${evt.payload.reasons?.length ? ` — ${evt.payload.reasons[0]}` : ''}${evt.payload.gate_id ? ` (${evt.payload.gate_id})` : ''}`
|
|
72
75
|
: '';
|
|
73
|
-
console.log(`${chalk.dim(ts)} ${type} ${chalk.cyan(runId)} ${phase}${turnInfo}${rejectionDetail}${phaseTransitionDetail}${gateFailedDetail}`);
|
|
76
|
+
console.log(`${chalk.dim(ts)} ${type} ${chalk.cyan(runId)} ${phase}${turnInfo}${conflictDetail}${rejectionDetail}${phaseTransitionDetail}${gateFailedDetail}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatConflictDetail(evt) {
|
|
80
|
+
const payload = evt.payload || {};
|
|
81
|
+
const fileSummary = summarizeList(payload.conflicting_files, 3) || 'unknown files';
|
|
82
|
+
const overlapRatio = typeof payload.overlap_ratio === 'number'
|
|
83
|
+
? `${Math.round(payload.overlap_ratio * 100)}% overlap`
|
|
84
|
+
: null;
|
|
85
|
+
const detectionCount = Number.isInteger(payload.detection_count)
|
|
86
|
+
? `detection ${payload.detection_count}`
|
|
87
|
+
: null;
|
|
88
|
+
const turnSummary = summarizeList(payload.accepted_since_turn_ids, 2);
|
|
89
|
+
const parts = [fileSummary, overlapRatio, detectionCount];
|
|
90
|
+
if (turnSummary) {
|
|
91
|
+
parts.push(`accepted since ${turnSummary}`);
|
|
92
|
+
}
|
|
93
|
+
if (evt.status === 'blocked') {
|
|
94
|
+
parts.push('run blocked');
|
|
95
|
+
}
|
|
96
|
+
return parts.filter(Boolean).join(' | ');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function summarizeList(items, limit) {
|
|
100
|
+
if (!Array.isArray(items) || items.length === 0) return '';
|
|
101
|
+
const shown = items.slice(0, limit).join(', ');
|
|
102
|
+
if (items.length <= limit) return shown;
|
|
103
|
+
return `${shown} +${items.length - limit} more`;
|
|
74
104
|
}
|
|
75
105
|
|
|
76
106
|
function colorEventType(type) {
|
|
@@ -81,12 +111,14 @@ function colorEventType(type) {
|
|
|
81
111
|
turn_dispatched: chalk.blue,
|
|
82
112
|
turn_accepted: chalk.green,
|
|
83
113
|
turn_rejected: chalk.yellow,
|
|
114
|
+
turn_conflicted: chalk.redBright,
|
|
84
115
|
phase_entered: chalk.magenta,
|
|
85
116
|
escalation_raised: chalk.red.bold,
|
|
86
117
|
escalation_resolved: chalk.green,
|
|
87
118
|
gate_pending: chalk.yellow,
|
|
88
119
|
gate_approved: chalk.green,
|
|
89
120
|
gate_failed: chalk.red,
|
|
121
|
+
budget_exceeded_warn: chalk.yellowBright,
|
|
90
122
|
};
|
|
91
123
|
const colorFn = colors[type] || chalk.white;
|
|
92
124
|
return colorFn(pad(type, 22));
|
|
@@ -2652,11 +2652,32 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2652
2652
|
},
|
|
2653
2653
|
});
|
|
2654
2654
|
|
|
2655
|
+
// DEC-RUN-LOOP-CONFLICT-002: Persist turn_conflicted as a durable run event
|
|
2656
|
+
emitRunEvent(root, 'turn_conflicted', {
|
|
2657
|
+
run_id: state.run_id,
|
|
2658
|
+
phase: state.phase,
|
|
2659
|
+
status: updatedState.status,
|
|
2660
|
+
turn: { turn_id: currentTurn.turn_id, role_id: currentTurn.assigned_role },
|
|
2661
|
+
payload: {
|
|
2662
|
+
error_code: 'conflict',
|
|
2663
|
+
detection_count: detectionCount,
|
|
2664
|
+
conflicting_files: conflict.conflicting_files,
|
|
2665
|
+
accepted_since_turn_ids: conflict.accepted_since.map(entry => entry.turn_id),
|
|
2666
|
+
overlap_ratio: conflict.overlap_ratio,
|
|
2667
|
+
},
|
|
2668
|
+
});
|
|
2669
|
+
|
|
2655
2670
|
writeState(root, updatedState);
|
|
2656
2671
|
|
|
2657
2672
|
// DEC-RHTR-SPEC: Record conflict_loop blocked outcome in cross-run history (non-fatal)
|
|
2658
2673
|
if (updatedState.status === 'blocked') {
|
|
2659
2674
|
recordRunHistory(root, updatedState, config, 'blocked');
|
|
2675
|
+
// DEC-CONFLICT-NOTIFY-001: Emit run_blocked notification for conflict-loop exhaustion
|
|
2676
|
+
emitBlockedNotification(root, config, updatedState, {
|
|
2677
|
+
category: 'conflict_loop',
|
|
2678
|
+
blockedOn: updatedState.blocked_on,
|
|
2679
|
+
recovery: updatedState.blocked_reason?.recovery || null,
|
|
2680
|
+
}, currentTurn);
|
|
2660
2681
|
}
|
|
2661
2682
|
|
|
2662
2683
|
return {
|
package/src/lib/report.js
CHANGED
|
@@ -119,6 +119,34 @@ function yesNo(value) {
|
|
|
119
119
|
return value ? 'yes' : 'no';
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
function normalizeConflictingFiles(conflict) {
|
|
123
|
+
if (!conflict || typeof conflict !== 'object' || Array.isArray(conflict)) return [];
|
|
124
|
+
if (Array.isArray(conflict.conflicting_files)) {
|
|
125
|
+
return conflict.conflicting_files.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(conflict.files)) {
|
|
128
|
+
return conflict.files.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
129
|
+
}
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeAcceptedSinceTurnIds(conflict) {
|
|
134
|
+
if (!conflict || typeof conflict !== 'object' || Array.isArray(conflict)) return [];
|
|
135
|
+
if (Array.isArray(conflict.accepted_since_turn_ids)) {
|
|
136
|
+
return conflict.accepted_since_turn_ids.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(conflict.accepted_since)) {
|
|
139
|
+
return conflict.accepted_since
|
|
140
|
+
.map((entry) => {
|
|
141
|
+
if (typeof entry === 'string') return entry;
|
|
142
|
+
if (entry && typeof entry === 'object' && typeof entry.turn_id === 'string') return entry.turn_id;
|
|
143
|
+
return null;
|
|
144
|
+
})
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
}
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
122
150
|
function summarizeBlockedOn(blockedOn) {
|
|
123
151
|
if (!blockedOn) return 'none';
|
|
124
152
|
if (typeof blockedOn === 'string') return blockedOn;
|
|
@@ -143,6 +171,9 @@ function renderGovernanceEventDetailText(lines, evt, indent) {
|
|
|
143
171
|
if (evt.conflicting_files?.length > 0) {
|
|
144
172
|
lines.push(`${indent}files: ${evt.conflicting_files.join(', ')}`);
|
|
145
173
|
}
|
|
174
|
+
if (evt.accepted_since_turn_ids?.length > 0) {
|
|
175
|
+
lines.push(`${indent}accepted since: ${evt.accepted_since_turn_ids.join(', ')}`);
|
|
176
|
+
}
|
|
146
177
|
if (evt.overlap_ratio != null) {
|
|
147
178
|
lines.push(`${indent}overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%`);
|
|
148
179
|
}
|
|
@@ -151,8 +182,23 @@ function renderGovernanceEventDetailText(lines, evt, indent) {
|
|
|
151
182
|
if (evt.conflicting_files?.length > 0) {
|
|
152
183
|
lines.push(`${indent}files: ${evt.conflicting_files.join(', ')}`);
|
|
153
184
|
}
|
|
185
|
+
if (evt.accepted_since_turn_ids?.length > 0) {
|
|
186
|
+
lines.push(`${indent}accepted since: ${evt.accepted_since_turn_ids.join(', ')}`);
|
|
187
|
+
}
|
|
188
|
+
if (evt.operator_reason) {
|
|
189
|
+
lines.push(`${indent}operator reason: ${evt.operator_reason}`);
|
|
190
|
+
}
|
|
154
191
|
break;
|
|
155
192
|
case 'conflict_resolution_selected':
|
|
193
|
+
if (evt.conflicting_files?.length > 0) {
|
|
194
|
+
lines.push(`${indent}files: ${evt.conflicting_files.join(', ')}`);
|
|
195
|
+
}
|
|
196
|
+
if (evt.accepted_since_turn_ids?.length > 0) {
|
|
197
|
+
lines.push(`${indent}accepted since: ${evt.accepted_since_turn_ids.join(', ')}`);
|
|
198
|
+
}
|
|
199
|
+
if (evt.overlap_ratio != null) {
|
|
200
|
+
lines.push(`${indent}overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%`);
|
|
201
|
+
}
|
|
156
202
|
if (evt.resolution_method) {
|
|
157
203
|
lines.push(`${indent}resolution: ${evt.resolution_method}`);
|
|
158
204
|
}
|
|
@@ -179,6 +225,9 @@ function renderGovernanceEventDetailMarkdown(lines, evt) {
|
|
|
179
225
|
if (evt.conflicting_files?.length > 0) {
|
|
180
226
|
lines.push(` - Files: ${evt.conflicting_files.map((f) => `\`${f}\``).join(', ')}`);
|
|
181
227
|
}
|
|
228
|
+
if (evt.accepted_since_turn_ids?.length > 0) {
|
|
229
|
+
lines.push(` - Accepted since: ${evt.accepted_since_turn_ids.map((turnId) => `\`${turnId}\``).join(', ')}`);
|
|
230
|
+
}
|
|
182
231
|
if (evt.overlap_ratio != null) {
|
|
183
232
|
lines.push(` - Overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%`);
|
|
184
233
|
}
|
|
@@ -187,8 +236,21 @@ function renderGovernanceEventDetailMarkdown(lines, evt) {
|
|
|
187
236
|
if (evt.conflicting_files?.length > 0) {
|
|
188
237
|
lines.push(` - Files: ${evt.conflicting_files.map((f) => `\`${f}\``).join(', ')}`);
|
|
189
238
|
}
|
|
239
|
+
if (evt.accepted_since_turn_ids?.length > 0) {
|
|
240
|
+
lines.push(` - Accepted since: ${evt.accepted_since_turn_ids.map((turnId) => `\`${turnId}\``).join(', ')}`);
|
|
241
|
+
}
|
|
242
|
+
if (evt.operator_reason) lines.push(` - Operator reason: ${evt.operator_reason}`);
|
|
190
243
|
break;
|
|
191
244
|
case 'conflict_resolution_selected':
|
|
245
|
+
if (evt.conflicting_files?.length > 0) {
|
|
246
|
+
lines.push(` - Files: ${evt.conflicting_files.map((f) => `\`${f}\``).join(', ')}`);
|
|
247
|
+
}
|
|
248
|
+
if (evt.accepted_since_turn_ids?.length > 0) {
|
|
249
|
+
lines.push(` - Accepted since: ${evt.accepted_since_turn_ids.map((turnId) => `\`${turnId}\``).join(', ')}`);
|
|
250
|
+
}
|
|
251
|
+
if (evt.overlap_ratio != null) {
|
|
252
|
+
lines.push(` - Overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%`);
|
|
253
|
+
}
|
|
192
254
|
if (evt.resolution_method) lines.push(` - Resolution: \`${evt.resolution_method}\``);
|
|
193
255
|
break;
|
|
194
256
|
case 'operator_escalated':
|
|
@@ -480,14 +542,21 @@ function extractGovernanceEventDigest(artifact, relPath = '.agentxchain/decision
|
|
|
480
542
|
})) : [];
|
|
481
543
|
break;
|
|
482
544
|
case 'conflict_detected':
|
|
483
|
-
base.conflicting_files =
|
|
545
|
+
base.conflicting_files = normalizeConflictingFiles(d.conflict);
|
|
546
|
+
base.accepted_since_turn_ids = normalizeAcceptedSinceTurnIds(d.conflict);
|
|
484
547
|
base.overlap_ratio = typeof d.conflict?.overlap_ratio === 'number' ? d.conflict.overlap_ratio : null;
|
|
485
548
|
break;
|
|
486
549
|
case 'conflict_rejected':
|
|
487
|
-
base.conflicting_files =
|
|
550
|
+
base.conflicting_files = normalizeConflictingFiles(d.conflict);
|
|
551
|
+
base.accepted_since_turn_ids = normalizeAcceptedSinceTurnIds(d.conflict);
|
|
552
|
+
base.overlap_ratio = typeof d.conflict?.overlap_ratio === 'number' ? d.conflict.overlap_ratio : null;
|
|
553
|
+
base.operator_reason = d.operator_reason || null;
|
|
488
554
|
break;
|
|
489
555
|
case 'conflict_resolution_selected':
|
|
490
|
-
base.
|
|
556
|
+
base.conflicting_files = normalizeConflictingFiles(d.conflict);
|
|
557
|
+
base.accepted_since_turn_ids = normalizeAcceptedSinceTurnIds(d.conflict);
|
|
558
|
+
base.overlap_ratio = typeof d.conflict?.overlap_ratio === 'number' ? d.conflict.overlap_ratio : null;
|
|
559
|
+
base.resolution_method = d.resolution_chosen || d.conflict?.resolution || null;
|
|
491
560
|
break;
|
|
492
561
|
case 'operator_escalated':
|
|
493
562
|
base.blocked_on = d.blocked_on || null;
|
|
@@ -2420,7 +2489,19 @@ function renderHtmlGovEventDetail(evt) {
|
|
|
2420
2489
|
break;
|
|
2421
2490
|
case 'conflict_detected':
|
|
2422
2491
|
if (evt.conflicting_files?.length > 0) parts.push(`<li>Files: ${evt.conflicting_files.map((f) => `<code>${esc(f)}</code>`).join(', ')}</li>`);
|
|
2492
|
+
if (evt.accepted_since_turn_ids?.length > 0) parts.push(`<li>Accepted since: ${evt.accepted_since_turn_ids.map((turnId) => `<code>${esc(turnId)}</code>`).join(', ')}</li>`);
|
|
2493
|
+
if (evt.overlap_ratio != null) parts.push(`<li>Overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%</li>`);
|
|
2494
|
+
break;
|
|
2495
|
+
case 'conflict_rejected':
|
|
2496
|
+
if (evt.conflicting_files?.length > 0) parts.push(`<li>Files: ${evt.conflicting_files.map((f) => `<code>${esc(f)}</code>`).join(', ')}</li>`);
|
|
2497
|
+
if (evt.accepted_since_turn_ids?.length > 0) parts.push(`<li>Accepted since: ${evt.accepted_since_turn_ids.map((turnId) => `<code>${esc(turnId)}</code>`).join(', ')}</li>`);
|
|
2498
|
+
if (evt.operator_reason) parts.push(`<li>Operator reason: ${esc(evt.operator_reason)}</li>`);
|
|
2499
|
+
break;
|
|
2500
|
+
case 'conflict_resolution_selected':
|
|
2501
|
+
if (evt.conflicting_files?.length > 0) parts.push(`<li>Files: ${evt.conflicting_files.map((f) => `<code>${esc(f)}</code>`).join(', ')}</li>`);
|
|
2502
|
+
if (evt.accepted_since_turn_ids?.length > 0) parts.push(`<li>Accepted since: ${evt.accepted_since_turn_ids.map((turnId) => `<code>${esc(turnId)}</code>`).join(', ')}</li>`);
|
|
2423
2503
|
if (evt.overlap_ratio != null) parts.push(`<li>Overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%</li>`);
|
|
2504
|
+
if (evt.resolution_method) parts.push(`<li>Resolution: <code>${esc(evt.resolution_method)}</code></li>`);
|
|
2424
2505
|
break;
|
|
2425
2506
|
case 'operator_escalated':
|
|
2426
2507
|
if (evt.reason) parts.push(`<li>Reason: ${esc(evt.reason)}</li>`);
|
package/src/lib/run-events.js
CHANGED
package/src/lib/run-loop.js
CHANGED
|
@@ -340,6 +340,26 @@ async function executeParallelTurns(root, config, state, maxConcurrent, callback
|
|
|
340
340
|
const acceptResult = acceptTurn(root, config, { turnId: turn.turn_id });
|
|
341
341
|
if (!acceptResult.ok) {
|
|
342
342
|
errors.push(`acceptTurn(${roleId}): ${acceptResult.error}`);
|
|
343
|
+
|
|
344
|
+
// Conflict-aware handling (DEC-RUN-LOOP-CONFLICT-001)
|
|
345
|
+
if (acceptResult.error_code === 'conflict') {
|
|
346
|
+
history.push({
|
|
347
|
+
role: roleId, turn_id: turn.turn_id, accepted: false,
|
|
348
|
+
error_code: 'conflict', accept_error: acceptResult.error,
|
|
349
|
+
conflict: acceptResult.conflict,
|
|
350
|
+
});
|
|
351
|
+
emit({
|
|
352
|
+
type: 'turn_conflicted', turn, role: roleId,
|
|
353
|
+
error_code: 'conflict', conflict: acceptResult.conflict,
|
|
354
|
+
state: acceptResult.state,
|
|
355
|
+
});
|
|
356
|
+
if (acceptResult.state?.status === 'blocked') {
|
|
357
|
+
emit({ type: 'blocked', state: acceptResult.state });
|
|
358
|
+
return { terminal: true, ok: false, stop_reason: 'conflict_loop', history, acceptedCount };
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
343
363
|
// Record failure but try other turns
|
|
344
364
|
history.push({ role: roleId, turn_id: turn.turn_id, accepted: false, accept_error: acceptResult.error });
|
|
345
365
|
continue;
|
|
@@ -372,8 +392,10 @@ async function executeParallelTurns(root, config, state, maxConcurrent, callback
|
|
|
372
392
|
if (acceptedCount === 0 && history.length > 0) {
|
|
373
393
|
const allFailed = history.every(h => !h.accepted);
|
|
374
394
|
if (allFailed) {
|
|
375
|
-
|
|
376
|
-
|
|
395
|
+
const allConflicts = history.every(h => h.error_code === 'conflict');
|
|
396
|
+
const stopReason = allConflicts ? 'conflict_stall' : 'blocked';
|
|
397
|
+
errors.push(`All parallel turns failed acceptance — ${stopReason}`);
|
|
398
|
+
return { terminal: true, ok: false, stop_reason: stopReason, history, acceptedCount };
|
|
377
399
|
}
|
|
378
400
|
}
|
|
379
401
|
|
|
@@ -419,6 +441,29 @@ async function dispatchAndProcess(root, config, turn, assignState, callbacks, em
|
|
|
419
441
|
const acceptResult = acceptTurn(root, config);
|
|
420
442
|
if (!acceptResult.ok) {
|
|
421
443
|
errors.push(`acceptTurn(${roleId}): ${acceptResult.error}`);
|
|
444
|
+
|
|
445
|
+
// Conflict-aware handling (DEC-RUN-LOOP-CONFLICT-001)
|
|
446
|
+
if (acceptResult.error_code === 'conflict') {
|
|
447
|
+
history.push({
|
|
448
|
+
role: roleId, turn_id: turn.turn_id, accepted: false,
|
|
449
|
+
error_code: 'conflict', accept_error: acceptResult.error,
|
|
450
|
+
conflict: acceptResult.conflict,
|
|
451
|
+
});
|
|
452
|
+
emit({
|
|
453
|
+
type: 'turn_conflicted', turn, role: roleId,
|
|
454
|
+
error_code: 'conflict', conflict: acceptResult.conflict,
|
|
455
|
+
state: acceptResult.state,
|
|
456
|
+
});
|
|
457
|
+
// If the resulting state is blocked (conflict_loop), terminate
|
|
458
|
+
if (acceptResult.state?.status === 'blocked') {
|
|
459
|
+
emit({ type: 'blocked', state: acceptResult.state });
|
|
460
|
+
return { terminal: true, ok: false, stop_reason: 'conflict_loop', history };
|
|
461
|
+
}
|
|
462
|
+
// Otherwise the turn is conflicted but the run is still active — let the
|
|
463
|
+
// main loop re-enter and try another role or handle the paused state
|
|
464
|
+
return { terminal: false, accepted: false, history };
|
|
465
|
+
}
|
|
466
|
+
|
|
422
467
|
return { terminal: true, ok: false, stop_reason: 'blocked', history };
|
|
423
468
|
}
|
|
424
469
|
|