@runuai/host 0.2.3 → 0.2.4
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/lib/github-tokens.ts +58 -2
- package/package.json +1 -1
package/lib/github-tokens.ts
CHANGED
|
@@ -238,15 +238,19 @@ async function runRefresh(taskId: string, userId: string): Promise<void> {
|
|
|
238
238
|
} catch (err) {
|
|
239
239
|
const reason = err instanceof Error ? err.message : String(err);
|
|
240
240
|
// A revoked / expired refresh token can't recover — drop it to force a
|
|
241
|
-
// clean re-grant through the OAuth flow.
|
|
241
|
+
// clean re-grant through the OAuth flow. Anything else is likely transient
|
|
242
|
+
// (network / cloud blip) — self-heal with a bounded retry.
|
|
242
243
|
if (/\b40[13]\b|revoked|invalid_grant|bad_refresh/i.test(reason)) {
|
|
243
244
|
deleteToken(userId);
|
|
245
|
+
} else {
|
|
246
|
+
scheduleGithubRetry(taskId, userId, 0);
|
|
244
247
|
}
|
|
245
248
|
authExpiredHandler?.(taskId, userId, reason);
|
|
246
249
|
}
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
export function clearRefresh(taskId: string): void {
|
|
253
|
+
clearGithubRetry(taskId);
|
|
250
254
|
const t = timers.get(taskId);
|
|
251
255
|
if (t) {
|
|
252
256
|
clearTimeout(t);
|
|
@@ -254,6 +258,50 @@ export function clearRefresh(taskId: string): void {
|
|
|
254
258
|
}
|
|
255
259
|
}
|
|
256
260
|
|
|
261
|
+
// --- self-heal: retry a failed gh setup with bounded backoff ----------------
|
|
262
|
+
// A gh setup/refresh can fail transiently — e.g. the cloud token-exchange fetch
|
|
263
|
+
// times out while the host is briefly unreachable (as happened when the bridge
|
|
264
|
+
// was flapping). Without a retry the task is stranded on a stale/invalid token
|
|
265
|
+
// until someone runs /retry-gh, because ensureSessions won't re-inject once a
|
|
266
|
+
// channel's sessions are started. Retry with bounded backoff; cleared on
|
|
267
|
+
// success (clearGithubRetry) and on task teardown (via clearRefresh).
|
|
268
|
+
const RETRY_BACKOFF_MS = [5_000, 20_000, 60_000, 180_000, 300_000];
|
|
269
|
+
const retryTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
270
|
+
|
|
271
|
+
function clearGithubRetry(taskId: string): void {
|
|
272
|
+
const t = retryTimers.get(taskId);
|
|
273
|
+
if (t) {
|
|
274
|
+
clearTimeout(t);
|
|
275
|
+
retryTimers.delete(taskId);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function scheduleGithubRetry(
|
|
280
|
+
taskId: string,
|
|
281
|
+
userId: string,
|
|
282
|
+
attempt: number,
|
|
283
|
+
): void {
|
|
284
|
+
clearGithubRetry(taskId);
|
|
285
|
+
if (attempt >= RETRY_BACKOFF_MS.length) {
|
|
286
|
+
console.warn(
|
|
287
|
+
`[github] task ${taskId}: gh setup still failing after ${attempt} attempts — giving up until /retry-gh`,
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const delay = RETRY_BACKOFF_MS[attempt] ?? 300_000;
|
|
292
|
+
const timer = setTimeout(() => {
|
|
293
|
+
retryTimers.delete(taskId);
|
|
294
|
+
void setupTaskGithub(taskId, userId, {}, attempt + 1);
|
|
295
|
+
}, delay);
|
|
296
|
+
timer.unref?.();
|
|
297
|
+
retryTimers.set(taskId, timer);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Test/diagnostic: is a gh-setup retry currently armed for this task? */
|
|
301
|
+
export function hasPendingGithubRetry(taskId: string): boolean {
|
|
302
|
+
return retryTimers.has(taskId);
|
|
303
|
+
}
|
|
304
|
+
|
|
257
305
|
/**
|
|
258
306
|
* Injectable seams for {@link setupTaskGithub} (mocked in tests so the decision
|
|
259
307
|
* logic needs neither the host DB nor docker). All default to the real impls.
|
|
@@ -283,6 +331,7 @@ export async function setupTaskGithub(
|
|
|
283
331
|
taskId: string,
|
|
284
332
|
userId: string,
|
|
285
333
|
deps: SetupTaskDeps = {},
|
|
334
|
+
attempt = 0,
|
|
286
335
|
): Promise<boolean> {
|
|
287
336
|
const _hasToken = deps.hasToken ?? hasToken;
|
|
288
337
|
const _request = deps.requestAccessToken ?? requestAccessToken;
|
|
@@ -297,6 +346,7 @@ export async function setupTaskGithub(
|
|
|
297
346
|
if (tok) {
|
|
298
347
|
await _inject(taskId, tok.accessToken);
|
|
299
348
|
_schedule(taskId, userId, tok.expiresAt);
|
|
349
|
+
clearGithubRetry(taskId);
|
|
300
350
|
console.log(`[github] task ${taskId}: injected user access token`);
|
|
301
351
|
return true;
|
|
302
352
|
}
|
|
@@ -304,6 +354,7 @@ export async function setupTaskGithub(
|
|
|
304
354
|
const pat = process.env.UAI_GH_PAT_FALLBACK;
|
|
305
355
|
if (pat) {
|
|
306
356
|
await _inject(taskId, pat);
|
|
357
|
+
clearGithubRetry(taskId);
|
|
307
358
|
console.log(`[github] task ${taskId}: using PAT fallback (no refresh)`);
|
|
308
359
|
return true;
|
|
309
360
|
}
|
|
@@ -313,14 +364,19 @@ export async function setupTaskGithub(
|
|
|
313
364
|
const reason = err instanceof Error ? err.message : String(err);
|
|
314
365
|
console.warn(`[github] task ${taskId}: gh setup failed: ${reason}`);
|
|
315
366
|
authExpiredHandler?.(taskId, userId, reason);
|
|
367
|
+
// Self-heal: a transient exchange/inject failure shouldn't strand the task
|
|
368
|
+
// on a stale token until a manual /retry-gh. Retry with bounded backoff.
|
|
369
|
+
scheduleGithubRetry(taskId, userId, attempt);
|
|
316
370
|
return false;
|
|
317
371
|
} finally {
|
|
318
372
|
setupInFlight.delete(taskId);
|
|
319
373
|
}
|
|
320
374
|
}
|
|
321
375
|
|
|
322
|
-
/** Test/maintenance hook: drop all scheduled refresh timers. */
|
|
376
|
+
/** Test/maintenance hook: drop all scheduled refresh + retry timers. */
|
|
323
377
|
export function clearAllRefreshTimers(): void {
|
|
324
378
|
for (const t of timers.values()) clearTimeout(t);
|
|
325
379
|
timers.clear();
|
|
380
|
+
for (const t of retryTimers.values()) clearTimeout(t);
|
|
381
|
+
retryTimers.clear();
|
|
326
382
|
}
|
package/package.json
CHANGED