ework-daemon 0.4.38 → 0.4.39

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/opencode.ts +31 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-daemon",
3
- "version": "0.4.38",
3
+ "version": "0.4.39",
4
4
  "description": "Issue-driven AI development daemon. Spawns opencode subprocesses to resolve Gitea issues.",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/src/opencode.ts CHANGED
@@ -388,6 +388,25 @@ export class Engine {
388
388
  } catch { /* non-critical */ }
389
389
  }
390
390
 
391
+ private async webGateAllows(issue: Issue): Promise<{ allowed: boolean; reason: string }> {
392
+ const parts = issue.trackerScopeKey.split("/");
393
+ const owner = parts[0] ?? "";
394
+ const repo = parts.slice(1).join("/");
395
+ if (!owner || !repo) return { allowed: true, reason: "unparseable scope" };
396
+ const url = `${this.cfg.gitea.url}/api/v1/dispatch-state?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}&number=${encodeURIComponent(issue.trackerIssueId)}`;
397
+ try {
398
+ const resp = await fetch(url, { signal: AbortSignal.timeout(5000), headers: { Authorization: `token ${this.cfg.gitea.token}` } });
399
+ if (!resp.ok) return { allowed: false, reason: `web returned ${resp.status}` };
400
+ const data = await resp.json() as { dispatchOff?: boolean; aiStatus?: string };
401
+ if (data.dispatchOff) return { allowed: false, reason: "dispatch off" };
402
+ if (data.aiStatus === "halted" || data.aiStatus === "dispatch_off") return { allowed: false, reason: `ai_status=${data.aiStatus}` };
403
+ return { allowed: true, reason: "ok" };
404
+ } catch (err) {
405
+ log.warn(`engine: web gate query failed for ${issue.trackerScopeKey}#${issue.trackerIssueId}: ${(err as Error).message} — fail-closed (skipping)`);
406
+ return { allowed: false, reason: `web unreachable: ${(err as Error).message}` };
407
+ }
408
+ }
409
+
391
410
  setMaxConcurrent(n: number): void {
392
411
  if (!Number.isFinite(n) || n < 1) return;
393
412
  this.maxConcurrent = Math.floor(n);
@@ -1555,6 +1574,11 @@ export class Engine {
1555
1574
 
1556
1575
  for (const issue of ownedIssues) {
1557
1576
  try {
1577
+ const gate = await this.webGateAllows(issue);
1578
+ if (!gate.allowed) {
1579
+ log.info(`engine: observer — web gate blocked ${issue.trackerScopeKey}#${issue.trackerIssueId} (${gate.reason})`);
1580
+ continue;
1581
+ }
1558
1582
  await this.observeIssue(issue);
1559
1583
  } catch (err) {
1560
1584
  log.error(`engine: observer error for ${issue.trackerScopeKey}#${issue.trackerIssueId}:`, (err as Error).message);
@@ -1787,6 +1811,13 @@ export class Engine {
1787
1811
  const issue = await this.store.getIssue(session.issueId);
1788
1812
  if (!issue || issue.state === "closed") continue;
1789
1813
 
1814
+ const gate = await this.webGateAllows(issue);
1815
+ if (!gate.allowed) {
1816
+ log.info(`engine: recover — web gate blocked ${issue.trackerScopeKey}#${issue.trackerIssueId} (${gate.reason}), marking pending messages as skipped`);
1817
+ for (const m of msgs) await this.store.updateMessageStatus(m.id, "failed", `web gate: ${gate.reason}`);
1818
+ continue;
1819
+ }
1820
+
1790
1821
  const k = this.sessionKey(session, issue);
1791
1822
  if (this.running.has(k)) continue;
1792
1823