ework-daemon 0.4.58 → 0.4.60

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.58",
3
+ "version": "0.4.60",
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/opencode.ts CHANGED
@@ -309,6 +309,13 @@ function createDefaultBackend(cfg: Config): RuntimeBackend {
309
309
  return new OpencodeBackend(cfg.opencode.binary, cfg.opencode.dbPath, cfg.childEnvDeny);
310
310
  }
311
311
 
312
+ function createBackendFor(cfg: Config, runtime: string): RuntimeBackend {
313
+ if (runtime === "pi" && cfg.pi) {
314
+ return new PiBackend(cfg.pi.binary, cfg.pi.provider, cfg.pi.defaultModel, cfg.childEnvDeny);
315
+ }
316
+ return new OpencodeBackend(cfg.opencode.binary, cfg.opencode.dbPath, cfg.childEnvDeny);
317
+ }
318
+
312
319
  export class Engine {
313
320
  private cfg: Config;
314
321
  private store: Store;
@@ -352,6 +359,11 @@ export class Engine {
352
359
 
353
360
  private groupConfigs = new Map<string, GroupConfig>();
354
361
  private cloneUrls = new Map<string, string>();
362
+ // Per-issue runtime override ("opencode"|"pi") from webhook payloads.
363
+ // Existing sessions stay pinned to their original backend via the
364
+ // opencodeSessionId prefix (ses_=opencode, bare uuid=pi) in backendFor().
365
+ private issueRuntimes = new Map<string, string>();
366
+ private altBackend?: RuntimeBackend;
355
367
  private senders = new Map<string, string>();
356
368
  private envInitialized = new Set<string>();
357
369
 
@@ -378,6 +390,28 @@ export class Engine {
378
390
  void this.recover();
379
391
  }
380
392
 
393
+ // An existing session must keep the backend that owns it: opencode session
394
+ // ids are "ses_..." while pi ids are bare uuids, so the prefix outvotes the
395
+ // per-issue override. New sessions follow the issue's runtime setting.
396
+ // k is the session key "tracker:scope#issue@sessionName"; the runtime map is
397
+ // keyed by the issue part, so strip the "@sessionName" suffix.
398
+ private backendFor(k: string, opencodeSessionId?: string): RuntimeBackend {
399
+ const issueKey = k.slice(0, k.lastIndexOf("@"));
400
+ if (opencodeSessionId) {
401
+ const wants = opencodeSessionId.startsWith("ses_") ? "opencode" : "pi";
402
+ if (wants !== this.cfg.runtime) return this.altBackendFor(wants);
403
+ return this.backend;
404
+ }
405
+ const runtime = this.issueRuntimes.get(issueKey);
406
+ if (!runtime || runtime === this.cfg.runtime) return this.backend;
407
+ return this.altBackendFor(runtime);
408
+ }
409
+
410
+ private altBackendFor(runtime: string): RuntimeBackend {
411
+ if (!this.altBackend) this.altBackend = createBackendFor(this.cfg, runtime);
412
+ return this.altBackend;
413
+ }
414
+
381
415
  private workdirLink(workdir: string): string {
382
416
  const p = encodeURIComponent(workdir);
383
417
  return `[${workdir}](/file?path=${p}&daemon_id=${this.daemonId})`;
@@ -743,6 +777,9 @@ export class Engine {
743
777
  if (event.cloneUrl) {
744
778
  this.cloneUrls.set(issueMapKey, event.cloneUrl);
745
779
  }
780
+ if (event.runtime === "pi" || event.runtime === "opencode") {
781
+ this.issueRuntimes.set(issueMapKey, event.runtime);
782
+ }
746
783
  if (event.sender) {
747
784
  this.senders.set(issueMapKey, event.sender);
748
785
  }
@@ -1203,13 +1240,14 @@ export class Engine {
1203
1240
  const fromStrategy = await this.takeover.resumeOpenCodeSession(session);
1204
1241
  if (fromStrategy) resumeSessionId = fromStrategy;
1205
1242
  }
1206
- if (resumeSessionId && !(await this.backend.sessionExists(resumeSessionId))) {
1243
+ if (resumeSessionId && !(await this.backendFor(k, resumeSessionId).sessionExists(resumeSessionId))) {
1207
1244
  log.warn(`stale session ${resumeSessionId} not found in db, starting fresh`);
1208
1245
  resumeSessionId = undefined;
1209
1246
  await this.store.updateSession(session.id, { opencodeSessionId: undefined });
1210
1247
  }
1211
1248
 
1212
- const model = msg.model || (this.cfg.runtime === "pi" && this.cfg.pi ? this.cfg.pi.defaultModel : this.cfg.opencode.defaultModel);
1249
+ const backend = this.backendFor(k, resumeSessionId);
1250
+ const model = msg.model || (backend instanceof PiBackend && this.cfg.pi ? this.cfg.pi.defaultModel : this.cfg.opencode.defaultModel);
1213
1251
  this.currentModel.set(k, model);
1214
1252
 
1215
1253
  if (msg.sourceCommentId) {
@@ -1221,7 +1259,7 @@ export class Engine {
1221
1259
  let exitCode: number | null = null;
1222
1260
 
1223
1261
  try {
1224
- const handle = await this.backend.spawn(
1262
+ const handle = await backend.spawn(
1225
1263
  {
1226
1264
  workdir,
1227
1265
  prompt: msg.content,
@@ -1269,7 +1307,7 @@ export class Engine {
1269
1307
  await this.store.updateSession(session.id, { opencodePid: handle.pid });
1270
1308
  await this.persistRuntimeState(session.id);
1271
1309
 
1272
- log.info(`engine: spawned pid=${handle.pid} for ${k} (backend=${this.backend.name})`);
1310
+ log.info(`engine: spawned pid=${handle.pid} for ${k} (backend=${backend.name})`);
1273
1311
 
1274
1312
  exitCode = await handle.exited;
1275
1313
  const stderr = await handle.stderrText;
@@ -1382,7 +1420,7 @@ export class Engine {
1382
1420
  });
1383
1421
  log.info(`engine: [bot] reply found for ${k} after prompt (comment ${matched?.id ?? "?"} createdAt ${matched?.createdAt ?? "?"}), marking done`);
1384
1422
  if (matched && !usedModel) {
1385
- const fromSession = await this.backend.lastSessionModel(session.opencodeSessionId).catch(() => ({ model: "" }));
1423
+ const fromSession = await this.backendFor(k, session.opencodeSessionId).lastSessionModel(session.opencodeSessionId).catch(() => ({ model: "" }));
1386
1424
  if (fromSession.model) {
1387
1425
  void tracker.setCommentModel(ref, matched.id, fromSession.model).catch(() => { /* display-only */ });
1388
1426
  }
@@ -1392,7 +1430,7 @@ export class Engine {
1392
1430
  await this.persistRuntimeState(session.id);
1393
1431
  void tracker.updateStatus(ref, "");
1394
1432
  } else {
1395
- const sessionOutput = await this.backend.getSessionOutputTokens(session.opencodeSessionId);
1433
+ const sessionOutput = await this.backendFor(k, session.opencodeSessionId).getSessionOutputTokens(session.opencodeSessionId);
1396
1434
  const emptyRound = this.emptyResponseRounds.get(k) ?? 0;
1397
1435
 
1398
1436
  if (!sessionOutput.hasOutput && emptyRound < Engine.MAX_EMPTY_RESPONSE_ROUNDS) {
@@ -138,6 +138,8 @@ export class GiteaTracker implements IssueTracker {
138
138
  // Empty/missing = no model override; engine omits --model.
139
139
  const modelRaw = repository.ework_model;
140
140
  const model = typeof modelRaw === "string" && modelRaw.trim() ? modelRaw.trim() : undefined;
141
+ const runtimeRaw = repository.ework_runtime;
142
+ const runtime = runtimeRaw === "pi" || runtimeRaw === "opencode" ? runtimeRaw : undefined;
141
143
 
142
144
  const ref: TrackerRef = {
143
145
  trackerType: "gitea",
@@ -162,6 +164,7 @@ export class GiteaTracker implements IssueTracker {
162
164
  ai_status: aiStatus,
163
165
  },
164
166
  model,
167
+ runtime,
165
168
  cloneUrl,
166
169
  sender,
167
170
  };
@@ -186,6 +189,7 @@ export class GiteaTracker implements IssueTracker {
186
189
  authorKind: (comment as Record<string, unknown>).author_kind as string | undefined,
187
190
  },
188
191
  model,
192
+ runtime,
189
193
  cloneUrl,
190
194
  sender,
191
195
  };
@@ -203,6 +207,7 @@ export class GiteaTracker implements IssueTracker {
203
207
  ai_status: aiStatus,
204
208
  },
205
209
  model,
210
+ runtime,
206
211
  cloneUrl,
207
212
  sender,
208
213
  };
@@ -36,6 +36,8 @@ export interface TrackerEvent {
36
36
  // global default). Empty/undefined = no override; engine omits --model
37
37
  // and lets opencode pick per its own opencode.json + env.
38
38
  model?: string;
39
+ // Runtime backend override for this issue from ework-web ("opencode"|"pi").
40
+ runtime?: string;
39
41
  // Real clone URL from the upstream tracker (e.g. Gitea repository.clone_url).
40
42
  // When present, RecloneStrategy uses this instead of the ework shim URL.
41
43
  cloneUrl?: string;