ework-daemon 0.4.35 → 0.4.36
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 +1 -1
- package/src/index.ts +1 -0
- package/src/opencode.ts +69 -1
- package/src/server.ts +6 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -73,6 +73,7 @@ async function boot() {
|
|
|
73
73
|
if (claimed > 0) log.info(` first-boot migration: claimed ${claimed} previously-ownerless issue(s)`);
|
|
74
74
|
|
|
75
75
|
const engine = new Engine(config, store, trackers, { daemonId });
|
|
76
|
+
engine.restorePausedState();
|
|
76
77
|
engine.startHeartbeat(config.work.heartbeatMs);
|
|
77
78
|
const server = createServer(config, store, engine, trackers);
|
|
78
79
|
|
package/src/opencode.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
-
import { mkdirSync, writeFileSync, readdirSync, existsSync, readFileSync } from "fs";
|
|
2
|
+
import { mkdirSync, writeFileSync, unlinkSync, 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";
|
|
@@ -413,12 +413,14 @@ export class Engine {
|
|
|
413
413
|
async pause(): Promise<void> {
|
|
414
414
|
this.paused = true;
|
|
415
415
|
await this.store.markDaemonStatus(this.daemonId, "drained");
|
|
416
|
+
this.persistPaused(true);
|
|
416
417
|
log.info(`engine: daemon ${this.daemonId} paused (drained) — new issues rejected, existing sessions continue`);
|
|
417
418
|
}
|
|
418
419
|
|
|
419
420
|
async resume(): Promise<void> {
|
|
420
421
|
this.paused = false;
|
|
421
422
|
await this.store.markDaemonStatus(this.daemonId, "active");
|
|
423
|
+
this.persistPaused(false);
|
|
422
424
|
log.info(`engine: daemon ${this.daemonId} resumed (active)`);
|
|
423
425
|
}
|
|
424
426
|
|
|
@@ -426,6 +428,72 @@ export class Engine {
|
|
|
426
428
|
return this.paused;
|
|
427
429
|
}
|
|
428
430
|
|
|
431
|
+
getRunningCount(): number {
|
|
432
|
+
return this.running.size;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Force-terminate ALL running sessions on this daemon. Returns kill count.
|
|
437
|
+
* Unlike pause() (which only rejects new work), this actively kills
|
|
438
|
+
* in-progress opencode/Pi processes.
|
|
439
|
+
*/
|
|
440
|
+
async haltAll(): Promise<number> {
|
|
441
|
+
let killed = 0;
|
|
442
|
+
const issues = await this.store.listOwnedIssues(this.daemonId);
|
|
443
|
+
for (const issue of issues) {
|
|
444
|
+
const sessions = await this.store.getSessionsForIssue(issue.id);
|
|
445
|
+
for (const session of sessions) {
|
|
446
|
+
if (session.state !== "running") continue;
|
|
447
|
+
const k = this.sessionKey(session, issue);
|
|
448
|
+
const wasKilled = await this.killSessionProcess(session, k);
|
|
449
|
+
if (wasKilled) killed++;
|
|
450
|
+
this.clearRuntimeState(k);
|
|
451
|
+
const msgs = await this.store.getMessagesForSession(session.id);
|
|
452
|
+
for (const msg of msgs) {
|
|
453
|
+
if (msg.status === "pending" || msg.status === "running") {
|
|
454
|
+
await this.store.updateMessageStatus(msg.id, "interrupted", "halted by admin");
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
await this.store.updateSession(session.id, { state: "idle", opencodePid: undefined });
|
|
458
|
+
const tracker = this.trackers.get(issue.trackerType);
|
|
459
|
+
if (tracker) {
|
|
460
|
+
try {
|
|
461
|
+
await tracker.createComment(
|
|
462
|
+
{ trackerType: issue.trackerType, scope: issue.trackerScope, issueId: issue.trackerIssueId },
|
|
463
|
+
`[system] ⏹️ Session **${session.name}** force-stopped by admin (halt-all).`
|
|
464
|
+
);
|
|
465
|
+
} catch { /* tracker unavailable */ }
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
this.paused = true;
|
|
470
|
+
await this.store.markDaemonStatus(this.daemonId, "drained");
|
|
471
|
+
this.persistPaused(true);
|
|
472
|
+
log.info(`engine: haltAll complete — ${killed} sessions killed, daemon ${this.daemonId} now paused`);
|
|
473
|
+
return killed;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private pausedFilePath(): string {
|
|
477
|
+
return join(this.cfg.opencode.baseWorkdir, "..", ".ework-paused.flag");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private persistPaused(paused: boolean): void {
|
|
481
|
+
try {
|
|
482
|
+
const p = this.pausedFilePath();
|
|
483
|
+
if (paused) writeFileSync(p, String(Date.now()));
|
|
484
|
+
else if (existsSync(p)) unlinkSync(p);
|
|
485
|
+
} catch { /* best-effort persistence */ }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
restorePausedState(): void {
|
|
489
|
+
try {
|
|
490
|
+
if (existsSync(this.pausedFilePath())) {
|
|
491
|
+
this.paused = true;
|
|
492
|
+
log.info(`engine: daemon ${this.daemonId} restored paused state from flag file`);
|
|
493
|
+
}
|
|
494
|
+
} catch { /* best-effort */ }
|
|
495
|
+
}
|
|
496
|
+
|
|
429
497
|
/**
|
|
430
498
|
* Ensure this engine owns the issue before doing work on it. Returns true
|
|
431
499
|
* if we own it (either already, or just claimed). Returns false if another
|
package/src/server.ts
CHANGED
|
@@ -82,6 +82,8 @@ export function createServer(
|
|
|
82
82
|
pending: status.pendingCount,
|
|
83
83
|
processes: status.processCount,
|
|
84
84
|
maxConcurrent: engine.getMaxConcurrent(),
|
|
85
|
+
paused: engine.isPaused(),
|
|
86
|
+
runningCount: engine.getRunningCount(),
|
|
85
87
|
observedIssues: status.observedIssues,
|
|
86
88
|
issues: (await store.listAllIssues()).length,
|
|
87
89
|
sessions: (await store.listAllSessions()).length,
|
|
@@ -214,6 +216,10 @@ export function createServer(
|
|
|
214
216
|
await engine.resume();
|
|
215
217
|
return json({ ok: true, paused: false });
|
|
216
218
|
}
|
|
219
|
+
if (pathname === "/api/admin/halt-all" && req.method === "POST") {
|
|
220
|
+
const killed = await engine.haltAll();
|
|
221
|
+
return json({ ok: true, killed, paused: true });
|
|
222
|
+
}
|
|
217
223
|
if (pathname === "/api/admin/max-concurrent" && req.method === "POST") {
|
|
218
224
|
const body = await req.json().catch(() => ({} as unknown));
|
|
219
225
|
const value = (body as { value?: unknown })?.value;
|