@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.
package/dist/index.js CHANGED
@@ -356,6 +356,20 @@ function optionalStringArray(value, label) {
356
356
  }).filter(Boolean);
357
357
  return values.length ? values : undefined;
358
358
  }
359
+ var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
360
+ "e",
361
+ "exec",
362
+ "agent",
363
+ "start",
364
+ "--ephemeral",
365
+ "--ignore-rules",
366
+ "--skip-git-repo-check",
367
+ "--json",
368
+ "--output-last-message",
369
+ "-o",
370
+ "--output-schema",
371
+ "--dangerously-bypass-approvals-and-sandbox"
372
+ ]);
359
373
  function optionalAccountRef(value, label) {
360
374
  if (value === undefined)
361
375
  return;
@@ -470,6 +484,15 @@ function validateTarget(value, label, opts) {
470
484
  throw new Error(`${label}.variant is not supported for provider cursor`);
471
485
  if (value.provider === "codex" && value.agent !== undefined)
472
486
  throw new Error(`${label}.agent is not supported for provider codex`);
487
+ if (value.provider === "codewith" && value.agent !== undefined) {
488
+ throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
489
+ }
490
+ const extraArgs = optionalStringArray(value.extraArgs, `${label}.extraArgs`);
491
+ if (value.provider === "codewith") {
492
+ const unsafe = extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
493
+ if (unsafe)
494
+ throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
495
+ }
473
496
  optionalStringArray(value.addDirs, `${label}.addDirs`);
474
497
  if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
475
498
  throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
@@ -545,7 +568,9 @@ function validateTarget(value, label, opts) {
545
568
  if (value.routing.eventSource !== undefined)
546
569
  assertString(value.routing.eventSource, `${label}.routing.eventSource`);
547
570
  }
548
- const target = { ...value };
571
+ const target = { ...value, extraArgs };
572
+ if (!extraArgs)
573
+ delete target.extraArgs;
549
574
  delete target.promptFile;
550
575
  delete target.promptSource;
551
576
  return { ...target, ...promptFields };
@@ -1541,6 +1566,70 @@ class Store {
1541
1566
  throw error;
1542
1567
  }
1543
1568
  }
1569
+ cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
1570
+ const updated = (opts.now ?? new Date).toISOString();
1571
+ this.db.exec("BEGIN IMMEDIATE");
1572
+ try {
1573
+ const current = this.requireUniqueLoop(idOrName);
1574
+ if (current.target.type !== "workflow")
1575
+ throw new Error(`loop is not a workflow loop: ${idOrName}`);
1576
+ if (this.hasRunningRun(current.id))
1577
+ throw new Error(`refusing to retarget running loop: ${current.id}`);
1578
+ if (!current.goal)
1579
+ throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
1580
+ const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1581
+ if (!previousWorkflow.goal)
1582
+ throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
1583
+ const workflow = {
1584
+ id: genId(),
1585
+ name: opts.workflowName,
1586
+ description: previousWorkflow.description,
1587
+ version: previousWorkflow.version,
1588
+ status: "active",
1589
+ steps: previousWorkflow.steps,
1590
+ createdAt: updated,
1591
+ updatedAt: updated
1592
+ };
1593
+ this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
1594
+ VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
1595
+ $id: workflow.id,
1596
+ $name: workflow.name,
1597
+ $description: workflow.description ?? null,
1598
+ $version: workflow.version,
1599
+ $status: workflow.status,
1600
+ $steps: JSON.stringify(workflow.steps),
1601
+ $created: workflow.createdAt,
1602
+ $updated: workflow.updatedAt
1603
+ });
1604
+ const target = { ...current.target, workflowId: workflow.id };
1605
+ if (opts.workflowTimeoutMs !== undefined)
1606
+ target.timeoutMs = opts.workflowTimeoutMs;
1607
+ const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
1608
+ WHERE id=$id
1609
+ AND ($daemonLeaseId IS NULL OR EXISTS (
1610
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
1611
+ ))`).run({
1612
+ $id: current.id,
1613
+ $target: JSON.stringify(target),
1614
+ $updated: updated,
1615
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
1616
+ $now: updated
1617
+ });
1618
+ if (res.changes !== 1)
1619
+ throw new Error("daemon lease lost");
1620
+ const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
1621
+ this.db.exec("COMMIT");
1622
+ const loop = this.getLoop(current.id);
1623
+ if (!loop)
1624
+ throw new Error(`loop not found after retarget: ${current.id}`);
1625
+ return { loop, workflow, previousWorkflow, archivedOld };
1626
+ } catch (error) {
1627
+ try {
1628
+ this.db.exec("ROLLBACK");
1629
+ } catch {}
1630
+ throw error;
1631
+ }
1632
+ }
1544
1633
  renameLoop(id, name, opts = {}) {
1545
1634
  const current = this.getLoop(id);
1546
1635
  if (!current)
@@ -3549,6 +3638,9 @@ function appendBounded(current, chunk, maxBytes) {
3549
3638
  return `[truncated ${overflow} bytes]
3550
3639
  ${next.slice(-maxBytes)}`;
3551
3640
  }
3641
+ function appendBoundedText(current, chunk, maxBytes) {
3642
+ return appendBounded(current, Buffer.from(chunk, "utf8"), maxBytes);
3643
+ }
3552
3644
  function killProcessGroup(pid) {
3553
3645
  try {
3554
3646
  process.kill(-pid, "SIGTERM");
@@ -3596,6 +3688,21 @@ function metadataEnv(metadata) {
3596
3688
  env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
3597
3689
  return env;
3598
3690
  }
3691
+ function codewithAgentIdempotencyKey(metadata) {
3692
+ const parts = [
3693
+ "openloops",
3694
+ metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
3695
+ metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
3696
+ metadata.runId ? `loop-run:${metadata.runId}` : undefined,
3697
+ metadata.loopId ? `loop:${metadata.loopId}` : undefined,
3698
+ metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
3699
+ metadata.goalId ? `goal:${metadata.goalId}` : undefined,
3700
+ metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
3701
+ ].filter(Boolean);
3702
+ if (parts.length > 1)
3703
+ return parts.join(":");
3704
+ return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
3705
+ }
3599
3706
  function allowlistEnv(allowlist) {
3600
3707
  const env = {};
3601
3708
  if (allowlist?.tools?.length)
@@ -3632,6 +3739,20 @@ function codewithLikeSandbox(target) {
3632
3739
  function configStringValue(value) {
3633
3740
  return JSON.stringify(value);
3634
3741
  }
3742
+ var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2 = new Set([
3743
+ "e",
3744
+ "exec",
3745
+ "agent",
3746
+ "start",
3747
+ "--ephemeral",
3748
+ "--ignore-rules",
3749
+ "--skip-git-repo-check",
3750
+ "--json",
3751
+ "--output-last-message",
3752
+ "-o",
3753
+ "--output-schema",
3754
+ "--dangerously-bypass-approvals-and-sandbox"
3755
+ ]);
3635
3756
  function assertStringOption(value, label) {
3636
3757
  if (value !== undefined && typeof value !== "string")
3637
3758
  throw new Error(`${label} must be a string`);
@@ -3662,6 +3783,14 @@ function assertSupportedAgentOptions(target) {
3662
3783
  }
3663
3784
  if (target.provider === "codex" && target.agent !== undefined)
3664
3785
  throw new Error("codex.agent is not supported");
3786
+ if (target.provider === "codewith" && target.agent !== undefined) {
3787
+ throw new Error("codewith.agent is not supported by the durable background-agent adapter");
3788
+ }
3789
+ if (target.provider === "codewith") {
3790
+ const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS2.has(arg));
3791
+ if (unsafe)
3792
+ throw new Error(`codewith.extraArgs cannot include ${unsafe}; durable agent steps use codewith agent start, not exec/ephemeral flags`);
3793
+ }
3665
3794
  if (["codewith", "codex"].includes(target.provider)) {
3666
3795
  if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
3667
3796
  throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
@@ -3745,18 +3874,18 @@ function agentArgs(target) {
3745
3874
  args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
3746
3875
  if (target.variant)
3747
3876
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
3748
- args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
3749
- if (isolation === "safe")
3750
- args.push("--ignore-rules");
3877
+ args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
3751
3878
  if (target.cwd)
3752
3879
  args.push("--cd", target.cwd);
3753
3880
  for (const dir of target.addDirs ?? [])
3754
3881
  args.push("--add-dir", dir);
3755
3882
  if (target.model)
3756
3883
  args.push("--model", target.model);
3757
- if (target.agent)
3758
- args.push("--agent", target.agent);
3759
3884
  args.push(...target.extraArgs ?? []);
3885
+ args.push("agent", "start");
3886
+ if (target.cwd)
3887
+ args.push("--cwd", target.cwd);
3888
+ args.push(target.prompt);
3760
3889
  return args;
3761
3890
  case "codex":
3762
3891
  if (target.variant)
@@ -3832,8 +3961,9 @@ function commandSpec(target) {
3832
3961
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
3833
3962
  nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
3834
3963
  preflightAnyOf: agentTarget.provider === "cursor" ? ["agent"] : undefined,
3835
- stdin: agentTarget.prompt,
3836
- allowlist: agentTarget.allowlist
3964
+ stdin: agentTarget.provider === "codewith" ? undefined : agentTarget.prompt,
3965
+ allowlist: agentTarget.allowlist,
3966
+ codewithDurableAgent: agentTarget.provider === "codewith" ? { target: agentTarget } : undefined
3837
3967
  };
3838
3968
  }
3839
3969
  function executionEnv(spec, metadata, opts) {
@@ -3953,6 +4083,238 @@ function preflightNativeAuthProfile(spec, env) {
3953
4083
  throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
3954
4084
  }
3955
4085
  }
4086
+ function codewithAgentStartArgs(target, idempotencyKey) {
4087
+ const args = agentArgs(target);
4088
+ const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
4089
+ if (startIndex === -1)
4090
+ throw new Error("internal error: codewith durable agent args missing agent start");
4091
+ args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
4092
+ return args;
4093
+ }
4094
+ function codewithAgentControlArgs(target, command, agentId) {
4095
+ return [
4096
+ ...target.authProfile ? ["--auth-profile", target.authProfile] : [],
4097
+ "agent",
4098
+ command,
4099
+ ...command === "logs" ? ["--limit", "20"] : [],
4100
+ agentId
4101
+ ];
4102
+ }
4103
+ function parseJsonOutput(stdout, label) {
4104
+ try {
4105
+ const value = JSON.parse(stdout || "{}");
4106
+ if (!value || typeof value !== "object" || Array.isArray(value))
4107
+ throw new Error("not an object");
4108
+ return value;
4109
+ } catch (error) {
4110
+ throw new Error(`${label} did not return JSON${error instanceof Error ? `: ${error.message}` : ""}`);
4111
+ }
4112
+ }
4113
+ function recordField(value, key) {
4114
+ if (!value || typeof value !== "object" || Array.isArray(value))
4115
+ return;
4116
+ const field = value[key];
4117
+ return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
4118
+ }
4119
+ function stringField(value, key) {
4120
+ const field = value?.[key];
4121
+ return typeof field === "string" ? field : undefined;
4122
+ }
4123
+ function numberField(value, key) {
4124
+ const field = value?.[key];
4125
+ return typeof field === "number" && Number.isFinite(field) ? field : undefined;
4126
+ }
4127
+ function codewithAgentStatus(readJson) {
4128
+ return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
4129
+ }
4130
+ function codewithAgentEvidence(startJson, readJson, logsJson) {
4131
+ const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
4132
+ const statusSnapshot = recordField(readJson, "statusSnapshot");
4133
+ const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
4134
+ seq: numberField(event, "seq"),
4135
+ eventType: stringField(event, "eventType"),
4136
+ createdAt: numberField(event, "createdAt")
4137
+ })) : undefined;
4138
+ return JSON.stringify({
4139
+ codewithAgent: {
4140
+ agentId: stringField(agent, "agentId"),
4141
+ status: stringField(agent, "status"),
4142
+ desiredState: stringField(agent, "desiredState"),
4143
+ statusReason: stringField(agent, "statusReason"),
4144
+ threadId: stringField(agent, "threadId"),
4145
+ rolloutPath: stringField(agent, "rolloutPath"),
4146
+ pid: numberField(agent, "pid"),
4147
+ exitCode: numberField(agent, "exitCode"),
4148
+ created: typeof startJson.created === "boolean" ? startJson.created : undefined
4149
+ },
4150
+ statusSnapshot: statusSnapshot ? {
4151
+ seq: numberField(statusSnapshot, "seq"),
4152
+ status: stringField(statusSnapshot, "status"),
4153
+ summary: stringField(statusSnapshot, "summary"),
4154
+ pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
4155
+ lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
4156
+ } : undefined,
4157
+ events
4158
+ }, null, 2);
4159
+ }
4160
+ function sleep(ms) {
4161
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4162
+ }
4163
+ async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
4164
+ const target = spec.codewithDurableAgent?.target;
4165
+ if (!target)
4166
+ throw new Error("internal error: missing codewith durable target");
4167
+ const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
4168
+ const idempotencyKey = codewithAgentIdempotencyKey(metadata);
4169
+ const start = spawnSync2(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
4170
+ cwd: spec.cwd,
4171
+ env,
4172
+ encoding: "utf8",
4173
+ stdio: ["ignore", "pipe", "pipe"],
4174
+ timeout: 30000
4175
+ });
4176
+ let stderr = start.stderr || "";
4177
+ if (start.error || (start.status ?? 1) !== 0) {
4178
+ const finishedAt = nowIso();
4179
+ return {
4180
+ status: "failed",
4181
+ exitCode: start.status ?? undefined,
4182
+ stdout: start.stdout || "",
4183
+ stderr,
4184
+ error: start.error?.message ?? `codewith agent start exited with code ${start.status ?? "unknown"}`,
4185
+ startedAt,
4186
+ finishedAt,
4187
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4188
+ };
4189
+ }
4190
+ let startJson;
4191
+ try {
4192
+ startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
4193
+ } catch (error) {
4194
+ const finishedAt = nowIso();
4195
+ return {
4196
+ status: "failed",
4197
+ stdout: start.stdout || "",
4198
+ stderr,
4199
+ error: error instanceof Error ? error.message : String(error),
4200
+ startedAt,
4201
+ finishedAt,
4202
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4203
+ };
4204
+ }
4205
+ const agentId = stringField(recordField(startJson, "agent"), "agentId");
4206
+ if (!agentId) {
4207
+ const finishedAt = nowIso();
4208
+ return {
4209
+ status: "failed",
4210
+ stdout: start.stdout || "",
4211
+ stderr,
4212
+ error: "codewith agent start did not return agent.agentId",
4213
+ startedAt,
4214
+ finishedAt,
4215
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4216
+ };
4217
+ }
4218
+ const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
4219
+ let lastReadJson = startJson;
4220
+ let lastLogsJson;
4221
+ let stdout = "";
4222
+ while (true) {
4223
+ if (opts.signal?.aborted) {
4224
+ spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
4225
+ const finishedAt = nowIso();
4226
+ return {
4227
+ status: "failed",
4228
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4229
+ stderr,
4230
+ error: "cancelled",
4231
+ startedAt,
4232
+ finishedAt,
4233
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4234
+ };
4235
+ }
4236
+ const read = spawnSync2(spec.command, codewithAgentControlArgs(target, "read", agentId), {
4237
+ cwd: spec.cwd,
4238
+ env,
4239
+ encoding: "utf8",
4240
+ stdio: ["ignore", "pipe", "pipe"],
4241
+ timeout: 30000
4242
+ });
4243
+ stderr = appendBoundedText(stderr, read.stderr || "", maxOutputBytes);
4244
+ if (read.error || (read.status ?? 1) !== 0) {
4245
+ const finishedAt = nowIso();
4246
+ return {
4247
+ status: "failed",
4248
+ exitCode: read.status ?? undefined,
4249
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4250
+ stderr,
4251
+ error: read.error?.message ?? `codewith agent read exited with code ${read.status ?? "unknown"}`,
4252
+ startedAt,
4253
+ finishedAt,
4254
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4255
+ };
4256
+ }
4257
+ try {
4258
+ lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
4259
+ } catch (error) {
4260
+ const finishedAt = nowIso();
4261
+ return {
4262
+ status: "failed",
4263
+ stdout: appendBoundedText(stdout, read.stdout || "", maxOutputBytes),
4264
+ stderr,
4265
+ error: error instanceof Error ? error.message : String(error),
4266
+ startedAt,
4267
+ finishedAt,
4268
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4269
+ };
4270
+ }
4271
+ const status = codewithAgentStatus(lastReadJson);
4272
+ if (status === "completed" || status === "failed" || status === "cancelled") {
4273
+ const logs = spawnSync2(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
4274
+ cwd: spec.cwd,
4275
+ env,
4276
+ encoding: "utf8",
4277
+ stdio: ["ignore", "pipe", "pipe"],
4278
+ timeout: 30000
4279
+ });
4280
+ if (!logs.error && (logs.status ?? 1) === 0) {
4281
+ try {
4282
+ lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
4283
+ } catch {
4284
+ lastLogsJson = undefined;
4285
+ }
4286
+ } else {
4287
+ stderr = appendBoundedText(stderr, logs.stderr || logs.error?.message || "", maxOutputBytes);
4288
+ }
4289
+ const finishedAt = nowIso();
4290
+ const resultStatus = status === "completed" ? "succeeded" : "failed";
4291
+ return {
4292
+ status: resultStatus,
4293
+ exitCode: resultStatus === "succeeded" ? 0 : 1,
4294
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4295
+ stderr,
4296
+ error: resultStatus === "succeeded" ? undefined : stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`,
4297
+ startedAt,
4298
+ finishedAt,
4299
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4300
+ };
4301
+ }
4302
+ if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
4303
+ spawnSync2(spec.command, codewithAgentControlArgs(target, "stop", agentId), { cwd: spec.cwd, env, stdio: "ignore", timeout: 15000 });
4304
+ const finishedAt = nowIso();
4305
+ return {
4306
+ status: "timed_out",
4307
+ stdout: appendBoundedText(stdout, codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes),
4308
+ stderr,
4309
+ error: `timed out after ${spec.timeoutMs}ms`,
4310
+ startedAt,
4311
+ finishedAt,
4312
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
4313
+ };
4314
+ }
4315
+ await sleep(pollMs);
4316
+ }
4317
+ }
3956
4318
  function preflightRemoteSpec(spec, machine, metadata, opts) {
3957
4319
  const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
3958
4320
  const result = spawnSync2(plan.command, plan.args, {
@@ -4124,6 +4486,19 @@ function preflightTarget(target, metadata = {}, opts = {}) {
4124
4486
  async function executeTarget(target, metadata = {}, opts = {}) {
4125
4487
  const spec = commandSpec(target);
4126
4488
  const machine = resolvedMachine(opts);
4489
+ if (machine && !machine.local && spec.codewithDurableAgent) {
4490
+ const startedAt2 = nowIso();
4491
+ const finishedAt2 = nowIso();
4492
+ return {
4493
+ status: "failed",
4494
+ stdout: "",
4495
+ stderr: "",
4496
+ error: "remote Codewith durable background-agent steps require remote status polling support; run this Codewith step locally or add remote durable readback before dispatch",
4497
+ startedAt: startedAt2,
4498
+ finishedAt: finishedAt2,
4499
+ durationMs: new Date(finishedAt2).getTime() - new Date(startedAt2).getTime()
4500
+ };
4501
+ }
4127
4502
  if (machine && !machine.local)
4128
4503
  return executeRemoteSpec(spec, machine, metadata, opts);
4129
4504
  const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
@@ -4170,6 +4545,9 @@ async function executeTarget(target, metadata = {}, opts = {}) {
4170
4545
  durationMs: 0
4171
4546
  };
4172
4547
  }
4548
+ if (spec.codewithDurableAgent) {
4549
+ return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
4550
+ }
4173
4551
  const child = spawn(spec.command, spec.args, {
4174
4552
  cwd: spec.cwd,
4175
4553
  env,
@@ -4521,7 +4899,7 @@ function publicGoalRun(run) {
4521
4899
  // package.json
4522
4900
  var package_default = {
4523
4901
  name: "@hasna/loops",
4524
- version: "0.3.58",
4902
+ version: "0.3.60",
4525
4903
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4526
4904
  type: "module",
4527
4905
  main: "dist/index.js",
@@ -99,6 +99,16 @@ export declare class Store {
99
99
  previousWorkflow: WorkflowSpec;
100
100
  archivedOld?: WorkflowSpec;
101
101
  };
102
+ cloneWorkflowWithoutGoalAndRetargetLoop(idOrName: string, opts: DaemonLeaseFence & {
103
+ workflowName: string;
104
+ workflowTimeoutMs?: TimeoutMs;
105
+ archiveOld?: boolean;
106
+ }): {
107
+ loop: Loop;
108
+ workflow: WorkflowSpec;
109
+ previousWorkflow: WorkflowSpec;
110
+ archivedOld?: WorkflowSpec;
111
+ };
102
112
  renameLoop(id: string, name: string, opts?: DaemonLeaseFence): Loop;
103
113
  archiveLoop(idOrName: string): Loop;
104
114
  unarchiveLoop(idOrName: string): Loop;
package/dist/lib/store.js CHANGED
@@ -356,6 +356,20 @@ function optionalStringArray(value, label) {
356
356
  }).filter(Boolean);
357
357
  return values.length ? values : undefined;
358
358
  }
359
+ var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
360
+ "e",
361
+ "exec",
362
+ "agent",
363
+ "start",
364
+ "--ephemeral",
365
+ "--ignore-rules",
366
+ "--skip-git-repo-check",
367
+ "--json",
368
+ "--output-last-message",
369
+ "-o",
370
+ "--output-schema",
371
+ "--dangerously-bypass-approvals-and-sandbox"
372
+ ]);
359
373
  function optionalAccountRef(value, label) {
360
374
  if (value === undefined)
361
375
  return;
@@ -470,6 +484,15 @@ function validateTarget(value, label, opts) {
470
484
  throw new Error(`${label}.variant is not supported for provider cursor`);
471
485
  if (value.provider === "codex" && value.agent !== undefined)
472
486
  throw new Error(`${label}.agent is not supported for provider codex`);
487
+ if (value.provider === "codewith" && value.agent !== undefined) {
488
+ throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
489
+ }
490
+ const extraArgs = optionalStringArray(value.extraArgs, `${label}.extraArgs`);
491
+ if (value.provider === "codewith") {
492
+ const unsafe = extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
493
+ if (unsafe)
494
+ throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
495
+ }
473
496
  optionalStringArray(value.addDirs, `${label}.addDirs`);
474
497
  if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
475
498
  throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
@@ -545,7 +568,9 @@ function validateTarget(value, label, opts) {
545
568
  if (value.routing.eventSource !== undefined)
546
569
  assertString(value.routing.eventSource, `${label}.routing.eventSource`);
547
570
  }
548
- const target = { ...value };
571
+ const target = { ...value, extraArgs };
572
+ if (!extraArgs)
573
+ delete target.extraArgs;
549
574
  delete target.promptFile;
550
575
  delete target.promptSource;
551
576
  return { ...target, ...promptFields };
@@ -1541,6 +1566,70 @@ class Store {
1541
1566
  throw error;
1542
1567
  }
1543
1568
  }
1569
+ cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
1570
+ const updated = (opts.now ?? new Date).toISOString();
1571
+ this.db.exec("BEGIN IMMEDIATE");
1572
+ try {
1573
+ const current = this.requireUniqueLoop(idOrName);
1574
+ if (current.target.type !== "workflow")
1575
+ throw new Error(`loop is not a workflow loop: ${idOrName}`);
1576
+ if (this.hasRunningRun(current.id))
1577
+ throw new Error(`refusing to retarget running loop: ${current.id}`);
1578
+ if (!current.goal)
1579
+ throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
1580
+ const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1581
+ if (!previousWorkflow.goal)
1582
+ throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
1583
+ const workflow = {
1584
+ id: genId(),
1585
+ name: opts.workflowName,
1586
+ description: previousWorkflow.description,
1587
+ version: previousWorkflow.version,
1588
+ status: "active",
1589
+ steps: previousWorkflow.steps,
1590
+ createdAt: updated,
1591
+ updatedAt: updated
1592
+ };
1593
+ this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
1594
+ VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
1595
+ $id: workflow.id,
1596
+ $name: workflow.name,
1597
+ $description: workflow.description ?? null,
1598
+ $version: workflow.version,
1599
+ $status: workflow.status,
1600
+ $steps: JSON.stringify(workflow.steps),
1601
+ $created: workflow.createdAt,
1602
+ $updated: workflow.updatedAt
1603
+ });
1604
+ const target = { ...current.target, workflowId: workflow.id };
1605
+ if (opts.workflowTimeoutMs !== undefined)
1606
+ target.timeoutMs = opts.workflowTimeoutMs;
1607
+ const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
1608
+ WHERE id=$id
1609
+ AND ($daemonLeaseId IS NULL OR EXISTS (
1610
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
1611
+ ))`).run({
1612
+ $id: current.id,
1613
+ $target: JSON.stringify(target),
1614
+ $updated: updated,
1615
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
1616
+ $now: updated
1617
+ });
1618
+ if (res.changes !== 1)
1619
+ throw new Error("daemon lease lost");
1620
+ const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
1621
+ this.db.exec("COMMIT");
1622
+ const loop = this.getLoop(current.id);
1623
+ if (!loop)
1624
+ throw new Error(`loop not found after retarget: ${current.id}`);
1625
+ return { loop, workflow, previousWorkflow, archivedOld };
1626
+ } catch (error) {
1627
+ try {
1628
+ this.db.exec("ROLLBACK");
1629
+ } catch {}
1630
+ throw error;
1631
+ }
1632
+ }
1544
1633
  renameLoop(id, name, opts = {}) {
1545
1634
  const current = this.getLoop(id);
1546
1635
  if (!current)