agents-relay 2.0.80 → 2.0.81
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.
|
@@ -325,7 +325,10 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
325
325
|
return { applied: false, reconcile: false, reason: event.type + '.stale' };
|
|
326
326
|
// Planner-created reviewer tasks predate the final-review Job gate. Bind
|
|
327
327
|
// their first trustworthy review event to PENDING before dispatching it.
|
|
328
|
-
const
|
|
328
|
+
const freshAfterStale = job.review?.outcome === 'stale'
|
|
329
|
+
&& job.review.headSha !== headSha && Boolean(task.planningDecisionId);
|
|
330
|
+
const initialReviewOutcome = (!job.review || freshAfterStale) && task.planningDecisionId
|
|
331
|
+
? 'pending' : job.review?.outcome;
|
|
329
332
|
const taskDecision = this.store.mutateJob
|
|
330
333
|
? dispatchTaskLifecycleSync(structuredClone(task), event.type)
|
|
331
334
|
: tryTransitionTaskEvent(task, event.type);
|
|
@@ -395,7 +398,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
395
398
|
Object.assign(freshTask, candidate);
|
|
396
399
|
// Bind the first Planner review only after the generation fence has
|
|
397
400
|
// checked the unmodified outer snapshot and authoritative SQLite state.
|
|
398
|
-
if (!current.review && freshTask.planningDecisionId) {
|
|
401
|
+
if ((!current.review || (current.review.outcome === 'stale' && current.review.headSha !== headSha)) && freshTask.planningDecisionId) {
|
|
399
402
|
current.review = { outcome: 'pending', headSha, taskId: freshTask.id, evidence: null, updatedAt: reviewUpdatedAt };
|
|
400
403
|
}
|
|
401
404
|
freshTask.error = null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeReviewerTerminalEvent } from './review-terminal-normalization.js';
|
|
1
2
|
import { applyJobEffectOutcomes, applyTaskEffectOutcomes } from './workflow-effect-failures.js';
|
|
2
3
|
import { classifyWorkerRejection, reroutableWorkerRejections } from './worker-rejection.js';
|
|
3
4
|
import { applyTaskRetryAction } from './task-actions.js';
|
|
@@ -89,6 +90,12 @@ export class ReconcilerRuntimeBase {
|
|
|
89
90
|
return activeOwner.applyEvent(event);
|
|
90
91
|
if (event.type === 'task.started')
|
|
91
92
|
return this.markWorkerStarted(event);
|
|
93
|
+
if (event.type === 'task.completed') {
|
|
94
|
+
const task = (await this.store.load(event.job_id)).tasks.find(item => item.id === event.task_id);
|
|
95
|
+
const normalized = task?.agentName === 'reviewer' ? normalizeReviewerTerminalEvent(event) : null;
|
|
96
|
+
if (normalized)
|
|
97
|
+
return this.applyReviewEvent(normalized);
|
|
98
|
+
}
|
|
92
99
|
if (['task.completed', 'task.failed', 'task.blocked', 'task.cancelled'].includes(event.type)) {
|
|
93
100
|
const result = await this.applyTerminalEvent(event);
|
|
94
101
|
if (event.type !== 'task.failed' || event.source?.watchdog !== true || event.data?.workflowEvent !== 'watchdog.failed' || result.reconcile)
|
|
@@ -5,6 +5,14 @@ export function reviewGenerationMatches(current, original, task, headSha) {
|
|
|
5
5
|
|| (current.headSha && current.headSha !== headSha)
|
|
6
6
|
|| fresh.executionId !== task.executionId)
|
|
7
7
|
return false;
|
|
8
|
+
// A fresh Planner reviewer for a new head replaces the old STALE review
|
|
9
|
+
// generation. Reject competing reviewers of that same head deterministically.
|
|
10
|
+
if (current.review?.outcome === 'stale' && original.review?.outcome === 'stale'
|
|
11
|
+
&& current.review.headSha !== headSha && task.planningDecisionId) {
|
|
12
|
+
const currentHeadReviewers = current.tasks.filter(item => item.agentName === 'reviewer'
|
|
13
|
+
&& item.reviewHeadSha === headSha && item.state !== 'CANCELLED');
|
|
14
|
+
return currentHeadReviewers.at(-1)?.id === task.id;
|
|
15
|
+
}
|
|
8
16
|
if (current.review)
|
|
9
17
|
return current.review.taskId === task.id && current.review.headSha === headSha
|
|
10
18
|
&& current.review.outcome === (original.review?.outcome ?? 'pending');
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function record(value) {
|
|
2
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
3
|
+
}
|
|
4
|
+
/** Translate an authenticated reviewer terminal verdict without losing canonical review evidence. */
|
|
5
|
+
export function normalizeReviewerTerminalEvent(event) {
|
|
6
|
+
if (event.type !== 'task.completed')
|
|
7
|
+
return null;
|
|
8
|
+
const data = record(event.data);
|
|
9
|
+
const raw = data.verdict;
|
|
10
|
+
const type = raw === 'approved' || raw === 'pass' ? 'review.approved'
|
|
11
|
+
: raw === 'changes-required' || raw === 'changes_required' ? 'review.changes_required'
|
|
12
|
+
: raw === 'blocked' ? 'review.blocked' : null;
|
|
13
|
+
if (!type)
|
|
14
|
+
return null;
|
|
15
|
+
const outcome = record(data.outcome);
|
|
16
|
+
const review = record(data.review);
|
|
17
|
+
// Match canonical review head resolution: outcome > review > top-level data.
|
|
18
|
+
const head = [outcome.head_sha, outcome.headSha, review.head_sha, review.headSha,
|
|
19
|
+
data.head_sha, data.headSha]
|
|
20
|
+
.find(value => typeof value === 'string' && value.length > 0);
|
|
21
|
+
const existingFindings = outcome.blocking_findings ?? outcome.blockingFindings;
|
|
22
|
+
const findings = existingFindings ?? (Array.isArray(data.findings) ? data.findings : undefined);
|
|
23
|
+
return { ...event, type, data: { ...data, outcome: {
|
|
24
|
+
...outcome, ...(head ? { head_sha: head } : {}),
|
|
25
|
+
...(findings !== undefined && existingFindings === undefined ? { blocking_findings: findings } : {}),
|
|
26
|
+
} } };
|
|
27
|
+
}
|