ework-daemon 0.4.39 → 0.4.41

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-daemon",
3
- "version": "0.4.39",
3
+ "version": "0.4.41",
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/op.ts CHANGED
@@ -520,7 +520,7 @@ export class Store {
520
520
  `SELECT m.* FROM {{messages}} m
521
521
  INNER JOIN {{op_sessions}} s ON s.uid = m.session_id
522
522
  INNER JOIN {{issues}} i ON i.uid = s.issue_id
523
- WHERE i.owner_daemon_id = ? AND m.status IN ('pending', 'running')
523
+ WHERE i.owner_daemon_id = ? AND m.status IN ('pending', 'running', 'interrupted')
524
524
  ORDER BY m.created_at ASC`,
525
525
  [daemonId]
526
526
  );
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();
@@ -1119,6 +1122,17 @@ export class Engine {
1119
1122
  // ─── Process Manager ───
1120
1123
 
1121
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
+
1122
1136
  const gen = (this.generation.get(k) ?? 0) + 1;
1123
1137
  this.generation.set(k, gen);
1124
1138
 
@@ -1392,6 +1406,12 @@ export class Engine {
1392
1406
  void this.drainGlobalPending();
1393
1407
  }
1394
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
+
1395
1415
  private async drainGlobalPending(): Promise<void> {
1396
1416
  const slotsAvailable = this.maxConcurrent - this.running.size;
1397
1417
  if (slotsAvailable <= 0) return;
@@ -1574,7 +1594,7 @@ export class Engine {
1574
1594
 
1575
1595
  for (const issue of ownedIssues) {
1576
1596
  try {
1577
- const gate = await this.webGateAllows(issue);
1597
+ const gate = await this.gateChecker(issue);
1578
1598
  if (!gate.allowed) {
1579
1599
  log.info(`engine: observer — web gate blocked ${issue.trackerScopeKey}#${issue.trackerIssueId} (${gate.reason})`);
1580
1600
  continue;
@@ -1793,7 +1813,7 @@ export class Engine {
1793
1813
  // Reset running messages to pending
1794
1814
  for (const msg of stuck) {
1795
1815
  if (msg.status === "running") {
1796
- await this.store.updateMessageStatus(msg.id, "pending");
1816
+ await this.store.updateMessageStatus(msg.id, "interrupted");
1797
1817
  }
1798
1818
  }
1799
1819
 
@@ -1811,16 +1831,26 @@ export class Engine {
1811
1831
  const issue = await this.store.getIssue(session.issueId);
1812
1832
  if (!issue || issue.state === "closed") continue;
1813
1833
 
1814
- const gate = await this.webGateAllows(issue);
1834
+ const gate = await this.gateChecker(issue);
1815
1835
  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}`);
1836
+ log.info(`engine: recover — web gate blocked ${issue.trackerScopeKey}#${issue.trackerIssueId} (${gate.reason}), discarding queued (pending) messages`);
1837
+ for (const m of msgs) {
1838
+ if (m.status === "pending") {
1839
+ await this.store.updateMessageStatus(m.id, "failed", `web gate: ${gate.reason}`);
1840
+ }
1841
+ }
1818
1842
  continue;
1819
1843
  }
1820
1844
 
1821
1845
  const k = this.sessionKey(session, issue);
1822
1846
  if (this.running.has(k)) continue;
1823
1847
 
1848
+ for (const m of msgs) {
1849
+ if (m.status === "running") {
1850
+ await this.store.updateMessageStatus(m.id, "pending");
1851
+ }
1852
+ }
1853
+
1824
1854
  const first = msgs.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())[0]!;
1825
1855
 
1826
1856
  // Check if AI already replied before the crash — skip re-running if so