ework-daemon 0.4.38 → 0.4.40
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/package.json +1 -1
- package/src/opencode.ts +51 -0
package/package.json
CHANGED
package/src/opencode.ts
CHANGED
|
@@ -283,6 +283,7 @@ export interface EngineOptions {
|
|
|
283
283
|
daemonId: number;
|
|
284
284
|
takeover?: TakeoverStrategy;
|
|
285
285
|
backend?: RuntimeBackend;
|
|
286
|
+
gateChecker?: (issue: Issue) => Promise<{ allowed: boolean; reason: string }>;
|
|
286
287
|
}
|
|
287
288
|
|
|
288
289
|
function createDefaultBackend(cfg: Config): RuntimeBackend {
|
|
@@ -299,6 +300,7 @@ export class Engine {
|
|
|
299
300
|
private readonly daemonId: number;
|
|
300
301
|
private readonly takeover: TakeoverStrategy;
|
|
301
302
|
private readonly backend: RuntimeBackend;
|
|
303
|
+
private gateChecker: (issue: Issue) => Promise<{ allowed: boolean; reason: string }>;
|
|
302
304
|
private heartbeatTimer?: ReturnType<typeof setInterval>;
|
|
303
305
|
private maxConcurrent: number;
|
|
304
306
|
private maxConcurrentExplicit: boolean;
|
|
@@ -350,6 +352,7 @@ export class Engine {
|
|
|
350
352
|
this.daemonId = opts.daemonId;
|
|
351
353
|
this.takeover = opts.takeover ?? new RecloneStrategy(cfg);
|
|
352
354
|
this.backend = opts.backend ?? createDefaultBackend(cfg);
|
|
355
|
+
this.gateChecker = opts.gateChecker ?? ((issue: Issue) => this.webGateAllows(issue));
|
|
353
356
|
this.maxConcurrent = cfg.work.maxConcurrent;
|
|
354
357
|
this.maxConcurrentExplicit = cfg.work.maxConcurrentExplicit;
|
|
355
358
|
this.startGlobalObserver();
|
|
@@ -388,6 +391,25 @@ export class Engine {
|
|
|
388
391
|
} catch { /* non-critical */ }
|
|
389
392
|
}
|
|
390
393
|
|
|
394
|
+
private async webGateAllows(issue: Issue): Promise<{ allowed: boolean; reason: string }> {
|
|
395
|
+
const parts = issue.trackerScopeKey.split("/");
|
|
396
|
+
const owner = parts[0] ?? "";
|
|
397
|
+
const repo = parts.slice(1).join("/");
|
|
398
|
+
if (!owner || !repo) return { allowed: true, reason: "unparseable scope" };
|
|
399
|
+
const url = `${this.cfg.gitea.url}/api/v1/dispatch-state?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}&number=${encodeURIComponent(issue.trackerIssueId)}`;
|
|
400
|
+
try {
|
|
401
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(5000), headers: { Authorization: `token ${this.cfg.gitea.token}` } });
|
|
402
|
+
if (!resp.ok) return { allowed: false, reason: `web returned ${resp.status}` };
|
|
403
|
+
const data = await resp.json() as { dispatchOff?: boolean; aiStatus?: string };
|
|
404
|
+
if (data.dispatchOff) return { allowed: false, reason: "dispatch off" };
|
|
405
|
+
if (data.aiStatus === "halted" || data.aiStatus === "dispatch_off") return { allowed: false, reason: `ai_status=${data.aiStatus}` };
|
|
406
|
+
return { allowed: true, reason: "ok" };
|
|
407
|
+
} catch (err) {
|
|
408
|
+
log.warn(`engine: web gate query failed for ${issue.trackerScopeKey}#${issue.trackerIssueId}: ${(err as Error).message} — fail-closed (skipping)`);
|
|
409
|
+
return { allowed: false, reason: `web unreachable: ${(err as Error).message}` };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
391
413
|
setMaxConcurrent(n: number): void {
|
|
392
414
|
if (!Number.isFinite(n) || n < 1) return;
|
|
393
415
|
this.maxConcurrent = Math.floor(n);
|
|
@@ -1100,6 +1122,17 @@ export class Engine {
|
|
|
1100
1122
|
// ─── Process Manager ───
|
|
1101
1123
|
|
|
1102
1124
|
private async execProcess(k: string, session: OpSession, issue: Issue, msg: Message) {
|
|
1125
|
+
const gate = await this.gateChecker(issue);
|
|
1126
|
+
if (!gate.allowed) {
|
|
1127
|
+
log.info(`engine: execProcess blocked by web gate (${gate.reason}) for ${k}`);
|
|
1128
|
+
await this.store.updateMessageStatus(msg.id, "failed", `gate: ${gate.reason}`);
|
|
1129
|
+
this.running.delete(k);
|
|
1130
|
+
this.currentMessage.delete(k);
|
|
1131
|
+
this.currentModel.delete(k);
|
|
1132
|
+
void this.dequeueAfterGate(k, session, issue);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1103
1136
|
const gen = (this.generation.get(k) ?? 0) + 1;
|
|
1104
1137
|
this.generation.set(k, gen);
|
|
1105
1138
|
|
|
@@ -1373,6 +1406,12 @@ export class Engine {
|
|
|
1373
1406
|
void this.drainGlobalPending();
|
|
1374
1407
|
}
|
|
1375
1408
|
|
|
1409
|
+
private async dequeueAfterGate(k: string, session: OpSession, issue: Issue): Promise<void> {
|
|
1410
|
+
const next = await this.store.getNextPendingMessage(session.id);
|
|
1411
|
+
if (!next) return;
|
|
1412
|
+
await this.dequeueOrIdle(k, session, issue, next);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1376
1415
|
private async drainGlobalPending(): Promise<void> {
|
|
1377
1416
|
const slotsAvailable = this.maxConcurrent - this.running.size;
|
|
1378
1417
|
if (slotsAvailable <= 0) return;
|
|
@@ -1555,6 +1594,11 @@ export class Engine {
|
|
|
1555
1594
|
|
|
1556
1595
|
for (const issue of ownedIssues) {
|
|
1557
1596
|
try {
|
|
1597
|
+
const gate = await this.gateChecker(issue);
|
|
1598
|
+
if (!gate.allowed) {
|
|
1599
|
+
log.info(`engine: observer — web gate blocked ${issue.trackerScopeKey}#${issue.trackerIssueId} (${gate.reason})`);
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1558
1602
|
await this.observeIssue(issue);
|
|
1559
1603
|
} catch (err) {
|
|
1560
1604
|
log.error(`engine: observer error for ${issue.trackerScopeKey}#${issue.trackerIssueId}:`, (err as Error).message);
|
|
@@ -1787,6 +1831,13 @@ export class Engine {
|
|
|
1787
1831
|
const issue = await this.store.getIssue(session.issueId);
|
|
1788
1832
|
if (!issue || issue.state === "closed") continue;
|
|
1789
1833
|
|
|
1834
|
+
const gate = await this.webGateAllows(issue);
|
|
1835
|
+
if (!gate.allowed) {
|
|
1836
|
+
log.info(`engine: recover — web gate blocked ${issue.trackerScopeKey}#${issue.trackerIssueId} (${gate.reason}), marking pending messages as skipped`);
|
|
1837
|
+
for (const m of msgs) await this.store.updateMessageStatus(m.id, "failed", `web gate: ${gate.reason}`);
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1790
1841
|
const k = this.sessionKey(session, issue);
|
|
1791
1842
|
if (this.running.has(k)) continue;
|
|
1792
1843
|
|