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 +3 -3
- package/dist/adapters.js +8 -8
- package/dist/dashboard-html.js +26 -5
- package/dist/dashboard.js +5 -0
- package/dist/reconciler-runtime.js +2 -2
- package/dist/relayd.js +1 -1
- package/dist/reviewer-thread.js +1 -1
- package/dist/troubleshooter-qa-api.js +61 -0
- package/package.json +1 -1
- package/skills/agents-relay/agents/troubleshooter.agent.md +3 -1
- package/skills/agents-relay/troubleshooter-qa.md +25 -0
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/
|
|
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: `
|
|
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 `
|
|
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: '
|
|
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() || `
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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', '
|
|
350
|
+
capabilities = ['model', 'chatgpt', 'webhelm'];
|
|
351
351
|
runner;
|
|
352
352
|
tabClosePolicy;
|
|
353
353
|
constructor(options = {}) {
|
package/dist/dashboard-html.js
CHANGED
|
@@ -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
|
|
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
|
-
: /
|
|
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: '
|
|
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', '
|
|
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(),
|
package/dist/reviewer-thread.js
CHANGED
|
@@ -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', '
|
|
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
|
@@ -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 `
|
|
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:** ... -->
|