agents-relay 1.0.136 → 1.0.138
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 +2 -1
- package/dist/pool.js +7 -7
- package/dist/relayd.js +3 -3
- package/dist/store-github.js +76 -33
- package/dist/store.js +31 -1
- package/dist/usage.js +5 -1
- package/dist/worker-routing.js +3 -3
- package/package.json +1 -1
package/dist/dashboard-html.js
CHANGED
|
@@ -372,7 +372,8 @@ function usageProvider(provider){const card=element('section','usage-provider');
|
|
|
372
372
|
function formatCredit(value,currency){if(value===null||value===undefined||!Number.isFinite(Number(value)))return '—';const number=Number(value);if(currency==='USD')return '$'+number.toFixed(2);if(currency==='CNY')return '¥'+number.toFixed(2);return number.toFixed(2)+' '+currency;}
|
|
373
373
|
function creditProvider(provider){const row=element('section','usage-provider');row.append(element('h3','',text(provider.label)));const credits=provider.credits||{};if(!credits.available||!Array.isArray(credits.balances)||!credits.balances.length){row.append(element('p','empty',text(credits.reason||'Credit balance is unavailable.')));return row;}const balance=credits.balances[0];row.append(element('div','detail-value',text(formatCredit(balance.remaining,balance.currency))));return row;}
|
|
374
374
|
function githubBucket(label,bucket){const row=element('div','usage-row');const heading=element('div','usage-label');heading.append(element('span','',text(label)),element('span','',text(bucket.remaining+' remaining')));row.append(heading);row.append(element('div','subtle',text(bucket.used+' used / '+bucket.limit+' limit')));const reset=resetText({resetsAt:bucket.reset});if(reset)row.append(element('div','subtle',text(reset)));return row;}
|
|
375
|
-
function
|
|
375
|
+
function githubCounterLine(label,values){const entries=Object.entries(values||{}).sort((a,b)=>Number(b[1])-Number(a[1])).slice(0,8);return entries.length?element('div','subtle',text(label+' · '+entries.map(([key,count])=>key+' '+count).join(' · '))):null;}
|
|
376
|
+
function githubUsageCard(snapshot,status){const card=element('section','usage-provider');card.append(element('h3','',text('GitHub API')));const resources=snapshot?.resources||{};const bars=element('div','usage-bars');if(resources.core)bars.append(githubBucket('REST core',resources.core));if(resources.graphql)bars.append(githubBucket('GraphQL',resources.graphql));if(resources.search)bars.append(githubBucket('Search',resources.search));if(!bars.children.length)bars.append(element('p','empty',text('GitHub quota telemetry is unavailable.')));if(status?.limited)bars.append(element('div','error-state',text('Relay cooldown active'+(status.cooldownUntil?' until '+new Date(status.cooldownUntil).toLocaleString():''))));else bars.append(element('div','subtle',text('Relay cooldown inactive')));if(status?.calls)bars.append(element('div','subtle',text('Calls since start · GraphQL '+(status.calls.graphql||0)+' · REST '+(status.calls.rest||0)+' · quota '+(status.calls.rateLimit||0))));const operations=githubCounterLine('GraphQL operations',status?.graphql?.operations);if(operations)bars.append(operations);const contexts=githubCounterLine('GraphQL contexts',status?.graphql?.contexts);if(contexts)bars.append(contexts);if(status?.readPaths)bars.append(element('div','subtle',text('Read paths · workspace scans '+(status.readPaths.workspaceScans||0)+' · PR refreshes '+(status.readPaths.prRefreshes||0))));card.append(bars);return card;}
|
|
376
377
|
async function drawUsage(){try{const overview=await loadJSON('/api/usage');usageRoot.replaceChildren();usageMeta.textContent='Real account telemetry when available';const providers=Array.isArray(overview.providers)?overview.providers:[];const credits=Array.isArray(overview.credits)?overview.credits:[];const grid=element('div','usage-providers');if(overview.github||overview.githubStatus)grid.append(githubUsageCard(overview.github,overview.githubStatus));for(const provider of providers)grid.append(usageProvider(provider));for(const provider of credits)grid.append(creditProvider(provider));if(!grid.children.length){usageRoot.append(element('p','empty',text('Usage telemetry is unavailable.')));return;}usageRoot.append(grid);}catch(error){usageMeta.textContent='Unavailable';usageRoot.replaceChildren(element('p','error-state',text('Usage unavailable: '+error.message)));}}
|
|
377
378
|
async function drawRules(){rulesStatus.textContent='';try{const rules=await loadJSON('/api/model-router-rules');rulesText.value=typeof rules.text==='string'?rules.text:'';rulesMeta.textContent=rules.hash&&rules.hash!=='none'?'Loaded · '+rules.hash.slice(0,12):'No custom rules saved';}catch(error){rulesMeta.textContent='Unavailable';rulesStatus.textContent='Could not load rules: '+error.message;}}
|
|
378
379
|
async function saveRules(){rulesSave.disabled=true;rulesStatus.textContent='Saving…';try{const response=await fetch('/api/model-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;}}
|
package/dist/pool.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GitHubStore, listManagedGitHubJobs } from './store.js';
|
|
1
|
+
import { GitHubStore, listManagedGitHubJobs, withGitHubRequestContext } from './store.js';
|
|
2
2
|
import { eventFor } from './events.js';
|
|
3
3
|
const RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
4
4
|
export function compareJobPriority(a, b) { return RANK[a.priority ?? 'P2'] - RANK[b.priority ?? 'P2'] || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id); }
|
|
@@ -60,7 +60,7 @@ export class RepositoryWorkerPool {
|
|
|
60
60
|
for (const job of terminalManagedJobs(discovered)) {
|
|
61
61
|
await this.reconcileExclusive(job.id, async () => {
|
|
62
62
|
const store = new GitHubStore(this.client, job.repository, job.prNumber, this.trustedAuthors);
|
|
63
|
-
await this.makeReconciler(store, 0).reconcile(job.id);
|
|
63
|
+
await withGitHubRequestContext(this.client, 'pool.terminal-reconcile', () => this.makeReconciler(store, 0).reconcile(job.id));
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
const jobs = runnableManagedJobs(discovered);
|
|
@@ -75,7 +75,7 @@ export class RepositoryWorkerPool {
|
|
|
75
75
|
continue;
|
|
76
76
|
const allowance = Math.max(1, Math.min(capacity, ready || (expiredRunning ? 1 : 0) || 1));
|
|
77
77
|
await this.reconcileExclusive(job.id, async () => {
|
|
78
|
-
await this.makeReconciler(store, allowance).reconcile(job.id);
|
|
78
|
+
await withGitHubRequestContext(this.client, 'pool.discovered-reconcile', () => this.makeReconciler(store, allowance).reconcile(job.id));
|
|
79
79
|
});
|
|
80
80
|
capacity -= allowance;
|
|
81
81
|
}
|
|
@@ -92,7 +92,7 @@ export class RepositoryWorkerPool {
|
|
|
92
92
|
try {
|
|
93
93
|
do {
|
|
94
94
|
this.pending = false;
|
|
95
|
-
await this.reconcileDiscovered(await aggregateManagedGitHubJobs(this.client, this.repositories, this.trustedAuthors));
|
|
95
|
+
await this.reconcileDiscovered(await withGitHubRequestContext(this.client, 'pool.workspace-scan', () => aggregateManagedGitHubJobs(this.client, this.repositories, this.trustedAuthors)));
|
|
96
96
|
} while (this.pending);
|
|
97
97
|
}
|
|
98
98
|
finally {
|
|
@@ -109,7 +109,7 @@ export class RepositoryWorkerPool {
|
|
|
109
109
|
return existing;
|
|
110
110
|
const flight = this.reconcileExclusive(jobId, async () => {
|
|
111
111
|
const store = new GitHubStore(this.client, repository, prNumber, this.trustedAuthors);
|
|
112
|
-
await this.makeReconciler(store, Math.max(1, this.concurrency)).reconcile(jobId);
|
|
112
|
+
await withGitHubRequestContext(this.client, 'pool.targeted-reconcile', () => this.makeReconciler(store, Math.max(1, this.concurrency)).reconcile(jobId));
|
|
113
113
|
});
|
|
114
114
|
this.targetedReconcileFlights.set(key, flight);
|
|
115
115
|
try {
|
|
@@ -130,9 +130,9 @@ export class RepositoryWorkerPool {
|
|
|
130
130
|
}
|
|
131
131
|
const store = new GitHubStore(this.client, target.repository, target.prNumber, this.trustedAuthors);
|
|
132
132
|
const reconciler = this.makeReconciler(store, Math.max(1, this.concurrency));
|
|
133
|
-
await reconciler.handleEvent(event);
|
|
133
|
+
await withGitHubRequestContext(this.client, `pool.event.${event.type}`, () => reconciler.handleEvent(event));
|
|
134
134
|
if (['task.completed', 'task.failed', 'task.blocked', 'task.cancelled'].includes(event.type))
|
|
135
|
-
await reconciler.reconcile(event.job_id);
|
|
135
|
+
await withGitHubRequestContext(this.client, `pool.event-reconcile.${event.type}`, () => reconciler.reconcile(event.job_id));
|
|
136
136
|
}
|
|
137
137
|
async watch(bus) {
|
|
138
138
|
return bus.subscribeAll(async (event) => {
|
package/dist/relayd.js
CHANGED
|
@@ -9,7 +9,7 @@ import { serveDashboard } from './dashboard.js';
|
|
|
9
9
|
import { NatsEventBus } from './events.js';
|
|
10
10
|
import { RepositoryWorkerPool, aggregateManagedGitHubJobs } from './pool.js';
|
|
11
11
|
import { Reconciler } from './reconciler.js';
|
|
12
|
-
import { dashboardManagedJobs, GitHubStore, InMemoryStore, loadManagedGitHubPullRequestGraphql } from './store.js';
|
|
12
|
+
import { dashboardManagedJobs, GitHubStore, InMemoryStore, loadManagedGitHubPullRequestGraphql, withGitHubRequestContext } from './store.js';
|
|
13
13
|
import { githubAuthContext } from './github-auth.js';
|
|
14
14
|
import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
|
|
15
15
|
import { dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
@@ -119,7 +119,7 @@ export async function runDaemon(argv) {
|
|
|
119
119
|
return cachedOverviews;
|
|
120
120
|
try {
|
|
121
121
|
githubReadPaths.workspaceScans += 1;
|
|
122
|
-
const refreshed = await aggregateManagedGitHubJobs(client, scanRepositories, trusted);
|
|
122
|
+
const refreshed = await withGitHubRequestContext(client, 'relayd.workspace-refresh', () => aggregateManagedGitHubJobs(client, scanRepositories, trusted));
|
|
123
123
|
const scanned = new Set(scanRepositories);
|
|
124
124
|
cachedOverviews = dashboardManagedJobs([...cachedOverviews.filter(item => !scanned.has(item.job.repository)), ...refreshed]);
|
|
125
125
|
try {
|
|
@@ -152,7 +152,7 @@ export async function runDaemon(argv) {
|
|
|
152
152
|
return existing;
|
|
153
153
|
const flight = (async () => {
|
|
154
154
|
githubReadPaths.prRefreshes += 1;
|
|
155
|
-
const refreshed = await loadManagedGitHubPullRequestGraphql(client, repository, prNumber, trusted);
|
|
155
|
+
const refreshed = await withGitHubRequestContext(client, 'relayd.pr-refresh', () => loadManagedGitHubPullRequestGraphql(client, repository, prNumber, trusted));
|
|
156
156
|
cachedOverviews = cachedOverviews.filter(item => item.job.repository !== repository || item.job.prNumber !== prNumber);
|
|
157
157
|
cachedOverviews.push(...refreshed);
|
|
158
158
|
cachedOverviews = dashboardManagedJobs(cachedOverviews);
|
package/dist/store-github.js
CHANGED
|
@@ -7,6 +7,10 @@ export class GitHubStore {
|
|
|
7
7
|
repository;
|
|
8
8
|
prNumber;
|
|
9
9
|
trustedAuthors;
|
|
10
|
+
prNodeCache = null;
|
|
11
|
+
commentsCache = null;
|
|
12
|
+
commentsFlight = null;
|
|
13
|
+
clearReadSnapshot() { this.prNodeCache = null; this.commentsCache = null; this.commentsFlight = null; }
|
|
10
14
|
constructor(client, repository, prNumber, trustedAuthors = new Set()) {
|
|
11
15
|
this.client = client;
|
|
12
16
|
this.repository = repository;
|
|
@@ -49,43 +53,64 @@ export class GitHubStore {
|
|
|
49
53
|
return await this.client.graphql(query, variables);
|
|
50
54
|
}
|
|
51
55
|
async prNode() {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
if (this.prNodeCache)
|
|
57
|
+
return this.prNodeCache;
|
|
58
|
+
const flight = (async () => {
|
|
59
|
+
const { owner, name } = this.repoParts();
|
|
60
|
+
const data = await this.graphql('query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){id state merged mergedAt closedAt body headRefName headRefOid}}}', { owner, name, number: this.prNumber });
|
|
61
|
+
const repository = data.repository;
|
|
62
|
+
const pr = repository?.pullRequest;
|
|
63
|
+
if (!pr)
|
|
64
|
+
throw new Error(`Pull request #${this.prNumber} not found in ${this.repository}`);
|
|
65
|
+
return pr;
|
|
66
|
+
})();
|
|
67
|
+
this.prNodeCache = flight;
|
|
68
|
+
try {
|
|
69
|
+
return await flight;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (this.prNodeCache === flight)
|
|
73
|
+
this.prNodeCache = null;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
59
76
|
}
|
|
60
77
|
async pullRequestBody() {
|
|
61
78
|
const pr = await this.prNode();
|
|
62
79
|
return typeof pr.body === 'string' ? pr.body : '';
|
|
63
80
|
}
|
|
64
81
|
async list() {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
updatedAt: String(value.updatedAt ?? ''),
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
82
|
+
if (this.commentsCache)
|
|
83
|
+
return this.overlayTaskWrites(this.commentsCache);
|
|
84
|
+
if (this.commentsFlight)
|
|
85
|
+
return this.overlayTaskWrites(await this.commentsFlight);
|
|
86
|
+
const flight = (async () => {
|
|
87
|
+
const { owner, name } = this.repoParts();
|
|
88
|
+
const all = [];
|
|
89
|
+
let cursor = null;
|
|
90
|
+
do {
|
|
91
|
+
const data = await this.graphql('query($owner:String!,$name:String!,$number:Int!,$cursor:String){repository(owner:$owner,name:$name){pullRequest(number:$number){comments(first:100,after:$cursor){nodes{id databaseId url body updatedAt author{login}} pageInfo{hasNextPage endCursor}}}}}', { owner, name, number: this.prNumber, cursor });
|
|
92
|
+
const repository = data.repository;
|
|
93
|
+
const pr = repository?.pullRequest;
|
|
94
|
+
const comments = pr?.comments;
|
|
95
|
+
const nodes = Array.isArray(comments?.nodes) ? comments.nodes : [];
|
|
96
|
+
for (const value of nodes) {
|
|
97
|
+
const author = value.author;
|
|
98
|
+
all.push({ id: Number(value.databaseId ?? 0), nodeId: typeof value.id === 'string' ? value.id : undefined, url: typeof value.url === 'string' ? value.url : undefined, body: String(value.body ?? ''), updatedAt: String(value.updatedAt ?? ''), authorLogin: typeof author?.login === 'string' ? author.login : null });
|
|
99
|
+
}
|
|
100
|
+
const pageInfo = comments?.pageInfo;
|
|
101
|
+
cursor = pageInfo?.hasNextPage === true && typeof pageInfo.endCursor === 'string' ? pageInfo.endCursor : null;
|
|
102
|
+
} while (cursor);
|
|
103
|
+
return all;
|
|
104
|
+
})();
|
|
105
|
+
this.commentsFlight = flight;
|
|
106
|
+
try {
|
|
107
|
+
this.commentsCache = await flight;
|
|
108
|
+
return this.overlayTaskWrites(this.commentsCache);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
if (this.commentsFlight === flight)
|
|
112
|
+
this.commentsFlight = null;
|
|
113
|
+
}
|
|
89
114
|
}
|
|
90
115
|
async comments() { return this.list(); }
|
|
91
116
|
async pullRequestState() {
|
|
@@ -104,12 +129,18 @@ export class GitHubStore {
|
|
|
104
129
|
});
|
|
105
130
|
if (result.merged !== true)
|
|
106
131
|
throw new Error(typeof result.message === 'string' ? result.message : 'Pull request #' + this.prNumber + ' was not merged');
|
|
132
|
+
this.prNodeCache = null;
|
|
107
133
|
}
|
|
108
134
|
async closePullRequest() {
|
|
109
135
|
await this.client.request('repos/' + this.repository + '/pulls/' + this.prNumber, { method: 'PATCH', body: JSON.stringify({ state: 'closed' }) });
|
|
136
|
+
this.prNodeCache = null;
|
|
110
137
|
}
|
|
111
138
|
async updatePullRequestBody(body) {
|
|
112
139
|
await this.client.request('repos/' + this.repository + '/pulls/' + this.prNumber, { method: 'PATCH', body: JSON.stringify({ body }) });
|
|
140
|
+
if (this.prNodeCache) {
|
|
141
|
+
const current = await this.prNodeCache;
|
|
142
|
+
this.prNodeCache = Promise.resolve({ ...current, body });
|
|
143
|
+
}
|
|
113
144
|
}
|
|
114
145
|
async pullRequestHead() {
|
|
115
146
|
const pr = await this.prNode();
|
|
@@ -139,6 +170,7 @@ export class GitHubStore {
|
|
|
139
170
|
const job = jobs.find(x => x.id === jobId);
|
|
140
171
|
if (!job) {
|
|
141
172
|
const conflicting = bodyJob ?? commentJobs[0];
|
|
173
|
+
this.clearReadSnapshot();
|
|
142
174
|
if (conflicting)
|
|
143
175
|
throw new Error(`JOB_ID_MISMATCH: PR #${this.prNumber} is bound to ${conflicting.id}, not ${jobId}`);
|
|
144
176
|
throw new Error(`JOB_MARKER_MISSING: Managed GitHub job ${jobId} has no durable ${JOB_MARKER} marker in PR #${this.prNumber}`);
|
|
@@ -188,7 +220,7 @@ export class GitHubStore {
|
|
|
188
220
|
const data = await this.graphql('mutation($subjectId:ID!,$body:String!){addComment(input:{subjectId:$subjectId,body:$body}){commentEdge{node{id databaseId url body updatedAt author{login}}}}}', { subjectId: pr.id, body });
|
|
189
221
|
const edge = data.addComment?.commentEdge;
|
|
190
222
|
const node = edge?.node;
|
|
191
|
-
|
|
223
|
+
const persisted = {
|
|
192
224
|
id: Number(node?.databaseId ?? 0),
|
|
193
225
|
nodeId: typeof node?.id === 'string' ? node.id : undefined,
|
|
194
226
|
url: typeof node?.url === 'string' ? node.url : undefined,
|
|
@@ -196,13 +228,16 @@ export class GitHubStore {
|
|
|
196
228
|
updatedAt: typeof node?.updatedAt === 'string' ? node.updatedAt : new Date().toISOString(),
|
|
197
229
|
authorLogin: typeof node?.author?.login === 'string' ? String((node?.author).login) : [...this.trustedAuthors][0] ?? null,
|
|
198
230
|
};
|
|
231
|
+
if (this.commentsCache)
|
|
232
|
+
this.commentsCache.push(persisted);
|
|
233
|
+
return persisted;
|
|
199
234
|
}
|
|
200
235
|
async updateComment(comment, body) {
|
|
201
236
|
if (!comment.nodeId)
|
|
202
237
|
throw new Error('GitHub comment node id is required for GraphQL update');
|
|
203
238
|
const data = await this.graphql('mutation($id:ID!,$body:String!){updateIssueComment(input:{id:$id,body:$body}){issueComment{id databaseId url body updatedAt author{login}}}}', { id: comment.nodeId, body });
|
|
204
239
|
const node = data.updateIssueComment?.issueComment;
|
|
205
|
-
|
|
240
|
+
const persisted = {
|
|
206
241
|
id: Number(node?.databaseId ?? comment.id),
|
|
207
242
|
nodeId: typeof node?.id === 'string' ? node.id : comment.nodeId,
|
|
208
243
|
url: typeof node?.url === 'string' ? node.url : comment.url,
|
|
@@ -210,11 +245,19 @@ export class GitHubStore {
|
|
|
210
245
|
updatedAt: typeof node?.updatedAt === 'string' ? node.updatedAt : new Date().toISOString(),
|
|
211
246
|
authorLogin: typeof node?.author?.login === 'string' ? String((node?.author).login) : comment.authorLogin,
|
|
212
247
|
};
|
|
248
|
+
if (this.commentsCache) {
|
|
249
|
+
const index = this.commentsCache.findIndex(item => item.nodeId === comment.nodeId || (item.id > 0 && item.id === comment.id));
|
|
250
|
+
if (index >= 0)
|
|
251
|
+
this.commentsCache[index] = persisted;
|
|
252
|
+
}
|
|
253
|
+
return persisted;
|
|
213
254
|
}
|
|
214
255
|
async deleteComment(comment) {
|
|
215
256
|
if (!comment.nodeId)
|
|
216
257
|
throw new Error('GitHub comment node id is required for GraphQL delete');
|
|
217
258
|
await this.graphql('mutation($id:ID!){deleteIssueComment(input:{id:$id}){clientMutationId}}', { id: comment.nodeId });
|
|
259
|
+
if (this.commentsCache)
|
|
260
|
+
this.commentsCache = this.commentsCache.filter(item => item.nodeId !== comment.nodeId && !(item.id > 0 && item.id === comment.id));
|
|
218
261
|
}
|
|
219
262
|
async saveJob(job) {
|
|
220
263
|
const currentBody = await this.pullRequestBody();
|
package/dist/store.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createSign } from 'node:crypto';
|
|
3
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
4
|
import { parseAgent, parseJob, parseTask, renderAgentComment, renderJobComment, renderTaskComment, AGENT_MARKER, JOB_MARKER, TASK_MARKER } from './markers.js';
|
|
4
5
|
import { isTaskForJob, sameTaskDefinition } from './store-task-definition.js';
|
|
5
6
|
import { parsePackagePublicationMarker } from './github-webhook.js';
|
|
@@ -97,6 +98,7 @@ export class InMemoryStore {
|
|
|
97
98
|
return [...this.values.values()].map(parseTask).filter((x) => x !== null).filter(task => isTaskForJob(task, jobId, jobs.length === 1));
|
|
98
99
|
}
|
|
99
100
|
}
|
|
101
|
+
export async function withGitHubRequestContext(client, context, work) { return client.withRequestContext ? client.withRequestContext(context, work) : work(); }
|
|
100
102
|
async function githubGraphql(client, query, variables) {
|
|
101
103
|
if (!client.graphql)
|
|
102
104
|
throw new Error('GitHub GraphQL client is required');
|
|
@@ -248,12 +250,36 @@ export class GhApiClient {
|
|
|
248
250
|
rateLimitFlight = null;
|
|
249
251
|
rateLimitCacheTtlMs = 5 * 60_000;
|
|
250
252
|
apiDiagnostics = { rest: 0, graphql: 0, rateLimit: 0 };
|
|
253
|
+
graphqlDiagnostics = { operations: {}, contexts: {} };
|
|
254
|
+
requestContext = new AsyncLocalStorage();
|
|
251
255
|
constructor(appAuth = null) {
|
|
252
256
|
this.appAuth = appAuth;
|
|
253
257
|
}
|
|
258
|
+
async withRequestContext(context, work) { return await this.requestContext.run(context, work); }
|
|
259
|
+
graphqlOperation(query) {
|
|
260
|
+
if (/mutation\s*\([^)]*\)\s*\{addComment\(/.test(query))
|
|
261
|
+
return 'comment.add';
|
|
262
|
+
if (/updateIssueComment\(/.test(query))
|
|
263
|
+
return 'comment.update';
|
|
264
|
+
if (/deleteIssueComment\(/.test(query))
|
|
265
|
+
return 'comment.delete';
|
|
266
|
+
if (/viewer\s*\{login/.test(query))
|
|
267
|
+
return 'viewer.login';
|
|
268
|
+
if (/comments\(first:100,after:\$cursor\)/.test(query))
|
|
269
|
+
return 'pr.comments';
|
|
270
|
+
if (/comments\(last:100\)/.test(query))
|
|
271
|
+
return 'pr.snapshot';
|
|
272
|
+
if (/headRefName/.test(query) && /headRefOid/.test(query))
|
|
273
|
+
return 'pr.node';
|
|
274
|
+
if (/repository\(owner:\$owner,name:\$name\)\{id\}/.test(query))
|
|
275
|
+
return 'repository.id';
|
|
276
|
+
if (/pullRequests\(/.test(query))
|
|
277
|
+
return 'repository.pull-requests';
|
|
278
|
+
return /^\s*mutation/.test(query) ? 'mutation.other' : 'query.other';
|
|
279
|
+
}
|
|
254
280
|
rateLimitStatus() {
|
|
255
281
|
const limited = Date.now() < this.cooldownUntil;
|
|
256
|
-
return { limited, cooldownUntil: limited ? new Date(this.cooldownUntil).toISOString() : null, calls: { ...this.apiDiagnostics } };
|
|
282
|
+
return { limited, cooldownUntil: limited ? new Date(this.cooldownUntil).toISOString() : null, calls: { ...this.apiDiagnostics }, graphql: { operations: { ...this.graphqlDiagnostics.operations }, contexts: { ...this.graphqlDiagnostics.contexts } } };
|
|
257
283
|
}
|
|
258
284
|
async rateLimitSnapshot(force = false) {
|
|
259
285
|
const now = Date.now();
|
|
@@ -368,6 +394,10 @@ export class GhApiClient {
|
|
|
368
394
|
async graphql(query, variables = {}) {
|
|
369
395
|
this.assertAvailable();
|
|
370
396
|
this.apiDiagnostics.graphql += 1;
|
|
397
|
+
const operation = this.graphqlOperation(query);
|
|
398
|
+
const context = this.requestContext.getStore() ?? 'unscoped';
|
|
399
|
+
this.graphqlDiagnostics.operations[operation] = (this.graphqlDiagnostics.operations[operation] ?? 0) + 1;
|
|
400
|
+
this.graphqlDiagnostics.contexts[context] = (this.graphqlDiagnostics.contexts[context] ?? 0) + 1;
|
|
371
401
|
const payload = JSON.stringify({ query, variables });
|
|
372
402
|
const stdout = await this.spawnGh(['api', 'graphql', '--input', '-'], payload, await this.commandEnv());
|
|
373
403
|
const parsed = JSON.parse(stdout);
|
package/dist/usage.js
CHANGED
|
@@ -51,7 +51,11 @@ export class BrowserWorkspaceUsageReader {
|
|
|
51
51
|
const idle = numberOrNull(raw.idle);
|
|
52
52
|
const leased = numberOrNull(raw.leased);
|
|
53
53
|
const poolSize = numberOrNull(raw.poolSize);
|
|
54
|
-
|
|
54
|
+
// `available` is allocatable capacity, not the number of currently idle tabs.
|
|
55
|
+
// Keep accepting the old boolean probe shape while callers transition.
|
|
56
|
+
const available = typeof raw.available === 'number' && Number.isFinite(raw.available) && raw.available >= 0
|
|
57
|
+
? raw.available
|
|
58
|
+
: typeof raw.available === 'boolean' ? (raw.available ? 1 : 0) : null;
|
|
55
59
|
if (idle !== null || leased !== null || poolSize !== null || available !== null) {
|
|
56
60
|
finish({ name, poolSize, idle, leased, available, observedAt: rawObservedAt, reason: null });
|
|
57
61
|
return;
|
package/dist/worker-routing.js
CHANGED
|
@@ -83,7 +83,7 @@ export function satisfiesWorker(candidate, requirements) {
|
|
|
83
83
|
return false;
|
|
84
84
|
if (requirements.runtime && candidate.runtime !== requirements.runtime)
|
|
85
85
|
return false;
|
|
86
|
-
if (candidate.adapter === 'chatgpt' && candidate.browserWorkspaceCapacity?.
|
|
86
|
+
if (candidate.adapter === 'chatgpt' && candidate.browserWorkspaceCapacity?.available === 0)
|
|
87
87
|
return false;
|
|
88
88
|
return true;
|
|
89
89
|
}
|
|
@@ -120,8 +120,8 @@ export function selectWorker(candidates, requirements) {
|
|
|
120
120
|
return { candidate: selected, selection: selection(selected, 'worker-router:fallback') };
|
|
121
121
|
}
|
|
122
122
|
function noEligibleWorkerReason(candidates) {
|
|
123
|
-
if (candidates.some(candidate => candidate.adapter === 'chatgpt' && candidate.browserWorkspaceCapacity?.
|
|
124
|
-
return 'No eligible worker: ChatGPT Browser Workspace has no
|
|
123
|
+
if (candidates.some(candidate => candidate.adapter === 'chatgpt' && candidate.browserWorkspaceCapacity?.available === 0)) {
|
|
124
|
+
return 'No eligible worker: ChatGPT Browser Workspace has no allocatable capacity.';
|
|
125
125
|
}
|
|
126
126
|
return 'No eligible worker satisfies the authoritative worker constraints';
|
|
127
127
|
}
|