ework-daemon 0.4.28 β†’ 0.4.29

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.28",
3
+ "version": "0.4.29",
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
@@ -88,6 +88,13 @@ async function boot() {
88
88
  process.on("SIGTERM", () => shutdown("SIGTERM"));
89
89
  process.on("SIGINT", () => shutdown("SIGINT"));
90
90
 
91
+ process.on("unhandledRejection", (reason) => {
92
+ log.error("unhandledRejection (continuing):", reason);
93
+ });
94
+ process.on("uncaughtException", (err) => {
95
+ log.error("uncaughtException (continuing):", err);
96
+ });
97
+
91
98
  const activeCount = (await store.listActiveIssues()).length;
92
99
  log.info(`\n${isTest ? "πŸ§ͺ" : "βœ…"} ework-daemon ready at http://${server.hostname}:${server.port}/webhook`);
93
100
  log.info(` Configure Gitea webhook to POST to /webhook/gitea`);
package/src/opencode.ts CHANGED
@@ -562,8 +562,8 @@ export class Engine {
562
562
  const tracker = this.getTracker(ref.trackerType);
563
563
  const scopeKey = tracker.formatScopeKey(ref.scope);
564
564
 
565
- if (this.paused && event.type === "issue_opened") {
566
- log.info(`engine: paused β€” skipping issue_opened for ${ref.trackerType}:${scopeKey}#${ref.issueId}`);
565
+ if (this.paused && (event.type === "issue_opened" || event.type === "comment_created")) {
566
+ log.info(`engine: paused β€” skipping ${event.type} for ${ref.trackerType}:${scopeKey}#${ref.issueId}`);
567
567
  return;
568
568
  }
569
569
 
@@ -796,13 +796,11 @@ export class Engine {
796
796
 
797
797
  // Kill all running processes for this issue's sessions
798
798
  const sessions = await this.store.getSessionsForIssue(issue.id);
799
+ let killedCount = 0;
799
800
  for (const session of sessions) {
800
801
  const k = this.sessionKey(session, issue);
801
- const proc = this.processes.get(k);
802
- if (proc) {
803
- this.stopping.add(k);
804
- try { this.killProcessTree(proc.pid, "SIGTERM"); } catch { /* already dead */ }
805
- }
802
+ const killed = await this.killSessionProcess(session, k);
803
+ if (killed) killedCount++;
806
804
  this.clearRuntimeState(k);
807
805
  const msgs = await this.store.getMessagesForSession(session.id);
808
806
  for (const msg of msgs) {
@@ -829,7 +827,7 @@ export class Engine {
829
827
  this.cloneUrls.delete(gcKey);
830
828
  this.senders.delete(gcKey);
831
829
 
832
- log.info(`engine: issue closed, ${sessions.length} sessions paused for ${scopeKey}#${ref.issueId}`);
830
+ log.info(`engine: issue closed, ${killedCount}/${sessions.length} sessions killed for ${scopeKey}#${ref.issueId}`);
833
831
  void tracker.updateStatus(ref, "completed");
834
832
  }
835
833
 
@@ -842,13 +840,11 @@ export class Engine {
842
840
  if (!issue) return;
843
841
  this.stopObserver(issue.id);
844
842
  const sessions = await this.store.getSessionsForIssue(issue.id);
843
+ let killedCount = 0;
845
844
  for (const session of sessions) {
846
845
  const k = this.sessionKey(session, issue);
847
- const proc = this.processes.get(k);
848
- if (proc) {
849
- this.stopping.add(k);
850
- try { this.killProcessTree(proc.pid, "SIGTERM"); } catch { /* already dead */ }
851
- }
846
+ const killed = await this.killSessionProcess(session, k);
847
+ if (killed) killedCount++;
852
848
  this.clearRuntimeState(k);
853
849
  const msgs = await this.store.getMessagesForSession(session.id);
854
850
  for (const msg of msgs) {
@@ -858,7 +854,7 @@ export class Engine {
858
854
  }
859
855
  await this.store.updateSession(session.id, { state: "idle", opencodePid: undefined });
860
856
  }
861
- log.info(`engine: issue halted, ${sessions.length} sessions killed for ${scopeKey}#${ref.issueId}`);
857
+ log.info(`engine: issue halted, ${killedCount}/${sessions.length} sessions killed for ${scopeKey}#${ref.issueId}`);
862
858
  try { await tracker.createComment(ref, "[system] ⏸️ AI processing halted by user."); } catch { /* tracker unavailable */ }
863
859
  }
864
860
 
@@ -878,6 +874,36 @@ export class Engine {
878
874
  this.generation.delete(k);
879
875
  }
880
876
 
877
+ private async killSessionProcess(session: OpSession, k: string): Promise<boolean> {
878
+ const handle = this.processes.get(k);
879
+ if (handle) {
880
+ this.stopping.add(k);
881
+ try { this.killProcessTree(handle.pid, "SIGTERM"); } catch { /* already dead */ }
882
+ this.processes.delete(k);
883
+ return true;
884
+ }
885
+
886
+ const pid = session.opencodePid;
887
+ if (!pid) return false;
888
+ try {
889
+ process.kill(pid, 0);
890
+ } catch {
891
+ return false;
892
+ }
893
+
894
+ log.info(`engine: killing orphaned pid=${pid} for ${k} (cross-restart)`);
895
+ this.stopping.add(k);
896
+ try {
897
+ this.killProcessTree(pid, "SIGTERM");
898
+ for (let i = 0; i < 30; i++) {
899
+ await new Promise(r => setTimeout(r, 100));
900
+ try { process.kill(pid, 0); } catch { break; }
901
+ }
902
+ try { process.kill(pid, "SIGKILL"); } catch { /* dead */ }
903
+ } catch { /* already dead */ }
904
+ return true;
905
+ }
906
+
881
907
  // ─── Preemptive Scheduler ───
882
908
 
883
909
  private async enqueueOrRun(session: OpSession, issue: Issue, prompt: string, sourceCommentId?: string, model?: string) {
@@ -1393,16 +1419,20 @@ export class Engine {
1393
1419
  }
1394
1420
 
1395
1421
  private async runObserverCycle() {
1396
- // Multi-machine: periodically release stale owners so we can adopt their
1397
- // work, and only iterate issues/sessions this daemon owns.
1398
1422
  try {
1399
1423
  await this.store.releaseDeadOwners(this.cfg.work.leaseTtlMs);
1400
1424
  } catch (err) {
1401
1425
  log.error("engine: releaseDeadOwners failed:", (err as Error).message);
1402
1426
  }
1403
1427
 
1404
- const ownedIssues = (await this.store.listOwnedIssues(this.daemonId))
1405
- .filter((i) => this.observedIssues.has(i.id));
1428
+ let ownedIssues;
1429
+ try {
1430
+ ownedIssues = (await this.store.listOwnedIssues(this.daemonId))
1431
+ .filter((i) => this.observedIssues.has(i.id));
1432
+ } catch (err) {
1433
+ log.error("engine: listOwnedIssues failed:", (err as Error).message);
1434
+ return;
1435
+ }
1406
1436
 
1407
1437
  for (const issue of ownedIssues) {
1408
1438
  try {