ework-daemon 0.4.30 → 0.4.32

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.30",
3
+ "version": "0.4.32",
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
@@ -260,6 +260,16 @@ export class Store {
260
260
  return rows.map(rowToSession);
261
261
  }
262
262
 
263
+ async listSessionsWithPid(daemonId: number): Promise<Array<{ id: string; opencodePid: number }>> {
264
+ const rows = await getDB().all<{ uid: string; opencode_pid: number }>(
265
+ `SELECT s.uid, s.opencode_pid FROM {{op_sessions}} s
266
+ INNER JOIN {{issues}} i ON i.uid = s.issue_id
267
+ WHERE s.opencode_pid IS NOT NULL AND i.owner_daemon_id = ?`,
268
+ [daemonId]
269
+ );
270
+ return rows.map((r) => ({ id: r.uid, opencodePid: r.opencode_pid }));
271
+ }
272
+
263
273
  async listNonIdleSessions(): Promise<OpSession[]> {
264
274
  const rows = await getDB().all<SessionRow>("SELECT * FROM {{op_sessions}} WHERE state != 'idle'");
265
275
  return rows.map(rowToSession);
package/src/opencode.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Database } from "bun:sqlite";
2
- import { mkdirSync, writeFileSync, readdirSync, existsSync } from "fs";
2
+ import { mkdirSync, writeFileSync, readdirSync, existsSync, readFileSync } from "fs";
3
3
  import { join, resolve, isAbsolute } from "path";
4
4
  import { homedir } from "os";
5
5
  import { log } from "./logger";
@@ -1617,6 +1617,8 @@ export class Engine {
1617
1617
  log.error("engine: releaseDeadOwners at boot failed:", (err as Error).message);
1618
1618
  }
1619
1619
 
1620
+ await this.cleanupGlobalOrphans();
1621
+
1620
1622
  // Multi-machine: recover ONLY this daemon's sessions. Other daemons own
1621
1623
  // the rest; touching their state would race them.
1622
1624
  const ownedSessions = await this.store.listOwnedSessions(this.daemonId);
@@ -1711,6 +1713,39 @@ export class Engine {
1711
1713
  }
1712
1714
  }
1713
1715
 
1716
+ private async cleanupGlobalOrphans(): Promise<void> {
1717
+ let sessions: Array<{ id: string; opencodePid: number }>;
1718
+ try {
1719
+ sessions = await this.store.listSessionsWithPid(this.daemonId);
1720
+ } catch { return; }
1721
+ const binaryName = this.cfg.opencode.binary.split("/").pop() ?? "opencode";
1722
+ let killed = 0;
1723
+ for (const s of sessions) {
1724
+ let alive = false;
1725
+ try { process.kill(s.opencodePid, 0); alive = true; } catch { /* dead */ }
1726
+ if (!alive) continue;
1727
+
1728
+ try {
1729
+ const cmdline = readFileSync(`/proc/${s.opencodePid}/cmdline`, "utf8");
1730
+ if (!cmdline.includes(binaryName) && !cmdline.includes("opencode") && !cmdline.includes("pi")) continue;
1731
+ } catch { continue; }
1732
+
1733
+ let ppid = -1;
1734
+ try {
1735
+ const stat = readFileSync(`/proc/${s.opencodePid}/stat`, "utf8");
1736
+ const m = stat.match(/\)\s+\S+\s+(\d+)/);
1737
+ ppid = m ? Number(m[1]) : -1;
1738
+ } catch { continue; }
1739
+
1740
+ if (ppid === 1) {
1741
+ log.info(`engine: killing orphaned pid=${s.opencodePid} (PPID=1, session ${s.id})`);
1742
+ try { this.killProcessTree(s.opencodePid); } catch { /* dead */ }
1743
+ killed++;
1744
+ }
1745
+ }
1746
+ if (killed > 0) log.info(`engine: cleaned up ${killed} orphaned processes (PPID=1)`);
1747
+ }
1748
+
1714
1749
  // ─── API Methods ───
1715
1750
 
1716
1751
  async retryMessage(messageId: string): Promise<boolean> {