ework-daemon 0.4.24 → 0.4.26

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.24",
3
+ "version": "0.4.26",
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/config.ts CHANGED
@@ -36,6 +36,7 @@ export const configSchema = z.object({
36
36
  }),
37
37
  work: z.object({
38
38
  capacity: z.coerce.number().int().positive().default(4),
39
+ maxConcurrent: z.coerce.number().int().positive().default(4),
39
40
  heartbeatMs: z.coerce.number().int().positive().default(10_000),
40
41
  leaseTtlMs: z.coerce.number().int().positive().default(60_000),
41
42
  }),
@@ -85,8 +86,10 @@ const TEST_DEFAULTS = {
85
86
  };
86
87
 
87
88
  function readWorkSection() {
89
+ const capacity = process.env.WORK_DAEMON_CAPACITY ? Number(process.env.WORK_DAEMON_CAPACITY) : 4;
88
90
  return {
89
- capacity: process.env.WORK_DAEMON_CAPACITY ? Number(process.env.WORK_DAEMON_CAPACITY) : 4,
91
+ capacity,
92
+ maxConcurrent: process.env.WORK_MAX_CONCURRENT ? Number(process.env.WORK_MAX_CONCURRENT) : capacity,
90
93
  heartbeatMs: process.env.WORK_DAEMON_HEARTBEAT_MS ? Number(process.env.WORK_DAEMON_HEARTBEAT_MS) : 10_000,
91
94
  leaseTtlMs: process.env.WORK_DAEMON_LEASE_TTL_MS ? Number(process.env.WORK_DAEMON_LEASE_TTL_MS) : 60_000,
92
95
  };
package/src/op.ts CHANGED
@@ -299,6 +299,14 @@ export class Store {
299
299
  return row ? rowToMessage(row) : undefined;
300
300
  }
301
301
 
302
+ async getGlobalPendingMessages(limit: number): Promise<Message[]> {
303
+ const rows = await getDB().all<MessageRow>(
304
+ "SELECT * FROM {{messages}} WHERE status = 'pending' ORDER BY created_at ASC LIMIT ?",
305
+ [limit]
306
+ );
307
+ return rows.map(rowToMessage);
308
+ }
309
+
302
310
  async updateMessageStatus(id: string, status: Message["status"], error?: string): Promise<void> {
303
311
  const row = await getDB().get<MessageRow>("SELECT * FROM {{messages}} WHERE uid = ?", [id]);
304
312
  const attempts = row ? row.attempts + (status === "failed" ? 1 : 0) : 0;
package/src/opencode.ts CHANGED
@@ -889,6 +889,11 @@ export class Engine {
889
889
  return;
890
890
  }
891
891
 
892
+ if (this.running.size >= this.cfg.work.maxConcurrent) {
893
+ log.info(`engine: concurrency limit reached (${this.running.size}/${this.cfg.work.maxConcurrent}), message ${msg.id.slice(0, 8)} queued for ${k}`);
894
+ return;
895
+ }
896
+
892
897
  // Not running — execute directly
893
898
  await this.executeMessage(k, session, issue, msg);
894
899
  }
@@ -1267,13 +1272,38 @@ export class Engine {
1267
1272
  if (nextMsg) {
1268
1273
  const current = await this.store.getSession(session.id);
1269
1274
  if (current && current.state !== "idle") {
1270
- await this.dequeueOrIdle(k, current, issue, nextMsg);
1271
- return;
1275
+ if (this.running.size >= this.cfg.work.maxConcurrent) {
1276
+ log.info(`engine: concurrency limit (${this.running.size}/${this.cfg.work.maxConcurrent}), keeping msg ${nextMsg.id.slice(0, 8)} pending for ${k}`);
1277
+ } else {
1278
+ await this.dequeueOrIdle(k, current, issue, nextMsg);
1279
+ return;
1280
+ }
1272
1281
  }
1273
1282
  }
1274
1283
 
1275
1284
  this.clearRuntimeState(k);
1276
1285
  await this.store.updateSession(session.id, { state: "idle" });
1286
+
1287
+ void this.drainGlobalPending();
1288
+ }
1289
+
1290
+ private async drainGlobalPending(): Promise<void> {
1291
+ const slotsAvailable = this.cfg.work.maxConcurrent - this.running.size;
1292
+ if (slotsAvailable <= 0) return;
1293
+ const pending = await this.store.getGlobalPendingMessages(slotsAvailable);
1294
+ for (const msg of pending) {
1295
+ if (this.running.size >= this.cfg.work.maxConcurrent) break;
1296
+ const session = await this.store.getSession(msg.sessionId);
1297
+ if (!session || session.state === "running") continue;
1298
+ const issue = await this.store.getIssue(session.issueId);
1299
+ if (!issue || issue.state === "closed") continue;
1300
+ const k = this.sessionKey(session, issue);
1301
+ if (this.running.has(k)) continue;
1302
+ const won = await this.store.claimMessage(msg.id);
1303
+ if (!won) continue;
1304
+ log.info(`engine: drainGlobalPending picked up msg ${msg.id.slice(0, 8)} for ${k}`);
1305
+ await this.dequeueOrIdle(k, session, issue, msg);
1306
+ }
1277
1307
  }
1278
1308
 
1279
1309
  private async dequeueOrIdle(k: string, session: OpSession, issue: Issue, msg: Message) {