@hasna/loops 0.3.58 → 0.3.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.
@@ -358,6 +358,20 @@ function optionalStringArray(value, label) {
358
358
  }).filter(Boolean);
359
359
  return values.length ? values : undefined;
360
360
  }
361
+ var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
362
+ "e",
363
+ "exec",
364
+ "agent",
365
+ "start",
366
+ "--ephemeral",
367
+ "--ignore-rules",
368
+ "--skip-git-repo-check",
369
+ "--json",
370
+ "--output-last-message",
371
+ "-o",
372
+ "--output-schema",
373
+ "--dangerously-bypass-approvals-and-sandbox"
374
+ ]);
361
375
  function optionalAccountRef(value, label) {
362
376
  if (value === undefined)
363
377
  return;
@@ -472,6 +486,15 @@ function validateTarget(value, label, opts) {
472
486
  throw new Error(`${label}.variant is not supported for provider cursor`);
473
487
  if (value.provider === "codex" && value.agent !== undefined)
474
488
  throw new Error(`${label}.agent is not supported for provider codex`);
489
+ if (value.provider === "codewith" && value.agent !== undefined) {
490
+ throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
491
+ }
492
+ const extraArgs = optionalStringArray(value.extraArgs, `${label}.extraArgs`);
493
+ if (value.provider === "codewith") {
494
+ const unsafe = extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
495
+ if (unsafe)
496
+ throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
497
+ }
475
498
  optionalStringArray(value.addDirs, `${label}.addDirs`);
476
499
  if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
477
500
  throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
@@ -547,7 +570,9 @@ function validateTarget(value, label, opts) {
547
570
  if (value.routing.eventSource !== undefined)
548
571
  assertString(value.routing.eventSource, `${label}.routing.eventSource`);
549
572
  }
550
- const target = { ...value };
573
+ const target = { ...value, extraArgs };
574
+ if (!extraArgs)
575
+ delete target.extraArgs;
551
576
  delete target.promptFile;
552
577
  delete target.promptSource;
553
578
  return { ...target, ...promptFields };
@@ -1543,6 +1568,70 @@ class Store {
1543
1568
  throw error;
1544
1569
  }
1545
1570
  }
1571
+ cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
1572
+ const updated = (opts.now ?? new Date).toISOString();
1573
+ this.db.exec("BEGIN IMMEDIATE");
1574
+ try {
1575
+ const current = this.requireUniqueLoop(idOrName);
1576
+ if (current.target.type !== "workflow")
1577
+ throw new Error(`loop is not a workflow loop: ${idOrName}`);
1578
+ if (this.hasRunningRun(current.id))
1579
+ throw new Error(`refusing to retarget running loop: ${current.id}`);
1580
+ if (!current.goal)
1581
+ throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
1582
+ const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1583
+ if (!previousWorkflow.goal)
1584
+ throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
1585
+ const workflow = {
1586
+ id: genId(),
1587
+ name: opts.workflowName,
1588
+ description: previousWorkflow.description,
1589
+ version: previousWorkflow.version,
1590
+ status: "active",
1591
+ steps: previousWorkflow.steps,
1592
+ createdAt: updated,
1593
+ updatedAt: updated
1594
+ };
1595
+ this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
1596
+ VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
1597
+ $id: workflow.id,
1598
+ $name: workflow.name,
1599
+ $description: workflow.description ?? null,
1600
+ $version: workflow.version,
1601
+ $status: workflow.status,
1602
+ $steps: JSON.stringify(workflow.steps),
1603
+ $created: workflow.createdAt,
1604
+ $updated: workflow.updatedAt
1605
+ });
1606
+ const target = { ...current.target, workflowId: workflow.id };
1607
+ if (opts.workflowTimeoutMs !== undefined)
1608
+ target.timeoutMs = opts.workflowTimeoutMs;
1609
+ const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
1610
+ WHERE id=$id
1611
+ AND ($daemonLeaseId IS NULL OR EXISTS (
1612
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
1613
+ ))`).run({
1614
+ $id: current.id,
1615
+ $target: JSON.stringify(target),
1616
+ $updated: updated,
1617
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
1618
+ $now: updated
1619
+ });
1620
+ if (res.changes !== 1)
1621
+ throw new Error("daemon lease lost");
1622
+ const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
1623
+ this.db.exec("COMMIT");
1624
+ const loop = this.getLoop(current.id);
1625
+ if (!loop)
1626
+ throw new Error(`loop not found after retarget: ${current.id}`);
1627
+ return { loop, workflow, previousWorkflow, archivedOld };
1628
+ } catch (error) {
1629
+ try {
1630
+ this.db.exec("ROLLBACK");
1631
+ } catch {}
1632
+ throw error;
1633
+ }
1634
+ }
1546
1635
  renameLoop(id, name, opts = {}) {
1547
1636
  const current = this.getLoop(id);
1548
1637
  if (!current)
@@ -3424,6 +3513,9 @@ function appendBounded(current, chunk, maxBytes) {
3424
3513
  return `[truncated ${overflow} bytes]
3425
3514
  ${next.slice(-maxBytes)}`;
3426
3515
  }
3516
+ function appendBoundedText(current, chunk, maxBytes) {
3517
+ return appendBounded(current, Buffer.from(chunk, "utf8"), maxBytes);
3518
+ }
3427
3519
  function killProcessGroup(pid) {
3428
3520
  try {
3429
3521
  process.kill(-pid, "SIGTERM");
@@ -3471,6 +3563,21 @@ function metadataEnv(metadata) {
3471
3563
  env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
3472
3564
  return env;
3473
3565
  }
3566
+ function codewithAgentIdempotencyKey(metadata) {
3567
+ const parts = [
3568
+ "openloops",
3569
+ metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
3570
+ metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
3571
+ metadata.runId ? `loop-run:${metadata.runId}` : undefined,
3572
+ metadata.loopId ? `loop:${metadata.loopId}` : undefined,
3573
+ metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
3574
+ metadata.goalId ? `goal:${metadata.goalId}` : undefined,
3575
+ metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
3576
+ ].filter(Boolean);
3577
+ if (parts.length > 1)
3578
+ return parts.join(":");
3579
+ return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
3580
+ }
3474
3581
  function allowlistEnv(allowlist) {
3475
3582
  const env = {};
3476
3583
  if (allowlist?.tools?.length)
@@ -3507,6 +3614,20 @@ function codewithLikeSandbox(target) {
3507
3614
  function configStringValue(value) {
3508
3615
  return JSON.stringify(value);
3509
3616
  }
3617
+ var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2 = new Set([
3618
+ "e",
3619
+ "exec",
3620
+ "agent",
3621
+ "start",
3622
+ "--ephemeral",
3623
+ "--ignore-rules",
3624
+ "--skip-git-repo-check",
3625
+ "--json",
3626
+ "--output-last-message",
3627
+ "-o",
3628
+ "--output-schema",
3629
+ "--dangerously-bypass-approvals-and-sandbox"
3630
+ ]);
3510
3631
  function assertStringOption(value, label) {
3511
3632
  if (value !== undefined && typeof value !== "string")
3512
3633
  throw new Error(`${label} must be a string`);
@@ -3537,6 +3658,14 @@ function assertSupportedAgentOptions(target) {
3537
3658
  }
3538
3659
  if (target.provider === "codex" && target.agent !== undefined)
3539
3660
  throw new Error("codex.agent is not supported");
3661
+ if (target.provider === "codewith" && target.agent !== undefined) {
3662
+ throw new Error("codewith.agent is not supported by the durable background-agent adapter");
3663
+ }
3664
+ if (target.provider === "codewith") {
3665
+ const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2.has(arg));
3666
+ if (unsafe)
3667
+ throw new Error(`codewith.extraArgs cannot include ${unsafe}; durable agent steps use codewith agent start, not exec/ephemeral flags`);
3668
+ }
3540
3669
  if (["codewith", "codex"].includes(target.provider)) {
3541
3670
  if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
3542
3671
  throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
@@ -3620,18 +3749,18 @@ function agentArgs(target) {
3620
3749
  args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
3621
3750
  if (target.variant)
3622
3751
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
3623
- args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
3624
- if (isolation === "safe")
3625
- args.push("--ignore-rules");
3752
+ args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
3626
3753
  if (target.cwd)
3627
3754
  args.push("--cd", target.cwd);
3628
3755
  for (const dir of target.addDirs ?? [])
3629
3756
  args.push("--add-dir", dir);
3630
3757
  if (target.model)
3631
3758
  args.push("--model", target.model);
3632
- if (target.agent)
3633
- args.push("--agent", target.agent);
3634
3759
  args.push(...target.extraArgs ?? []);
3760
+ args.push("agent", "start");
3761
+ if (target.cwd)
3762
+ args.push("--cwd", target.cwd);
3763
+ args.push(target.prompt);
3635
3764
  return args;
3636
3765
  case "codex":
3637
3766
  if (target.variant)
@@ -3707,8 +3836,9 @@ function commandSpec(target) {
3707
3836
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
3708
3837
  nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
3709
3838
  preflightAnyOf: agentTarget.provider === "cursor" ? ["agent"] : undefined,
3710
- stdin: agentTarget.prompt,
3711
- allowlist: agentTarget.allowlist
3839
+ stdin: agentTarget.provider === "codewith" ? undefined : agentTarget.prompt,
3840
+ allowlist: agentTarget.allowlist,
3841
+ codewithDurableAgent: agentTarget.provider === "codewith" ? { target: agentTarget } : undefined
3712
3842
  };
3713
3843
  }
3714
3844
  function executionEnv(spec, metadata, opts) {
@@ -3828,6 +3958,238 @@ function preflightNativeAuthProfile(spec, env) {
3828
3958
  throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
3829
3959
  }
3830
3960
  }
3961
+ function codewithAgentStartArgs(target, idempotencyKey) {
3962
+ const args = agentArgs(target);
3963
+ const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
3964
+ if (startIndex === -1)
3965
+ throw new Error("internal error: codewith durable agent args missing agent start");
3966
+ args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
3967
+ return args;
3968
+ }
3969
+ function codewithAgentControlArgs(target, command, agentId) {
3970
+ return [
3971
+ ...target.authProfile ? ["--auth-profile", target.authProfile] : [],
3972
+ "agent",
3973
+ command,
3974
+ ...command === "logs" ? ["--limit", "20"] : [],
3975
+ agentId
3976
+ ];
3977
+ }
3978
+ function parseJsonOutput(stdout, label) {
3979
+ try {
3980
+ const value = JSON.parse(stdout || "{}");
3981
+ if (!value || typeof value !== "object" || Array.isArray(value))
3982
+ throw new Error("not an object");
3983
+ return value;
3984
+ } catch (error) {
3985
+ throw new Error(`${label} did not return JSON${error instanceof Error ? `: ${error.message}` : ""}`);
3986
+ }
3987
+ }
3988
+ function recordField(value, key) {
3989
+ if (!value || typeof value !== "object" || Array.isArray(value))
3990
+ return;
3991
+ const field = value[key];
3992
+ return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
3993
+ }
3994
+ function stringField(value, key) {
3995
+ const field = value?.[key];
3996
+ return typeof field === "string" ? field : undefined;
3997
+ }
3998
+ function numberField(value, key) {
3999
+ const field = value?.[key];
4000
+ return typeof field === "number" && Number.isFinite(field) ? field : undefined;
4001
+ }
4002
+ function codewithAgentStatus(readJson) {
4003
+ return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
4004
+ }
4005
+ function codewithAgentEvidence(startJson, readJson, logsJson) {
4006
+ const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
4007
+ const statusSnapshot = recordField(readJson, "statusSnapshot");
4008
+ const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
4009
+ seq: numberField(event, "seq"),
4010
+ eventType: stringField(event, "eventType"),
4011
+ createdAt: numberField(event, "createdAt")
4012
+ })) : undefined;
4013
+ return JSON.stringify({
4014
+ codewithAgent: {
4015
+ agentId: stringField(agent, "agentId"),
4016
+ status: stringField(agent, "status"),
4017
+ desiredState: stringField(agent, "desiredState"),
4018
+ statusReason: stringField(agent, "statusReason"),
4019
+ threadId: stringField(agent, "threadId"),
4020
+ rolloutPath: stringField(agent, "rolloutPath"),
4021
+ pid: numberField(agent, "pid"),
4022
+ exitCode: numberField(agent, "exitCode"),
4023
+ created: typeof startJson.created === "boolean" ? startJson.created : undefined
4024
+ },
4025
+ statusSnapshot: statusSnapshot ? {
4026
+ seq: numberField(statusSnapshot, "seq"),
4027
+ status: stringField(statusSnapshot, "status"),
4028
+ summary: stringField(statusSnapshot, "summary"),
4029
+ pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
4030
+ lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
4031
+ } : undefined,
4032
+ events
4033
+ }, null, 2);
4034
+ }
4035
+ function sleep(ms) {
4036
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4037
+ }
4038
+ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
4039
+ const target = spec.codewithDurableAgent?.target;
4040
+ if (!target)
4041
+ throw new Error("internal error: missing codewith durable target");
4042
+ const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
4043
+ const idempotencyKey = codewithAgentIdempotencyKey(metadata);
4044
+ const start = spawnSync2(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
4045
+ cwd: spec.cwd,
4046
+ env,
4047
+ encoding: "utf8",
4048
+ stdio: ["ignore", "pipe", "pipe"],
4049
+ timeout: 30000
4050
+ });
4051
+ let stderr = start.stderr || "";
4052
+ if (start.error || (start.status ?? 1) !== 0) {
4053
+ const finishedAt = nowIso();
4054
+ return {
4055
+ status: "failed",
4056
+ exitCode: start.status ?? undefined,
4057
+ stdout: start.stdout || "",
4058
+ stderr,
4059
+ error: start.error?.message ?? `codewith agent start exited with code ${start.status ?? "unknown"}`,
4060
+ startedAt,
4061
+ finishedAt,
4062
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4063
+ };
4064
+ }
4065
+ let startJson;
4066
+ try {
4067
+ startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
4068
+ } catch (error) {
4069
+ const finishedAt = nowIso();
4070
+ return {
4071
+ status: "failed",
4072
+ stdout: start.stdout || "",
4073
+ stderr,
4074
+ error: error instanceof Error ? error.message : String(error),
4075
+ startedAt,
4076
+ finishedAt,
4077
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4078
+ };
4079
+ }
4080
+ const agentId = stringField(recordField(startJson, "agent"), "agentId");
4081
+ if (!agentId) {
4082
+ const finishedAt = nowIso();
4083
+ return {
4084
+ status: "failed",
4085
+ stdout: start.stdout || "",
4086
+ stderr,
4087
+ error: "codewith agent start did not return agent.agentId",
4088
+ startedAt,
4089
+ finishedAt,
4090
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4091
+ };
4092
+ }
4093
+ const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
4094
+ let lastReadJson = startJson;
4095
+ let lastLogsJson;
4096
+ let stdout = "";
4097
+ while (true) {
4098
+ if (opts.signal?.aborted) {
4099
+ spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
4100
+ const finishedAt = nowIso();
4101
+ return {
4102
+ status: "failed",
4103
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4104
+ stderr,
4105
+ error: "cancelled",
4106
+ startedAt,
4107
+ finishedAt,
4108
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4109
+ };
4110
+ }
4111
+ const read = spawnSync2(spec.command, codewithAgentControlArgs(target, "read", agentId), {
4112
+ cwd: spec.cwd,
4113
+ env,
4114
+ encoding: "utf8",
4115
+ stdio: ["ignore", "pipe", "pipe"],
4116
+ timeout: 30000
4117
+ });
4118
+ stderr = appendBoundedText(stderr, read.stderr || "", maxOutputBytes);
4119
+ if (read.error || (read.status ?? 1) !== 0) {
4120
+ const finishedAt = nowIso();
4121
+ return {
4122
+ status: "failed",
4123
+ exitCode: read.status ?? undefined,
4124
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4125
+ stderr,
4126
+ error: read.error?.message ?? `codewith agent read exited with code ${read.status ?? "unknown"}`,
4127
+ startedAt,
4128
+ finishedAt,
4129
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4130
+ };
4131
+ }
4132
+ try {
4133
+ lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
4134
+ } catch (error) {
4135
+ const finishedAt = nowIso();
4136
+ return {
4137
+ status: "failed",
4138
+ stdout: appendBoundedText(stdout, read.stdout || "", maxOutputBytes),
4139
+ stderr,
4140
+ error: error instanceof Error ? error.message : String(error),
4141
+ startedAt,
4142
+ finishedAt,
4143
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4144
+ };
4145
+ }
4146
+ const status = codewithAgentStatus(lastReadJson);
4147
+ if (status === "completed" || status === "failed" || status === "cancelled") {
4148
+ const logs = spawnSync2(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
4149
+ cwd: spec.cwd,
4150
+ env,
4151
+ encoding: "utf8",
4152
+ stdio: ["ignore", "pipe", "pipe"],
4153
+ timeout: 30000
4154
+ });
4155
+ if (!logs.error && (logs.status ?? 1) === 0) {
4156
+ try {
4157
+ lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
4158
+ } catch {
4159
+ lastLogsJson = undefined;
4160
+ }
4161
+ } else {
4162
+ stderr = appendBoundedText(stderr, logs.stderr || logs.error?.message || "", maxOutputBytes);
4163
+ }
4164
+ const finishedAt = nowIso();
4165
+ const resultStatus = status === "completed" ? "succeeded" : "failed";
4166
+ return {
4167
+ status: resultStatus,
4168
+ exitCode: resultStatus === "succeeded" ? 0 : 1,
4169
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4170
+ stderr,
4171
+ error: resultStatus === "succeeded" ? undefined : stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`,
4172
+ startedAt,
4173
+ finishedAt,
4174
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4175
+ };
4176
+ }
4177
+ if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
4178
+ spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
4179
+ const finishedAt = nowIso();
4180
+ return {
4181
+ status: "timed_out",
4182
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4183
+ stderr,
4184
+ error: `timed out after ${spec.timeoutMs}ms`,
4185
+ startedAt,
4186
+ finishedAt,
4187
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4188
+ };
4189
+ }
4190
+ await sleep(pollMs);
4191
+ }
4192
+ }
3831
4193
  function preflightRemoteSpec(spec, machine, metadata, opts) {
3832
4194
  const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
3833
4195
  const result = spawnSync2(plan.command, plan.args, {
@@ -3999,6 +4361,19 @@ function preflightTarget(target, metadata = {}, opts = {}) {
3999
4361
  async function executeTarget(target, metadata = {}, opts = {}) {
4000
4362
  const spec = commandSpec(target);
4001
4363
  const machine = resolvedMachine(opts);
4364
+ if (machine && !machine.local && spec.codewithDurableAgent) {
4365
+ const startedAt2 = nowIso();
4366
+ const finishedAt2 = nowIso();
4367
+ return {
4368
+ status: "failed",
4369
+ stdout: "",
4370
+ stderr: "",
4371
+ error: "remote Codewith durable background-agent steps require remote status polling support; run this Codewith step locally or add remote durable readback before dispatch",
4372
+ startedAt: startedAt2,
4373
+ finishedAt: finishedAt2,
4374
+ durationMs: new Date(finishedAt2).getTime() - new Date(startedAt2).getTime()
4375
+ };
4376
+ }
4002
4377
  if (machine && !machine.local)
4003
4378
  return executeRemoteSpec(spec, machine, metadata, opts);
4004
4379
  const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
@@ -4045,6 +4420,9 @@ async function executeTarget(target, metadata = {}, opts = {}) {
4045
4420
  durationMs: 0
4046
4421
  };
4047
4422
  }
4423
+ if (spec.codewithDurableAgent) {
4424
+ return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
4425
+ }
4048
4426
  const child = spawn(spec.command, spec.args, {
4049
4427
  cwd: spec.cwd,
4050
4428
  env,
@@ -5256,7 +5634,7 @@ function realSleep(ms) {
5256
5634
  return new Promise((resolve2) => setTimeout(resolve2, ms));
5257
5635
  }
5258
5636
  async function runLoop(opts) {
5259
- const sleep = opts.sleep ?? realSleep;
5637
+ const sleep2 = opts.sleep ?? realSleep;
5260
5638
  const sliceMs = opts.sliceMs ?? 200;
5261
5639
  while (!opts.shouldStop()) {
5262
5640
  try {
@@ -5267,7 +5645,7 @@ async function runLoop(opts) {
5267
5645
  let waited = 0;
5268
5646
  while (waited < opts.intervalMs && !opts.shouldStop()) {
5269
5647
  const chunk = Math.min(sliceMs, opts.intervalMs - waited);
5270
- await sleep(chunk);
5648
+ await sleep2(chunk);
5271
5649
  waited += chunk;
5272
5650
  }
5273
5651
  }
@@ -5349,7 +5727,7 @@ async function stopDaemon(opts = {}) {
5349
5727
  } finally {
5350
5728
  store.close();
5351
5729
  }
5352
- const sleep = opts.sleep ?? realSleep;
5730
+ const sleep2 = opts.sleep ?? realSleep;
5353
5731
  try {
5354
5732
  process.kill(state.pid, "SIGTERM");
5355
5733
  } catch {
@@ -5358,7 +5736,7 @@ async function stopDaemon(opts = {}) {
5358
5736
  }
5359
5737
  const steps = Math.max(1, Math.ceil((opts.timeoutMs ?? 6000) / 100));
5360
5738
  for (let i = 0;i < steps; i++) {
5361
- await sleep(100);
5739
+ await sleep2(100);
5362
5740
  if (!isAlive(state.pid)) {
5363
5741
  removePid(path);
5364
5742
  return { wasRunning: true, stopped: true, forced: false, pid: state.pid };
@@ -5367,7 +5745,7 @@ async function stopDaemon(opts = {}) {
5367
5745
  try {
5368
5746
  process.kill(state.pid, "SIGKILL");
5369
5747
  } catch {}
5370
- await sleep(150);
5748
+ await sleep2(150);
5371
5749
  removePid(path);
5372
5750
  return { wasRunning: true, stopped: !isAlive(state.pid), forced: true, pid: state.pid };
5373
5751
  }
@@ -5533,10 +5911,10 @@ async function startDaemon(opts) {
5533
5911
  stdio: ["ignore", out, out]
5534
5912
  });
5535
5913
  child.unref();
5536
- const sleep = opts.sleep ?? realSleep;
5914
+ const sleep2 = opts.sleep ?? realSleep;
5537
5915
  const deadline = Math.max(1, Math.ceil((opts.waitMs ?? 4000) / 100));
5538
5916
  for (let i = 0;i < deadline; i++) {
5539
- await sleep(100);
5917
+ await sleep2(100);
5540
5918
  const current = isDaemonRunning();
5541
5919
  if (current.running)
5542
5920
  return { started: true, alreadyRunning: false, pid: current.pid };
@@ -5631,7 +6009,7 @@ function enableStartup(result) {
5631
6009
  // package.json
5632
6010
  var package_default = {
5633
6011
  name: "@hasna/loops",
5634
- version: "0.3.58",
6012
+ version: "0.3.60",
5635
6013
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5636
6014
  type: "module",
5637
6015
  main: "dist/index.js",