agents-relay 2.0.6 → 2.0.8

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/README.md CHANGED
@@ -4,6 +4,44 @@ Agents Relay is a generic TypeScript runtime for durable asynchronous agent jobs
4
4
 
5
5
  V2 separates **operational execution state** from that collaboration record. A local RelayStore may use SQLite/WAL for runtime snapshots, event idempotency and a durable GitHub projection outbox. ProjectedDurableStore bootstraps from the existing GitHub representation, commits Job/Plan/Task runtime changes locally, and lets GitHubProjectionWriter project them asynchronously. GitHub therefore remains the human-readable durable Job/Plan/Task/Review surface without being required for every high-frequency runtime transition.
6
6
 
7
+ ## V2 architecture
8
+
9
+ ```mermaid
10
+ flowchart LR
11
+ Human[Human / orchestrator] --> GH[GitHub PR + comments<br/>collaboration + audit surface]
12
+ GH --> Inbound[Typed inbound GitHub facts]
13
+ Worker[Workers / adapters] --> Events[Event Bus<br/>lifecycle + progress facts]
14
+ Inbound --> Coord[ReconcileCoordinator]
15
+ Events --> Coord
16
+ Coord --> Graphs[Executable Mermaid graphs<br/>workflow authority]
17
+ Graphs --> Runtime[TypeScript primitives<br/>facts + guards + effects]
18
+ Runtime --> Store[(SQLite/WAL RelayStore<br/>operational execution truth)]
19
+ Store --> Outbox[Projection outbox]
20
+ Outbox --> Writer[GitHubProjectionWriter]
21
+ Writer --> GH
22
+ Store --> Views[Dashboard / scheduler / recovery]
23
+ ```
24
+
25
+ RelayStore owns local operational execution facts; GitHub remains the human-readable collaboration and audit projection. Worker events are authoritative lifecycle inputs for model-backed tasks, but they are not a separate durable database. The graph engine owns workflow branching; TypeScript owns facts, persistence, locking/claims, I/O, and primitive guard/action implementations.
26
+
27
+ ### Workflow graph navigation
28
+
29
+ | Area | Canonical graph |
30
+ | --- | --- |
31
+ | Job lifecycle | [`workflow/job-lifecycle-v2.mmd`](workflow/job-lifecycle-v2.mmd) |
32
+ | Task lifecycle | [`workflow/task-lifecycle-v2.mmd`](workflow/task-lifecycle-v2.mmd) |
33
+ | Review lifecycle | [`workflow/review-lifecycle-v2.mmd`](workflow/review-lifecycle-v2.mmd) |
34
+ | Planner trigger | [`workflow/planner-trigger-v2.mmd`](workflow/planner-trigger-v2.mmd) |
35
+ | Planner orchestration | [`workflow/planner-orchestration-v2.mmd`](workflow/planner-orchestration-v2.mmd) |
36
+ | Blocker resolution | [`workflow/blocker-resolution-v2.mmd`](workflow/blocker-resolution-v2.mmd) |
37
+ | Authoritative unblock | [`workflow/authoritative-unblock-v2.mmd`](workflow/authoritative-unblock-v2.mmd) |
38
+ | Scheduled run | [`workflow/scheduled-run-v2.mmd`](workflow/scheduled-run-v2.mmd) |
39
+ | Schedule cycle | [`workflow/schedule-cycle-v2.mmd`](workflow/schedule-cycle-v2.mmd) |
40
+ | Troubleshooter | [`workflow/troubleshooter-v2.mmd`](workflow/troubleshooter-v2.mmd) |
41
+ | Related-task action policy | [`workflow/task-related-action-v2.mmd`](workflow/task-related-action-v2.mmd) |
42
+
43
+ Legacy [`workflow/job-lifecycle.mmd`](workflow/job-lifecycle.mmd) and [`workflow/task-lifecycle.mmd`](workflow/task-lifecycle.mmd) are compatibility/parity fixtures only. See [`docs/workflow-engine.md`](docs/workflow-engine.md) and [`docs/architecture-v2.md`](docs/architecture-v2.md).
44
+
7
45
  ## Quick start
8
46
 
9
47
  For GitHub-backed work, create or adopt the PR through Agents Relay itself. This guarantees the durable job marker exists before any managed task is submitted.
@@ -50,7 +88,7 @@ npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id chil
50
88
 
51
89
  Jobs separate durable lifecycle from derived runtime status. Durable lifecycle is `DRAFT | ACTIVE | COMPLETED | CANCELLED`: a newly created unscheduled Job is DRAFT, creating executable work activates it, and scheduled Jobs are ACTIVE because the scheduler is allowed to advance them. Runtime status (`IDLE`, `WAITING`, `QUEUED`, `RUNNING`, `BLOCKED`, `FAILED`, `REVIEWING`, `MERGE_PENDING`, `MERGED`, or `CANCELLED`) is computed from Tasks, Review, PR, and schedule facts and is never persisted as lifecycle truth.
52
90
 
53
- Lifecycle transition authority lives in the directly renderable v2 flowchart sources [`workflow/job-lifecycle-v2.mmd`](workflow/job-lifecycle-v2.mmd) and [`workflow/task-lifecycle-v2.mmd`](workflow/task-lifecycle-v2.mmd). The legacy `stateDiagram-v2` files remain compatibility fixtures only; see [`docs/workflow-engine.md`](docs/workflow-engine.md) for the executable Mermaid profile and runtime boundary.
91
+ Lifecycle transition authority lives in the directly renderable V2 flowchart sources linked in [Workflow graph navigation](#workflow-graph-navigation). The legacy `stateDiagram-v2` files remain compatibility fixtures only; see [`docs/workflow-engine.md`](docs/workflow-engine.md) for the executable Mermaid profile and runtime boundary.
54
92
 
55
93
  The canonical Mermaid Graph Engine is the authoritative durable workflow decision path. Job/Task lifecycle transitions plus Review, Planner, Blocker/Unblock, Schedule, and Troubleshooter orchestration branches are defined by the v2 workflow graphs and executed through the shared graph runtime. TypeScript supplies facts and primitive action/effect implementations; it must not duplicate those workflow branches as a second procedural state machine.
56
94
 
@@ -86,10 +124,7 @@ Task producers must follow `skills/agents-relay/contracts/task-job-contract.md`;
86
124
  Execution routing is code-driven: Relay builds complete executable worker+model routes from authoritative runtime/model/resource facts, removes only impossible routes, then calls the generic typed `decide(request, contract)` boundary once. Free-text routing guidance and task/resource/capability signals are soft decision facts; decision failure falls back deterministically to a usable Codex default route. See [Execution Router and typed decisions](docs/worker-model-routers.md).
87
125
  All task producers use the same story-size contract: `S`≈5 minutes, `M`≈15 minutes, and `L`≈45 minutes of normal-reasoning execution, with `M` as the default. `high` reasoning doubles that overall window. ChatGPT Browser Worker start-ack timeout is derived as one third of the effective overall timeout, clamped between 1 and 5 minutes. Orchestrators and planners should choose `storySize`, not raw timeout milliseconds; `--timeout` remains only as an explicit compatibility/debug override.
88
126
 
89
- Autonomous jobs can also be scheduled without a Relay database. The job marker
90
- stores a five-field cron expression, IANA timezone, next run, and missed-run /
91
- concurrency policy; GitHub issue labels provide the indexed discovery surface.
92
- A repository heartbeat invokes `tick` at 10-minute resolution. See
127
+ Autonomous jobs can also be scheduled. The Job marker stores a five-field cron expression, IANA timezone, next run, and missed-run/concurrency policy. The daemon evaluates the SQLite-backed managed Job view and validates durable schedule facts before claiming a due slot. A repository heartbeat invokes `tick` at 10-minute resolution. See
93
128
  [Scheduled autonomous jobs](docs/scheduled-autonomous-jobs.md) for the exact
94
129
  operational contract.
95
130
 
@@ -152,7 +187,7 @@ This small live suite covers two Temporary Chat submission gates: a normal text
152
187
  prompt and a prompt with an attachment. It does not exercise worker lifecycle
153
188
  events, durable-thread reuse, resume, or retry behavior.
154
189
 
155
- Open the dashboard on localhost port 8787. The marker format is intentionally public and append-friendly; the CLI uses GitHubStore with the authenticated gh client in operational mode.
190
+ Open the dashboard on localhost port 8787. The marker format is intentionally public and append-friendly. Daemon/workspace operation uses `ProjectedDurableStore` over SQLite RelayStore plus the authenticated GitHub projection adapter; GitHubStore is not the daemon operational state database.
156
191
 
157
192
  External product feedback uses the reusable `src/feedback.ts` contract. See [docs/feedback-intake.md](docs/feedback-intake.md) for the schema, attachment limits, privacy rules, materialization path, and provenance envelope for later Jobs/Tasks/PRs.
158
193
 
@@ -162,7 +197,7 @@ reconcile leases and launches ready tasks without waiting inside the runtime sch
162
197
 
163
198
  For automation, Agents Relay can use GitHub App installation authentication via `AGENTS_RELAY_GITHUB_APP_ID`, `AGENTS_RELAY_GITHUB_APP_INSTALLATION_ID`, and `AGENTS_RELAY_GITHUB_APP_PRIVATE_KEY_FILE` (or matching CLI flags). Webhook mode is local-first: targeted webhook reconciliation is primary, dashboard auto-refresh reads the disposable in-process view restored from SQLite RelayStore state, and the broad watchdog drops to a 30-minute fallback. No separate persisted dashboard Job cache is maintained. See [docs/github-app-rate-limit.md](docs/github-app-rate-limit.md).
164
199
 
165
- V1 assumes one active runner/daemon per job. Use a distributed atomic lease before running multiple reconcilers. Do not put secrets, credentials, private prompts, or large private payloads in the PR-body Job marker or PR-comment Task markers; store only summaries and artifact references.
200
+ Run one logical reconciliation authority per Job. `ReconcileCoordinator` serializes in-process work; use a distributed atomic lease before multiple independent reconcilers own the same Job. Do not put secrets, credentials, private prompts, or large private payloads in the PR-body Job marker or PR-comment Task markers; store only summaries and artifact references.
166
201
 
167
202
  Model-backed tasks are routed by Relay before launch. Normal task producers omit adapter/provider/model; explicit routing flags remain compatibility/debug constraints. See [docs/architecture.md](docs/architecture.md) and [skills/agents-relay/SKILL.md](skills/agents-relay/SKILL.md).
168
203
 
@@ -182,7 +217,7 @@ Managed tasks are descriptive work for agents, not shell commands. Use `codex` o
182
217
 
183
218
  Every model-backed task declares a durable output with `--output task-pr` or `--output file --output-path PATH`. Agents Relay bootstraps workers with the managed PR URL, durable task-comment URL, job/task identity, project, output contract, and mandatory terminal-event contract; workers retrieve the task intent from GitHub rather than receiving a duplicate copy of the stored task prompt.
184
219
 
185
- For Codex and ChatGPT, **events are authoritative task state**. Event publication uses the canonical Neo `events-bus` surface (`events__publish` for sandboxed/hosted workers, or `NEO_EVENTS_EMIT` for direct local workers); Agents Relay only subscribes and reconciles. Relay publishes `task.process.launched` only after successful adapter launch; `task.started` is worker-owned and must be a new, correlated start acknowledgement. Codex publishes `task.process.exited` when its synchronous runtime exits, while ChatGPT publishes `task.process.async_exited` after its submission runtime exits; neither substitutes for `task.completed`, `task.failed`, `task.blocked`, or `task.cancelled`. The ChatGPT worker-owned tab follows the configured close policy: `after-start` waits for the correlated start acknowledgement, `never` keeps it open for debug, and `after-terminal` waits for a new exact-task terminal event; start-ack failure closes only the owned tab after 60 seconds. The executing agent must publish exactly one correlated terminal event. Relay persists the matching durable state only from that terminal event. Model-backed tasks require an event bus and time out explicitly if no terminal event arrives.
220
+ For Codex and ChatGPT, **correlated worker events are authoritative lifecycle inputs**, not a separate durable store. Event publication uses the canonical Neo `events-bus` surface (`events__publish` for sandboxed/hosted workers, or `NEO_EVENTS_EMIT` for direct local workers); Agents Relay subscribes, validates correlation, applies graph transitions, persists results in RelayStore, and projects them to GitHub. Relay publishes `task.process.launched` only after successful adapter launch; `task.started` is worker-owned and must be a new, correlated start acknowledgement. Codex publishes `task.process.exited` when its synchronous runtime exits, while ChatGPT publishes `task.process.async_exited` after its submission runtime exits; neither substitutes for `task.completed`, `task.failed`, `task.blocked`, or `task.cancelled`. The ChatGPT worker-owned tab follows the configured close policy: `after-start` waits for the correlated start acknowledgement, `never` keeps it open for debug, and `after-terminal` waits for a new exact-task terminal event; start-ack failure closes only the owned tab after 60 seconds. The executing agent must publish exactly one correlated terminal event. Relay persists the matching durable state only from that terminal event. Model-backed tasks require an event bus and time out explicitly if no terminal event arrives.
186
221
 
187
222
  The ChatGPT adapter uses the `chatgpt-browser-worker` path selected by the shared execution-context resolver; Relay does not bundle its own copy. The resolver applies project-local > Neo shared > global `~/.agents` precedence at execution time, so durable tasks store names/intent rather than stale absolute skill paths. It opens a fresh Temporary Chat tab, uses the account defaults, submits the prompt, verifies acceptance, and applies `--chatgpt-tab-close-policy` (or `AGENTS_RELAY_CHATGPT_TAB_CLOSE_POLICY`): `after-start` by default, `never` for test/debug, or `after-terminal` after a new exact-task terminal event. It never closes a user tab and does not poll for the assistant response, resume/reopen a thread, or use conversation text as the result channel. Any observed ChatGPT thread ID is diagnostic only.
188
223
 
@@ -91,7 +91,20 @@ export const dashboardHtml = `<!doctype html>
91
91
  .event-meta { display:flex; flex-wrap:wrap; gap:.4rem; color:var(--muted); font-size:.68rem; }
92
92
  .event-kind { color:var(--accent); font-weight:800; }
93
93
  .event-message { margin:.12rem 0 0; overflow-wrap:anywhere; }
94
- #usage-view,#rules-view,#shortcuts-view,#worker-rules-view { grid-column:1 / -1; }
94
+ #usage-view,#graphs-view,#rules-view,#shortcuts-view,#worker-rules-view { grid-column:1 / -1; }
95
+ .graphs-browser { display:grid; grid-template-columns:minmax(190px,260px) minmax(0,1fr); gap:.75rem; min-height:60vh; }
96
+ .graph-list { display:grid; align-content:start; gap:.35rem; }
97
+ .graph-button { width:100%; display:grid; gap:.08rem; padding:.48rem .55rem; border:1px solid var(--border); border-radius:.4rem; background:var(--surface2); color:var(--text); text-align:left; font:inherit; cursor:pointer; }
98
+ .graph-button[aria-current="true"] { border-color:var(--accent); background:var(--accent2); }
99
+ .graph-button strong { font-size:.78rem; }
100
+ .graph-button span { color:var(--muted); font:.66rem ui-monospace,SFMono-Regular,Menlo,monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
101
+ .graph-preview { min-width:0; display:grid; align-content:start; gap:.55rem; }
102
+ .graph-preview-head { display:flex; justify-content:space-between; gap:.7rem; align-items:baseline; flex-wrap:wrap; }
103
+ .graph-preview-head h3 { margin:0; font-size:.95rem; }
104
+ .graph-canvas { min-height:24rem; display:grid; place-items:center; overflow:auto; padding:1rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); }
105
+ .graph-canvas svg { max-width:none; height:auto; }
106
+ .graph-source { margin:0; max-height:22rem; overflow:auto; padding:.7rem; border:1px solid var(--border); border-radius:.4rem; background:var(--surface2); color:var(--muted); font:.72rem/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; white-space:pre; }
107
+ .graph-source-details summary { cursor:pointer; color:var(--muted); font-size:.74rem; font-weight:700; }
95
108
  .rules-editor { display:grid; gap:.65rem; }
96
109
  .rules-help { margin:0; color:var(--muted); font-size:.8rem; }
97
110
  .rules-textarea { width:100%; min-height:55vh; resize:vertical; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); color:var(--text); padding:.8rem; font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace; }
@@ -135,6 +148,9 @@ export const dashboardHtml = `<!doctype html>
135
148
  #jobs-workspace .panel-body { max-height:none; }
136
149
  #usage-view .panel-head { padding:.6rem .7rem .35rem; }
137
150
  #usage-view .panel-head .subtle { display:none; }
151
+ .graphs-browser { grid-template-columns:1fr; min-height:0; }
152
+ .graph-list { grid-template-columns:repeat(2,minmax(0,1fr)); }
153
+ .graph-canvas { min-height:18rem; }
138
154
  #usage-view .panel-body { padding:.45rem .5rem .65rem; max-height:none; }
139
155
  #usage-view .usage-providers { gap:.45rem; }
140
156
  #usage-view .usage-provider {
@@ -170,6 +186,7 @@ export const dashboardHtml = `<!doctype html>
170
186
  <nav role="tablist" aria-label="Console views">
171
187
  <button id="jobs-tab" type="button" role="tab" aria-selected="true" aria-controls="jobs-view">Jobs</button>
172
188
  <button id="usage-tab" type="button" role="tab" aria-selected="false" aria-controls="usage-view">Usage</button>
189
+ <button id="graphs-tab" type="button" role="tab" aria-selected="false" aria-controls="graphs-view">Graphs</button>
173
190
  <button id="rules-tab" type="button" role="tab" aria-selected="false" aria-controls="rules-view">Execution Rules</button>
174
191
  <button id="shortcuts-tab" type="button" role="tab" aria-selected="false" aria-controls="shortcuts-view">Shortcuts</button>
175
192
  </nav>
@@ -201,6 +218,21 @@ export const dashboardHtml = `<!doctype html>
201
218
  <div class="panel-body" id="usage-root" aria-live="polite"><div class="skeleton"></div></div>
202
219
  </div>
203
220
  </section>
221
+ <section id="graphs-view" role="tabpanel" aria-labelledby="graphs-tab" hidden>
222
+ <div class="panel">
223
+ <div class="panel-head"><h2>Workflow Graphs</h2><span id="graphs-meta" class="subtle"></span></div>
224
+ <div class="panel-body">
225
+ <div class="graphs-browser">
226
+ <div id="graph-list" class="graph-list" aria-label="Workflow graphs"></div>
227
+ <div class="graph-preview">
228
+ <div class="graph-preview-head"><h3 id="graph-title">Select a graph</h3><span id="graph-path" class="subtle"></span></div>
229
+ <div id="graph-canvas" class="graph-canvas"><p class="empty">Select a graph to inspect it.</p></div>
230
+ <details class="graph-source-details"><summary>Mermaid source</summary><pre id="graph-source" class="graph-source"></pre></details>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ </section>
204
236
  <section id="rules-view" role="tabpanel" aria-labelledby="rules-tab" hidden>
205
237
  <div class="panel">
206
238
  <div class="panel-head"><h2>Execution Router Rules</h2><span id="rules-meta" class="subtle"></span></div>
@@ -226,6 +258,7 @@ export const dashboardHtml = `<!doctype html>
226
258
  </div>
227
259
  </section>
228
260
  </main>
261
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
229
262
  <script>
230
263
  'use strict';
231
264
  // Compatibility contracts retained for dashboard tests:
@@ -244,10 +277,12 @@ const MAX_EVENTS=60;
244
277
  const DASHBOARD_VERSION=${JSON.stringify(dashboardVersion)};
245
278
  const jobsTab=document.querySelector('#jobs-tab');
246
279
  const usageTab=document.querySelector('#usage-tab');
280
+ const graphsTab=document.querySelector('#graphs-tab');
247
281
  const rulesTab=document.querySelector('#rules-tab');
248
282
  const shortcutsTab=document.querySelector('#shortcuts-tab');
249
283
  const jobsView=document.querySelector('#jobs-view');
250
284
  const usageView=document.querySelector('#usage-view');
285
+ const graphsView=document.querySelector('#graphs-view');
251
286
  const rulesView=document.querySelector('#rules-view');
252
287
  const shortcutsView=document.querySelector('#shortcuts-view');
253
288
  const jobsWorkspace=document.querySelector('#jobs-workspace');
@@ -271,6 +306,12 @@ const connection=document.querySelector('#connection');
271
306
  const selectedJobTitle=document.querySelector('#selected-job-title');
272
307
  const usageRoot=document.querySelector('#usage-root');
273
308
  const usageMeta=document.querySelector('#usage-meta');
309
+ const graphsMeta=document.querySelector('#graphs-meta');
310
+ const graphList=document.querySelector('#graph-list');
311
+ const graphTitle=document.querySelector('#graph-title');
312
+ const graphPath=document.querySelector('#graph-path');
313
+ const graphCanvas=document.querySelector('#graph-canvas');
314
+ const graphSource=document.querySelector('#graph-source');
274
315
  const rulesText=document.querySelector('#rules-text');
275
316
  const rulesMeta=document.querySelector('#rules-meta');
276
317
  const rulesStatus=document.querySelector('#rules-status');
@@ -287,6 +328,8 @@ let selectedRepo=sessionStorage.getItem('agents-relay:repo-filter')||'';
287
328
  let quickMode=sessionStorage.getItem('agents-relay:quick-filter')||'jobs';
288
329
  let selectedRuntimeStatus=sessionStorage.getItem('agents-relay:runtime-status-filter')||'';
289
330
  let searchQuery=sessionStorage.getItem('agents-relay:job-search')||'';
331
+ let workflowGraphs=[];
332
+ let selectedGraphId=sessionStorage.getItem('agents-relay:selected-graph')||'';
290
333
  let eventTaskId=selectedTaskId;
291
334
  const collapsedTasks=new Set();
292
335
  const expandedTasks=new Set();
@@ -408,11 +451,15 @@ function githubUsageCard(effective,status){
408
451
  return card;
409
452
  }
410
453
  async function drawUsage(){try{const overview=await loadJSON('/api/usage');usageRoot.replaceChildren();usageMeta.textContent='Real account telemetry when available';const providers=Array.isArray(overview.providers)?overview.providers:[];const credits=Array.isArray(overview.credits)?overview.credits:[];const grid=element('div','usage-providers');if(overview.githubEffective||overview.githubStatus)grid.append(githubUsageCard(overview.githubEffective,overview.githubStatus));for(const provider of providers)grid.append(usageProvider(provider));for(const provider of credits)grid.append(creditProvider(provider));if(!grid.children.length){usageRoot.append(element('p','empty',text('Usage telemetry is unavailable.')));return;}usageRoot.append(grid);}catch(error){usageMeta.textContent='Unavailable';usageRoot.replaceChildren(element('p','error-state',text('Usage unavailable: '+error.message)));}}
454
+ function selectedWorkflowGraph(){return workflowGraphs.find(graph=>graph.id===selectedGraphId)||workflowGraphs[0]||null;}
455
+ function renderGraphList(){graphList.replaceChildren();if(!workflowGraphs.length){graphList.append(element('p','empty',text('No workflow graphs found.')));return;}for(const graph of workflowGraphs){const button=document.createElement('button');button.type='button';button.className='graph-button';button.setAttribute('aria-current',String(graph.id===selectedGraphId));button.append(element('strong','',text(graph.name)),element('span','',text(graph.path)));button.addEventListener('click',()=>{selectedGraphId=graph.id;sessionStorage.setItem('agents-relay:selected-graph',selectedGraphId);renderGraphList();void renderSelectedGraph();});graphList.append(button);}}
456
+ async function renderSelectedGraph(){const graph=selectedWorkflowGraph();if(!graph){graphTitle.textContent='Select a graph';graphPath.textContent='';graphSource.textContent='';graphCanvas.replaceChildren(element('p','empty',text('No workflow graphs found.')));return;}selectedGraphId=graph.id;sessionStorage.setItem('agents-relay:selected-graph',selectedGraphId);graphTitle.textContent=graph.name;graphPath.textContent=graph.path;graphSource.textContent=graph.source;graphCanvas.replaceChildren(element('p','subtle',text('Rendering…')));try{if(!window.mermaid)throw new Error('Mermaid renderer unavailable');if(!window.__relayMermaidReady){window.mermaid.initialize({startOnLoad:false,securityLevel:'strict'});window.__relayMermaidReady=true;}const rendered=await window.mermaid.render('relay-graph-'+graph.id.replace(/[^A-Za-z0-9_-]/g,'-'),graph.source);const parsed=new DOMParser().parseFromString(rendered.svg,'image/svg+xml');const svg=parsed.documentElement;if(svg.nodeName.toLowerCase()!=='svg')throw new Error('Mermaid did not return SVG');graphCanvas.replaceChildren(document.importNode(svg,true));}catch(error){graphCanvas.replaceChildren(element('p','error-state',text('Visual preview unavailable: '+error.message)));}}
457
+ async function drawGraphs(){try{const graphs=await loadJSON('/api/graphs');workflowGraphs=Array.isArray(graphs)?graphs:[];if(!workflowGraphs.some(graph=>graph.id===selectedGraphId))selectedGraphId=workflowGraphs[0]?.id||'';graphsMeta.textContent=workflowGraphs.length+' graph'+(workflowGraphs.length===1?'':'s')+' · workflow/*.mmd';renderGraphList();await renderSelectedGraph();}catch(error){graphsMeta.textContent='Unavailable';workflowGraphs=[];renderGraphList();graphCanvas.replaceChildren(element('p','error-state',text('Graphs unavailable: '+error.message)));}}
411
458
  async function drawRules(){rulesStatus.textContent='';try{const rules=await loadJSON('/api/execution-router-rules');rulesText.value=typeof rules.text==='string'?rules.text:'';rulesMeta.textContent=rules.hash&&rules.hash!=='none'?'Loaded · '+rules.hash.slice(0,12):'No custom rules saved';}catch(error){rulesMeta.textContent='Unavailable';rulesStatus.textContent='Could not load rules: '+error.message;}}
412
459
  async function saveRules(){rulesSave.disabled=true;rulesStatus.textContent='Saving…';try{const response=await fetch('/api/execution-router-rules',{method:'PUT',headers:{'content-type':'text/plain; charset=utf-8'},body:rulesText.value});if(!response.ok)throw new Error(response.status+' '+await response.text());const rules=await response.json();rulesMeta.textContent='Saved · '+rules.hash.slice(0,12);rulesStatus.textContent='Saved. New routing decisions will load these rules.';}catch(error){rulesStatus.textContent='Save failed: '+error.message;}finally{rulesSave.disabled=false;}}
413
460
  async function drawShortcuts(){shortcutsStatus.textContent='';shortcutsText.value=chatShortcutText();}
414
461
  function saveShortcuts(){const value=shortcutsText.value.trim();if(value)localStorage.setItem(CHAT_SHORTCUTS_KEY,value);else localStorage.removeItem(CHAT_SHORTCUTS_KEY);shortcutsText.value=chatShortcutText();shortcutsStatus.textContent='Saved.';}
415
- function showView(view){const isJobs=view==='jobs';const isUsage=view==='usage';const isRules=view==='rules';const isShortcuts=view==='shortcuts';jobsView.hidden=!isJobs;jobsWorkspace.hidden=!isJobs;usageView.hidden=!isUsage;rulesView.hidden=!isRules;shortcutsView.hidden=!isShortcuts;jobsTab.setAttribute('aria-selected',String(isJobs));usageTab.setAttribute('aria-selected',String(isUsage));rulesTab.setAttribute('aria-selected',String(isRules));shortcutsTab.setAttribute('aria-selected',String(isShortcuts));if(isJobs)void refresh();else if(isUsage)void drawUsage();else if(isRules)void drawRules();else void drawShortcuts();}
462
+ function showView(view){const isJobs=view==='jobs';const isUsage=view==='usage';const isGraphs=view==='graphs';const isRules=view==='rules';const isShortcuts=view==='shortcuts';jobsView.hidden=!isJobs;jobsWorkspace.hidden=!isJobs;usageView.hidden=!isUsage;graphsView.hidden=!isGraphs;rulesView.hidden=!isRules;shortcutsView.hidden=!isShortcuts;jobsTab.setAttribute('aria-selected',String(isJobs));usageTab.setAttribute('aria-selected',String(isUsage));graphsTab.setAttribute('aria-selected',String(isGraphs));rulesTab.setAttribute('aria-selected',String(isRules));shortcutsTab.setAttribute('aria-selected',String(isShortcuts));if(isJobs)void refresh();else if(isUsage)void drawUsage();else if(isGraphs)void drawGraphs();else if(isRules)void drawRules();else void drawShortcuts();}
416
463
  jobsTab.addEventListener('click',()=>{location.hash=selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs';showView('jobs');});
417
464
  jobsRefresh.addEventListener('click',()=>void refresh(true));
418
465
  eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
@@ -426,19 +473,20 @@ mobileJobsBack.addEventListener('click',()=>{location.hash='#/jobs';syncMobileJo
426
473
  mobileJobs.addEventListener('change',syncMobileJobsView);
427
474
  window.addEventListener('hashchange',syncMobileJobsView);
428
475
  usageTab.addEventListener('click',()=>{location.hash='#/usage';showView('usage');});
476
+ graphsTab.addEventListener('click',()=>{location.hash='#/graphs';showView('graphs');});
429
477
  rulesTab.addEventListener('click',()=>{location.hash='#/execution-router-rules';showView('rules');});
430
478
  shortcutsTab.addEventListener('click',()=>{location.hash='#/shortcuts';showView('shortcuts');});
431
479
  rulesSave.addEventListener('click',()=>void saveRules());
432
480
  shortcutsSave.addEventListener('click',saveShortcuts);
433
- window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const next=location.hash==='#/usage'?'rules':location.hash==='#/execution-router-rules'?'jobs':'usage';location.hash=next==='usage'?'#/usage':next==='rules'?'#/execution-router-rules':(selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs');showView(next);}});
481
+ window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const views=['jobs','usage','graphs','rules','shortcuts'];const current=location.hash==='#/usage'?'usage':location.hash==='#/graphs'?'graphs':location.hash==='#/execution-router-rules'?'rules':location.hash==='#/shortcuts'?'shortcuts':'jobs';const next=views[(views.indexOf(current)+1)%views.length];location.hash=next==='usage'?'#/usage':next==='graphs'?'#/graphs':next==='rules'?'#/execution-router-rules':next==='shortcuts'?'#/shortcuts':(selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs');showView(next);}});
434
482
  function parseMessage(message){try{return JSON.parse(message.data);}catch{return null;}}
435
483
  const stream=new EventSource('/events');
436
484
  stream.onopen=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream connected');connection.className='status-pill ok';};
437
485
  stream.onerror=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream reconnecting');connection.className='status-pill warn';};
438
486
  stream.onmessage=message=>{const payload=parseMessage(message);if(payload?.job){const index=jobs.findIndex(job=>job.id===payload.job.id);if(index>=0)jobs[index]=payload.job;else jobs.push(payload.job);renderAll();void drawRuntimeSummary();}};
439
487
  stream.addEventListener('relay',message=>{const payload=parseMessage(message);if(payload?.job){const index=jobs.findIndex(job=>job.id===payload.job.id);if(index>=0)jobs[index]=payload.job;else jobs.push(payload.job);}if(payload?.event)addEvent(payload.event);renderAll();void drawRuntimeSummary();});
440
- if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else showView('jobs');
441
- setInterval(()=>{if(location.hash!=='#/usage'&&location.hash!=='#/execution-router-rules'&&location.hash!=='#/shortcuts')void refresh();},300000);
488
+ if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/graphs')showView('graphs');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else showView('jobs');
489
+ setInterval(()=>{if(location.hash!=='#/usage'&&location.hash!=='#/graphs'&&location.hash!=='#/execution-router-rules'&&location.hash!=='#/shortcuts')void refresh();},300000);
442
490
  setInterval(()=>{if(location.hash==='#/usage')void drawUsage();},30000);
443
491
  setInterval(()=>void refreshVersion(),30000);
444
492
  async function refreshSelected(){if(!selectedJob){await refresh();return;}selectedJobRefresh.disabled=true;selectedJobRefresh.textContent='Refreshing…';try{const updated=await loadJSON('/api/job?id='+encodeURIComponent(selectedJob));const index=jobs.findIndex(job=>job.id===selectedJob);if(index>=0)jobs[index]=updated;else jobs.push(updated);rememberSelection();renderAll();connection.textContent='';connection.setAttribute('aria-label','Live durable state connected');connection.className='status-pill ok';}catch(error){connection.textContent='';connection.setAttribute('aria-label','Durable state unavailable');connection.className='status-pill bad';}finally{selectedJobRefresh.disabled=false;selectedJobRefresh.textContent='Refresh';}}
package/dist/dashboard.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { createServer } from 'node:http';
2
- import { readFile } from 'node:fs/promises';
2
+ import { readFile, readdir } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { readUsageSnapshot } from './usage.js';
6
7
  import { deriveJobRuntimeStatus } from './runtime-status.js';
7
8
  import { DeliveryDeduper, parseGitHubWebhook, verifyGitHubWebhook } from './github-webhook.js';
@@ -10,6 +11,16 @@ import { summarizeRuntime } from './runtime-summary.js';
10
11
  const GITHUB_REPOSITORY_PATTERN = /^([A-Za-z0-9](?:[A-Za-z0-9-]{0,36}[A-Za-z0-9])?)\/([A-Za-z0-9._-]{1,100})$/;
11
12
  const SSE_HEARTBEAT_MS = 15000;
12
13
  const EVENTS_HISTORY_FILE = process.env.EVENTS_BUS_DATA_DIR ? join(process.env.EVENTS_BUS_DATA_DIR, 'events.jsonl') : join(homedir(), 'Library', 'Application Support', 'EventsBus', 'events.jsonl');
14
+ const WORKFLOW_DIR = fileURLToPath(new URL('../workflow/', import.meta.url));
15
+ export async function dashboardWorkflowGraphs() {
16
+ const names = (await readdir(WORKFLOW_DIR)).filter(name => name.endsWith('.mmd')).sort();
17
+ return Promise.all(names.map(async (name) => ({
18
+ id: name.replace(/\.mmd$/, ''),
19
+ name: name.replace(/\.mmd$/, '').split('-').map(part => part === 'v2' ? 'V2' : part.charAt(0).toUpperCase() + part.slice(1)).join(' '),
20
+ path: `workflow/${name}`,
21
+ source: await readFile(join(WORKFLOW_DIR, name), 'utf8'),
22
+ })));
23
+ }
13
24
  function effectiveGitHubUsage(snapshot, runtime) {
14
25
  const candidates = [snapshot?.resources.core, snapshot?.resources.graphql, snapshot?.resources.search, runtime?.resources.core, runtime?.resources.graphql, runtime?.resources.search]
15
26
  .filter((candidate) => candidate !== undefined && candidate !== null)
@@ -171,6 +182,18 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
171
182
  res.end(JSON.stringify({ version: dashboardVersion }));
172
183
  return;
173
184
  }
185
+ if (req.url === '/api/graphs') {
186
+ try {
187
+ const graphs = await dashboardWorkflowGraphs();
188
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
189
+ res.end(JSON.stringify(graphs));
190
+ }
191
+ catch (error) {
192
+ res.writeHead(500);
193
+ res.end(error instanceof Error ? error.message : String(error));
194
+ }
195
+ return;
196
+ }
174
197
  if (req.url === '/webhooks/github' && req.method === 'POST') {
175
198
  const chunks = [];
176
199
  for await (const chunk of req)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {