ework-daemon 0.4.30 → 0.4.31

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.31",
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,13 @@ export class Store {
260
260
  return rows.map(rowToSession);
261
261
  }
262
262
 
263
+ async listSessionsWithPid(): Promise<Array<{ id: string; opencodePid: number }>> {
264
+ const rows = await getDB().all<{ uid: string; opencode_pid: number }>(
265
+ "SELECT uid, opencode_pid FROM {{op_sessions}} WHERE opencode_pid IS NOT NULL"
266
+ );
267
+ return rows.map((r) => ({ id: r.uid, opencodePid: r.opencode_pid }));
268
+ }
269
+
263
270
  async listNonIdleSessions(): Promise<OpSession[]> {
264
271
  const rows = await getDB().all<SessionRow>("SELECT * FROM {{op_sessions}} WHERE state != 'idle'");
265
272
  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,33 @@ 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();
1720
+ } catch { return; }
1721
+ let killed = 0;
1722
+ for (const s of sessions) {
1723
+ let alive = false;
1724
+ try { process.kill(s.opencodePid, 0); alive = true; } catch { /* dead */ }
1725
+ if (!alive) continue;
1726
+
1727
+ let ppid = -1;
1728
+ try {
1729
+ const stat = readFileSync(`/proc/${s.opencodePid}/stat`, "utf8");
1730
+ const m = stat.match(/\)\s+\S+\s+(\d+)/);
1731
+ ppid = m ? Number(m[1]) : -1;
1732
+ } catch { continue; }
1733
+
1734
+ if (ppid === 1) {
1735
+ log.info(`engine: killing global orphan pid=${s.opencodePid} (PPID=1, session ${s.id})`);
1736
+ try { this.killProcessTree(s.opencodePid); } catch { /* dead */ }
1737
+ killed++;
1738
+ }
1739
+ }
1740
+ if (killed > 0) log.info(`engine: cleaned up ${killed} orphaned processes (PPID=1)`);
1741
+ }
1742
+
1714
1743
  // ─── API Methods ───
1715
1744
 
1716
1745
  async retryMessage(messageId: string): Promise<boolean> {