ework-daemon 0.4.45 → 0.4.47

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/opencode.ts +31 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-daemon",
3
- "version": "0.4.45",
3
+ "version": "0.4.47",
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
@@ -207,13 +207,20 @@ export class RecloneStrategy implements TakeoverStrategy {
207
207
  const gitArgs = ["git"];
208
208
  if (credHelper) gitArgs.push("-c", `credential.helper=${credHelper}`);
209
209
  gitArgs.push("clone", url, dir);
210
+ // ssh without these can sleep forever: no TCP keepalive on a blackholed
211
+ // connection, and host-key/passphrase prompts block a headless daemon.
212
+ const sshCmd = process.env.WORK_GIT_SSH_COMMAND
213
+ ?? "ssh -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -o BatchMode=yes";
210
214
  let r: { exitCode: number | null; stderr?: Uint8Array | undefined };
211
215
  try {
212
- // async spawn: a slow/hung remote (e.g. SSH host-key prompt) must never
213
- // block the daemon event loop — spawnSync froze healthz+heartbeats for
214
- // the whole clone duration. Capped at 10 minutes.
215
- const proc = Bun.spawn({ cmd: gitArgs, stdout: "ignore", stderr: "pipe", env: { ...process.env, ...env } });
216
- const killTimer = setTimeout(() => proc.kill("SIGKILL"), 10 * 60_000);
216
+ // async spawn: a slow/hung remote must never block the daemon event
217
+ // loop — spawnSync froze healthz+heartbeats for the whole clone
218
+ // duration. Capped at 10 minutes.
219
+ const proc = Bun.spawn({
220
+ cmd: gitArgs, stdout: "ignore", stderr: "pipe",
221
+ env: { ...process.env, ...env, GIT_SSH_COMMAND: sshCmd },
222
+ });
223
+ const killTimer = setTimeout(() => { try { proc.kill("SIGKILL"); } catch { /* already dead */ } }, 10 * 60_000);
217
224
  const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).arrayBuffer()]);
218
225
  clearTimeout(killTimer);
219
226
  r = { exitCode, stderr: new Uint8Array(stderr) };
@@ -328,6 +335,7 @@ export class Engine {
328
335
  private lastOutputAt = new Map<string, number>();
329
336
  private startedAt = new Map<string, number>();
330
337
  private progressCommentId = new Map<string, string>();
338
+ private pickupCommentId = new Map<string, string>();
331
339
 
332
340
  private nudgeRounds = new Map<string, number>();
333
341
  private emptyResponseRounds = new Map<string, number>();
@@ -795,11 +803,18 @@ export class Engine {
795
803
  }
796
804
 
797
805
  const k = this.sessionKey(session, issue);
798
- const workdir = await this.resolveWorkdir(session, issue);
799
- log.info(`engine: session "${session.name}" created for ${k}, workdir=${workdir}`);
800
806
 
801
- await tracker.createComment(ref, `[system] 🔄 **${session.name}** picked up this issue.\n> session: ${this.sessionRef(session)} | workdir: ${this.workdirLink(workdir)}`);
807
+ // Ack before resolving the workdir: a first-touch clone can take minutes
808
+ // (or hit the 10-min cap), and the user must see pickup feedback immediately.
809
+ // The session link is a placeholder until the backend reports its session id;
810
+ // the onSessionId callback rewrites this comment with the real reference.
811
+ await tracker.createComment(ref, `[system] 🔄 **${session.name}** picked up this issue — preparing workspace…`).then(
812
+ (c) => this.pickupCommentId.set(k, c.id),
813
+ () => {},
814
+ );
802
815
  void tracker.updateStatus(ref, "processing");
816
+ const workdir = await this.resolveWorkdir(session, issue);
817
+ log.info(`engine: session "${session.name}" created for ${k}, workdir=${workdir}`);
803
818
 
804
819
  const instructions = tracker.getTrackerInstructions(ref);
805
820
  const prompt = this.buildInitialPrompt(
@@ -1201,6 +1216,13 @@ export class Engine {
1201
1216
  await this.store.updateSession(session.id, { opencodeSessionId: id });
1202
1217
  session.opencodeSessionId = id;
1203
1218
  log.info(`engine: captured sessionID=${id.slice(0, 8)} for ${k} (early persist)`);
1219
+ const pickupId = this.pickupCommentId.get(k);
1220
+ if (pickupId) {
1221
+ this.pickupCommentId.delete(k);
1222
+ await tracker
1223
+ .editComment(ref, pickupId, `[system] 🔄 **${session.name}** picked up this issue.\n> session: ${this.sessionRef(session)} | workdir: ${this.workdirLink(workdir)}`)
1224
+ .catch((err) => log.error(`engine: failed to rewrite pickup comment for ${k}:`, (err as Error).message));
1225
+ }
1204
1226
  }
1205
1227
  },
1206
1228
  },
@@ -1297,6 +1319,7 @@ export class Engine {
1297
1319
  return;
1298
1320
  }
1299
1321
  this.progressCommentId.delete(k);
1322
+ this.pickupCommentId.delete(k);
1300
1323
  this.currentPrompt.delete(k);
1301
1324
  await this.persistRuntimeState(session.id);
1302
1325