cueclaw 0.1.2 → 0.1.4

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.
@@ -4,18 +4,23 @@ import {
4
4
  updateStepRunStatus,
5
5
  updateWorkflowPhase,
6
6
  updateWorkflowRunStatus
7
- } from "./chunk-G43R5ASK.js";
7
+ } from "./chunk-HKZ6IN7X.js";
8
8
  import {
9
9
  cueclawHome,
10
+ getDefaultImage,
10
11
  loadConfig
11
- } from "./chunk-RSKXBXSJ.js";
12
+ } from "./chunk-5TV4LNC3.js";
12
13
  import {
13
14
  ConfigError,
14
15
  ExecutorError
15
16
  } from "./chunk-BVQG3WYO.js";
16
17
  import {
18
+ isDev
19
+ } from "./chunk-ZCK3IFLC.js";
20
+ import {
21
+ createExecutionLogger,
17
22
  logger
18
- } from "./chunk-QBOYMF4A.js";
23
+ } from "./chunk-KBLMQZ3P.js";
19
24
 
20
25
  // src/executor.ts
21
26
  import { nanoid as nanoid3 } from "nanoid";
@@ -239,7 +244,7 @@ async function runContainerAgent(opts) {
239
244
  mkdirSync2(opts.workDir, { recursive: true });
240
245
  mkdirSync2(join3(opts.ipcDir, "input"), { recursive: true });
241
246
  mkdirSync2(join3(opts.ipcDir, "output"), { recursive: true });
242
- const image = config.container?.image ?? "cueclaw-agent:latest";
247
+ const image = config.container?.image ?? getDefaultImage();
243
248
  const network = config.container?.network ?? "none";
244
249
  const volumeMounts = buildVolumeMounts(opts);
245
250
  const dockerArgs = [
@@ -292,7 +297,8 @@ function buildVolumeMounts(opts) {
292
297
  return mounts;
293
298
  }
294
299
  async function spawnContainer(dockerArgs, opts, config) {
295
- return new Promise((resolve) => {
300
+ return new Promise((resolve2) => {
301
+ logger.info({ containerName: opts.containerName, image: dockerArgs[dockerArgs.length - 1] }, "Starting container");
296
302
  const proc = spawn("docker", dockerArgs, {
297
303
  stdio: ["pipe", "pipe", "pipe"]
298
304
  });
@@ -348,8 +354,9 @@ async function spawnContainer(dockerArgs, opts, config) {
348
354
  proc.on("close", (code) => {
349
355
  clearTimeout(hardTimer);
350
356
  clearInterval(idleCheck);
357
+ logger.info({ containerName: opts.containerName, exitCode: code }, "Container exited");
351
358
  if (truncated) {
352
- resolve({ status: "failed", error: "Container output size cap exceeded" });
359
+ resolve2({ status: "failed", error: "Container output size cap exceeded" });
353
360
  return;
354
361
  }
355
362
  const startIdx = stdout.indexOf(OUTPUT_START_MARKER);
@@ -358,7 +365,7 @@ async function spawnContainer(dockerArgs, opts, config) {
358
365
  resultBuffer = stdout.slice(startIdx + OUTPUT_START_MARKER.length + 1, endIdx).trim();
359
366
  }
360
367
  if (code !== 0 && !resultBuffer) {
361
- resolve({
368
+ resolve2({
362
369
  status: "failed",
363
370
  error: stderr || `Container exited with code ${code}`
364
371
  });
@@ -366,16 +373,16 @@ async function spawnContainer(dockerArgs, opts, config) {
366
373
  }
367
374
  try {
368
375
  const parsed = JSON.parse(resultBuffer);
369
- resolve({
376
+ resolve2({
370
377
  status: "succeeded",
371
378
  output: parsed.result ?? null,
372
379
  sessionId: parsed.sessionId
373
380
  });
374
381
  } catch {
375
382
  if (resultBuffer) {
376
- resolve({ status: "succeeded", output: resultBuffer });
383
+ resolve2({ status: "succeeded", output: resultBuffer });
377
384
  } else {
378
- resolve({
385
+ resolve2({
379
386
  status: "failed",
380
387
  error: stderr || "No output captured from container"
381
388
  });
@@ -385,7 +392,8 @@ async function spawnContainer(dockerArgs, opts, config) {
385
392
  proc.on("error", (err) => {
386
393
  clearTimeout(hardTimer);
387
394
  clearInterval(idleCheck);
388
- resolve({
395
+ logger.error({ containerName: opts.containerName, err }, "Docker spawn error");
396
+ resolve2({
389
397
  status: "failed",
390
398
  error: `Docker spawn error: ${err.message}`
391
399
  });
@@ -393,6 +401,7 @@ async function spawnContainer(dockerArgs, opts, config) {
393
401
  });
394
402
  }
395
403
  function gracefulStop(containerName) {
404
+ logger.debug({ containerName }, "Gracefully stopping container");
396
405
  try {
397
406
  spawn("docker", ["stop", containerName]);
398
407
  } catch {
@@ -418,44 +427,123 @@ function prepareContainerOpts(workflowId, stepId, runId, prompt, cwd, allowedToo
418
427
  };
419
428
  }
420
429
 
430
+ // src/container-runtime.ts
431
+ import { execFileSync } from "child_process";
432
+ import { resolve, dirname } from "path";
433
+ import { fileURLToPath } from "url";
434
+ import { existsSync as existsSync3 } from "fs";
435
+ var cached;
436
+ var imageCache = /* @__PURE__ */ new Map();
437
+ function isDockerAvailable() {
438
+ if (cached !== void 0) return cached;
439
+ try {
440
+ execFileSync("docker", ["info"], { encoding: "utf-8", stdio: "pipe" });
441
+ cached = true;
442
+ } catch {
443
+ cached = false;
444
+ }
445
+ return cached;
446
+ }
447
+ function isDockerImageAvailable(image) {
448
+ const hit = imageCache.get(image);
449
+ if (hit !== void 0) return hit;
450
+ try {
451
+ execFileSync("docker", ["image", "inspect", image], { encoding: "utf-8", stdio: "pipe" });
452
+ imageCache.set(image, true);
453
+ return true;
454
+ } catch {
455
+ imageCache.set(image, false);
456
+ return false;
457
+ }
458
+ }
459
+ function ensureDockerImage(image) {
460
+ if (isDockerImageAvailable(image)) return true;
461
+ if (isDev) {
462
+ return buildDevImage(image);
463
+ }
464
+ logger.info({ image }, "Docker image not found locally, attempting pull");
465
+ try {
466
+ execFileSync("docker", ["pull", image], { encoding: "utf-8", stdio: "pipe", timeout: 3e5 });
467
+ imageCache.set(image, true);
468
+ logger.info({ image }, "Docker image pulled successfully");
469
+ return true;
470
+ } catch (err) {
471
+ logger.warn({ image, err }, "Failed to pull Docker image");
472
+ return false;
473
+ }
474
+ }
475
+ function buildDevImage(image) {
476
+ const thisDir = dirname(fileURLToPath(import.meta.url));
477
+ const projectRoot = resolve(thisDir, "..");
478
+ const buildScript = resolve(projectRoot, "container", "build.sh");
479
+ if (!existsSync3(buildScript)) {
480
+ logger.warn({ buildScript }, "container/build.sh not found, cannot auto-build");
481
+ return false;
482
+ }
483
+ logger.info({ image }, "Dev mode: auto-building container image via container/build.sh");
484
+ try {
485
+ execFileSync("bash", [buildScript], {
486
+ encoding: "utf-8",
487
+ stdio: "inherit",
488
+ cwd: resolve(projectRoot, "container")
489
+ });
490
+ imageCache.set(image, true);
491
+ logger.info({ image }, "Dev mode: container image built successfully");
492
+ return true;
493
+ } catch (err) {
494
+ logger.warn({ image, err }, "Dev mode: container image build failed");
495
+ return false;
496
+ }
497
+ }
498
+
421
499
  // src/agent-runner.ts
422
500
  function runAgent(opts) {
423
501
  const config = loadConfig();
424
- if (config.container?.enabled && opts.workflowId && opts.stepId && opts.runId) {
425
- const containerOpts = prepareContainerOpts(
426
- opts.workflowId,
427
- opts.stepId,
428
- opts.runId,
429
- opts.prompt,
430
- opts.cwd,
431
- opts.allowedTools
432
- );
433
- const resultPromise2 = runContainerAgent({
434
- ...containerOpts,
435
- signal: opts.signal,
436
- onProgress: opts.onProgress
437
- });
438
- return {
439
- resultPromise: resultPromise2,
440
- abort: () => {
441
- try {
442
- import("child_process").then(({ spawn: spawn2 }) => {
443
- spawn2("docker", ["stop", containerOpts.containerName]);
444
- });
445
- } catch {
502
+ const containerEnabled = config.container?.enabled ?? false;
503
+ if (containerEnabled && opts.workflowId && opts.stepId && opts.runId) {
504
+ if (!isDockerAvailable()) {
505
+ logger.warn({ stepId: opts.stepId }, "Docker not available, falling back to local execution");
506
+ } else if (!ensureDockerImage(config.container?.image ?? getDefaultImage())) {
507
+ logger.warn({ stepId: opts.stepId, image: config.container?.image ?? getDefaultImage() }, "Docker image not available (pull failed), falling back to local execution");
508
+ } else {
509
+ logger.info({ stepId: opts.stepId, mode: "container" }, "Running agent in container mode");
510
+ const containerOpts = prepareContainerOpts(
511
+ opts.workflowId,
512
+ opts.stepId,
513
+ opts.runId,
514
+ opts.prompt,
515
+ opts.cwd,
516
+ opts.allowedTools
517
+ );
518
+ const resultPromise2 = runContainerAgent({
519
+ ...containerOpts,
520
+ signal: opts.signal,
521
+ onProgress: opts.onProgress
522
+ });
523
+ return {
524
+ resultPromise: resultPromise2,
525
+ abort: () => {
526
+ try {
527
+ import("child_process").then(({ spawn: spawn2 }) => {
528
+ spawn2("docker", ["stop", containerOpts.containerName]);
529
+ });
530
+ } catch {
531
+ }
446
532
  }
447
- }
448
- };
533
+ };
534
+ }
449
535
  }
536
+ logger.info({ stepId: opts.stepId, mode: "local" }, "Running agent in local mode");
450
537
  let aborted = false;
451
538
  const resultPromise = (async () => {
452
539
  const authToken = config.claude.executor.api_key ?? config.claude.api_key;
453
540
  const baseUrl = config.claude.executor.base_url ?? config.claude.base_url;
454
- const prevAuthToken = process.env["ANTHROPIC_AUTH_TOKEN"];
455
- const prevBaseUrl = process.env["ANTHROPIC_BASE_URL"];
456
- process.env["ANTHROPIC_AUTH_TOKEN"] = authToken;
541
+ const stepEnv = { ...process.env };
542
+ if (authToken) stepEnv["ANTHROPIC_AUTH_TOKEN"] = authToken;
457
543
  if (baseUrl !== "https://api.anthropic.com") {
458
- process.env["ANTHROPIC_BASE_URL"] = baseUrl;
544
+ stepEnv["ANTHROPIC_BASE_URL"] = baseUrl;
545
+ } else {
546
+ delete stepEnv["ANTHROPIC_BASE_URL"];
459
547
  }
460
548
  try {
461
549
  const { query } = await import("@anthropic-ai/claude-agent-sdk");
@@ -477,7 +565,8 @@ function runAgent(opts) {
477
565
  "WebFetch"
478
566
  ],
479
567
  settingSources: ["project"],
480
- permissionMode: permMode
568
+ permissionMode: permMode,
569
+ env: stepEnv
481
570
  }
482
571
  });
483
572
  let sessionId;
@@ -511,16 +600,13 @@ function runAgent(opts) {
511
600
  }
512
601
  opts.onProgress?.(message);
513
602
  }
603
+ logger.debug({ stepId: opts.stepId, status: "succeeded" }, "Agent completed");
514
604
  return { status: "succeeded", output: result, sessionId };
515
605
  } catch (err) {
516
606
  const errorMsg = err instanceof Error ? err.message : String(err);
517
607
  logger.error({ err, stepId: opts.stepId }, "Agent execution failed");
518
608
  return { status: "failed", error: errorMsg };
519
609
  } finally {
520
- if (prevAuthToken !== void 0) process.env["ANTHROPIC_AUTH_TOKEN"] = prevAuthToken;
521
- else delete process.env["ANTHROPIC_AUTH_TOKEN"];
522
- if (prevBaseUrl !== void 0) process.env["ANTHROPIC_BASE_URL"] = prevBaseUrl;
523
- else delete process.env["ANTHROPIC_BASE_URL"];
524
610
  }
525
611
  })();
526
612
  return {
@@ -547,13 +633,22 @@ function createSession(db, stepRunId, sdkSessionId) {
547
633
  INSERT INTO sessions (id, step_run_id, sdk_session_id, created_at, last_used_at, is_active)
548
634
  VALUES (?, ?, ?, ?, ?, ?)
549
635
  `).run(session.id, session.step_run_id, session.sdk_session_id ?? null, session.created_at, session.last_used_at, 1);
636
+ logger.debug({ sessionId: session.id, stepRunId }, "Session created");
550
637
  return session;
551
638
  }
552
639
  function deactivateSession(db, sessionId) {
553
640
  db.prepare("UPDATE sessions SET is_active = 0, last_used_at = ? WHERE id = ?").run((/* @__PURE__ */ new Date()).toISOString(), sessionId);
641
+ logger.debug({ sessionId }, "Session deactivated");
554
642
  }
555
643
  function updateSessionSdkId(db, sessionId, sdkSessionId) {
556
644
  db.prepare("UPDATE sessions SET sdk_session_id = ?, last_used_at = ? WHERE id = ?").run(sdkSessionId, (/* @__PURE__ */ new Date()).toISOString(), sessionId);
645
+ logger.debug({ sessionId, sdkSessionId }, "Session SDK ID updated");
646
+ }
647
+ function cleanupStaleSessions(db, maxAgeMs = 7 * 24 * 60 * 60 * 1e3) {
648
+ const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
649
+ const result = db.prepare("DELETE FROM sessions WHERE is_active = 0 AND last_used_at < ?").run(cutoff);
650
+ logger.info({ deletedCount: result.changes, cutoff }, "Stale sessions cleaned up");
651
+ return result.changes;
557
652
  }
558
653
 
559
654
  // src/executor.ts
@@ -599,7 +694,7 @@ function hasSkipMarker(inputs) {
599
694
  }
600
695
  return false;
601
696
  }
602
- async function executeStepOnce(step, resolvedInputs, runId, db, cwd, onProgress) {
697
+ async function executeStepOnce(step, resolvedInputs, workflowId, runId, db, cwd, onProgress, execLogger) {
603
698
  const stepRunId = `sr_${nanoid3()}`;
604
699
  const now = (/* @__PURE__ */ new Date()).toISOString();
605
700
  const stepRun = {
@@ -610,6 +705,7 @@ async function executeStepOnce(step, resolvedInputs, runId, db, cwd, onProgress)
610
705
  started_at: now
611
706
  };
612
707
  insertStepRun(db, stepRun);
708
+ execLogger?.info({ stepId: step.id, stepRunId, attempt: "start" }, `Step started: ${step.id}`);
613
709
  const inputContext = Object.keys(resolvedInputs).length > 0 ? `
614
710
 
615
711
  Inputs:
@@ -618,7 +714,7 @@ ${JSON.stringify(resolvedInputs, null, 2)}` : "";
618
714
  const handle = runAgent({
619
715
  prompt,
620
716
  cwd,
621
- workflowId: step.id,
717
+ workflowId,
622
718
  stepId: step.id,
623
719
  runId,
624
720
  onProgress: onProgress ? (msg) => onProgress(step.id, msg) : void 0
@@ -628,15 +724,21 @@ ${JSON.stringify(resolvedInputs, null, 2)}` : "";
628
724
  const session = createSession(db, stepRunId, result.sessionId);
629
725
  updateSessionSdkId(db, session.id, result.sessionId);
630
726
  deactivateSession(db, session.id);
727
+ execLogger?.debug({ stepId: step.id, sessionId: session.id }, "Session stored");
631
728
  }
632
729
  updateStepRunStatus(db, stepRunId, result.status, result.output ?? void 0, result.error);
730
+ if (result.status === "failed") {
731
+ execLogger?.error({ stepId: step.id, stepRunId, error: result.error }, `Step failed: ${step.id}`);
732
+ } else {
733
+ execLogger?.info({ stepId: step.id, stepRunId, status: result.status }, `Step completed: ${step.id}`);
734
+ }
633
735
  return result;
634
736
  }
635
- async function executeStepWithRetry(step, resolvedInputs, runId, db, cwd, policy, onProgress) {
737
+ async function executeStepWithRetry(step, resolvedInputs, workflowId, runId, db, cwd, policy, onProgress, execLogger) {
636
738
  const maxRetries = policy.max_retries ?? 0;
637
739
  let delay = policy.retry_delay_ms ?? 5e3;
638
740
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
639
- const result = await executeStepOnce(step, resolvedInputs, runId, db, cwd, onProgress);
741
+ const result = await executeStepOnce(step, resolvedInputs, workflowId, runId, db, cwd, onProgress, execLogger);
640
742
  if (result.status !== "failed" || attempt === maxRetries) return result;
641
743
  logger.info({ stepId: step.id, attempt, delay }, "Retrying step");
642
744
  await new Promise((r) => setTimeout(r, delay));
@@ -656,6 +758,8 @@ async function executeWorkflow(opts) {
656
758
  };
657
759
  insertWorkflowRun(db, run);
658
760
  updateWorkflowPhase(db, workflow.id, "executing");
761
+ const execLogger = createExecutionLogger(workflow.id, runId);
762
+ execLogger.info({ workflowId: workflow.id, runId, steps: workflow.steps.length }, "Workflow execution started");
659
763
  const completed = /* @__PURE__ */ new Map();
660
764
  const remaining = new Set(workflow.steps.map((s) => s.id));
661
765
  const stepMap = new Map(workflow.steps.map((s) => [s.id, s]));
@@ -663,6 +767,7 @@ async function executeWorkflow(opts) {
663
767
  try {
664
768
  while (remaining.size > 0) {
665
769
  if (signal?.aborted) {
770
+ execLogger.warn({ workflowId: workflow.id, runId, remainingSteps: remaining.size }, "Execution aborted via signal");
666
771
  for (const id of remaining) {
667
772
  completed.set(id, { status: "skipped", error: "Aborted" });
668
773
  onProgress?.(id, { status: "skipped" });
@@ -676,6 +781,7 @@ async function executeWorkflow(opts) {
676
781
  return step.depends_on.every((dep) => completed.has(dep));
677
782
  });
678
783
  if (ready.length === 0) {
784
+ execLogger.error({ workflowId: workflow.id, runId, remaining: [...remaining] }, "Deadlock detected");
679
785
  throw new ExecutorError("Deadlock: no ready steps but remaining steps exist");
680
786
  }
681
787
  const executable = [];
@@ -685,6 +791,7 @@ async function executeWorkflow(opts) {
685
791
  (dep) => completed.get(dep)?.status === "failed" || completed.get(dep)?.status === "skipped"
686
792
  );
687
793
  if (depsFailed && workflow.failure_policy.on_step_failure !== "ask_user") {
794
+ execLogger.debug({ stepId: id, reason: "dependency_failed" }, "Step skipped");
688
795
  remaining.delete(id);
689
796
  completed.set(id, { status: "skipped" });
690
797
  const skipRunId = `sr_${nanoid3()}`;
@@ -693,6 +800,7 @@ async function executeWorkflow(opts) {
693
800
  }
694
801
  const resolvedInputs = resolveInputs(step.inputs, completed, triggerData);
695
802
  if (hasSkipMarker(resolvedInputs)) {
803
+ execLogger.debug({ stepId: id, reason: "skip_marker" }, "Step skipped");
696
804
  remaining.delete(id);
697
805
  completed.set(id, { status: "skipped" });
698
806
  const skipRunId = `sr_${nanoid3()}`;
@@ -709,20 +817,38 @@ async function executeWorkflow(opts) {
709
817
  const result = await executeStepWithRetry(
710
818
  step,
711
819
  resolvedInputs,
820
+ workflow.id,
712
821
  runId,
713
822
  db,
714
823
  cwd,
715
824
  workflow.failure_policy,
716
- onProgress
825
+ onProgress,
826
+ execLogger
717
827
  );
718
828
  return { stepId: step.id, result };
719
829
  })
720
830
  );
721
831
  for (const { stepId, result } of results) {
722
832
  completed.set(stepId, result);
723
- if (result.status === "failed") {
724
- const policy = workflow.failure_policy.on_step_failure;
725
- if (policy === "stop") {
833
+ }
834
+ for (const { stepId, result } of results) {
835
+ if (result.status !== "failed") continue;
836
+ const policy = workflow.failure_policy.on_step_failure;
837
+ if (policy === "stop") {
838
+ execLogger.warn({ stepId, policy: "stop" }, "Stop policy triggered, skipping remaining steps");
839
+ for (const remainingId of remaining) {
840
+ completed.set(remainingId, { status: "skipped" });
841
+ const skipRunId = `sr_${nanoid3()}`;
842
+ insertStepRun(db, { id: skipRunId, run_id: runId, step_id: remainingId, status: "skipped" });
843
+ }
844
+ remaining.clear();
845
+ runFailed = true;
846
+ break;
847
+ }
848
+ if (policy === "ask_user" && onStepFailure) {
849
+ const decision = await onStepFailure(stepMap.get(stepId), result.error ?? "Unknown error");
850
+ execLogger.info({ stepId, decision }, "ask_user decision received");
851
+ if (decision === "stop") {
726
852
  for (const remainingId of remaining) {
727
853
  completed.set(remainingId, { status: "skipped" });
728
854
  const skipRunId = `sr_${nanoid3()}`;
@@ -732,18 +858,11 @@ async function executeWorkflow(opts) {
732
858
  runFailed = true;
733
859
  break;
734
860
  }
735
- if (policy === "ask_user" && onStepFailure) {
736
- const decision = await onStepFailure(stepMap.get(stepId), result.error ?? "Unknown error");
737
- if (decision === "stop") {
738
- for (const remainingId of remaining) {
739
- completed.set(remainingId, { status: "skipped" });
740
- const skipRunId = `sr_${nanoid3()}`;
741
- insertStepRun(db, { id: skipRunId, run_id: runId, step_id: remainingId, status: "skipped" });
742
- }
743
- remaining.clear();
744
- runFailed = true;
745
- break;
746
- }
861
+ if (decision === "retry") {
862
+ remaining.add(stepId);
863
+ completed.delete(stepId);
864
+ execLogger.info({ stepId }, "Re-queuing step for retry");
865
+ continue;
747
866
  }
748
867
  }
749
868
  }
@@ -759,10 +878,12 @@ async function executeWorkflow(opts) {
759
878
  } else {
760
879
  updateWorkflowPhase(db, workflow.id, "active");
761
880
  }
881
+ execLogger.info({ workflowId: workflow.id, runId, status: finalStatus }, "Workflow execution finished");
762
882
  return { runId, status: finalStatus, results: completed };
763
883
  }
764
884
 
765
885
  export {
886
+ cleanupStaleSessions,
766
887
  resolveValue,
767
888
  resolveInputs,
768
889
  executeWorkflow