agents-relay 2.0.153 → 2.0.155

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
@@ -168,7 +168,7 @@ Review destinations are durable Job facts. `job create` and `job update` accept
168
168
  `--reviewer-thread-id` and `--creator-thread-id`; a reviewer ID takes precedence
169
169
  over the creator/origin ID. A local runtime invocation may pass
170
170
  `--thread-id` as an explicit one-run review override. When a destination exists,
171
- final-review routing requires a ChatGPT/browser-workspace worker and submits the
171
+ final-review routing requires a ChatGPT/webhelm worker and submits the
172
172
  review request as a verified `new-turn` to that existing thread with the Job ID,
173
173
  PR number, and immutable review head SHA. Failure still follows the ordinary
174
174
  review task retry/recovery path; a transport failure is never treated as
@@ -255,7 +255,7 @@ Every model-backed task declares a durable output with `--output task-pr` or `--
255
255
 
256
256
  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 one-shot submission runtime exits; neither substitutes for `task.completed`, `task.failed`, `task.blocked`, or `task.cancelled`. 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.
257
257
 
258
- The ChatGPT adapter uses Relay's `agents/chatgpt-worker.agent.md` lifecycle contract and invokes Browser Workspace exactly as a one-shot platform action: `browser-workspace platform run chatgpt temporary-submit --auto-session --config <file>`. Browser Workspace owns creation/release of the fresh Temporary Chat session, selectors, attachments, and submit verification; Relay does not self-heal browser failures and instead lets delivery failures enter its normal retry/recovery lifecycle. Legacy `--chatgpt-tab-close-policy` values remain accepted for compatibility around Relay's event barriers, but they do not control the Browser Workspace auto-session lifetime. The adapter never uses a pre-existing user tab, polls for the assistant response, resumes/reopens a thread, or treats conversation text as the result channel. Any observed ChatGPT thread ID is diagnostic only.
258
+ The ChatGPT adapter uses Relay's `agents/chatgpt-worker.agent.md` lifecycle contract and invokes Browser Workspace exactly as a one-shot platform action: `webhelm platform run chatgpt temporary-submit --auto-session --config <file>`. Browser Workspace owns creation/release of the fresh Temporary Chat session, selectors, attachments, and submit verification; Relay does not self-heal browser failures and instead lets delivery failures enter its normal retry/recovery lifecycle. Legacy `--chatgpt-tab-close-policy` values remain accepted for compatibility around Relay's event barriers, but they do not control the Browser Workspace auto-session lifetime. The adapter never uses a pre-existing user tab, polls for the assistant response, resumes/reopens a thread, or treats conversation text as the result channel. Any observed ChatGPT thread ID is diagnostic only.
259
259
 
260
260
  ~~~sh
261
261
  npx agents-relay task create --repo OWNER/REPO --pr 5 --id job-1 \
@@ -306,7 +306,7 @@ For autonomous Jobs, `UNBLOCKED` is historical: Relay does not retry that old Ta
306
306
 
307
307
  ### Browser Workspace product boundary
308
308
 
309
- Agents Relay owns the persistent `Relay` browser workspace as product infrastructure. At daemon startup it runs `browser-workspace status`, dynamically imports the returned absolute `admin_helper`, and calls `ensureWorkspace("Relay", 6)`. This bootstrap is best-effort: Browser Workspace discovery/helper failures are logged and skipped so they do not prevent `agents-relayd` from starting; ChatGPT browser work may remain unavailable until Browser Workspace recovers. Ordinary ChatGPT worker activity uses the public Browser Workspace one-shot `platform run ... --auto-session` command; orchestrator-owned self-healing flows may still use explicit session commands. Relay does not create or delete workspaces through CLI lifecycle commands.
309
+ Agents Relay owns the persistent `Relay` browser workspace as product infrastructure. At daemon startup it runs `webhelm status`, dynamically imports the returned absolute `admin_helper`, and calls `ensureWorkspace("Relay", 6)`. This bootstrap is best-effort: Browser Workspace discovery/helper failures are logged and skipped so they do not prevent `agents-relayd` from starting; ChatGPT browser work may remain unavailable until Browser Workspace recovers. Ordinary ChatGPT worker activity uses the public Browser Workspace one-shot `platform run ... --auto-session` command; orchestrator-owned self-healing flows may still use explicit session commands. Relay does not create or delete workspaces through CLI lifecycle commands.
310
310
 
311
311
  ## Shared publication reporting
312
312
 
package/dist/adapters.js CHANGED
@@ -172,7 +172,7 @@ export function parseDriverJson(output) {
172
172
  throw new Error('browser-worker driver returned no submitted receipt');
173
173
  }
174
174
  function browserWorkspaceCli(override = process.env.AGENTS_RELAY_BROWSER_WORKSPACE_COMMAND) {
175
- return override ? { command: override, prefix: [] } : { command: 'uvx', prefix: ['browser-workspace'] };
175
+ return override ? { command: override, prefix: [] } : { command: 'uvx', prefix: ['--from', 'browser-workspace', 'webhelm'] };
176
176
  }
177
177
  const browserWorkspaceInjection = new AsyncLocalStorage();
178
178
  function browserWorkspaceCommand(cli, args, stdin, signal) {
@@ -181,7 +181,7 @@ function browserWorkspaceCommand(cli, args, stdin, signal) {
181
181
  function rawBrowserWorkspaceCommand(cli, args, stdin, signal) {
182
182
  return new Promise((resolve, reject) => {
183
183
  const child = spawnOwnedProcess(cli.command, [...cli.prefix, ...args], {
184
- invocation: 'browser-workspace.command',
184
+ invocation: 'webhelm.command',
185
185
  signal,
186
186
  spawn: { shell: false, stdio: [stdin === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'] },
187
187
  });
@@ -200,7 +200,7 @@ function rawBrowserWorkspaceCommand(cli, args, stdin, signal) {
200
200
  else {
201
201
  // Browser Workspace reports a compact code/message JSON on nonzero exit.
202
202
  // Preserve existing Relay failure behavior; only simplify the error text.
203
- let reason = stderr.trim() || stdout.trim() || `browser-workspace exited ${code}`;
203
+ let reason = stderr.trim() || stdout.trim() || `webhelm exited ${code}`;
204
204
  try {
205
205
  const envelope = parseWorkspaceJson(stdout);
206
206
  const result = envelope.result;
@@ -234,7 +234,7 @@ function parseWorkspaceJson(output) {
234
234
  }
235
235
  catch { /* diagnostics may precede JSON */ }
236
236
  }
237
- throw new Error('browser-workspace returned no JSON result');
237
+ throw new Error('webhelm returned no JSON result');
238
238
  }
239
239
  /** Parse only the trusted manifest emitted before the explicit task-data boundary. */
240
240
  function declaredPromptReferences(prompt) {
@@ -277,11 +277,11 @@ async function accessiblePromptReferences(prompt) {
277
277
  export async function ensureChatGptBrowserWorkspace(command = process.env.AGENTS_RELAY_BROWSER_WORKSPACE_COMMAND) {
278
278
  const status = parseWorkspaceJson(await browserWorkspaceCommand(browserWorkspaceCli(command), ['status']));
279
279
  if (typeof status.admin_helper !== 'string' || !status.admin_helper.startsWith('/')) {
280
- throw new Error('browser-workspace status returned no absolute admin_helper path');
280
+ throw new Error('webhelm status returned no absolute admin_helper path');
281
281
  }
282
282
  const admin = await import(pathToFileURL(status.admin_helper).href);
283
283
  if (typeof admin.ensureWorkspace !== 'function') {
284
- throw new Error('browser-workspace admin helper does not export ensureWorkspace');
284
+ throw new Error('webhelm admin helper does not export ensureWorkspace');
285
285
  }
286
286
  await admin.ensureWorkspace(CHATGPT_BROWSER_WORKSPACE, CHATGPT_BROWSER_WORKSPACE_POOL_SIZE);
287
287
  }
@@ -289,7 +289,7 @@ async function runBrowserWorker(cli, configFile, signal, action = 'temporary-sub
289
289
  const session = parseWorkspaceJson(await browserWorkspaceCommand(cli, ['session', 'start', '--workspace', CHATGPT_BROWSER_WORKSPACE], undefined, signal));
290
290
  const sessionId = session.session_id;
291
291
  if (typeof sessionId !== 'string' || !sessionId)
292
- throw new Error('browser-workspace session start returned no session_id');
292
+ throw new Error('webhelm session start returned no session_id');
293
293
  try {
294
294
  return await browserWorkspaceCommand(cli, ['platform', 'run', 'chatgpt', action, '--session-id', sessionId, '--config', configFile], undefined, signal);
295
295
  }
@@ -347,7 +347,7 @@ function parseBrowserWorkerResponse(output) {
347
347
  export class ChatGptAdapter {
348
348
  name = 'chatgpt';
349
349
  id = 'chatgpt/browser-worker';
350
- capabilities = ['model', 'chatgpt', 'browser-workspace'];
350
+ capabilities = ['model', 'chatgpt', 'webhelm'];
351
351
  runner;
352
352
  tabClosePolicy;
353
353
  constructor(options = {}) {
@@ -34,6 +34,7 @@ ${dashboardStyles}
34
34
  <button id="jobs-tab" type="button" role="tab" aria-selected="true" aria-controls="jobs-view">Jobs</button>
35
35
  <button id="usage-tab" type="button" role="tab" aria-selected="false" aria-controls="usage-view">Usage</button>
36
36
  <button id="rules-tab" type="button" role="tab" aria-selected="false" aria-controls="rules-view">Router Rules</button>
37
+ <button id="qa-tab" type="button" role="tab" aria-selected="false" aria-controls="qa-view">Troubleshooter Q&A</button>
37
38
  <button id="shortcuts-tab" type="button" role="tab" aria-selected="false" aria-controls="shortcuts-view">Shortcuts</button>
38
39
  <button id="graphs-tab" type="button" role="tab" aria-selected="false" aria-controls="graphs-view">Workflow</button>
39
40
  </nav>
@@ -83,6 +84,14 @@ ${dashboardStyles}
83
84
  </section>
84
85
  <dialog id="graph-dialog" class="graph-dialog" aria-label="Workflow graph viewer"><div class="graph-dialog-header"><strong id="graph-dialog-title">Workflow graph</strong><div class="graph-navigation"><button type="button" id="graph-zoom-out" class="refresh-button" aria-label="Zoom out">−</button><span id="graph-zoom-level" aria-live="polite">100%</span><button type="button" id="graph-zoom-in" class="refresh-button" aria-label="Zoom in">+</button><button type="button" id="graph-zoom-reset" class="refresh-button">Fit</button><button type="button" id="graph-dialog-close" class="refresh-button">Close</button></div></div><div id="graph-dialog-viewport" class="graph-dialog-viewport"></div></dialog>
85
86
  <section id="rules-view" role="tabpanel" aria-labelledby="rules-tab" hidden><div class="panel"><div class="panel-head"><h2>Router Rules</h2><span id="rules-meta" class="subtle"></span></div><div class="panel-body"><div class="router-rules-workspace"><div class="router-rules-pane rules-editor"><h3>Rules Editor</h3><p class="rules-help">Execution Router rules with an explicit <code>[choices]</code> section (provider: model [reasoning] entries, additive to profile defaults) and a <code>[routing-instruction]</code> section forwarded verbatim to Laya. Relay deduplicates, applies hard gates, and sends one merged choice list; impossible routes remain excluded by code and Codex stays the deterministic fallback.</p><textarea id="rules-text" class="rules-textarea" spellcheck="true" aria-label="Execution router rules" placeholder="[choices]\nopenai: sol medium, luna high\nzai: glm-5.3-flash high\n\n[routing-instruction]\nPrefer zai until its quota resets; otherwise use the openai default."></textarea><div class="rules-footer"><span id="rules-status" class="subtle" aria-live="polite"></span><div><button id="rules-test" class="rules-save" type="button">Test Router</button> <button id="rules-save" class="rules-save" type="button">Save rules</button></div></div></div><div class="router-rules-pane"><div class="panel-head"><h3>Audit Log</h3><button id="audit-refresh" type="button">Refresh</button></div><div id="audit-log" class="audit-log" aria-live="polite"><p class="empty">Loading latest routing decisions…</p></div></div></div><div class="router-test-panel"><h3>Test Router Result</h3><div id="router-test-results" class="router-test-results" aria-live="polite"><p class="empty">Select a Job and Task, then run Test Router.</p></div></div></div></div></section>
87
+ <section id="qa-view" role="tabpanel" aria-labelledby="qa-tab" hidden>
88
+ <div class="panel"><div class="panel-head"><h2>Troubleshooter Q&A</h2><span class="subtle">skills/agents-relay/troubleshooter-qa.md</span></div>
89
+ <div class="panel-body"><div class="rules-editor">
90
+ <p class="rules-help">Edit Markdown directly. Troubleshooter reads this file as guidance, not workflow policy.</p>
91
+ <textarea id="qa-text" class="rules-textarea" spellcheck="true" aria-label="Troubleshooter Q&A Markdown"></textarea>
92
+ <div class="rules-footer"><span id="qa-status" class="subtle" aria-live="polite"></span><div><button id="qa-reload" class="rules-save" type="button">Reload</button> <button id="qa-save" class="rules-save" type="button">Save Q&A</button></div></div>
93
+ </div></div></div>
94
+ </section>
86
95
  <section id="shortcuts-view" role="tabpanel" aria-labelledby="shortcuts-tab" hidden>
87
96
  <div class="panel">
88
97
  <div class="panel-head"><h2>ChatGPT Shortcuts</h2><span class="subtle">One per line · %1 = current repo/pr</span></div>
@@ -117,6 +126,13 @@ const usageTab=document.querySelector('#usage-tab');
117
126
  const graphsTab=document.querySelector('#graphs-tab');
118
127
  const rulesTab=document.querySelector('#rules-tab');
119
128
  const shortcutsTab=document.querySelector('#shortcuts-tab');
129
+ const qaTab=document.querySelector('#qa-tab');
130
+ const qaView=document.querySelector('#qa-view');
131
+ const qaText=document.querySelector('#qa-text');
132
+ const qaStatus=document.querySelector('#qa-status');
133
+ const qaSave=document.querySelector('#qa-save');
134
+ let qaRevision='';
135
+
120
136
  const jobsView=document.querySelector('#jobs-view');
121
137
  const usageView=document.querySelector('#usage-view');
122
138
  const graphsView=document.querySelector('#graphs-view');
@@ -318,7 +334,9 @@ async function testRouter(){rulesTest.disabled=true;routerTestResults.replaceChi
318
334
  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;}}
319
335
  async function drawShortcuts(){shortcutsStatus.textContent='';shortcutsText.value=chatShortcutText();}
320
336
  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.';}
321
- 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();}
337
+ async function drawQa(){qaStatus.textContent='Loading…';try{const result=await loadJSON('/api/troubleshooter-qa');qaText.value=result.text;qaRevision=result.hash;qaStatus.textContent='Loaded';}catch(error){qaStatus.textContent='Load failed: '+error.message;}}
338
+ async function saveQa(){qaSave.disabled=true;qaStatus.textContent='Saving…';try{const response=await fetch('/api/troubleshooter-qa',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({text:qaText.value,hash:qaRevision})});if(!response.ok)throw new Error(response.status+' '+await response.text());const saved=await response.json();qaRevision=saved.hash;qaStatus.textContent='Saved to Markdown file';}catch(error){qaStatus.textContent='Save failed: '+error.message;}finally{qaSave.disabled=false;}}
339
+ function showView(view){const isJobs=view==='jobs';const isUsage=view==='usage';const isGraphs=view==='graphs';const isRules=view==='rules';const isShortcuts=view==='shortcuts';const isQa=view==='qa';qaView.hidden=!isQa;qaTab.setAttribute('aria-selected',String(isQa));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 if(isQa)void drawQa();else void drawShortcuts();}
322
340
  jobsTab.addEventListener('click',()=>{location.hash=selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs';showView('jobs');});
323
341
  jobsRefresh.addEventListener('click',()=>void refresh(true));
324
342
  eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
@@ -337,15 +355,18 @@ document.querySelector('#graph-entry').addEventListener('click',()=>navigateGrap
337
355
  const graphDialog=document.querySelector('#graph-dialog');const graphViewport=document.querySelector('#graph-dialog-viewport');let graphZoom=1;const graphCanvasHome=graphCanvas.parentNode;function setGraphZoom(value){graphZoom=Math.min(4,Math.max(.35,value));const svg=graphCanvas.querySelector('svg');if(svg){svg.style.width=(Number(svg.getAttribute('width'))||Math.max(900,svg.viewBox.baseVal.width))*graphZoom+'px';svg.style.maxWidth='none';}document.querySelector('#graph-zoom-level').textContent=Math.round(graphZoom*100)+'%';}document.querySelector('#graph-maximize').addEventListener('click',()=>{graphDialog.showModal();graphViewport.append(graphCanvas);document.querySelector('#graph-dialog-title').textContent=graphTitle.textContent;setGraphZoom(1);});function closeGraphViewer(){graphCanvasHome.insertBefore(graphCanvas,graphCanvasHome.querySelector('.graph-source-details'));setGraphZoom(1);}graphDialog.addEventListener('close',closeGraphViewer);document.querySelector('#graph-dialog-close').addEventListener('click',()=>graphDialog.close());document.querySelector('#graph-zoom-in').addEventListener('click',()=>setGraphZoom(graphZoom*1.25));document.querySelector('#graph-zoom-out').addEventListener('click',()=>setGraphZoom(graphZoom/1.25));document.querySelector('#graph-zoom-reset').addEventListener('click',()=>setGraphZoom(1));graphViewport.addEventListener('wheel',event=>{if(!event.ctrlKey&&!event.metaKey)return;event.preventDefault();setGraphZoom(graphZoom*(event.deltaY<0?1.15:1/1.15));},{passive:false});let drag=null;graphViewport.addEventListener('pointerdown',event=>{if(event.target.closest('.drillable'))return;drag={x:event.clientX,y:event.clientY,left:graphViewport.scrollLeft,top:graphViewport.scrollTop};graphViewport.setPointerCapture(event.pointerId);});graphViewport.addEventListener('pointermove',event=>{if(!drag)return;graphViewport.scrollLeft=drag.left-(event.clientX-drag.x);graphViewport.scrollTop=drag.top-(event.clientY-drag.y);});graphViewport.addEventListener('pointerup',()=>drag=null);graphViewport.addEventListener('pointercancel',()=>drag=null);
338
356
  document.querySelector('#graph-back').addEventListener('click',()=>history.back());
339
357
  document.querySelector('#graph-forward').addEventListener('click',()=>history.forward());
340
- window.addEventListener('popstate',()=>{if(graphFromHash()){renderGraphNavigation();showView('graphs');}else 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');});
341
- window.addEventListener('hashchange',()=>{if(graphFromHash()){renderGraphNavigation();showView('graphs');}else 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');});
358
+ window.addEventListener('popstate',()=>{if(graphFromHash()){renderGraphNavigation();showView('graphs');}else if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else if(location.hash==='#/troubleshooter-qa')showView('qa');else showView('jobs');});
359
+ window.addEventListener('hashchange',()=>{if(graphFromHash()){renderGraphNavigation();showView('graphs');}else if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else if(location.hash==='#/troubleshooter-qa')showView('qa');else showView('jobs');});
342
360
  rulesTab.addEventListener('click',()=>{location.hash='#/execution-router-rules';showView('rules');});
361
+ qaTab.addEventListener('click',()=>{location.hash='#/troubleshooter-qa';showView('qa');});
362
+ qaSave.addEventListener('click',()=>void saveQa());
363
+ document.querySelector('#qa-reload').addEventListener('click',()=>{if(!qaText.value||confirm('Discard unsaved Q&A edits and reload?'))void drawQa();});
343
364
  shortcutsTab.addEventListener('click',()=>{location.hash='#/shortcuts';showView('shortcuts');});
344
365
  rulesSave.addEventListener('click',()=>void saveRules());
345
366
  rulesTest.addEventListener('click',()=>void testRouter());
346
367
  auditRefresh.addEventListener('click',()=>void drawAudit());
347
368
  shortcutsSave.addEventListener('click',saveShortcuts);
348
- 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':graphFromHash()?'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);}});
369
+ window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const views=['jobs','usage','graphs','rules','qa','shortcuts'];const current=location.hash==='#/usage'?'usage':graphFromHash()?'graphs':location.hash==='#/execution-router-rules'?'rules':location.hash==='#/shortcuts'?'shortcuts':location.hash==='#/troubleshooter-qa'?'qa':'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':next==='qa'?'#/troubleshooter-qa':(selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs');showView(next);}});
349
370
  function parseMessage(message){try{return JSON.parse(message.data);}catch{return null;}}
350
371
  function isFreshEnough(incoming,current){const a=Date.parse(incoming||''),b=Date.parse(current||'');return !Number.isFinite(b)||!Number.isFinite(a)||a>=b;}
351
372
  function applyStatePatch(patch){if(!patch?.jobId)return false;const job=jobs.find(candidate=>candidate.id===patch.jobId);if(!job)return false;if(patch.jobState&&isFreshEnough(patch.jobUpdatedAt,job.updatedAt)){job['state']=patch.jobState;if(patch.jobUpdatedAt)job.updatedAt=patch.jobUpdatedAt;}if(patch.taskId&&patch.taskState){const task=(job.tasks||[]).find(candidate=>candidate.id===patch.taskId);if(task&&isFreshEnough(patch.taskUpdatedAt,task.updatedAt)){task['state']=patch.taskState;if(patch.taskUpdatedAt)task.updatedAt=patch.taskUpdatedAt;}}return true;}
@@ -355,7 +376,7 @@ stream.onopen=()=>{connection.textContent='';connection.setAttribute('aria-label
355
376
  stream.onerror=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream reconnecting');connection.className='status-pill warn';};
356
377
  stream.onmessage=message=>{const payload=parseMessage(message);if(payload?.job){upsertFreshJob(payload.job);renderAll();void drawRuntimeSummary();}};
357
378
  stream.addEventListener('relay',message=>{const payload=parseMessage(message);if(payload?.job)upsertFreshJob(payload.job);else if(payload?.statePatch)applyStatePatch(payload.statePatch);if(payload?.event)addEvent(payload.event);renderAll();void drawRuntimeSummary();});
358
- if(location.hash==='#/usage')showView('usage');else if(graphFromHash())showView('graphs');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else showView('jobs');
379
+ if(location.hash==='#/usage')showView('usage');else if(graphFromHash())showView('graphs');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else if(location.hash==='#/troubleshooter-qa')showView('qa');else showView('jobs');
359
380
  setInterval(()=>{if(location.hash==='#/usage')void drawUsage();},30000);
360
381
  setInterval(()=>void refreshVersion(),30000);
361
382
  async function refreshSelected(){const job=selected();if(!job){await refresh();return;}selectedJobRefresh.disabled=true;selectedJobRefresh.textContent='Refreshing…';try{const params=new URLSearchParams({id:job.id,repo:job.repository,pr:String(job.prNumber)});const updated=await loadJSON('/api/job?'+params.toString());const index=jobs.findIndex(candidate=>jobSelectionKey(candidate)===jobSelectionKey(job));if(index>=0)jobs[index]=updated;else jobs.push(updated);selectedJob=jobSelectionKey(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,6 +1,7 @@
1
1
  import { inspectWorkflowSymbol } from './dashboard-workflow-inspection.js';
2
2
  import { createServer } from 'node:http';
3
3
  import { subscribeLocalCommits } from './local-commit-events.js';
4
+ import { handleTroubleshooterQa } from './troubleshooter-qa-api.js';
4
5
  import { readFile, readdir } from 'node:fs/promises';
5
6
  import { join } from 'node:path';
6
7
  import { fileURLToPath } from 'node:url';
@@ -192,6 +193,10 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
192
193
  }
193
194
  return;
194
195
  }
196
+ if (req.url === '/api/troubleshooter-qa') {
197
+ await handleTroubleshooterQa(req, res);
198
+ return;
199
+ }
195
200
  if (req.url === '/api/graphs') {
196
201
  try {
197
202
  const graphs = await dashboardWorkflowGraphs();
@@ -259,7 +259,7 @@ export class ReconcilerRuntimeBase {
259
259
  const currentTask = currentJob.tasks.find(task => task.id === taskId);
260
260
  const platformFailure = currentTask?.adapter === 'chatgpt' ? classifyChatGptPlatformFailure(failure, phase) : undefined;
261
261
  const failureReason = phase === 'start-ack' ? 'worker_start_timeout'
262
- : /BROWSER_WORKSPACE_UNAVAILABLE|browser-workspace runtime dependency unavailable|browser-workspace session start returned no session_id|no close frame received or sent/i.test(message)
262
+ : /WEBHELM_UNAVAILABLE|webhelm runtime dependency unavailable|webhelm session start returned no session_id|no close frame received or sent/i.test(message)
263
263
  ? 'browser_workspace_unavailable'
264
264
  : undefined;
265
265
  const claim = executionId ?? `${jobId}:${taskId}`;
@@ -305,7 +305,7 @@ export class ReconcilerRuntimeBase {
305
305
  this.detach(this.emit(event), jobId, taskId, 'worker failure event');
306
306
  if (runtimeRetry) {
307
307
  this.detach(this.emit(eventFor(jobId, taskId, runtimeRetry.parentTaskId, 'task.rerouting', 'queued', 'ChatGPT platform rejection recorded; routing the same Task through Execution Router', 'orchestrator', { previousFailure: transition.task.platformFailure?.classification, executionId, attempt: runtimeRetry.attempt, maxAttempts: runtimeRetry.maxAttempts })), jobId, taskId, 'runtime retry event');
308
- this.detach(this.eventCoordinator.accept({ jobId, reason: 'browser-workspace-runtime-retry' }), jobId, taskId, 'runtime retry reconcile wake');
308
+ this.detach(this.eventCoordinator.accept({ jobId, reason: 'webhelm-runtime-retry' }), jobId, taskId, 'runtime retry reconcile wake');
309
309
  return;
310
310
  }
311
311
  this.detach(this.eventCoordinator.accept({ jobId, reason: `worker-${phase}-failure` }), jobId, taskId, 'worker failure reconcile wake');
package/dist/relayd.js CHANGED
@@ -465,7 +465,7 @@ export async function runDaemon(argv) {
465
465
  const runtime = summarizeRuntime(cachedOverviews.map(item => item.job));
466
466
  const workers = workerCandidates([
467
467
  { name: 'codex', id: 'codex', capabilities: ['model', 'codex'] },
468
- { name: 'chatgpt', id: 'chatgpt/browser-worker', capabilities: ['model', 'chatgpt', 'browser-workspace'] },
468
+ { name: 'chatgpt', id: 'chatgpt/browser-worker', capabilities: ['model', 'chatgpt', 'webhelm'] },
469
469
  ], [], usage);
470
470
  const result = await routeExecution(workers, task.workerRequirements ?? { capabilities: task.capabilities ?? [] }, {
471
471
  decisionEngine: executionDecisionEngine(),
@@ -50,7 +50,7 @@ export function prepareReviewerThreadDelivery(job, task, override) {
50
50
  }
51
51
  export function chatgptThreadRequirements(configured) {
52
52
  const base = configured ?? { capabilities: ['model'] };
53
- const capabilities = new Set([...base.capabilities, 'model', 'chatgpt', 'browser-workspace']);
53
+ const capabilities = new Set([...base.capabilities, 'model', 'chatgpt', 'webhelm']);
54
54
  return { ...base, capabilities: [...capabilities], runtime: 'chatgpt' };
55
55
  }
56
56
  export function runtimeReviewer(args = []) {
@@ -0,0 +1,61 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import { fileURLToPath } from 'node:url';
4
+ const FILE = fileURLToPath(new URL('../skills/agents-relay/troubleshooter-qa.md', import.meta.url));
5
+ const qaHash = (content) => createHash('sha256').update(content).digest('hex');
6
+ export async function handleTroubleshooterQa(req, res) {
7
+ try {
8
+ const current = await readFile(FILE, 'utf8');
9
+ if (req.method === 'GET') {
10
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
11
+ res.end(JSON.stringify({ text: current, hash: qaHash(current) }));
12
+ return;
13
+ }
14
+ if (req.method === 'PUT') {
15
+ if (req.headers['content-type']?.split(';')[0] !== 'application/json') {
16
+ res.writeHead(415);
17
+ res.end('Expected JSON');
18
+ return;
19
+ }
20
+ const chunks = [];
21
+ let size = 0;
22
+ for await (const chunk of req) {
23
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
24
+ size += buffer.length;
25
+ if (size > 128 * 1024) {
26
+ res.writeHead(413);
27
+ res.end('Q&A too large');
28
+ return;
29
+ }
30
+ chunks.push(buffer);
31
+ }
32
+ const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
33
+ if (!body || typeof body !== 'object' || !('text' in body) || !('hash' in body) || typeof body.text !== 'string' || typeof body.hash !== 'string') {
34
+ res.writeHead(400);
35
+ res.end('Invalid Q&A payload');
36
+ return;
37
+ }
38
+ if (body.hash !== qaHash(current)) {
39
+ res.writeHead(409);
40
+ res.end('Q&A changed on disk; reload before saving');
41
+ return;
42
+ }
43
+ if (Buffer.byteLength(body.text) > 128 * 1024) {
44
+ res.writeHead(413);
45
+ res.end('Q&A too large');
46
+ return;
47
+ }
48
+ await writeFile(FILE, body.text, 'utf8');
49
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
50
+ res.end(JSON.stringify({ hash: qaHash(body.text) }));
51
+ return;
52
+ }
53
+ res.writeHead(405, { allow: 'GET, PUT' });
54
+ res.end('Method not allowed');
55
+ }
56
+ catch (error) {
57
+ res.writeHead(500);
58
+ res.end(error instanceof Error ? error.message : String(error));
59
+ }
60
+ return;
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.153",
3
+ "version": "2.0.155",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,6 +15,8 @@ Your goal is not to explain the failure. Your goal is to make the original task
15
15
 
16
16
  Read and follow the shared Task / Job contract and orchestrator P0/self-job workflow before acting.
17
17
 
18
+ Read `skills/agents-relay/troubleshooter-qa.md` for user-editable best practices before diagnosis. Treat its answers as guidance only, below current evidence and authoritative contracts. After a verified recovery, add or refine a concise Q&A entry when it provides reusable knowledge; never include secrets or unverified claims.
19
+
18
20
  ## Recovery loop
19
21
 
20
22
  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.
@@ -25,7 +27,7 @@ Read and follow the shared Task / Job contract and orchestrator P0/self-job work
25
27
  - shared worker/runtime/tool/Agents Relay defect;
26
28
  - genuine external/human blocker.
27
29
  4. Diagnose the smallest root cause from the combined durable Task + event sequence + logs. The final `task.error` is only one piece of evidence. Never retry first and investigate later.
28
- 5. For ChatGPT browser failures, run `browser-workspace platform run chatgpt validate --session-id SESSION_ID` in a caller-owned `Relay` session and close the session afterward. Parse the action JSON from the platform-run `result.stdout` (the outer `result.ok` only reports command execution). The action returns exactly JSON `true` on validation success or one failure ID: `PAGE_NOT_READY`, `COMPOSER_NOT_READY`, or `DELIVERY_ERROR`. Treat any unexpected output or runner error as validation unavailable. These are UI-readiness results, NOT authentication results. None permits a human login escalation. Never independently interpret DOM snapshots, sign-in buttons, or disabled Send; only confirmed authentication failure from the actual Worker authentication protocol can justify login escalation. Inspect original task delivery/start-ack evidence separately.
30
+ 5. For ChatGPT browser failures, run `webhelm platform run chatgpt validate --session-id SESSION_ID` in a caller-owned `Relay` session and close the session afterward. Parse the action JSON from the platform-run `result.stdout` (the outer `result.ok` only reports command execution). The action returns exactly JSON `true` on validation success or one failure ID: `PAGE_NOT_READY`, `COMPOSER_NOT_READY`, or `DELIVERY_ERROR`. Treat any unexpected output or runner error as validation unavailable. These are UI-readiness results, NOT authentication results. None permits a human login escalation. Never independently interpret DOM snapshots, sign-in buttons, or disabled Send; only confirmed authentication failure from the actual Worker authentication protocol can justify login escalation. Inspect original task delivery/start-ack evidence separately.
29
31
 
30
32
  6. Repair:
31
33
  - For a transient failure, verify the transient condition is gone.
@@ -0,0 +1,25 @@
1
+ # Troubleshooter Q&A
2
+
3
+ A simple, user-editable reference for recurring failures and proven recovery practices.
4
+
5
+ Troubleshooter: read this file before diagnosis, but always inspect current task events and logs. Answers are guidance, not authority to retry, unblock, change routing, or change workflow state. The canonical workflow graphs and contracts remain authoritative.
6
+
7
+ Users can add or edit entries directly. After a recovery is verified, the Troubleshooter may add a short entry or improve an existing answer. Do not write secrets, tokens, or private log contents here. Keep unverified hypotheses clearly labeled.
8
+
9
+ ## Q: Browser Workspace validation says COMPOSER_NOT_READY. Does that mean the user is logged out?
10
+
11
+ **A:** No. This code reports UI readiness, not authentication. Use the platform's validated session and inspect worker authentication and delivery evidence separately. Do not infer login failure from a disabled Send button or DOM snapshot.
12
+
13
+ ## Q: A worker fails or times out. Should Troubleshooter immediately retry?
14
+
15
+ **A:** No. Read the original task's correlated lifecycle events, attempts, routing, and relevant logs. Diagnose the actual cause first. Only recover the original Task after verifying that the blocker is resolved, using the canonical recovery path.
16
+
17
+ ## Q: A shared runtime or tool defect blocks several tasks. What should Troubleshooter do?
18
+
19
+ **A:** Reuse an existing blocker Job when possible. Otherwise create the owning P0/self-job through Agents Relay. Verify the repair and any required release or deployment before recovering dependent tasks. For deferred repair, register the durable blocker relation instead of keeping a worker alive.
20
+
21
+ ## Q: When can a BLOCKED task resume?
22
+
23
+ **A:** Only after an authoritative unblock operation or verified blocker-Job resolution. A Q&A answer cannot unblock a task.
24
+
25
+ <!-- Add more entries below using: ## Q: ... / **A:** ... -->