agentxchain 2.145.0 → 2.147.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 +3 -0
- package/dashboard/components/notifications.js +127 -0
- package/dashboard/index.html +1 -0
- package/package.json +1 -1
- package/scripts/publish-npm.sh +16 -0
- package/scripts/release-downstream-truth.sh +16 -8
- package/scripts/sync-homebrew.sh +14 -1
- package/scripts/verify-post-publish.sh +55 -4
- package/src/commands/init.js +66 -31
- package/src/commands/reissue-turn.js +16 -0
- package/src/commands/reject-turn.js +14 -1
- package/src/commands/restart.js +33 -3
- package/src/commands/resume.js +78 -66
- package/src/commands/run.js +67 -10
- package/src/commands/schedule.js +34 -7
- package/src/commands/status.js +38 -5
- package/src/commands/step.js +117 -34
- package/src/lib/adapters/api-proxy-adapter.js +8 -0
- package/src/lib/adapters/local-cli-adapter.js +131 -13
- package/src/lib/adapters/manual-adapter.js +9 -10
- package/src/lib/adapters/mcp-adapter.js +3 -5
- package/src/lib/adapters/remote-agent-adapter.js +3 -5
- package/src/lib/config.js +4 -1
- package/src/lib/continuous-run.js +71 -6
- package/src/lib/dashboard/actions.js +9 -3
- package/src/lib/dashboard/bridge-server.js +11 -0
- package/src/lib/dashboard/notifications-reader.js +91 -0
- package/src/lib/dashboard/state-reader.js +16 -4
- package/src/lib/dispatch-bundle.js +1 -1
- package/src/lib/dispatch-progress.js +5 -3
- package/src/lib/governed-state.js +355 -13
- package/src/lib/intake.js +10 -1
- package/src/lib/normalized-config.js +51 -1
- package/src/lib/recent-event-summary.js +12 -0
- package/src/lib/run-events.js +4 -0
- package/src/lib/run-loop.js +67 -2
- package/src/lib/runner-interface.js +1 -0
- package/src/lib/schema.js +7 -0
- package/src/lib/schemas/agentxchain-config.schema.json +15 -1
- package/src/lib/staged-result-proof.js +43 -0
- package/src/lib/stale-turn-watchdog.js +308 -34
- package/src/lib/turn-result-shape.js +38 -0
- package/src/lib/turn-result-validator.js +4 -1
|
@@ -99,6 +99,9 @@ function describeContinuousTerminalStep(step, contOpts) {
|
|
|
99
99
|
return `Continuous loop failed: ${reason}. Check "agentxchain status" for details.`;
|
|
100
100
|
}
|
|
101
101
|
if (step.status === 'blocked') {
|
|
102
|
+
if (step.recovery_action) {
|
|
103
|
+
return `Continuous loop paused on blocker. Recovery: ${step.recovery_action}`;
|
|
104
|
+
}
|
|
102
105
|
return 'Continuous loop paused on blocker. Use "agentxchain unblock <id>" to resume.';
|
|
103
106
|
}
|
|
104
107
|
return null;
|
|
@@ -116,6 +119,14 @@ function isBlockedContinuousExecution(execution) {
|
|
|
116
119
|
|| stopReason === 'reject_exhausted';
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
function getBlockedRecoveryAction(state) {
|
|
123
|
+
return state?.blocked_reason?.recovery?.recovery_action || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getBlockedCategory(state) {
|
|
127
|
+
return state?.blocked_reason?.category || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
119
130
|
// ---------------------------------------------------------------------------
|
|
120
131
|
// Intake queue check
|
|
121
132
|
// ---------------------------------------------------------------------------
|
|
@@ -361,7 +372,14 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
361
372
|
if (governedState?.status === 'blocked') {
|
|
362
373
|
// Still blocked — stay paused, do not attempt new work
|
|
363
374
|
writeContinuousSession(root, session);
|
|
364
|
-
return {
|
|
375
|
+
return {
|
|
376
|
+
ok: true,
|
|
377
|
+
status: 'blocked',
|
|
378
|
+
action: 'still_blocked',
|
|
379
|
+
run_id: session.current_run_id,
|
|
380
|
+
recovery_action: getBlockedRecoveryAction(governedState),
|
|
381
|
+
blocked_category: getBlockedCategory(governedState),
|
|
382
|
+
};
|
|
365
383
|
}
|
|
366
384
|
// Unblocked — resume by continuing the existing governed run directly.
|
|
367
385
|
// Skip the intake pipeline: the run is already in progress, and startIntent
|
|
@@ -388,10 +406,20 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
388
406
|
const resumeStopReason = execution.result?.stop_reason;
|
|
389
407
|
|
|
390
408
|
if (isBlockedContinuousExecution(execution)) {
|
|
409
|
+
const blockedRecoveryAction = getBlockedRecoveryAction(execution?.result?.state || loadProjectState(root, context.config));
|
|
391
410
|
session.status = 'paused';
|
|
392
|
-
log(
|
|
411
|
+
log(blockedRecoveryAction
|
|
412
|
+
? `Resumed run blocked again — continuous loop re-paused. Recovery: ${blockedRecoveryAction}`
|
|
413
|
+
: 'Resumed run blocked again — continuous loop re-paused.');
|
|
393
414
|
writeContinuousSession(root, session);
|
|
394
|
-
return {
|
|
415
|
+
return {
|
|
416
|
+
ok: true,
|
|
417
|
+
status: 'blocked',
|
|
418
|
+
action: 'run_blocked',
|
|
419
|
+
run_id: session.current_run_id,
|
|
420
|
+
recovery_action: blockedRecoveryAction,
|
|
421
|
+
blocked_category: getBlockedCategory(execution?.result?.state || loadProjectState(root, context.config)),
|
|
422
|
+
};
|
|
395
423
|
}
|
|
396
424
|
|
|
397
425
|
if (execution.exitCode !== 0 || !execution.result) {
|
|
@@ -473,9 +501,35 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
473
501
|
return { ok: false, status: 'failed', action: 'prepare_failed', stop_reason: preparedIntent.error, intent_id: targetIntentId };
|
|
474
502
|
}
|
|
475
503
|
|
|
504
|
+
// BUG-53: Auto-chain audit trail. When this advance step seeds a NEXT run
|
|
505
|
+
// (i.e., at least one prior run already completed in this session), emit a
|
|
506
|
+
// `session_continuation` event so operators have a visible record that the
|
|
507
|
+
// loop auto-derived the next vision objective without intervention. Event
|
|
508
|
+
// is emitted BEFORE we overwrite session.current_run_id so previous_run_id
|
|
509
|
+
// reflects the just-completed run and next_run_id reflects the newly
|
|
510
|
+
// prepared one. See HUMAN-ROADMAP BUG-53 fix #4.
|
|
511
|
+
const previousRunId = session.current_run_id;
|
|
512
|
+
const nextObjective = visionObjective || preparedIntent.intent?.charter || null;
|
|
513
|
+
if ((session.runs_completed || 0) >= 1 && previousRunId && previousRunId !== preparedIntent.run_id) {
|
|
514
|
+
emitRunEvent(root, 'session_continuation', {
|
|
515
|
+
run_id: preparedIntent.run_id,
|
|
516
|
+
phase: null,
|
|
517
|
+
status: 'active',
|
|
518
|
+
payload: {
|
|
519
|
+
session_id: session.session_id,
|
|
520
|
+
previous_run_id: previousRunId,
|
|
521
|
+
next_run_id: preparedIntent.run_id,
|
|
522
|
+
next_objective: nextObjective,
|
|
523
|
+
next_intent_id: targetIntentId,
|
|
524
|
+
runs_completed: session.runs_completed || 0,
|
|
525
|
+
trigger: visionObjective ? 'vision_scan' : 'intake',
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
476
530
|
// Execute the governed run
|
|
477
531
|
session.current_run_id = preparedIntent.run_id;
|
|
478
|
-
session.current_vision_objective =
|
|
532
|
+
session.current_vision_objective = nextObjective;
|
|
479
533
|
session.status = 'running';
|
|
480
534
|
writeContinuousSession(root, session);
|
|
481
535
|
|
|
@@ -519,6 +573,7 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
519
573
|
}
|
|
520
574
|
|
|
521
575
|
if (isBlockedContinuousExecution(execution)) {
|
|
576
|
+
const blockedRecoveryAction = getBlockedRecoveryAction(execution?.result?.state || loadProjectState(root, context.config));
|
|
522
577
|
const resolved = resolveIntent(root, targetIntentId);
|
|
523
578
|
if (!resolved.ok) {
|
|
524
579
|
log(`Continuous resolve error: ${resolved.error}`);
|
|
@@ -527,9 +582,19 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
527
582
|
return { ok: false, status: 'failed', action: 'resolve_failed', stop_reason: resolved.error, intent_id: targetIntentId };
|
|
528
583
|
}
|
|
529
584
|
session.status = 'paused';
|
|
530
|
-
log(
|
|
585
|
+
log(blockedRecoveryAction
|
|
586
|
+
? `Run blocked — continuous loop paused. Recovery: ${blockedRecoveryAction}`
|
|
587
|
+
: 'Run blocked — continuous loop paused. Use `agentxchain unblock <id>` to resume.');
|
|
531
588
|
writeContinuousSession(root, session);
|
|
532
|
-
return {
|
|
589
|
+
return {
|
|
590
|
+
ok: true,
|
|
591
|
+
status: 'blocked',
|
|
592
|
+
action: 'run_blocked',
|
|
593
|
+
run_id: session.current_run_id,
|
|
594
|
+
intent_id: targetIntentId,
|
|
595
|
+
recovery_action: blockedRecoveryAction,
|
|
596
|
+
blocked_category: getBlockedCategory(execution?.result?.state || loadProjectState(root, context.config)),
|
|
597
|
+
};
|
|
533
598
|
}
|
|
534
599
|
|
|
535
600
|
if (stopReason === 'caller_stopped') {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dirname } from 'path';
|
|
2
|
-
import { loadProjectContext } from '../config.js';
|
|
2
|
+
import { loadProjectContext, loadProjectState } from '../config.js';
|
|
3
3
|
import { approvePhaseTransition, approveRunCompletion } from '../governed-state.js';
|
|
4
4
|
import { deriveGovernedRunNextActions, deriveRecoveryDescriptor } from '../blocked-state.js';
|
|
5
5
|
import {
|
|
@@ -205,10 +205,16 @@ function approveCoordinatorGate(workspacePath, state, config) {
|
|
|
205
205
|
|
|
206
206
|
export function approvePendingDashboardGate(agentxchainDir) {
|
|
207
207
|
const workspacePath = dirname(agentxchainDir);
|
|
208
|
-
const
|
|
208
|
+
const context = loadProjectContext(workspacePath);
|
|
209
|
+
|
|
210
|
+
// Use loadProjectState to get reconciled state — approval-pause repair
|
|
211
|
+
// may surface a pending_run_completion from an orphaned blocked_on marker,
|
|
212
|
+
// and we must route on the reconciled truth, not the raw state.json.
|
|
213
|
+
const repoState = (context?.config?.protocol_mode === 'governed'
|
|
214
|
+
? loadProjectState(workspacePath, context.config)
|
|
215
|
+
: null) || readJsonFile(agentxchainDir, 'state.json');
|
|
209
216
|
|
|
210
217
|
if (repoState?.pending_phase_transition || repoState?.pending_run_completion) {
|
|
211
|
-
const context = loadProjectContext(workspacePath);
|
|
212
218
|
return approveRepoGate(workspacePath, context?.config, repoState);
|
|
213
219
|
}
|
|
214
220
|
|
|
@@ -23,6 +23,7 @@ import { readCoordinatorRepoStatusRows } from './coordinator-repo-status.js';
|
|
|
23
23
|
import { readCoordinatorTimeoutStatus } from './coordinator-timeout-status.js';
|
|
24
24
|
import { readAggregatedCoordinatorEvents, watchChildRepoEvents } from './coordinator-event-aggregation.js';
|
|
25
25
|
import { readWorkflowKitArtifacts } from './workflow-kit-artifacts.js';
|
|
26
|
+
import { readNotificationSnapshot } from './notifications-reader.js';
|
|
26
27
|
import { readConnectorHealthSnapshot } from './connectors.js';
|
|
27
28
|
import { readTimeoutStatus } from './timeout-status.js';
|
|
28
29
|
import { queryRunHistory } from '../run-history.js';
|
|
@@ -431,6 +432,16 @@ export function createBridgeServer({ agentxchainDir, dashboardDir, port = 3847,
|
|
|
431
432
|
return;
|
|
432
433
|
}
|
|
433
434
|
|
|
435
|
+
if (pathname === '/api/notifications') {
|
|
436
|
+
if (replayMode) {
|
|
437
|
+
writeJson(res, 200, { ok: true, replay_mode: true, message: 'Notification audit is live-only and not available in replay mode.' });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const result = readNotificationSnapshot(workspacePath);
|
|
441
|
+
writeJson(res, result.status, result.body);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
434
445
|
if (pathname === '/api/connectors') {
|
|
435
446
|
const result = readConnectorHealthSnapshot(workspacePath);
|
|
436
447
|
writeJson(res, result.status, result.body);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { loadConfig, loadProjectContext } from '../config.js';
|
|
2
|
+
import { readJsonlFile } from './state-reader.js';
|
|
3
|
+
|
|
4
|
+
function summarizeAuditEntries(entries) {
|
|
5
|
+
const summary = {
|
|
6
|
+
total_attempts: entries.length,
|
|
7
|
+
delivered: 0,
|
|
8
|
+
failed: 0,
|
|
9
|
+
timed_out: 0,
|
|
10
|
+
last_emitted_at: null,
|
|
11
|
+
last_failure_at: null,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (entry?.delivered === true) {
|
|
16
|
+
summary.delivered += 1;
|
|
17
|
+
} else {
|
|
18
|
+
summary.failed += 1;
|
|
19
|
+
if (!summary.last_failure_at || String(entry?.emitted_at || '') > summary.last_failure_at) {
|
|
20
|
+
summary.last_failure_at = entry?.emitted_at || null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (entry?.timed_out === true) {
|
|
24
|
+
summary.timed_out += 1;
|
|
25
|
+
}
|
|
26
|
+
if (!summary.last_emitted_at || String(entry?.emitted_at || '') > summary.last_emitted_at) {
|
|
27
|
+
summary.last_emitted_at = entry?.emitted_at || null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return summary;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeWebhook(webhook) {
|
|
35
|
+
return {
|
|
36
|
+
name: webhook.name,
|
|
37
|
+
timeout_ms: webhook.timeout_ms,
|
|
38
|
+
event_count: Array.isArray(webhook.events) ? webhook.events.length : 0,
|
|
39
|
+
events: Array.isArray(webhook.events) ? webhook.events : [],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readNotificationSnapshot(workspacePath) {
|
|
44
|
+
const context = loadProjectContext(workspacePath);
|
|
45
|
+
const governedContext = context?.config ? context : null;
|
|
46
|
+
const legacyConfigResult = governedContext ? null : loadConfig(workspacePath);
|
|
47
|
+
if (!governedContext && !legacyConfigResult) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
status: 404,
|
|
51
|
+
body: {
|
|
52
|
+
ok: false,
|
|
53
|
+
code: 'config_missing',
|
|
54
|
+
error: 'Project config not found. Run `agentxchain init --governed` first.',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const root = governedContext?.root || legacyConfigResult.root;
|
|
60
|
+
const config = governedContext?.config || legacyConfigResult.config;
|
|
61
|
+
const notifications = config?.notifications || {};
|
|
62
|
+
const webhooks = Array.isArray(notifications.webhooks)
|
|
63
|
+
? notifications.webhooks.map(normalizeWebhook)
|
|
64
|
+
: [];
|
|
65
|
+
const configured = webhooks.length > 0;
|
|
66
|
+
const approvalSla = notifications.approval_sla
|
|
67
|
+
? {
|
|
68
|
+
enabled: notifications.approval_sla.enabled !== false,
|
|
69
|
+
reminder_after_seconds: Array.isArray(notifications.approval_sla.reminder_after_seconds)
|
|
70
|
+
? notifications.approval_sla.reminder_after_seconds
|
|
71
|
+
: [],
|
|
72
|
+
}
|
|
73
|
+
: null;
|
|
74
|
+
|
|
75
|
+
const auditEntries = (readJsonlFile(`${root}/.agentxchain`, 'notification-audit.jsonl') || [])
|
|
76
|
+
.slice()
|
|
77
|
+
.sort((a, b) => String(b?.emitted_at || '').localeCompare(String(a?.emitted_at || '')));
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
status: 200,
|
|
82
|
+
body: {
|
|
83
|
+
ok: true,
|
|
84
|
+
configured,
|
|
85
|
+
webhooks,
|
|
86
|
+
approval_sla: approvalSla,
|
|
87
|
+
summary: summarizeAuditEntries(auditEntries),
|
|
88
|
+
recent: auditEntries.slice(0, 10),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -12,8 +12,9 @@ import {
|
|
|
12
12
|
deriveGovernedRunNextActions,
|
|
13
13
|
deriveRuntimeBlockedGuidance,
|
|
14
14
|
} from '../blocked-state.js';
|
|
15
|
-
import { loadProjectContext } from '../config.js';
|
|
15
|
+
import { loadProjectContext, loadProjectState } from '../config.js';
|
|
16
16
|
import { getContinuityStatus } from '../continuity-status.js';
|
|
17
|
+
import { reconcileStaleTurns } from '../stale-turn-watchdog.js';
|
|
17
18
|
import { readRepoDecisions, summarizeRepoDecisions } from '../repo-decisions.js';
|
|
18
19
|
import { readAllDispatchProgress } from '../dispatch-progress.js';
|
|
19
20
|
|
|
@@ -136,10 +137,21 @@ function enrichGovernedState(agentxchainDir, state) {
|
|
|
136
137
|
return state;
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
// Use loadProjectState to get reconciled state (approval-pause repair,
|
|
141
|
+
// budget reconciliation, recovery-action reconciliation applied and
|
|
142
|
+
// persisted to disk). Then apply stale-turn reconciliation so recovery
|
|
143
|
+
// and next-action surfaces reflect the post-watchdog truth — matching
|
|
144
|
+
// the same ordering used by the CLI `status` command.
|
|
145
|
+
let reconciledState = loadProjectState(workspacePath, context.config) || state;
|
|
146
|
+
const staleResult = reconcileStaleTurns(workspacePath, reconciledState, context.config);
|
|
147
|
+
if (staleResult.changed) {
|
|
148
|
+
reconciledState = staleResult.state;
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
return {
|
|
140
|
-
...
|
|
141
|
-
runtime_guidance: deriveRuntimeBlockedGuidance(
|
|
142
|
-
next_actions: deriveGovernedRunNextActions(
|
|
152
|
+
...reconciledState,
|
|
153
|
+
runtime_guidance: deriveRuntimeBlockedGuidance(reconciledState, context.config),
|
|
154
|
+
next_actions: deriveGovernedRunNextActions(reconciledState, context.config),
|
|
143
155
|
dispatch_progress: readAllDispatchProgress(workspacePath),
|
|
144
156
|
};
|
|
145
157
|
}
|
|
@@ -55,7 +55,7 @@ const RESERVED_PATHS = [
|
|
|
55
55
|
* Write a dispatch bundle for the currently assigned turn.
|
|
56
56
|
*
|
|
57
57
|
* @param {string} root - project root directory
|
|
58
|
-
* @param {object} state - current governed state (must
|
|
58
|
+
* @param {object} state - current governed state (must expose an active turn via active_turns; current_turn is a non-enumerable compatibility alias re-attached on load, not a persisted schema field)
|
|
59
59
|
* @param {object} config - normalized config
|
|
60
60
|
* @param {object} [opts]
|
|
61
61
|
* @param {string} [opts.turnId]
|
|
@@ -71,9 +71,10 @@ export function createDispatchProgressTracker(root, turn, options = {}) {
|
|
|
71
71
|
runtime_id: turn.runtime_id || null,
|
|
72
72
|
adapter_type,
|
|
73
73
|
started_at: null,
|
|
74
|
+
first_output_at: null,
|
|
74
75
|
last_activity_at: null,
|
|
75
|
-
activity_type: '
|
|
76
|
-
activity_summary: '
|
|
76
|
+
activity_type: 'starting',
|
|
77
|
+
activity_summary: 'Waiting for first output',
|
|
77
78
|
output_lines: 0,
|
|
78
79
|
stderr_lines: 0,
|
|
79
80
|
silent_since: null,
|
|
@@ -123,7 +124,7 @@ export function createDispatchProgressTracker(root, turn, options = {}) {
|
|
|
123
124
|
const now = new Date().toISOString();
|
|
124
125
|
state.started_at = now;
|
|
125
126
|
state.last_activity_at = now;
|
|
126
|
-
state.activity_type = '
|
|
127
|
+
state.activity_type = 'starting';
|
|
127
128
|
state.activity_summary = 'Subprocess started';
|
|
128
129
|
dirty = true;
|
|
129
130
|
writeProgress();
|
|
@@ -137,6 +138,7 @@ export function createDispatchProgressTracker(root, turn, options = {}) {
|
|
|
137
138
|
const now = new Date().toISOString();
|
|
138
139
|
const wasSilent = state.activity_type === 'silent';
|
|
139
140
|
state.last_activity_at = now;
|
|
141
|
+
state.first_output_at = state.first_output_at || now;
|
|
140
142
|
state.activity_type = 'output';
|
|
141
143
|
state.silent_since = null;
|
|
142
144
|
if (stream === 'stderr') {
|