agents-relay 2.0.149 → 2.0.151

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.
@@ -3,19 +3,36 @@ import { GraphRuntime } from './graph-v2.js';
3
3
  import { dispatchWorkflowGraphSync } from './dispatcher.js';
4
4
  const source = readFileSync(new URL('../../workflow/projection-conflict.mmd', import.meta.url), 'utf8');
5
5
  const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
6
+ const guards = {
7
+ deterministicDefinitionConflict: boolGuard('deterministicDefinitionConflict'),
8
+ 'projection.recoveryInProgress': boolGuard('recoveryInProgress'),
9
+ 'projection.recoveryEligible': (facts) => {
10
+ const values = facts.durableFacts;
11
+ return values.jobState === 'COMPLETED' && values.mergeSucceeded === true
12
+ && values.projectionStrategy === 'target_ref_push' && values.divergenceConfirmed === true
13
+ && values.remoteShaAvailable === true
14
+ && values.sameIdempotencyKey !== true && values.recoveryInProgress !== true
15
+ && Number(values.attempts ?? 0) < Number(values.maxAttempts ?? 0);
16
+ },
17
+ 'projection.recoveryExhausted': (facts) => {
18
+ const values = facts.durableFacts;
19
+ return values.jobState === 'COMPLETED' && values.divergenceConfirmed === true && values.remoteShaAvailable === true
20
+ && values.sameIdempotencyKey !== true && values.recoveryInProgress !== true
21
+ && Number(values.attempts ?? 0) >= Number(values.maxAttempts ?? 0);
22
+ },
23
+ 'projection.retryAvailable': (facts) => facts.durableFacts.taskRetryAvailable === true
24
+ && Number(facts.durableFacts.attempts ?? 0) < Number(facts.durableFacts.maxAttempts ?? 0),
25
+ 'projection.repairReviewed': (facts) => facts.durableFacts.repairTaskState === 'SUCCEEDED' && facts.durableFacts.reviewOutcome === 'approved',
26
+ 'projection.repairFailed': (facts) => facts.durableFacts.repairTaskState === 'FAILED' || facts.durableFacts.repairTaskState === 'BLOCKED'
27
+ || facts.durableFacts.reviewOutcome === 'blocked' || facts.durableFacts.reviewTaskState === 'FAILED',
28
+ };
6
29
  export const projectionConflictV2 = new GraphRuntime(source, {
7
- states: new Set(['classify', 'retry', 'conflict']),
8
- guards: { deterministicDefinitionConflict: boolGuard('deterministicDefinitionConflict') },
9
- actions: {},
10
- events: { 'projection.failed': { id: 'projection.failed' } },
30
+ states: new Set(['classify', 'repairing', 'retry', 'conflict', 'escalated', 'projected']),
31
+ guards,
32
+ actions: Object.fromEntries(['projection.createRecoveryTask', 'effect.projectLocalMerge', 'projection.escalate'].map(id => [id, { id }])),
33
+ events: Object.fromEntries(['projection.failed', 'projection.delivery_failed', 'projection.delivered', 'reconcile'].map(id => [id, { id }])),
11
34
  });
12
- /**
13
- * Core projection-failure disposition policy.
14
- *
15
- * A deterministic same-id/different-definition conflict stops consuming retry budget;
16
- * any other failure keeps the normal backoff/retry path. This branch is owned by the
17
- * canonical Mermaid graph, not by a procedural `if` in the projection writer.
18
- */
35
+ /** The decision is graph-owned; callers supply SQLite and inspected Git facts. */
19
36
  export function decideProjectionFailure(facts) {
20
37
  return dispatchWorkflowGraphSync(projectionConflictV2, {
21
38
  initialState: 'classify',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.149",
3
+ "version": "2.0.151",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,13 +20,14 @@
20
20
  "test:fast": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=2 test/workflow-lifecycle.test.js test/workflow-v2-lifecycle-parity.test.js test/workflow-effects.test.js test/relay-store.test.js test/local-authority.test.js test/local-jobid-routing.test.js test/local-sqlite-jobid.test.js test/no-github.test.js test/scheduled-jobs.test.js test/reconcile-coordinator.test.js test/service-publication.test.js test/github-webhook.test.js",
21
21
  "test:full": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=1 test/*.test.js && npm run test:verification:l3",
22
22
  "test:verification:l1": "npm run build && node --test test/verification/harness.test.mjs test/verification/contract.test.mjs test/verification/graph-inventory.test.mjs test/verification/dashboard-drilldown.test.mjs test/verification/evidence-matrix.test.mjs",
23
- "test:verification:l3": "npm run build && node --test test/verification/harness.test.mjs test/verification/l3-sqlite.test.mjs test/verification/l3-ingress.test.mjs test/verification/l3-replay.test.mjs test/verification/objective-authority.test.mjs test/verification/l3-convergence.test.mjs test/verification/l3-review-convergence.test.mjs test/verification/l3-human-escalation.test.mjs test/verification/l3-escalation-notifier.test.mjs test/verification/l3-unknown-effect.test.mjs test/verification/l3-cli-actions.test.mjs test/verification/l3-planner-round.test.mjs test/verification/full-recovery-gates.test.mjs test/verification/production-wiring.test.mjs",
23
+ "test:verification:l3": "npm run build && node --test --test-concurrency=1 test/verification/harness.test.mjs test/verification/l3-sqlite.test.mjs test/verification/l3-ingress.test.mjs test/verification/l3-replay.test.mjs test/verification/objective-authority.test.mjs test/verification/l3-convergence.test.mjs test/verification/l3-review-convergence.test.mjs test/verification/l3-human-escalation.test.mjs test/verification/l3-escalation-notifier.test.mjs test/verification/l3-unknown-effect.test.mjs test/verification/l3-cli-actions.test.mjs test/verification/l3-planner-round.test.mjs test/verification/full-recovery-gates.test.mjs test/verification/production-wiring.test.mjs test/verification/l3-full-path.test.mjs test/verification/l3-projection-recovery.test.mjs",
24
24
  "test:verification": "npm run test:verification:l1 && npm run test:verification:l2 && npm run test:verification:l3 && npm run test:verification:cross-a && node --test test/workflow-production-outbox.test.js && npm run test:verification:matrix",
25
25
  "e2e": "node e2e/runner/run.mjs",
26
26
  "test:verification:cross-a": "npm run build && RELAY_A_DIST=$PWD/dist node --test test/verification/action-ledger-cross-branch.test.mjs test/verification/outbox-cross-a.test.mjs",
27
27
  "test:verification:matrix": "node test/verification/recovery-report.mjs /tmp/agents-relay-job481-evidence",
28
28
  "test:verification:l2": "npm run build && node --test test/verification/l2/*.test.mjs",
29
- "test:verification:l2:live": "node test/verification/l2/live-provider.mjs"
29
+ "test:verification:l2:live": "node test/verification/l2/live-provider.mjs",
30
+ "test:l2:agents": "npm run build && node test/l2-agent-prompts/evaluate.mjs"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@types/node": "^22.10.2",
@@ -54,7 +54,7 @@ The event must carry the exact durable `job_id` and `task_id`. Progress events a
54
54
  Agents Relay maps those terminal events to durable state:
55
55
  - `task.completed` → `SUCCEEDED`
56
56
  - `task.failed` → `FAILED`
57
- Relay may authoritatively move a Task to `BLOCKED` for an external condition and may resume it only through an authorized unblock operation. A human response resumes the blocked Troubleshooter, not the failed original Task.
57
+ Relay may authoritatively move a Task to `BLOCKED` for an external condition and may resume it only through an authorized unblock operation. A human response is an append-only Task Comment and resumes the blocked Troubleshooter, not the failed original Task. Task input remains stable; recovery agents receive current authoritative state and relevant comments as execution context.
58
58
 
59
59
  A model-backed task must not launch without an event bus. If no terminal event arrives before the task timeout, Relay records an explicit timeout failure. A Codex process exit or ChatGPT submission receipt alone never changes the task to a terminal state.
60
60
 
@@ -17,7 +17,7 @@ Read and follow the shared Task / Job contract and orchestrator P0/self-job work
17
17
 
18
18
  ## Recovery loop
19
19
 
20
- 1. Retrieve the durable original Job and failed/blocked Task from GitHub.
20
+ 1. Retrieve the durable original Job and Task from the authoritative Relay store with `agents-relay task context`; GitHub is only an optional projection and is never lifecycle authority.
21
21
  2. **Mandatory before diagnosis:** retrieve the failed Task's task-scoped lifecycle event history. When the local Relay dashboard is available, use `GET http://127.0.0.1:8787/api/events?job=<job_id>&task=<task_id>&limit=200`. Inspect event order, type/status, source, data/phase, execution IDs, duplicates, missing expected lifecycle events, and late events. Then inspect task error/failureReason, worker selection, routing, attempt count, task input/output contract, PR state, and relevant runtime/service logs.
22
22
  3. Classify the blocker:
23
23
  - transient execution/delivery/lease failure;
@@ -32,8 +32,8 @@ Read and follow the shared Task / Job contract and orchestrator P0/self-job work
32
32
  - For task-local failure, make the smallest durable repair in the original Job/PR when that is within scope.
33
33
  - For a shared platform/runtime defect, first create or reuse the owning blocker P0/self-job. If it can be completed inside this recovery execution, fix/verify it and retry the original Task. If it cannot be completed now, register the original Task with `agents-relay blocker add` on that blocker Job and finish this Troubleshooter task; do not stay running while waiting for another Job.
34
34
  - For a suspected genuine human/external blocker, do not publish `task.blocked` and do not branch on Neo's result yourself. Invoke `agents-relay escalation` with the original intent, blocker, and relevant context. The canonical graph owns the `resolved`, `failed`, and `needs_user` outcomes. When it returns `needs_user`, call Relay's `task escalate` operation with the concrete required action, resolution condition, and diagnostic context. Relay records the escalation and blocks this Troubleshooter; no worker remains running while waiting.
35
- 7. For an immediate repair, only after the blocker is removed run `agents-relay task recover` for the original Task with this Troubleshooter's recovery Task ID and reconcile its Job. An authorized human response resumes this same Troubleshooter with its diagnostic context; verify the action before requesting original-task recovery. For deferred repair, rely on the durable blocker relation: merge-mode blockers wake matching dependents after merge; release/deploy/runtime blockers use `--resolution verified` and `agents-relay blocker resolve` only after verification.
36
- 8. Verify durable evidence that the original Task moved: at minimum QUEUED after retry, preferably a new execution / task.started / observable progress.
35
+ 7. For an immediate repair, only after the blocker is removed request `agents-relay task recover` with this Troubleshooter's recovery Task ID, then reconcile the original Job. Verify the SQLite postcondition: original Task state QUEUED, expected resolution and recovery correlation. An authorized human response is a Task Comment, not a rewrite of Task input. Retain historical conditions as provenance, independently verify the original failing execution context, and do not submit an identical escalation with no new evidence. Deferred repair uses the durable blocker relation: merge-mode blockers wake dependents after merge; release/deploy/runtime blockers require `--resolution verified` and `agents-relay blocker resolve` only after verification.
36
+ 8. Verify durable evidence the original Task moved: at minimum QUEUED after retry, preferably a new execution / task.started / observable progress.
37
37
  9. Publish this Troubleshooter Task's terminal event only after the recovery outcome is durable.
38
38
 
39
39
  ## Rules
@@ -25,7 +25,7 @@ A Reviewer must **not** publish unstructured `task.completed`: missing verdict o
25
25
 
26
26
  Only a real user action merits escalation. Troubleshooter invokes the Relay escalation operation, specifying the required action, resolution condition, and diagnostic context. Relay durably records the human action and blocks **the Troubleshooter Task**, leaving the original failed Task `FAILED`. This is a Relay state transition, not a third agent terminal event.
27
27
 
28
- When the human responds, the **Troubleshooter Task** is authoritatively unblocked and resumed with its prior context plus the response. Troubleshooter independently verifies resolution, requests an authorized original Task recovery, and only then publishes its own `task.completed`. If unresolved, it continues diagnosis or escalates again; human assertion alone is not technical verification.
28
+ When the human responds, Relay records the response as an append-only Task Comment and authoritatively unblocks the **Troubleshooter Task**. The stable Task input is unchanged. Troubleshooter receives current authoritative state and relevant comments, independently verifies the response against the original failing execution context, requests an authorized original Task recovery, and only then publishes its own `task.completed`. A repeated identical escalation without new evidence is idempotent. If unresolved, it continues diagnosis or escalates again; human assertion alone is not technical verification.
29
29
 
30
30
  See [task-recovery-flows.md](task-recovery-flows.md) for unified architecture, overall flow, failure recovery, and post-blocked resumption.
31
31
 
@@ -32,6 +32,13 @@ A later Planner round must inspect the existing semantic tree and attach review,
32
32
 
33
33
  A Task input records **what is known and what has already been decided before execution**. A Task result records **what execution discovered and what was actually done**.
34
34
 
35
+ After creation, keep `Task.input` stable as the objective and initial evidence.
36
+ Responses, resolution changes, and escalation history belong in append-only
37
+ Task Comments. SQLite atomically records typed comments with authoritative
38
+ resolution/escalation mutations; PR-bound Jobs project those comments through
39
+ the durable idempotent projection outbox. Comments supplement current state and
40
+ never replace `Task.resolution` or `Task.escalation`.
41
+
35
42
  Task input should include, as applicable:
36
43
 
37
44
  - goal and observed behavior;
@@ -1,6 +1,6 @@
1
1
  # Task Failure Recovery and Human Escalation — Agreed Target Design
2
2
 
3
- **Status:** Design agreement; not an assertion of current implementation. This document describes two flows. Existing runtime, workflow graphs, and agent instructions may need changes to conform.
3
+ **Status:** Current recovery contract. SQLite Task snapshots retain append-only typed comments alongside authoritative state.
4
4
 
5
5
  ## Principles and ownership
6
6
 
@@ -133,3 +133,20 @@ flowchart TD
133
133
  - Align escalation graph, CLI, reconciler, and unblock commands so human intervention blocks **Troubleshooter**, not the original failed Task; preserve the original failure and correlated recovery context.
134
134
  - Ensure the blocked Troubleshooter can resume after an authorized human unblock rather than being treated as a finished terminal execution.
135
135
  - Add integrated checks: technical retry, blocker-Job handoff, human request/block, human unblock/resume, failed verification/re-escalation, stale/duplicate responses, and correct original-Task retry.
136
+
137
+ ## Durable recovery history
138
+
139
+ `Task.input` remains the stable objective and original evidence. `Task.resolution` and
140
+ `Task.escalation` remain the current authoritative states; comments are append-only
141
+ history, not a second state authority. SQLite mutation transactions compare the
142
+ before/after authoritative values and append typed comments with Job, Task,
143
+ execution, transition, and idempotency identity in the same commit. Do not add a
144
+ `resolutionHistory` field.
145
+
146
+ Human responses are comments. Resuming a Troubleshooter must not rewrite its
147
+ input or append a stale “Required resolution condition” as new objective text.
148
+ The condition remains in escalation provenance. Reviewer and Troubleshooter
149
+ prompts receive current authoritative state and relevant task comments as runtime
150
+ context; Troubleshooters independently verify a response against evidence from
151
+ the original failing execution. Repeating an identical escalation with unchanged
152
+ evidence is idempotent.
@@ -1,15 +1,66 @@
1
1
  ---
2
2
  title: Projection Conflict
3
- description: Detects and resolves conflicting workflow state projections against authoritative facts.
3
+ description: Recover local-main and origin-main divergence after a completed Job's GitHub projection fails.
4
+ name: projection-conflict
5
+ authority: SQLite Relay Job snapshot
6
+ trigger: projection.delivery_failed
7
+ idempotency: job-id plus observed origin/main SHA
8
+ actions:
9
+ - projection.createRecoveryTask
10
+ - projection.escalate
11
+ effects:
12
+ - effect.projectLocalMerge
13
+ guards:
14
+ - deterministicDefinitionConflict
15
+ - projection.recoveryEligible
16
+ - projection.recoveryExhausted
17
+ - projection.recoveryInProgress
18
+ - projection.retryAvailable
19
+ - projection.repairReviewed
20
+ - projection.repairFailed
4
21
  ---
5
22
  flowchart LR
6
23
  %% relay: initial classify
7
24
  classify["state: classify"]
8
- retry["state: retry"]
9
25
  conflict["state: conflict"]
26
+ repairing["state: repairing"]
27
+ retry["state: retry"]
28
+ escalated["state: escalated"]
29
+ projected["state: projected"]
10
30
 
31
+ eligible["guard: projection.recoveryEligible"]
32
+ inProgress["guard: projection.recoveryInProgress"]
11
33
  deterministic["guard: deterministicDefinitionConflict"]
34
+ exhausted["guard: projection.recoveryExhausted"]
35
+ retryAvailable["guard: projection.retryAvailable"]
36
+ reviewed["guard: projection.repairReviewed"]
37
+ repairFailed["guard: projection.repairFailed"]
38
+ createTask["action: projection.createRecoveryTask"]
39
+ retryProjection["action: effect.projectLocalMerge"]
40
+ escalate["action: projection.escalate"]
12
41
 
42
+ classify -->|on: projection.delivery_failed| inProgress
43
+ inProgress -->|true| classify
44
+ inProgress -->|false| eligible
13
45
  classify -->|on: projection.failed| deterministic
14
46
  deterministic -->|true| conflict
15
47
  deterministic -->|false| retry
48
+ eligible -->|true| createTask
49
+ eligible -->|false| exhausted
50
+ exhausted -->|true| escalate
51
+ exhausted -->|false| classify
52
+ createTask --> repairing
53
+
54
+ repairing -->|on: reconcile| reviewed
55
+ retry -->|on: reconcile| retryAvailable
56
+ reviewed -->|true| retryAvailable
57
+ retryAvailable -->|true| retryProjection
58
+ retryAvailable -->|false| escalate
59
+ reviewed -->|false| repairFailed
60
+ repairFailed -->|true| escalate
61
+ repairFailed -->|false| repairing
62
+ retryProjection --> retry
63
+ escalate --> escalated
64
+
65
+ retry -->|on: projection.delivered| projected
66
+ retry -->|on: projection.delivery_failed| eligible