agents-relay 2.0.117 → 2.0.118
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/dist/dashboard-html.js
CHANGED
|
@@ -388,7 +388,7 @@ function jobChatRef(job){const pr=Number(job?.prNumber||0);return repoName(job?.
|
|
|
388
388
|
function chatShortcutUrl(prompt){return 'https://chatgpt.com/?prompt='+encodeURIComponent(prompt);}
|
|
389
389
|
const CHAT_SHORTCUT_TARGET='agents-relay-shortcut';
|
|
390
390
|
function chatShortcutField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('ChatGPT shortcut')));const select=document.createElement('select');select.className='job-filter job-chat-shortcut-select';select.setAttribute('aria-label','ChatGPT shortcut');select.append(new Option('',''));for(const item of parseChatShortcuts(chatShortcutText()))select.append(new Option(item.name,item.prompt));select.value='';select.addEventListener('change',()=>{if(!select.value)return;const prompt=select.value.replaceAll('%1',jobChatRef(job));window.open(chatShortcutUrl(prompt),CHAT_SHORTCUT_TARGET,'noopener');select.value='';});box.append(select);return box;}
|
|
391
|
-
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected repo.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));row.addEventListener('click',()=>selectJob(job));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job);});select.append(element('span','job-title',text(repoName(job.repository)+'/'+value(job.id)+(Number(job.prNumber||0)>0?'/PR#'+value(job.prNumber):''))));const states=element('span','job-status-line');states.append(element('span','badge '+value(job.state),text(value(job.state))),element('span','badge '+value(job.runtimeStatus||'IDLE'),text(value(job.runtimeStatus||'IDLE'))));if(job.publicationFailure)states.append(element('span','badge FAILED',text('PUBLISH FAILED: '+value(job.publicationFailure.stage))));else if(job.publication?.version)states.append(element('span','badge SUCCEEDED',text('v'+job.publication.version)));else if(Number(job.prNumber||0)>0&&job.githubState==='MERGED')states.append(element('span','badge MERGED',text('PR Merged')));else if(job.state==='COMPLETED'&&job.githubState==='CLOSED')states.append(element('span','badge SUCCEEDED',text('PR Archived')));if(job.executionMode==='autonomous')states.append(element('span','badge flag-autonomous',text('AUTONOMOUS')));if(job.state==='ACTIVE'&&job.reviewEnabled!==false)states.append(element('span','badge flag-review',text('REVIEW REQUIRED')));if(isCompleteUnmerged(job))states.append(element('span','badge MERGE_PENDING',text('MERGE PENDING')));const primary=element('div','job-primary');primary.append(states);const lastLine=element('div','job-last-line');lastLine.append(element('span','subtle',text(compactCounts(counts(job.tasks)))),element('span','subtle',text(relativeTime(job.updatedAt))));content.append(select,primary,lastLine);row.append(content);jobList.append(element('li','',row));}}
|
|
391
|
+
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected repo.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));row.addEventListener('click',()=>selectJob(job));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job);});select.append(element('span','job-title',text(repoName(job.repository)+'/'+value(job.id)+(Number(job.prNumber||0)>0?'/PR#'+value(job.prNumber):''))));const states=element('span','job-status-line');states.append(element('span','badge '+value(job.state),text(value(job.state))),element('span','badge '+value(job.runtimeStatus||'IDLE'),text(value(job.runtimeStatus||'IDLE'))));if(job.publicationFailure)states.append(element('span','badge FAILED',text('PUBLISH FAILED: '+value(job.publicationFailure.stage))));else if(job.publication?.version)states.append(element('span','badge SUCCEEDED',text('v'+job.publication.version)));else if(Number(job.prNumber||0)>0&&job.githubState==='MERGED')states.append(element('span','badge MERGED',text('PR Merged')));else if(job.state==='COMPLETED'&&job.githubState==='CLOSED')states.append(element('span','badge SUCCEEDED',text('PR Archived')));if(job.executionMode==='autonomous')states.append(element('span','badge flag-autonomous',text('AUTONOMOUS')));if(job.state==='ACTIVE'&&job.reviewEnabled!==false)states.append(element('span','badge flag-review',text('REVIEW REQUIRED')));if(job.localMerge?.status==='failed')states.append(element('span','badge FAILED',text('MERGE FAILED: '+value(job.localMerge.error||'Unknown error'))));else if(isCompleteUnmerged(job))states.append(element('span','badge MERGE_PENDING',text('MERGE PENDING')));const primary=element('div','job-primary');primary.append(states);const lastLine=element('div','job-last-line');lastLine.append(element('span','subtle',text(compactCounts(counts(job.tasks)))),element('span','subtle',text(relativeTime(job.updatedAt))));content.append(select,primary,lastLine);row.append(content);jobList.append(element('li','',row));}}
|
|
392
392
|
function taskTagDisplay(job,task){const display=task.tag&&job?.tag?.values?.[task.tag];return typeof display==='string'&&display?display:null;}
|
|
393
393
|
function taskSubtreeComplete(task,children){return ['SUCCEEDED','CANCELLED'].includes(task.state)&&(children.get(task.id)||[]).every(child=>taskSubtreeComplete(child,children));}
|
|
394
394
|
function taskNode(task,children,depth){const li=element('li');const nested=children.get(task.id)||[];const line=element('div','task-line');if(nested.length){const key=(selectedJob||'')+':'+task.id;const collapsed=collapsedTasks.has(key)||(!expandedTasks.has(key)&&taskSubtreeComplete(task,children));const fold=document.createElement('button');fold.type='button';fold.className='task-fold';fold.setAttribute('aria-expanded',String(!collapsed));fold.setAttribute('aria-label',(collapsed?'Expand ':'Collapse ')+task.id);fold.textContent=collapsed?'▸':'▾';line.append(fold);const ul=element('ul','tree');ul.hidden=collapsed;for(const child of nested)ul.append(taskNode(child,children,depth+1));fold.addEventListener('click',()=>{const next=!ul.hidden;ul.hidden=next;fold.setAttribute('aria-expanded',String(!next));fold.setAttribute('aria-label',(next?'Expand ':'Collapse ')+task.id);fold.textContent=next?'▸':'▾';if(next){collapsedTasks.add(key);expandedTasks.delete(key);}else{collapsedTasks.delete(key);expandedTasks.add(key);}});li.append(line,ul);}else li.append(line);const row=document.createElement('button');row.type='button';row.className='task-row';row.setAttribute('aria-current',String(task.id===selectedTaskId));row.title=task.id;row.addEventListener('click',()=>{const deselect=selectedTaskId===task.id;selectedTaskId=deselect?null:task.id;eventTaskId=deselect?null:task.id;rememberSelection();clearEvents();renderAll();void loadEventHistory();});const label=element('span','task-id',text(value(task.id)+': '+value(task.title||task.input?.split(/\\r?\\n/).find(line=>line.trim())?.trim()||shortManagedId(task.id,selected()?.repository,'t'))));label.style.paddingLeft=Math.min(depth,4)*6+'px';row.append(label,element('span','badge '+value(task.state),text(task.state)));const tag=taskTagDisplay(selected(),task);if(tag){const badge=element('span','task-tag',text(tag));badge.title=(selected()?.tag?.description||'Task tag')+': '+task.tag;row.append(badge);}line.append(row);return li;}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { eventFor } from './events.js';
|
|
1
2
|
import { transitionJobEvent } from './scheduler.js';
|
|
2
3
|
import { deliverLocalMerge, localMergeEligible, requiresLocalGitMerge } from './local-git-merge.js';
|
|
3
4
|
import { errorMessage } from './observability.js';
|
|
4
5
|
/** Graph-selected durable local merge and completion; no checked-out worktree is ever changed. */
|
|
5
|
-
export async function reconcileLocalGitCompletion(store, finalState, headSha, noGitHub, projectPathForJob) {
|
|
6
|
+
export async function reconcileLocalGitCompletion(store, finalState, headSha, noGitHub, projectPathForJob, emit) {
|
|
6
7
|
if (localMergeEligible(finalState) && !requiresLocalGitMerge(finalState)) {
|
|
7
8
|
await transitionJobEvent(finalState, 'local.completed', { localCompletionAllowed: true });
|
|
8
9
|
await store.saveJob(finalState);
|
|
@@ -41,6 +42,13 @@ export async function reconcileLocalGitCompletion(store, finalState, headSha, no
|
|
|
41
42
|
}
|
|
42
43
|
finalState.updatedAt = new Date().toISOString();
|
|
43
44
|
await store.saveJob(finalState);
|
|
45
|
+
const merge = finalState.localMerge;
|
|
46
|
+
if (merge && emit) {
|
|
47
|
+
const succeeded = merge.status === 'succeeded';
|
|
48
|
+
const reason = merge.error?.includes('tracked or index changes') ? 'dirty_target_worktree' : 'merge_delivery_failed';
|
|
49
|
+
await emit(eventFor(finalState.id, 'merge', null, succeeded ? 'job.merge.succeeded' : 'job.merge.failed', succeeded ? 'succeeded' : 'failed', succeeded ? 'Local merge succeeded' : (merge.error ?? 'Local merge failed'), 'user', succeeded ? { mergedCommitSha: merge.mergedCommitSha, sourceSha: merge.sourceSha }
|
|
50
|
+
: { reason, message: merge.error, attempts: merge.attempts, retryable: true, nextAttemptAt: merge.nextAttemptAt }, { id: `job/${finalState.id}/task/merge/execution/attempt-${merge.attempts ?? 0}`, agent: 'relay' }));
|
|
51
|
+
}
|
|
44
52
|
});
|
|
45
53
|
}
|
|
46
54
|
if (finalState.localMerge?.status === 'succeeded') {
|
package/dist/pool.js
CHANGED
|
@@ -15,6 +15,9 @@ export function needsLifecycleReconciliation(job, now = Date.now()) {
|
|
|
15
15
|
// DRAFT create-only Jobs stay dormant until a caller explicitly creates work.
|
|
16
16
|
if (job.tasks.length === 0)
|
|
17
17
|
return false;
|
|
18
|
+
if (job.state === 'ACTIVE' && job.localMerge?.status === 'failed' &&
|
|
19
|
+
(!job.localMerge.nextAttemptAt || Date.parse(job.localMerge.nextAttemptAt) <= now))
|
|
20
|
+
return true;
|
|
18
21
|
if (job.executionMode === 'autonomous' && unblockNeedsNewPlan(job))
|
|
19
22
|
return true;
|
|
20
23
|
if (job.tasks.some(task => (task.state === 'QUEUED' && task.executionId === null)
|
package/dist/reconciler.js
CHANGED
|
@@ -190,7 +190,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
190
190
|
await this.ensureFinalReview(finalState, headSha);
|
|
191
191
|
await this.dispatchExecutableTasks(jobId);
|
|
192
192
|
finalState = await this.store.load(jobId);
|
|
193
|
-
if (await reconcileLocalGitCompletion(this.store, finalState, headSha, this.options.noGitHub === true, this.options.projectPathForJob))
|
|
193
|
+
if (await reconcileLocalGitCompletion(this.store, finalState, headSha, this.options.noGitHub === true, this.options.projectPathForJob, event => this.emit(event)))
|
|
194
194
|
return this.store.load(jobId);
|
|
195
195
|
await dispatchJobReconcile(finalState, { jobActive: finalState.state === 'ACTIVE', implementationComplete: implementationComplete(finalState), headAvailable: Boolean(headSha), deliveryReady: finalState.localMerge?.status === 'succeeded' }, async (actionId) => {
|
|
196
196
|
const outcomes = await this.workflowEffects.execute([actionId], { jobId: finalState.id, transitionKey: `reconcile:${headSha ?? 'no-head'}`, data: { headSha } });
|
package/package.json
CHANGED
|
@@ -34,3 +34,7 @@ Reviewer unavailable evidence is an execution failure (`task.failed`), not a blo
|
|
|
34
34
|
A host-mediated interactive invocation is a separate Relay-controlled wait. It requires an already-bound `interactiveInvocation` with status `awaiting_decision`. It is not permission for a Worker or Reviewer to choose a blocked terminal outcome.
|
|
35
35
|
|
|
36
36
|
A legacy Job-level `review: blocked` gate is reset to `pending` only after authoritative `task.unblocked` metadata exists on its bound Reviewer Task and the immutable review head still matches. The same Reviewer Task then resumes without creating a duplicate.
|
|
37
|
+
|
|
38
|
+
## Local Git merge delivery history
|
|
39
|
+
|
|
40
|
+
The Relay-owned merge effect persists the current `job.localMerge` receipt in SQLite. Every attempted delivery also emits `job.merge.failed` or `job.merge.succeeded` to the canonical events-bus, keyed by the Job and merge attempt. Failure `data` contains `reason`, exact `message`, `attempts`, `retryable`, and `nextAttemptAt`; success contains `mergedCommitSha` and `sourceSha`. A dirty checked-out target is `reason: dirty_target_worktree`. The dashboard displays the durable failure reason while `localMerge.status=failed`. After the retry deadline, periodic reconciliation must select ACTIVE Jobs with failed local merges even when all Tasks have succeeded. Historical failure events remain visible after successful recovery.
|