agentxchain 2.91.0 → 2.92.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/package.json +1 -1
- package/src/commands/doctor.js +19 -1
- package/src/commands/status.js +10 -0
- package/src/lib/adapters/local-cli-adapter.js +1 -1
- package/src/lib/export.js +43 -0
- package/src/lib/governed-state.js +84 -2
- package/src/lib/repo-observer.js +39 -5
- package/src/lib/report.js +40 -0
- package/src/lib/run-loop.js +16 -0
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -5,6 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { loadConfig, loadLock, findProjectRoot } from '../lib/config.js';
|
|
6
6
|
import { validateProject } from '../lib/validation.js';
|
|
7
7
|
import { getWatchPid } from './watch.js';
|
|
8
|
+
import { getDashboardPid, getDashboardSession } from './dashboard.js';
|
|
8
9
|
import { loadNormalizedConfig, detectConfigVersion } from '../lib/normalized-config.js';
|
|
9
10
|
import { readDaemonState, evaluateDaemonStatus } from '../lib/run-schedule.js';
|
|
10
11
|
import { getGovernedVersionSurface, formatGovernedVersionLabel } from '../lib/protocol-version.js';
|
|
@@ -219,6 +220,21 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
219
220
|
}
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
// 9. Dashboard session health (unconditional — dashboard is a general operator surface)
|
|
224
|
+
{
|
|
225
|
+
const dashPid = getDashboardPid(root);
|
|
226
|
+
const dashSession = getDashboardSession(root);
|
|
227
|
+
if (dashPid && dashSession) {
|
|
228
|
+
checks.push({ id: 'dashboard_session', name: 'Dashboard session', level: 'pass', detail: `Dashboard running at ${dashSession.url} (PID: ${dashPid})` });
|
|
229
|
+
} else if (dashPid && !dashSession) {
|
|
230
|
+
checks.push({ id: 'dashboard_session', name: 'Dashboard session', level: 'warn', detail: `Dashboard PID ${dashPid} alive but session file missing` });
|
|
231
|
+
} else if (!dashPid && dashSession) {
|
|
232
|
+
checks.push({ id: 'dashboard_session', name: 'Dashboard session', level: 'warn', detail: `Stale dashboard session files (PID ${dashSession.pid || '?'} not running)` });
|
|
233
|
+
} else {
|
|
234
|
+
checks.push({ id: 'dashboard_session', name: 'Dashboard session', level: 'info', detail: 'No dashboard session' });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
// Compute summary
|
|
223
239
|
const failCount = checks.filter(c => c.level === 'fail').length;
|
|
224
240
|
const warnCount = checks.filter(c => c.level === 'warn').length;
|
|
@@ -253,7 +269,9 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
253
269
|
? chalk.green('PASS')
|
|
254
270
|
: c.level === 'warn'
|
|
255
271
|
? chalk.yellow('WARN')
|
|
256
|
-
:
|
|
272
|
+
: c.level === 'info'
|
|
273
|
+
? chalk.dim('INFO')
|
|
274
|
+
: chalk.red('FAIL');
|
|
257
275
|
console.log(` ${badge} ${c.name.padEnd(24)} ${chalk.dim(c.detail)}`);
|
|
258
276
|
}
|
|
259
277
|
|
package/src/commands/status.js
CHANGED
|
@@ -9,6 +9,7 @@ import { getConnectorHealth } from '../lib/connector-health.js';
|
|
|
9
9
|
import { deriveWorkflowKitArtifacts } from '../lib/workflow-kit-artifacts.js';
|
|
10
10
|
import { evaluateTimeouts } from '../lib/timeout-evaluator.js';
|
|
11
11
|
import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
12
|
+
import { getDashboardPid, getDashboardSession } from './dashboard.js';
|
|
12
13
|
|
|
13
14
|
export async function statusCommand(opts) {
|
|
14
15
|
const context = loadProjectContext();
|
|
@@ -87,6 +88,14 @@ function renderGovernedStatus(context, opts) {
|
|
|
87
88
|
const workflowKitArtifacts = deriveWorkflowKitArtifacts(root, config, state);
|
|
88
89
|
|
|
89
90
|
if (opts.json) {
|
|
91
|
+
const dashPid = getDashboardPid(root);
|
|
92
|
+
const dashSession = getDashboardSession(root);
|
|
93
|
+
const dashboardSessionObj = dashPid
|
|
94
|
+
? { status: 'running', pid: dashPid, url: dashSession?.url || null, started_at: dashSession?.started_at || null }
|
|
95
|
+
: dashSession
|
|
96
|
+
? { status: 'stale', pid: dashSession.pid || null, url: dashSession.url || null, started_at: dashSession.started_at || null }
|
|
97
|
+
: { status: 'not_running', pid: null, url: null, started_at: null };
|
|
98
|
+
|
|
90
99
|
console.log(JSON.stringify({
|
|
91
100
|
version,
|
|
92
101
|
protocol_mode: config.protocol_mode,
|
|
@@ -99,6 +108,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
99
108
|
continuity,
|
|
100
109
|
connector_health: connectorHealth,
|
|
101
110
|
workflow_kit_artifacts: workflowKitArtifacts,
|
|
111
|
+
dashboard_session: dashboardSessionObj,
|
|
102
112
|
}, null, 2));
|
|
103
113
|
return;
|
|
104
114
|
}
|
|
@@ -115,7 +115,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
115
115
|
child = spawn(command, args, {
|
|
116
116
|
cwd: runtime.cwd ? join(root, runtime.cwd) : root,
|
|
117
117
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
|
-
env: { ...process.env },
|
|
118
|
+
env: { ...process.env, AGENTXCHAIN_TURN_ID: turn.turn_id },
|
|
119
119
|
});
|
|
120
120
|
} catch (err) {
|
|
121
121
|
resolve({ ok: false, error: `Failed to spawn "${command}": ${err.message}`, logs });
|
package/src/lib/export.js
CHANGED
|
@@ -7,6 +7,7 @@ import { loadProjectContext, loadProjectState } from './config.js';
|
|
|
7
7
|
import { loadCoordinatorConfig, COORDINATOR_CONFIG_FILE } from './coordinator-config.js';
|
|
8
8
|
import { loadCoordinatorState } from './coordinator-state.js';
|
|
9
9
|
import { normalizeRunProvenance } from './run-provenance.js';
|
|
10
|
+
import { getDashboardPid, getDashboardSession } from '../commands/dashboard.js';
|
|
10
11
|
|
|
11
12
|
const EXPORT_SCHEMA_VERSION = '0.3';
|
|
12
13
|
|
|
@@ -23,6 +24,8 @@ const COORDINATOR_INCLUDED_ROOTS = [
|
|
|
23
24
|
export const RUN_EXPORT_INCLUDED_ROOTS = [
|
|
24
25
|
'agentxchain.json',
|
|
25
26
|
'TALK.md',
|
|
27
|
+
'.agentxchain-dashboard.pid',
|
|
28
|
+
'.agentxchain-dashboard.json',
|
|
26
29
|
'.agentxchain/state.json',
|
|
27
30
|
'.agentxchain/session.json',
|
|
28
31
|
'.agentxchain/history.jsonl',
|
|
@@ -166,6 +169,45 @@ function countDirectoryFiles(files, prefix) {
|
|
|
166
169
|
return Object.keys(files).filter((path) => path.startsWith(`${prefix}/`)).length;
|
|
167
170
|
}
|
|
168
171
|
|
|
172
|
+
function buildDashboardSessionSummary(root) {
|
|
173
|
+
const dashPid = getDashboardPid(root);
|
|
174
|
+
const dashSession = getDashboardSession(root);
|
|
175
|
+
|
|
176
|
+
if (dashPid && dashSession) {
|
|
177
|
+
return {
|
|
178
|
+
status: 'running',
|
|
179
|
+
pid: dashPid,
|
|
180
|
+
url: dashSession.url || null,
|
|
181
|
+
started_at: dashSession.started_at || null,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (dashPid && !dashSession) {
|
|
186
|
+
return {
|
|
187
|
+
status: 'pid_only',
|
|
188
|
+
pid: dashPid,
|
|
189
|
+
url: null,
|
|
190
|
+
started_at: null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!dashPid && dashSession) {
|
|
195
|
+
return {
|
|
196
|
+
status: 'stale',
|
|
197
|
+
pid: dashSession.pid || null,
|
|
198
|
+
url: dashSession.url || null,
|
|
199
|
+
started_at: dashSession.started_at || null,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
status: 'not_running',
|
|
205
|
+
pid: null,
|
|
206
|
+
url: null,
|
|
207
|
+
started_at: null,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
169
211
|
export function buildDelegationSummary(files) {
|
|
170
212
|
const historyData = files['.agentxchain/history.jsonl']?.data;
|
|
171
213
|
if (!Array.isArray(historyData)) {
|
|
@@ -404,6 +446,7 @@ export function buildRunExport(startDir = process.cwd()) {
|
|
|
404
446
|
staging_artifact_files: countDirectoryFiles(files, '.agentxchain/staging'),
|
|
405
447
|
intake_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/intake/')),
|
|
406
448
|
coordinator_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/multirepo/')),
|
|
449
|
+
dashboard_session: buildDashboardSessionSummary(root),
|
|
407
450
|
delegation_summary: buildDelegationSummary(files),
|
|
408
451
|
},
|
|
409
452
|
workspace: buildRunWorkspaceMetadata(root),
|
|
@@ -799,6 +799,61 @@ function readJsonlEntries(root, relPath) {
|
|
|
799
799
|
.filter(Boolean);
|
|
800
800
|
}
|
|
801
801
|
|
|
802
|
+
function collectPendingConcurrentSiblingDeclarations(root, state, currentTurn, historyEntries = []) {
|
|
803
|
+
const concurrentIds = new Set(
|
|
804
|
+
Array.isArray(currentTurn?.concurrent_with)
|
|
805
|
+
? currentTurn.concurrent_with.filter((id) => typeof id === 'string' && id.length > 0)
|
|
806
|
+
: [],
|
|
807
|
+
);
|
|
808
|
+
const activeTurns = getActiveTurns(state);
|
|
809
|
+
for (const turn of Object.values(activeTurns)) {
|
|
810
|
+
if (
|
|
811
|
+
turn?.turn_id !== currentTurn?.turn_id
|
|
812
|
+
&& Array.isArray(turn?.concurrent_with)
|
|
813
|
+
&& turn.concurrent_with.includes(currentTurn?.turn_id)
|
|
814
|
+
) {
|
|
815
|
+
concurrentIds.add(turn.turn_id);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (concurrentIds.size === 0) {
|
|
819
|
+
return [];
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const acceptedIds = new Set(
|
|
823
|
+
(Array.isArray(historyEntries) ? historyEntries : [])
|
|
824
|
+
.map((entry) => entry?.turn_id)
|
|
825
|
+
.filter((turnId) => typeof turnId === 'string' && turnId.length > 0),
|
|
826
|
+
);
|
|
827
|
+
const declarations = [];
|
|
828
|
+
|
|
829
|
+
for (const siblingTurnId of concurrentIds) {
|
|
830
|
+
if (siblingTurnId === currentTurn?.turn_id || acceptedIds.has(siblingTurnId) || !activeTurns[siblingTurnId]) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const stagedSibling = loadHookStagedTurn(root, getTurnStagingResultPath(siblingTurnId));
|
|
835
|
+
const siblingResult = stagedSibling.turnResult;
|
|
836
|
+
if (!siblingResult || siblingResult.turn_id !== siblingTurnId) {
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const siblingFiles = [...new Set(
|
|
841
|
+
(Array.isArray(siblingResult.files_changed) ? siblingResult.files_changed : [])
|
|
842
|
+
.filter((filePath) => typeof filePath === 'string' && filePath.length > 0),
|
|
843
|
+
)];
|
|
844
|
+
if (siblingFiles.length === 0) {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
declarations.push({
|
|
849
|
+
turn_id: siblingTurnId,
|
|
850
|
+
files_changed: siblingFiles,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return declarations;
|
|
855
|
+
}
|
|
856
|
+
|
|
802
857
|
function getObservedFiles(entry) {
|
|
803
858
|
if (Array.isArray(entry?.observed_artifact?.files_changed)) {
|
|
804
859
|
return entry.observed_artifact.files_changed;
|
|
@@ -2373,7 +2428,17 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2373
2428
|
const baseline = currentTurn.baseline || null;
|
|
2374
2429
|
const rawObservation = observeChanges(root, baseline);
|
|
2375
2430
|
const historyEntries = readJsonlEntries(root, HISTORY_PATH);
|
|
2376
|
-
const
|
|
2431
|
+
const pendingConcurrentSiblingDeclarations = collectPendingConcurrentSiblingDeclarations(
|
|
2432
|
+
root,
|
|
2433
|
+
state,
|
|
2434
|
+
currentTurn,
|
|
2435
|
+
historyEntries,
|
|
2436
|
+
);
|
|
2437
|
+
const observation = attributeObservedChangesToTurn(rawObservation, currentTurn, historyEntries, {
|
|
2438
|
+
currentDeclaredFiles: turnResult.files_changed || [],
|
|
2439
|
+
concurrentSiblingIds: pendingConcurrentSiblingDeclarations.map((entry) => entry.turn_id),
|
|
2440
|
+
pendingConcurrentSiblingDeclarations,
|
|
2441
|
+
});
|
|
2377
2442
|
const role = config.roles?.[turnResult.role];
|
|
2378
2443
|
const runtimeId = turnResult.runtime_id;
|
|
2379
2444
|
const runtime = config.runtimes?.[runtimeId];
|
|
@@ -2381,11 +2446,28 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2381
2446
|
materializeDerivedReviewArtifact(root, turnResult, state, runtimeType, baseline);
|
|
2382
2447
|
materializeDerivedProposalArtifact(root, turnResult, state, runtimeType);
|
|
2383
2448
|
const writeAuthority = role?.write_authority || 'review_only';
|
|
2449
|
+
|
|
2450
|
+
// When concurrent siblings exist but have not yet been accepted, the
|
|
2451
|
+
// observation includes their file changes too. The attribution system
|
|
2452
|
+
// (attributeObservedChangesToTurn) only removes sibling files for
|
|
2453
|
+
// *later*-accepted turns. For the *first*-accepted concurrent turn,
|
|
2454
|
+
// undeclared files are expected noise from concurrency — downgrade to
|
|
2455
|
+
// warnings so the governance contract is not broken by turn-acceptance
|
|
2456
|
+
// ordering.
|
|
2457
|
+
const concurrentIds = new Set(
|
|
2458
|
+
Array.isArray(currentTurn.concurrent_with) ? currentTurn.concurrent_with : [],
|
|
2459
|
+
);
|
|
2460
|
+
const acceptedTurnIds = new Set(historyEntries.map(h => h.turn_id));
|
|
2461
|
+
const hasUnacceptedConcurrentSiblings = [...concurrentIds].some(id => !acceptedTurnIds.has(id));
|
|
2462
|
+
|
|
2384
2463
|
const diffComparison = compareDeclaredVsObserved(
|
|
2385
2464
|
turnResult.files_changed || [],
|
|
2386
2465
|
observation.files_changed,
|
|
2387
2466
|
writeAuthority,
|
|
2388
|
-
{
|
|
2467
|
+
{
|
|
2468
|
+
observation_available: observation.observation_available,
|
|
2469
|
+
has_unaccepted_concurrent_siblings: hasUnacceptedConcurrentSiblings,
|
|
2470
|
+
},
|
|
2389
2471
|
);
|
|
2390
2472
|
if (diffComparison.errors.length > 0) {
|
|
2391
2473
|
return {
|
package/src/lib/repo-observer.js
CHANGED
|
@@ -175,7 +175,7 @@ export function observeChanges(root, baseline) {
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
export function attributeObservedChangesToTurn(observation, currentTurn, historyEntries = []) {
|
|
178
|
+
export function attributeObservedChangesToTurn(observation, currentTurn, historyEntries = [], options = {}) {
|
|
179
179
|
const observedFiles = Array.isArray(observation?.files_changed) ? observation.files_changed : [];
|
|
180
180
|
if (observedFiles.length === 0) {
|
|
181
181
|
return observation;
|
|
@@ -184,6 +184,11 @@ export function attributeObservedChangesToTurn(observation, currentTurn, history
|
|
|
184
184
|
const concurrentIds = new Set(
|
|
185
185
|
Array.isArray(currentTurn?.concurrent_with) ? currentTurn.concurrent_with : [],
|
|
186
186
|
);
|
|
187
|
+
for (const siblingId of Array.isArray(options.concurrentSiblingIds) ? options.concurrentSiblingIds : []) {
|
|
188
|
+
if (typeof siblingId === 'string' && siblingId.length > 0) {
|
|
189
|
+
concurrentIds.add(siblingId);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
187
192
|
if (concurrentIds.size === 0) {
|
|
188
193
|
return observation;
|
|
189
194
|
}
|
|
@@ -195,11 +200,17 @@ export function attributeObservedChangesToTurn(observation, currentTurn, history
|
|
|
195
200
|
.filter((entry) => (
|
|
196
201
|
Number.isInteger(entry?.accepted_sequence)
|
|
197
202
|
&& entry.accepted_sequence > assignedSequence
|
|
198
|
-
&&
|
|
203
|
+
&& (
|
|
204
|
+
concurrentIds.has(entry.turn_id)
|
|
205
|
+
|| (Array.isArray(entry?.concurrent_with) && entry.concurrent_with.includes(currentTurn?.turn_id))
|
|
206
|
+
)
|
|
199
207
|
))
|
|
200
208
|
.sort((left, right) => left.accepted_sequence - right.accepted_sequence);
|
|
201
209
|
|
|
202
|
-
|
|
210
|
+
const pendingConcurrentSiblingDeclarations = Array.isArray(options.pendingConcurrentSiblingDeclarations)
|
|
211
|
+
? options.pendingConcurrentSiblingDeclarations
|
|
212
|
+
: [];
|
|
213
|
+
if (acceptedConcurrentSiblings.length === 0 && pendingConcurrentSiblingDeclarations.length === 0) {
|
|
203
214
|
return observation;
|
|
204
215
|
}
|
|
205
216
|
|
|
@@ -221,7 +232,20 @@ export function attributeObservedChangesToTurn(observation, currentTurn, history
|
|
|
221
232
|
}
|
|
222
233
|
}
|
|
223
234
|
|
|
224
|
-
|
|
235
|
+
const currentDeclaredFiles = new Set(
|
|
236
|
+
Array.isArray(options.currentDeclaredFiles) ? options.currentDeclaredFiles : [],
|
|
237
|
+
);
|
|
238
|
+
const pendingConcurrentSiblingFiles = new Set();
|
|
239
|
+
for (const declaration of pendingConcurrentSiblingDeclarations) {
|
|
240
|
+
const siblingFiles = Array.isArray(declaration?.files_changed) ? declaration.files_changed : [];
|
|
241
|
+
for (const filePath of siblingFiles) {
|
|
242
|
+
if (typeof filePath === 'string' && filePath.length > 0 && !currentDeclaredFiles.has(filePath)) {
|
|
243
|
+
pendingConcurrentSiblingFiles.add(filePath);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (siblingMarkersByFile.size === 0 && pendingConcurrentSiblingFiles.size === 0) {
|
|
225
249
|
return observation;
|
|
226
250
|
}
|
|
227
251
|
|
|
@@ -239,6 +263,10 @@ export function attributeObservedChangesToTurn(observation, currentTurn, history
|
|
|
239
263
|
attributedToConcurrentSiblings.push(filePath);
|
|
240
264
|
continue;
|
|
241
265
|
}
|
|
266
|
+
if (pendingConcurrentSiblingFiles.has(filePath)) {
|
|
267
|
+
attributedToConcurrentSiblings.push(filePath);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
242
270
|
nextFiles.push(filePath);
|
|
243
271
|
if (typeof currentMarker === 'string') {
|
|
244
272
|
nextMarkers[filePath] = currentMarker;
|
|
@@ -503,7 +531,13 @@ export function compareDeclaredVsObserved(declared, observed, writeAuthority, op
|
|
|
503
531
|
|
|
504
532
|
if (writeAuthority === 'authoritative') {
|
|
505
533
|
if (undeclared.length > 0) {
|
|
506
|
-
|
|
534
|
+
if (options.has_unaccepted_concurrent_siblings) {
|
|
535
|
+
// Concurrent siblings may have written these files; downgrade to warning.
|
|
536
|
+
// The attribution system will handle later-accepted siblings correctly.
|
|
537
|
+
warnings.push(`Undeclared file changes detected (likely from concurrent sibling turns): ${undeclared.join(', ')}`);
|
|
538
|
+
} else {
|
|
539
|
+
errors.push(`Undeclared file changes detected (observed but not in files_changed): ${undeclared.join(', ')}`);
|
|
540
|
+
}
|
|
507
541
|
}
|
|
508
542
|
if (phantom.length > 0) {
|
|
509
543
|
warnings.push(`Declared files not observed in actual diff: ${phantom.join(', ')}`);
|
package/src/lib/report.js
CHANGED
|
@@ -5,6 +5,7 @@ import { normalizeRunProvenance, summarizeRunProvenance } from './run-provenance
|
|
|
5
5
|
export const GOVERNANCE_REPORT_VERSION = '0.1';
|
|
6
6
|
|
|
7
7
|
const VALID_DELEGATION_OUTCOMES = new Set(['completed', 'failed', 'mixed', 'pending']);
|
|
8
|
+
const VALID_DASHBOARD_SESSION_STATUSES = new Set(['running', 'pid_only', 'stale', 'not_running']);
|
|
8
9
|
|
|
9
10
|
function normalizeDelegationSummary(summary) {
|
|
10
11
|
if (!summary || typeof summary !== 'object' || Array.isArray(summary)) return null;
|
|
@@ -58,6 +59,37 @@ function extractDelegationSummary(artifact) {
|
|
|
58
59
|
return normalizeDelegationSummary(buildDelegationSummary(artifact.files || {}));
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
function normalizeDashboardSessionSummary(summary) {
|
|
63
|
+
if (!summary || typeof summary !== 'object' || Array.isArray(summary)) return null;
|
|
64
|
+
if (!VALID_DASHBOARD_SESSION_STATUSES.has(summary.status)) return null;
|
|
65
|
+
return {
|
|
66
|
+
status: summary.status,
|
|
67
|
+
pid: Number.isInteger(summary.pid) ? summary.pid : null,
|
|
68
|
+
url: typeof summary.url === 'string' && summary.url.length > 0 ? summary.url : null,
|
|
69
|
+
started_at: typeof summary.started_at === 'string' && summary.started_at.length > 0 ? summary.started_at : null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractDashboardSessionSummary(artifact) {
|
|
74
|
+
return normalizeDashboardSessionSummary(artifact.summary?.dashboard_session);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatDashboardSessionLine(session) {
|
|
78
|
+
if (!session) return null;
|
|
79
|
+
switch (session.status) {
|
|
80
|
+
case 'running':
|
|
81
|
+
return `running at ${session.url || 'unknown url'} (PID: ${session.pid || '?'})`;
|
|
82
|
+
case 'pid_only':
|
|
83
|
+
return `pid_only (PID: ${session.pid || '?'}, session metadata missing)`;
|
|
84
|
+
case 'stale':
|
|
85
|
+
return `stale session files${session.pid ? ` (PID: ${session.pid})` : ''}${session.url ? ` at ${session.url}` : ''}`;
|
|
86
|
+
case 'not_running':
|
|
87
|
+
return 'not_running';
|
|
88
|
+
default:
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
61
93
|
function yesNo(value) {
|
|
62
94
|
return value ? 'yes' : 'no';
|
|
63
95
|
}
|
|
@@ -916,6 +948,7 @@ function buildRunSubject(artifact) {
|
|
|
916
948
|
const continuity = extractContinuityMetadata(artifact);
|
|
917
949
|
const governanceEvents = extractGovernanceEventDigest(artifact);
|
|
918
950
|
const delegationSummary = extractDelegationSummary(artifact);
|
|
951
|
+
const dashboardSession = extractDashboardSessionSummary(artifact);
|
|
919
952
|
|
|
920
953
|
return {
|
|
921
954
|
kind: 'governed_run',
|
|
@@ -942,6 +975,7 @@ function buildRunSubject(artifact) {
|
|
|
942
975
|
active_roles: activeRoles,
|
|
943
976
|
budget_status: normalizeBudgetStatus(artifact.state?.budget_status),
|
|
944
977
|
cost_summary: computeCostSummary(turns),
|
|
978
|
+
dashboard_session: dashboardSession,
|
|
945
979
|
created_at: timing.created_at,
|
|
946
980
|
completed_at: timing.completed_at,
|
|
947
981
|
duration_seconds: timing.duration_seconds,
|
|
@@ -1209,6 +1243,9 @@ export function formatGovernanceReportText(report) {
|
|
|
1209
1243
|
if (run.inherited_context?.parent_run_id) {
|
|
1210
1244
|
lines.push(`Inherited from: ${run.inherited_context.parent_run_id} (${run.inherited_context.parent_status || 'unknown'})`);
|
|
1211
1245
|
}
|
|
1246
|
+
if (run.dashboard_session) {
|
|
1247
|
+
lines.push(`Dashboard session: ${formatDashboardSessionLine(run.dashboard_session)}`);
|
|
1248
|
+
}
|
|
1212
1249
|
|
|
1213
1250
|
lines.push(
|
|
1214
1251
|
`History entries: ${artifacts.history_entries}`,
|
|
@@ -1688,6 +1725,9 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
1688
1725
|
if (run.inherited_context?.parent_run_id) {
|
|
1689
1726
|
lines.push(`- Inherited from: \`${run.inherited_context.parent_run_id}\` (${run.inherited_context.parent_status || 'unknown'})`);
|
|
1690
1727
|
}
|
|
1728
|
+
if (run.dashboard_session) {
|
|
1729
|
+
lines.push(`- Dashboard session: \`${formatDashboardSessionLine(run.dashboard_session)}\``);
|
|
1730
|
+
}
|
|
1691
1731
|
|
|
1692
1732
|
lines.push(
|
|
1693
1733
|
`- History entries: ${artifacts.history_entries}`,
|
package/src/lib/run-loop.js
CHANGED
|
@@ -217,7 +217,16 @@ async function executeParallelTurns(root, config, state, maxConcurrent, callback
|
|
|
217
217
|
|
|
218
218
|
// If selectRole returns a role we already tried (or assigned), try
|
|
219
219
|
// other eligible roles from the routing before giving up.
|
|
220
|
+
// Exception: when delegation queue is driving resolution, do not fill
|
|
221
|
+
// extra slots with non-delegation roles via the fallback — those roles
|
|
222
|
+
// would execute without delegation context and corrupt the lifecycle.
|
|
220
223
|
if (triedRoles.has(roleId)) {
|
|
224
|
+
const hasPendingDelegations = Array.isArray(state?.delegation_queue) &&
|
|
225
|
+
state.delegation_queue.some(d => d.status === 'pending' || d.status === 'active');
|
|
226
|
+
const hasPendingReview = !!state?.pending_delegation_review;
|
|
227
|
+
if (hasPendingDelegations || hasPendingReview) {
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
221
230
|
const phase = state.phase;
|
|
222
231
|
const allowed = config?.routing?.[phase]?.allowed_next_roles || [];
|
|
223
232
|
const alternateFound = allowed.some((alt) => {
|
|
@@ -248,6 +257,13 @@ async function executeParallelTurns(root, config, state, maxConcurrent, callback
|
|
|
248
257
|
turnsToDispatch.push({ turn: assignResult.turn, state: assignResult.state });
|
|
249
258
|
emit({ type: 'turn_assigned', turn: assignResult.turn, role: roleId, state: assignResult.state });
|
|
250
259
|
|
|
260
|
+
// Delegation review is a coordination checkpoint — do not fill additional
|
|
261
|
+
// slots alongside it. The review must execute alone so it can assess all
|
|
262
|
+
// delegation results before the run continues.
|
|
263
|
+
if (assignResult.turn.delegation_review) {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
251
267
|
// Reload state after assignment to get accurate active count
|
|
252
268
|
state = loadState(root, config);
|
|
253
269
|
activeCount = getActiveTurnCount(state);
|