ework-daemon 0.4.68 → 0.4.70
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/db.ts +8 -0
- package/src/op.ts +17 -0
- package/src/opencode.ts +30 -7
package/package.json
CHANGED
package/src/db.ts
CHANGED
|
@@ -524,6 +524,14 @@ async function runMigrations(db: AsyncDatabase): Promise<void> {
|
|
|
524
524
|
: "owner_daemon_id BIGINT NULL"
|
|
525
525
|
);
|
|
526
526
|
|
|
527
|
+
// issues.reset_at — last consumed web-side session-reset marker (ms epoch).
|
|
528
|
+
// Lets the issue-page 🔄 button force a fresh AI session on next trigger.
|
|
529
|
+
await ensureColumn(
|
|
530
|
+
tIssues,
|
|
531
|
+
"reset_at",
|
|
532
|
+
sqlite ? "reset_at INTEGER NOT NULL DEFAULT 0" : "reset_at BIGINT NOT NULL DEFAULT 0"
|
|
533
|
+
);
|
|
534
|
+
|
|
527
535
|
// op_sessions runtime-state columns (previously in-memory Maps; now persisted
|
|
528
536
|
// so a restarted daemon can recover the nudge/generation state).
|
|
529
537
|
await ensureColumn(tSessions, "last_output_at", "last_output_at VARCHAR(40)");
|
package/src/op.ts
CHANGED
|
@@ -447,6 +447,23 @@ export class Store {
|
|
|
447
447
|
);
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
+
// reset_at stores the last consumed web reset marker; newer markers clear session pointers.
|
|
451
|
+
async getIssueResetAt(issueUid: string): Promise<number> {
|
|
452
|
+
const row = await getDB().get<{ reset_at: number | null }>(
|
|
453
|
+
"SELECT reset_at FROM {{issues}} WHERE uid = ?",
|
|
454
|
+
[issueUid]
|
|
455
|
+
);
|
|
456
|
+
return row?.reset_at ?? 0;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async setIssueResetAt(issueUid: string, ms: number): Promise<void> {
|
|
460
|
+
await getDB().run("UPDATE {{issues}} SET reset_at = ? WHERE uid = ?", [ms, issueUid]);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async clearSessionPointers(issueUid: string): Promise<void> {
|
|
464
|
+
await getDB().run("UPDATE {{op_sessions}} SET opencode_session_id = NULL WHERE issue_id = ?", [issueUid]);
|
|
465
|
+
}
|
|
466
|
+
|
|
450
467
|
async markDaemonStatus(daemonId: number, status: "active" | "drained" | "dead"): Promise<void> {
|
|
451
468
|
await getDB().run(
|
|
452
469
|
"UPDATE {{daemons}} SET status = ? WHERE id = ?",
|
package/src/opencode.ts
CHANGED
|
@@ -300,7 +300,7 @@ export interface EngineOptions {
|
|
|
300
300
|
daemonId: number;
|
|
301
301
|
takeover?: TakeoverStrategy;
|
|
302
302
|
backend?: RuntimeBackend;
|
|
303
|
-
gateChecker?: (issue: Issue) => Promise<{ allowed: boolean; reason: string }>;
|
|
303
|
+
gateChecker?: (issue: Issue) => Promise<{ allowed: boolean; reason: string; resetMs?: number }>;
|
|
304
304
|
replyBurst?: { max: number; windowMs: number };
|
|
305
305
|
}
|
|
306
306
|
|
|
@@ -372,7 +372,7 @@ export class Engine {
|
|
|
372
372
|
private readonly daemonId: number;
|
|
373
373
|
private readonly takeover: TakeoverStrategy;
|
|
374
374
|
private readonly backend: RuntimeBackend;
|
|
375
|
-
private gateChecker: (issue: Issue) => Promise<{ allowed: boolean; reason: string }>;
|
|
375
|
+
private gateChecker: (issue: Issue) => Promise<{ allowed: boolean; reason: string; resetMs?: number }>;
|
|
376
376
|
private heartbeatTimer?: ReturnType<typeof setInterval>;
|
|
377
377
|
private maxConcurrent: number;
|
|
378
378
|
private maxConcurrentExplicit: boolean;
|
|
@@ -525,7 +525,18 @@ export class Engine {
|
|
|
525
525
|
}
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
-
|
|
528
|
+
// Consume-once: only a marker NEWER than issues.reset_at triggers the clear,
|
|
529
|
+
// so repeated triggers reuse the same fresh session until the next button press.
|
|
530
|
+
private async applySessionReset(session: OpSession, issue: Issue, resetMs: number): Promise<void> {
|
|
531
|
+
const last = await this.store.getIssueResetAt(issue.id);
|
|
532
|
+
if (resetMs <= last) return;
|
|
533
|
+
await this.store.clearSessionPointers(issue.id);
|
|
534
|
+
await this.store.setIssueResetAt(issue.id, resetMs);
|
|
535
|
+
session.opencodeSessionId = undefined;
|
|
536
|
+
log.info(`engine: session reset via web for ${issue.trackerScopeKey}#${issue.trackerIssueId} — pointers cleared, starting fresh session`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private async webGateAllows(issue: Issue): Promise<{ allowed: boolean; reason: string; resetMs?: number }> {
|
|
529
540
|
const parts = issue.trackerScopeKey.split("/");
|
|
530
541
|
const owner = parts[0] ?? "";
|
|
531
542
|
const repo = parts.slice(1).join("/");
|
|
@@ -534,10 +545,10 @@ export class Engine {
|
|
|
534
545
|
try {
|
|
535
546
|
const resp = await fetch(url, { signal: AbortSignal.timeout(5000), headers: { Authorization: `token ${this.cfg.gitea.token}` } });
|
|
536
547
|
if (!resp.ok) return { allowed: false, reason: `web returned ${resp.status}` };
|
|
537
|
-
const data = await resp.json() as { dispatchOff?: boolean; aiStatus?: string };
|
|
548
|
+
const data = await resp.json() as { dispatchOff?: boolean; aiStatus?: string; sessionResetMs?: number | null };
|
|
538
549
|
if (data.dispatchOff) return { allowed: false, reason: "dispatch off" };
|
|
539
550
|
if (data.aiStatus === "halted" || data.aiStatus === "dispatch_off") return { allowed: false, reason: `ai_status=${data.aiStatus}` };
|
|
540
|
-
return { allowed: true, reason: "ok" };
|
|
551
|
+
return { allowed: true, reason: "ok", resetMs: Number(data.sessionResetMs) || 0 };
|
|
541
552
|
} catch (err) {
|
|
542
553
|
log.warn(`engine: web gate query failed for ${issue.trackerScopeKey}#${issue.trackerIssueId}: ${(err as Error).message} — fail-closed (skipping)`);
|
|
543
554
|
return { allowed: false, reason: `web unreachable: ${(err as Error).message}` };
|
|
@@ -1345,6 +1356,9 @@ export class Engine {
|
|
|
1345
1356
|
|
|
1346
1357
|
private async execProcess(k: string, session: OpSession, issue: Issue, msg: Message) {
|
|
1347
1358
|
const gate = await this.gateChecker(issue);
|
|
1359
|
+
if (gate.allowed && gate.resetMs && gate.resetMs > 0) {
|
|
1360
|
+
await this.applySessionReset(session, issue, gate.resetMs);
|
|
1361
|
+
}
|
|
1348
1362
|
if (!gate.allowed) {
|
|
1349
1363
|
log.info(`engine: execProcess blocked by web gate (${gate.reason}) for ${k}`);
|
|
1350
1364
|
await this.store.updateMessageStatus(msg.id, "failed", `gate: ${gate.reason}`);
|
|
@@ -1458,7 +1472,10 @@ export class Engine {
|
|
|
1458
1472
|
|
|
1459
1473
|
this.processes.set(k, handle);
|
|
1460
1474
|
this.lastOutputAt.set(k, Date.now());
|
|
1461
|
-
|
|
1475
|
+
// Budget is strictly per-run: a replacement spawn must never inherit the
|
|
1476
|
+
// previous run's start timestamp (observed: a fresh pid killed 30s after
|
|
1477
|
+
// spawn because the 3h watchdog still held the superseded run's start).
|
|
1478
|
+
this.startedAt.set(k, Date.now());
|
|
1462
1479
|
this.currentPrompt.set(k, msg.content);
|
|
1463
1480
|
|
|
1464
1481
|
await this.store.updateSession(session.id, { opencodePid: handle.pid });
|
|
@@ -1974,7 +1991,13 @@ export class Engine {
|
|
|
1974
1991
|
log.warn(`engine: run exceeded max runtime (${hrs}h) on ${k}, stopping`);
|
|
1975
1992
|
await tracker.createComment(this.sessionToRef(session, issue), `[system] ⏹ **${session.name}** run exceeded ${hrs}h — stopped. Reply again on the issue to continue.`).catch(() => { /* best-effort */ });
|
|
1976
1993
|
this.nudgeRounds.set(k, Engine.MAX_NUDGE_ROUNDS);
|
|
1977
|
-
this.forceStop(k);
|
|
1994
|
+
await this.forceStop(k);
|
|
1995
|
+
// forceStop clears daemon-side state but intentionally skips
|
|
1996
|
+
// finishRun (stopping flag), so without this the web ai_status
|
|
1997
|
+
// stays "processing" forever — the exact stuck state users see
|
|
1998
|
+
// after a capped run. Capped runs may have delivered partial
|
|
1999
|
+
// work, so "completed" (not "failed") is the honest terminal.
|
|
2000
|
+
void tracker.updateStatus(this.sessionToRef(session, issue), "completed");
|
|
1978
2001
|
continue;
|
|
1979
2002
|
}
|
|
1980
2003
|
|