ework-daemon 0.2.0 → 0.2.2

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.2.0",
3
+ "version": "0.2.2",
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/index.ts CHANGED
@@ -44,8 +44,10 @@ async function boot() {
44
44
  const displayName = hostname();
45
45
  const internalEndpoint = `${config.daemon.host}:${config.daemon.port}`;
46
46
  const daemonId = await store.registerDaemon(displayName, internalEndpoint, config.work.capacity, config.work.leaseTtlMs);
47
+ const absorbed = await store.absorbSameHostDaemons(daemonId, displayName, internalEndpoint);
47
48
  const claimed = await store.claimAllOwnerless(daemonId);
48
49
  log.info(` daemon registered: id=${daemonId} (adopted orphan slot if id was reused)`);
50
+ if (absorbed > 0) log.info(` restart recovery: absorbed ${absorbed} issue(s) from previous incarnation`);
49
51
  if (claimed > 0) log.info(` first-boot migration: claimed ${claimed} previously-ownerless issue(s)`);
50
52
 
51
53
  const engine = new Engine(config, store, trackers, { daemonId });
package/src/op.ts CHANGED
@@ -378,6 +378,31 @@ export class Store {
378
378
  return ins.insertId;
379
379
  }
380
380
 
381
+ /**
382
+ * On restart, absorb ownership from a previous incarnation on the same host.
383
+ * Transfers issues + deletes stale daemon rows with matching display_name and
384
+ * endpoint. This fixes the boot-race where a killed daemon's lease hasn't
385
+ * expired yet, blocking the new daemon from claiming its own issues.
386
+ */
387
+ async absorbSameHostDaemons(
388
+ daemonId: number,
389
+ displayName: string,
390
+ endpoint: string,
391
+ ): Promise<number> {
392
+ const db = getDB();
393
+ const transferred = await db.run(
394
+ "UPDATE {{issues}} SET owner_daemon_id = ? WHERE owner_daemon_id IN (" +
395
+ "SELECT id FROM {{daemons}} WHERE display_name = ? AND internal_endpoint = ? AND id != ?" +
396
+ ")",
397
+ [daemonId, displayName, endpoint, daemonId],
398
+ );
399
+ await db.run(
400
+ "DELETE FROM {{daemons}} WHERE display_name = ? AND internal_endpoint = ? AND id != ?",
401
+ [displayName, endpoint, daemonId],
402
+ );
403
+ return transferred.changes;
404
+ }
405
+
381
406
  async heartbeat(daemonId: number): Promise<void> {
382
407
  await getDB().run(
383
408
  "UPDATE {{daemons}} SET last_heartbeat = ? WHERE id = ?",
package/src/opencode.ts CHANGED
@@ -481,10 +481,15 @@ export class Engine {
481
481
  // routing to the last-active lets the user continue without re-@mentioning,
482
482
  // while @mention switches to a different AI.
483
483
  const sessions = await this.store.getSessionsForIssue(issue.id);
484
- if (sessions.length === 0) return;
485
-
486
- const session = pickLastActive(sessions);
487
- if (!session) return;
484
+ let session: OpSession;
485
+ if (sessions.length === 0) {
486
+ log.info(`engine: no session for ${scopeKey}#${ref.issueId} — creating default "${this.cfg.bot.username}"`);
487
+ session = await this.store.createSession(issue.id, this.cfg.bot.username);
488
+ } else {
489
+ const picked = pickLastActive(sessions);
490
+ if (!picked) return;
491
+ session = picked;
492
+ }
488
493
  if (dirPath) {
489
494
  await this.store.updateSession(session.id, { workdir: dirPath });
490
495
  session.workdir = dirPath;