@alook/cli 0.0.16 → 0.0.18

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/dist/index.js CHANGED
@@ -15853,6 +15853,7 @@ var machineToken = sqliteTable("machine_token", {
15853
15853
  }, (t) => [index("idx_machine_token").on(t.token)]);
15854
15854
  // ../shared/src/db/queries/task.ts
15855
15855
  var DEFAULT_STALE_SECONDS = Number(process.env.ALOOK_STALE_DISPATCH_TIMEOUT_S) || 20;
15856
+ var DEFAULT_STALE_RUNNING_SECONDS = Number(process.env.ALOOK_STALE_RUNNING_TIMEOUT_S) || 3600;
15856
15857
  // ../shared/src/utils/email.ts
15857
15858
  var DOMAIN = `@${process.env.ALOOK_DOMAIN || "alook.ai"}`;
15858
15859
  var RESERVED_HANDLES = new Set([
@@ -16084,6 +16085,7 @@ function loadDaemonConfig(profile) {
16084
16085
  opencodeModel: process.env.ALOOK_OPENCODE_MODEL || "",
16085
16086
  pollInterval: parseDuration(process.env.ALOOK_DAEMON_POLL_INTERVAL || "3s"),
16086
16087
  agentTimeout: parseDuration(process.env.ALOOK_AGENT_TIMEOUT || "12h"),
16088
+ messageInactivityTimeout: parseDuration(process.env.ALOOK_MESSAGE_INACTIVITY_TIMEOUT || "5m"),
16087
16089
  maxConcurrentTasks: parseInt(process.env.ALOOK_DAEMON_MAX_CONCURRENT_TASKS || "20"),
16088
16090
  daemonId,
16089
16091
  deviceName: process.env.ALOOK_DAEMON_DEVICE_NAME || h,
@@ -16551,6 +16553,7 @@ function releaseSteeringLock(baseDir, contextKey) {
16551
16553
 
16552
16554
  // daemon/daemon.ts
16553
16555
  import { existsSync, mkdirSync as mkdirSync5, openSync, closeSync, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync4 } from "fs";
16556
+ import { readdir, readFile, unlink, stat as fsStat } from "fs/promises";
16554
16557
  import { execSync as execSync3, spawn as spawn2 } from "child_process";
16555
16558
  import { fileURLToPath as fileURLToPath2 } from "url";
16556
16559
  import { dirname as dirname3, join as join6 } from "path";
@@ -16591,6 +16594,122 @@ function pruneSessionRunnerLogs() {
16591
16594
  } catch {}
16592
16595
  }
16593
16596
  }
16597
+ function isClientError(error48) {
16598
+ if (!(error48 instanceof Error))
16599
+ return false;
16600
+ const match = error48.message.match(/^HTTP (\d+):/);
16601
+ if (!match)
16602
+ return false;
16603
+ const status = Number(match[1]);
16604
+ if (status === 408 || status === 429)
16605
+ return false;
16606
+ return status >= 400 && status < 500;
16607
+ }
16608
+ function isValidMarker(data) {
16609
+ if (!data || typeof data !== "object")
16610
+ return false;
16611
+ const d = data;
16612
+ if (typeof d.taskId !== "string")
16613
+ return false;
16614
+ if (typeof d.token !== "string")
16615
+ return false;
16616
+ if (typeof d.serverURL !== "string")
16617
+ return false;
16618
+ if (typeof d.createdAt !== "string" || isNaN(new Date(d.createdAt).getTime()))
16619
+ return false;
16620
+ if (!d.payload || typeof d.payload !== "object")
16621
+ return false;
16622
+ const payload = d.payload;
16623
+ if (d.type === "complete") {
16624
+ return typeof payload.output === "string";
16625
+ }
16626
+ if (d.type === "fail") {
16627
+ return typeof payload.error === "string";
16628
+ }
16629
+ return false;
16630
+ }
16631
+ var MARKER_STALE_MS = 24 * 60 * 60 * 1000;
16632
+ var TMP_STALE_MS = 60 * 60 * 1000;
16633
+ async function reconcilePendingCompletions(workspacesRoot) {
16634
+ const dir = join6(workspacesRoot, ".pending_completions");
16635
+ let entries;
16636
+ try {
16637
+ entries = await readdir(dir);
16638
+ } catch {
16639
+ return;
16640
+ }
16641
+ for (const name of entries) {
16642
+ if (!name.endsWith(".tmp"))
16643
+ continue;
16644
+ try {
16645
+ const s = await fsStat(join6(dir, name));
16646
+ if (Date.now() - s.mtimeMs > TMP_STALE_MS) {
16647
+ await unlink(join6(dir, name));
16648
+ }
16649
+ } catch {}
16650
+ }
16651
+ const jsonFiles = entries.filter((f) => f.endsWith(".json"));
16652
+ for (const name of jsonFiles) {
16653
+ const filePath = join6(dir, name);
16654
+ try {
16655
+ let raw;
16656
+ try {
16657
+ raw = await readFile(filePath, "utf-8");
16658
+ } catch {
16659
+ continue;
16660
+ }
16661
+ let parsed;
16662
+ try {
16663
+ parsed = JSON.parse(raw);
16664
+ } catch {
16665
+ log.warn(`reconcile: malformed marker ${name}, deleting`);
16666
+ try {
16667
+ await unlink(filePath);
16668
+ } catch {}
16669
+ continue;
16670
+ }
16671
+ if (!isValidMarker(parsed)) {
16672
+ log.warn(`reconcile: invalid marker structure ${name}, deleting`);
16673
+ try {
16674
+ await unlink(filePath);
16675
+ } catch {}
16676
+ continue;
16677
+ }
16678
+ const marker = parsed;
16679
+ const age = Date.now() - new Date(marker.createdAt).getTime();
16680
+ if (age > MARKER_STALE_MS) {
16681
+ log.warn(`reconcile: stale marker ${name} (${Math.round(age / 3600000)}h old), deleting`);
16682
+ try {
16683
+ await unlink(filePath);
16684
+ } catch {}
16685
+ continue;
16686
+ }
16687
+ const client = new DaemonClient(marker.serverURL);
16688
+ try {
16689
+ if (marker.type === "complete") {
16690
+ await client.completeTask(marker.token, marker.taskId, marker.payload);
16691
+ } else {
16692
+ await client.failTask(marker.token, marker.taskId, marker.payload.error);
16693
+ }
16694
+ try {
16695
+ await unlink(filePath);
16696
+ } catch (delErr) {
16697
+ log.warn(`reconcile: delivered marker ${name} but failed to delete: ${delErr}`);
16698
+ }
16699
+ } catch (deliverErr) {
16700
+ if (isClientError(deliverErr)) {
16701
+ try {
16702
+ await unlink(filePath);
16703
+ } catch {}
16704
+ } else {
16705
+ log.debug(`reconcile: delivery failed for ${name}, will retry next cycle`);
16706
+ }
16707
+ }
16708
+ } catch (e) {
16709
+ log.debug(`reconcile: error processing ${name}`, e);
16710
+ }
16711
+ }
16712
+ }
16594
16713
  async function startDaemon(profile, serverUrl) {
16595
16714
  pruneSessionRunnerLogs();
16596
16715
  if (!acquireDaemonPid(profile)) {
@@ -16771,6 +16890,11 @@ async function startDaemon(profile, serverUrl) {
16771
16890
  for (const id of evictedIds) {
16772
16891
  evictWorkspace(id);
16773
16892
  }
16893
+ try {
16894
+ await reconcilePendingCompletions(config2.workspacesRoot);
16895
+ } catch (e) {
16896
+ log.debug("reconciliation error", e);
16897
+ }
16774
16898
  if (workspaceStates.length === 0) {
16775
16899
  log.info("All workspaces evicted — shutting down");
16776
16900
  shutdown();
@@ -16968,7 +17092,8 @@ async function handleTask(client, config2, runtimeIndex, task, token, activeTask
16968
17092
  serverURL: config2.serverURL,
16969
17093
  token,
16970
17094
  workspacesRoot: config2.workspacesRoot,
16971
- agentTimeout: config2.agentTimeout
17095
+ agentTimeout: config2.agentTimeout,
17096
+ messageInactivityTimeout: config2.messageInactivityTimeout
16972
17097
  };
16973
17098
  const child = spawnSessionRunner(input);
16974
17099
  child.on("close", () => activeTasks.delete(task.id));
@@ -14,7 +14,7 @@ var __export = (target, all) => {
14
14
  };
15
15
 
16
16
  // daemon/session-runner.ts
17
- import { mkdir, writeFile, rm } from "fs/promises";
17
+ import { mkdir, writeFile, rm, rename } from "fs/promises";
18
18
  import path from "path";
19
19
 
20
20
  // ../shared/src/constants.ts
@@ -15570,6 +15570,7 @@ var machineToken = sqliteTable("machine_token", {
15570
15570
  }, (t) => [index("idx_machine_token").on(t.token)]);
15571
15571
  // ../shared/src/db/queries/task.ts
15572
15572
  var DEFAULT_STALE_SECONDS = Number(process.env.ALOOK_STALE_DISPATCH_TIMEOUT_S) || 20;
15573
+ var DEFAULT_STALE_RUNNING_SECONDS = Number(process.env.ALOOK_STALE_RUNNING_TIMEOUT_S) || 3600;
15573
15574
  // ../shared/src/utils/email.ts
15574
15575
  var DOMAIN = `@${process.env.ALOOK_DOMAIN || "alook.ai"}`;
15575
15576
  var RESERVED_HANDLES = new Set([
@@ -15963,7 +15964,7 @@ class CodexBackend {
15963
15964
  this.cliPath = cliPath;
15964
15965
  }
15965
15966
  execute(prompt, options) {
15966
- const proc = spawn2(this.cliPath, ["app-server", "--listen", "stdio://"], {
15967
+ const proc = spawn2(this.cliPath, ["app-server", "--listen", "stdio://", "--config", "sandbox_workspace_write.network_access=true"], {
15967
15968
  cwd: options.cwd,
15968
15969
  stdio: ["pipe", "pipe", "pipe"],
15969
15970
  env: { ...process.env, ...options.env }
@@ -15989,6 +15990,7 @@ class CodexBackend {
15989
15990
  let notificationProtocol = "unknown";
15990
15991
  let turnStarted = false;
15991
15992
  let turnDoneTriggered = false;
15993
+ let turnCompletedSuccessfully = false;
15992
15994
  let lastCompletedTurnId = "";
15993
15995
  let turnError = "";
15994
15996
  const pendingRequests = new Map;
@@ -16098,6 +16100,7 @@ class CodexBackend {
16098
16100
  lastCompletedTurnId = turnId;
16099
16101
  const status = turn?.status || params.status || "";
16100
16102
  if (status === "completed" || status === "finished") {
16103
+ turnCompletedSuccessfully = true;
16101
16104
  triggerTurnDone(false);
16102
16105
  } else if (status === "cancelled" || status === "aborted" || status === "interrupted") {
16103
16106
  triggerTurnDone(true);
@@ -16348,10 +16351,10 @@ class CodexBackend {
16348
16351
  closeAllPending("process closed");
16349
16352
  if (timedOut) {
16350
16353
  resultStatus = "timeout";
16351
- } else if (code !== 0 && resultStatus === "completed") {
16354
+ } else if (code !== 0 && resultStatus === "completed" && !turnCompletedSuccessfully) {
16352
16355
  resultStatus = "failed";
16353
16356
  }
16354
- const stderr = stderrChunks.join("");
16357
+ const stderr = stderrChunks.join("").replace(/\x1b\[[0-9;]*m/g, "");
16355
16358
  if (stderr && !lastError) {
16356
16359
  lastError = stderr;
16357
16360
  }
@@ -17214,6 +17217,26 @@ function buildPrompt(task, attachments) {
17214
17217
 
17215
17218
  // daemon/session-runner.ts
17216
17219
  var ATTACHMENTS_BASE = "/tmp/alook-attachments";
17220
+ async function writeMarkerFile(workspacesRoot, marker) {
17221
+ const dir = path.join(workspacesRoot, ".pending_completions");
17222
+ await mkdir(dir, { recursive: true, mode: 448 });
17223
+ const tmpPath = path.join(dir, `${marker.taskId}.tmp`);
17224
+ const finalPath = path.join(dir, `${marker.taskId}.json`);
17225
+ await writeFile(tmpPath, JSON.stringify(marker), { mode: 384 });
17226
+ await rename(tmpPath, finalPath);
17227
+ }
17228
+ async function reportToServer(fn, markerData, workspacesRoot) {
17229
+ try {
17230
+ await fn();
17231
+ } catch (e) {
17232
+ log.warn(`server report failed for task ${markerData.taskId}, writing marker: ${e}`);
17233
+ try {
17234
+ await writeMarkerFile(workspacesRoot, markerData);
17235
+ } catch (writeErr) {
17236
+ log.error(`marker write also failed for task ${markerData.taskId}: ${writeErr}`);
17237
+ }
17238
+ }
17239
+ }
17217
17240
  function sanitizeFilename(name) {
17218
17241
  return path.basename(name).replace(/[/\\]/g, "_").replace(/\.\./g, "_").slice(0, 255) || "file";
17219
17242
  }
@@ -17241,7 +17264,7 @@ async function downloadAttachments(client, token, workspaceId, taskId, attachmen
17241
17264
  return attachments;
17242
17265
  }
17243
17266
  async function runSession(input) {
17244
- const { task, provider, cliPath, model, serverURL, token, workspacesRoot, agentTimeout } = input;
17267
+ const { task, provider, cliPath, model, serverURL, token, workspacesRoot, agentTimeout, messageInactivityTimeout } = input;
17245
17268
  log.info(`starting (task=${task.id}, type=${task.type}, agent=${task.agentId}, provider=${provider}, model=${model || "default"})`);
17246
17269
  const client = new DaemonClient(serverURL);
17247
17270
  const backend = createBackend(provider, cliPath);
@@ -17329,27 +17352,47 @@ async function runSession(input) {
17329
17352
  entry.status = "cancelled";
17330
17353
  entry.errmsg = "cancelled by user";
17331
17354
  });
17332
- try {
17333
- await client.failTask(token, task.id, "cancelled by user");
17334
- } catch {}
17355
+ await reportToServer(() => client.failTask(token, task.id, "cancelled by user"), { taskId: task.id, type: "fail", payload: { error: "cancelled by user" }, token, serverURL, createdAt: new Date().toISOString() }, workspacesRoot);
17335
17356
  } else {
17336
17357
  updateEntry(timelineDir, task.id, (entry) => {
17337
17358
  entry.pid = null;
17338
17359
  entry.status = "killed";
17339
17360
  entry.errmsg = "killed by signal";
17340
17361
  });
17341
- try {
17342
- await client.failTask(token, task.id, "killed by signal");
17343
- } catch {}
17362
+ await reportToServer(() => client.failTask(token, task.id, "killed by signal"), { taskId: task.id, type: "fail", payload: { error: "killed by signal" }, token, serverURL, createdAt: new Date().toISOString() }, workspacesRoot);
17344
17363
  }
17345
17364
  process.exit(1);
17346
17365
  };
17347
17366
  process.on("SIGTERM", onKill);
17348
17367
  process.on("SIGINT", onKill);
17368
+ const INACTIVITY_TIMEOUT_MS = messageInactivityTimeout ?? 5 * 60 * 1000;
17369
+ let inactivityTimedOut = false;
17349
17370
  try {
17350
- for await (const msg of session2.messages) {
17351
- if (killed)
17371
+ const iter = session2.messages[Symbol.asyncIterator]();
17372
+ while (!killed) {
17373
+ const next = iter.next();
17374
+ const raceResult = await (INACTIVITY_TIMEOUT_MS > 0 ? Promise.race([
17375
+ next,
17376
+ new Promise((resolve) => {
17377
+ const timer = setTimeout(() => resolve("timeout"), INACTIVITY_TIMEOUT_MS);
17378
+ next.then(() => clearTimeout(timer), () => clearTimeout(timer));
17379
+ })
17380
+ ]) : next);
17381
+ if (raceResult === "timeout") {
17382
+ inactivityTimedOut = true;
17383
+ log.warn(`message inactivity timeout (${INACTIVITY_TIMEOUT_MS / 1000}s) — killing agent`);
17384
+ if (session2.pid) {
17385
+ try {
17386
+ process.kill(session2.pid, "SIGTERM");
17387
+ } catch {}
17388
+ }
17389
+ iter.return?.(undefined);
17352
17390
  break;
17391
+ }
17392
+ const iterResult = raceResult;
17393
+ if (iterResult.done)
17394
+ break;
17395
+ const msg = iterResult.value;
17353
17396
  seq++;
17354
17397
  pendingMessages.push({
17355
17398
  seq,
@@ -17388,6 +17431,10 @@ async function runSession(input) {
17388
17431
  process.removeListener("SIGINT", onKill);
17389
17432
  if (killed)
17390
17433
  return;
17434
+ if (inactivityTimedOut) {
17435
+ result.status = "failed";
17436
+ result.error = `message inactivity timeout (no messages for ${INACTIVITY_TIMEOUT_MS / 1000}s)`;
17437
+ }
17391
17438
  await cleanupAttachments(task.id);
17392
17439
  if (result.status === "completed") {
17393
17440
  updateEntry(timelineDir, task.id, (entry) => {
@@ -17399,7 +17446,7 @@ async function runSession(input) {
17399
17446
  updateEntry(timelineDir, task.id, (entry) => {
17400
17447
  entry.pid = null;
17401
17448
  entry.status = "failed";
17402
- entry.errmsg = result.error || "unknown error";
17449
+ entry.errmsg = result.error || "agent exited unexpectedly";
17403
17450
  });
17404
17451
  }
17405
17452
  if (result.status === "completed") {
@@ -17408,11 +17455,12 @@ async function runSession(input) {
17408
17455
  };
17409
17456
  if (result.sessionId)
17410
17457
  body.session_id = result.sessionId;
17411
- await client.completeTask(token, task.id, body);
17458
+ await reportToServer(() => client.completeTask(token, task.id, body), { taskId: task.id, type: "complete", payload: body, token, serverURL, createdAt: new Date().toISOString() }, workspacesRoot);
17412
17459
  const dur = (result.durationMs / 1000).toFixed(1);
17413
17460
  log.info(`completed (duration=${dur}s, messages=${seq}, tools=${toolCount})`);
17414
17461
  } else {
17415
- await client.failTask(token, task.id, result.error || "unknown error");
17462
+ const errorMsg = result.error || "agent exited unexpectedly";
17463
+ await reportToServer(() => client.failTask(token, task.id, errorMsg), { taskId: task.id, type: "fail", payload: { error: errorMsg }, token, serverURL, createdAt: new Date().toISOString() }, workspacesRoot);
17416
17464
  const dur = (result.durationMs / 1000).toFixed(1);
17417
17465
  log.info(`failed (duration=${dur}s, messages=${seq}, tools=${toolCount}) — ${result.error}`);
17418
17466
  }
@@ -17437,9 +17485,8 @@ async function main() {
17437
17485
  } catch (e) {
17438
17486
  log.error(`session-runner: unhandled error for task ${input.task.id}`, e);
17439
17487
  await cleanupAttachments(input.task.id);
17440
- try {
17441
- await client.failTask(input.token, input.task.id, `session-runner crash: ${e}`);
17442
- } catch {}
17488
+ const errorMsg = `session-runner crash: ${e}`;
17489
+ await reportToServer(() => client.failTask(input.token, input.task.id, errorMsg), { taskId: input.task.id, type: "fail", payload: { error: errorMsg }, token: input.token, serverURL: input.serverURL, createdAt: new Date().toISOString() }, input.workspacesRoot);
17443
17490
  process.exit(1);
17444
17491
  }
17445
17492
  }
@@ -17448,5 +17495,7 @@ if (isDirectExecution) {
17448
17495
  main();
17449
17496
  }
17450
17497
  export {
17451
- runSession
17498
+ writeMarkerFile,
17499
+ runSession,
17500
+ reportToServer
17452
17501
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alook/cli",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "Alook CLI — register and run always-on AI coding agents.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/alookai/alook#readme",