@hasna/loops 0.4.8 → 0.4.10

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.
@@ -56,8 +56,12 @@ function nowIso() {
56
56
  import { mkdirSync } from "fs";
57
57
  import { homedir } from "os";
58
58
  import { join } from "path";
59
+ function homeDir() {
60
+ const home = process.env.HOME?.trim();
61
+ return home ? home : homedir();
62
+ }
59
63
  function dataDir() {
60
- return process.env.LOOPS_DATA_DIR || join(homedir(), ".hasna", "loops");
64
+ return process.env.LOOPS_DATA_DIR || join(homeDir(), ".hasna", "loops");
61
65
  }
62
66
  function ensureDataDir() {
63
67
  const dir = dataDir();
@@ -74,10 +78,10 @@ function daemonLogPath() {
74
78
  return join(dataDir(), "daemon.log");
75
79
  }
76
80
  function systemdServicePath() {
77
- return join(homedir(), ".config", "systemd", "user", "loops-daemon.service");
81
+ return join(homeDir(), ".config", "systemd", "user", "loops-daemon.service");
78
82
  }
79
83
  function launchdPlistPath() {
80
- return join(homedir(), "Library", "LaunchAgents", "com.hasna.loops.daemon.plist");
84
+ return join(homeDir(), "Library", "LaunchAgents", "com.hasna.loops.daemon.plist");
81
85
  }
82
86
 
83
87
  // src/lib/process-identity.ts
@@ -581,7 +585,7 @@ import { isAbsolute, resolve } from "path";
581
585
 
582
586
  // src/lib/agent-adapter.ts
583
587
  import { spawn } from "child_process";
584
- var UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS = new Set([
588
+ var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
585
589
  "e",
586
590
  "exec",
587
591
  "agent",
@@ -630,12 +634,12 @@ function validateAgentOptions(target, label, capabilities) {
630
634
  throw new Error(`${label}.agent is not supported for provider codex`);
631
635
  }
632
636
  if (provider === "codewith" && target.agent !== undefined) {
633
- throw new Error(`${label}.agent is not supported for provider codewith durable background-agent execution`);
637
+ throw new Error(`${label}.agent is not supported for provider codewith`);
634
638
  }
635
639
  if (provider === "codewith") {
636
- const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS.has(arg));
640
+ const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
637
641
  if (unsafe) {
638
- throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith agent steps use durable agent start, not exec/ephemeral flags`);
642
+ throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
639
643
  }
640
644
  }
641
645
  if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
@@ -718,7 +722,10 @@ function buildAgentInvocation(target) {
718
722
  args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
719
723
  if (target.variant)
720
724
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
721
- args.push("--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
725
+ const sandbox = codewithLikeSandbox(target);
726
+ if (sandbox === "workspace-write")
727
+ args.push("-c", "sandbox_workspace_write.network_access=true");
728
+ args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
722
729
  if (target.cwd)
723
730
  args.push("--cd", target.cwd);
724
731
  for (const dir of target.addDirs ?? [])
@@ -726,11 +733,7 @@ function buildAgentInvocation(target) {
726
733
  if (target.model)
727
734
  args.push("--model", target.model);
728
735
  args.push(...target.extraArgs ?? []);
729
- args.push("agent", "start", "--json");
730
- if (target.cwd)
731
- args.push("--cwd", target.cwd);
732
- args.push(target.prompt);
733
- return { command: "codewith", args };
736
+ return { command: "codewith", args, stdin: target.prompt };
734
737
  }
735
738
  case "codex": {
736
739
  if (target.variant)
@@ -783,7 +786,7 @@ function adapterFor(provider, capabilities) {
783
786
  var PROVIDER_ADAPTERS = {
784
787
  claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
785
788
  cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
786
- codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: true, remote: false, promptChannel: "argv" }),
789
+ codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
787
790
  codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
788
791
  aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
789
792
  opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
@@ -1475,6 +1478,21 @@ function workItemStatusForLoopRun(status, attempt, maxAttempts) {
1475
1478
  function scrubbedOrNull(value) {
1476
1479
  return value == null ? null : scrubSecrets(value);
1477
1480
  }
1481
+ var MAX_PERSISTED_RUN_OUTPUT_CHARS = 64 * 1024;
1482
+ function clampPersistedRunOutput(value) {
1483
+ if (value == null || value.length <= MAX_PERSISTED_RUN_OUTPUT_CHARS)
1484
+ return value;
1485
+ const half = Math.floor(MAX_PERSISTED_RUN_OUTPUT_CHARS / 2);
1486
+ const head = value.slice(0, half);
1487
+ const tail = value.slice(value.length - half);
1488
+ const omitted = value.length - head.length - tail.length;
1489
+ return `${head}
1490
+ \u2026[${omitted} chars truncated by loops run-output retention]\u2026
1491
+ ${tail}`;
1492
+ }
1493
+ function persistedRunOutput(value) {
1494
+ return clampPersistedRunOutput(scrubbedOrNull(value));
1495
+ }
1478
1496
  function chmodIfExists(path, mode) {
1479
1497
  try {
1480
1498
  if (existsSync(path))
@@ -3221,8 +3239,8 @@ class Store {
3221
3239
  ))`).run({
3222
3240
  $workflowRunId: workflowRunId,
3223
3241
  $stepId: stepId,
3224
- $stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
3225
- $stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
3242
+ $stdout: progress.stdout === undefined ? null : persistedRunOutput(progress.stdout),
3243
+ $stderr: progress.stderr === undefined ? null : persistedRunOutput(progress.stderr),
3226
3244
  $updated: now,
3227
3245
  $daemonLeaseId: opts.daemonLeaseId ?? null,
3228
3246
  $now: now
@@ -3283,8 +3301,8 @@ class Store {
3283
3301
  $finished: finishedAt,
3284
3302
  $exitCode: patch.exitCode ?? null,
3285
3303
  $durationMs: patch.durationMs ?? null,
3286
- $stdout: scrubbedOrNull(patch.stdout),
3287
- $stderr: scrubbedOrNull(patch.stderr),
3304
+ $stdout: persistedRunOutput(patch.stdout),
3305
+ $stderr: persistedRunOutput(patch.stderr),
3288
3306
  $error: error ?? null,
3289
3307
  $updated: finishedAt,
3290
3308
  $daemonLeaseId: opts.daemonLeaseId ?? null,
@@ -3662,8 +3680,8 @@ class Store {
3662
3680
  $pid: patch.pid ?? null,
3663
3681
  $exitCode: patch.exitCode ?? null,
3664
3682
  $durationMs: patch.durationMs ?? null,
3665
- $stdout: scrubbedOrNull(patch.stdout),
3666
- $stderr: scrubbedOrNull(patch.stderr),
3683
+ $stdout: persistedRunOutput(patch.stdout),
3684
+ $stderr: persistedRunOutput(patch.stderr),
3667
3685
  $error: error ?? null,
3668
3686
  $updated: finishedAt,
3669
3687
  $claimedBy: opts.claimedBy ?? null,
@@ -4060,8 +4078,8 @@ class Store {
4060
4078
  $processStartedAt: run.processStartedAt ?? null,
4061
4079
  $exitCode: run.exitCode ?? null,
4062
4080
  $durationMs: run.durationMs ?? null,
4063
- $stdout: scrubbedOrNull(run.stdout),
4064
- $stderr: scrubbedOrNull(run.stderr),
4081
+ $stdout: persistedRunOutput(run.stdout),
4082
+ $stderr: persistedRunOutput(run.stderr),
4065
4083
  $error: scrubbedOrNull(run.error),
4066
4084
  $goalRunId: run.goalRunId ?? null,
4067
4085
  $created: run.createdAt,
@@ -5063,11 +5081,6 @@ var TRANSPORT_ENV_KEYS = new Set([
5063
5081
  "USER",
5064
5082
  "XDG_RUNTIME_DIR"
5065
5083
  ]);
5066
- function boundedText(text, maxBytes) {
5067
- const buffer = new BoundedOutputBuffer(maxBytes);
5068
- buffer.append(text);
5069
- return buffer.value();
5070
- }
5071
5084
  function buildResult(status, startedAt, fields = {}) {
5072
5085
  const finishedAt = fields.finishedAt ?? nowIso();
5073
5086
  return {
@@ -5105,6 +5118,20 @@ function notifySpawn(pid, opts) {
5105
5118
  function shellQuote(value) {
5106
5119
  return `'${value.replace(/'/g, `'\\''`)}'`;
5107
5120
  }
5121
+ function codewithProfileCandidateFromLine(line) {
5122
+ const cols = line.trim().split(/\s+/);
5123
+ if (cols[0] === "*")
5124
+ return cols[1];
5125
+ return cols[0];
5126
+ }
5127
+ function codewithProfileForError(profile) {
5128
+ return /[\u0000-\u001F\u007F]/.test(profile) ? JSON.stringify(profile) ?? profile : profile;
5129
+ }
5130
+ function assertCodewithAuthProfileSupported(profile) {
5131
+ if (profile.includes("\x00")) {
5132
+ throw new Error(`codewith auth profile contains unsupported NUL byte: ${codewithProfileForError(profile)}`);
5133
+ }
5134
+ }
5108
5135
  function metadataEnv(metadata) {
5109
5136
  const env = {};
5110
5137
  if (metadata.loopId)
@@ -5131,21 +5158,6 @@ function metadataEnv(metadata) {
5131
5158
  env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
5132
5159
  return env;
5133
5160
  }
5134
- function codewithAgentIdempotencyKey(metadata) {
5135
- const parts = [
5136
- "openloops",
5137
- metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
5138
- metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
5139
- metadata.runId ? `loop-run:${metadata.runId}` : undefined,
5140
- metadata.loopId ? `loop:${metadata.loopId}` : undefined,
5141
- metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
5142
- metadata.goalId ? `goal:${metadata.goalId}` : undefined,
5143
- metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
5144
- ].filter(Boolean);
5145
- if (parts.length > 1)
5146
- return parts.join(":");
5147
- return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
5148
- }
5149
5161
  function allowlistEnv(allowlist) {
5150
5162
  const env = {};
5151
5163
  if (allowlist?.tools?.length)
@@ -5186,6 +5198,9 @@ function commandSpec(target, opts) {
5186
5198
  };
5187
5199
  }
5188
5200
  const agentTarget = target;
5201
+ if (agentTarget.provider === "codewith" && agentTarget.authProfile) {
5202
+ assertCodewithAuthProfileSupported(agentTarget.authProfile);
5203
+ }
5189
5204
  const adapter = providerAdapter(agentTarget.provider);
5190
5205
  const invocation = adapter.buildInvocation(agentTarget);
5191
5206
  return {
@@ -5200,8 +5215,7 @@ function commandSpec(target, opts) {
5200
5215
  preflightAnyOf: invocation.preflightAnyOf,
5201
5216
  stdin: invocation.stdin,
5202
5217
  allowlist: agentTarget.allowlist,
5203
- worktree: agentTarget.worktree,
5204
- codewithDurableAgent: adapter.capabilities.durable ? { target: agentTarget } : undefined
5218
+ worktree: agentTarget.worktree
5205
5219
  };
5206
5220
  }
5207
5221
  function composeExecutionEnv(spec, metadata, opts, accountEnv) {
@@ -5359,7 +5373,8 @@ function remotePreflightScript(spec, metadata) {
5359
5373
  lines.push(`if ! ${spec.preflightAnyOf.map((command) => `command -v ${shellQuote(command)} >/dev/null 2>&1`).join(" && ! ")}; then`, ` echo 'none of required executables found: ${spec.preflightAnyOf.join(", ")}' >&2`, " exit 127", "fi");
5360
5374
  }
5361
5375
  if (spec.nativeAuthProfile?.provider === "codewith") {
5362
- lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
5376
+ const profileForError = codewithProfileForError(spec.nativeAuthProfile.profile);
5377
+ lines.push(`__OPENLOOPS_CODEWITH_PROFILE=${shellQuote(spec.nativeAuthProfile.profile)}`, "export __OPENLOOPS_CODEWITH_PROFILE", `__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { candidate = ($1 == "*" ? $2 : $1); if (candidate == ENVIRON["__OPENLOOPS_CODEWITH_PROFILE"]) found = 1 } END { exit(found ? 0 : 1) }'; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${profileForError}`)} >&2`, " exit 1", "fi");
5363
5378
  }
5364
5379
  return lines.join(`
5365
5380
  `);
@@ -5384,9 +5399,9 @@ function assertCodewithProfileListed(profile, result) {
5384
5399
  const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
5385
5400
  throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
5386
5401
  }
5387
- const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
5402
+ const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(codewithProfileCandidateFromLine).filter(Boolean));
5388
5403
  if (!profiles.has(profile)) {
5389
- throw new Error(`codewith auth profile not found: ${profile}`);
5404
+ throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
5390
5405
  }
5391
5406
  }
5392
5407
  function preflightNativeAuthProfileSync(spec, env) {
@@ -5411,266 +5426,6 @@ async function preflightNativeAuthProfile(spec, env) {
5411
5426
  const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
5412
5427
  assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
5413
5428
  }
5414
- function codewithAgentStartArgs(target, idempotencyKey) {
5415
- const args = providerAdapter(target.provider).buildInvocation(target).args;
5416
- const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
5417
- if (startIndex === -1)
5418
- throw new Error("internal error: codewith durable agent args missing agent start");
5419
- args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
5420
- return args;
5421
- }
5422
- function codewithAgentControlArgs(target, command, agentId) {
5423
- return [
5424
- ...target.authProfile ? ["--auth-profile", target.authProfile] : [],
5425
- "agent",
5426
- command,
5427
- ...command === "logs" ? ["--limit", "20"] : [],
5428
- "--json",
5429
- agentId
5430
- ];
5431
- }
5432
- function extractFirstJsonObject(text) {
5433
- let depth = 0;
5434
- let start = -1;
5435
- let inString = false;
5436
- let escaped = false;
5437
- for (let i = 0;i < text.length; i += 1) {
5438
- const ch = text[i];
5439
- if (inString) {
5440
- if (escaped)
5441
- escaped = false;
5442
- else if (ch === "\\")
5443
- escaped = true;
5444
- else if (ch === '"')
5445
- inString = false;
5446
- continue;
5447
- }
5448
- if (ch === '"') {
5449
- inString = true;
5450
- } else if (ch === "{") {
5451
- if (depth === 0)
5452
- start = i;
5453
- depth += 1;
5454
- } else if (ch === "}") {
5455
- if (depth > 0) {
5456
- depth -= 1;
5457
- if (depth === 0 && start !== -1)
5458
- return text.slice(start, i + 1);
5459
- }
5460
- }
5461
- }
5462
- return;
5463
- }
5464
- function parseJsonOutput(stdout, label) {
5465
- const raw = stdout || "{}";
5466
- const candidates = [raw.trim()];
5467
- const scanned = extractFirstJsonObject(raw);
5468
- if (scanned && scanned !== raw.trim())
5469
- candidates.push(scanned);
5470
- let lastError;
5471
- for (const candidate of candidates) {
5472
- try {
5473
- const value = JSON.parse(candidate);
5474
- if (!value || typeof value !== "object" || Array.isArray(value))
5475
- throw new Error("not an object");
5476
- return value;
5477
- } catch (error) {
5478
- lastError = error;
5479
- }
5480
- }
5481
- throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
5482
- }
5483
- function recordField(value, key) {
5484
- if (!value || typeof value !== "object" || Array.isArray(value))
5485
- return;
5486
- const field = value[key];
5487
- return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
5488
- }
5489
- function stringField(value, key) {
5490
- const field = value?.[key];
5491
- return typeof field === "string" ? field : undefined;
5492
- }
5493
- function numberField(value, key) {
5494
- const field = value?.[key];
5495
- return typeof field === "number" && Number.isFinite(field) ? field : undefined;
5496
- }
5497
- function codewithAgentStatus(readJson) {
5498
- return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
5499
- }
5500
- function codewithAgentLastEventSeq(readJson) {
5501
- const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
5502
- const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
5503
- if (agentSeq !== undefined && snapshotSeq !== undefined)
5504
- return Math.max(agentSeq, snapshotSeq);
5505
- return agentSeq ?? snapshotSeq;
5506
- }
5507
- function codewithAgentEvidence(startJson, readJson, logsJson) {
5508
- const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
5509
- const statusSnapshot = recordField(readJson, "statusSnapshot");
5510
- const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
5511
- seq: numberField(event, "seq"),
5512
- eventType: stringField(event, "eventType"),
5513
- createdAt: numberField(event, "createdAt")
5514
- })) : undefined;
5515
- return JSON.stringify({
5516
- codewithAgent: {
5517
- agentId: stringField(agent, "agentId"),
5518
- status: stringField(agent, "status"),
5519
- desiredState: stringField(agent, "desiredState"),
5520
- statusReason: stringField(agent, "statusReason"),
5521
- threadId: stringField(agent, "threadId"),
5522
- rolloutPath: stringField(agent, "rolloutPath"),
5523
- pid: numberField(agent, "pid"),
5524
- exitCode: numberField(agent, "exitCode"),
5525
- created: typeof startJson.created === "boolean" ? startJson.created : undefined
5526
- },
5527
- statusSnapshot: statusSnapshot ? {
5528
- seq: numberField(statusSnapshot, "seq"),
5529
- status: stringField(statusSnapshot, "status"),
5530
- summary: stringField(statusSnapshot, "summary"),
5531
- pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
5532
- lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
5533
- } : undefined,
5534
- events
5535
- }, null, 2);
5536
- }
5537
- function codewithAgentProgress(readJson) {
5538
- const agent = recordField(readJson, "agent");
5539
- const statusSnapshot = recordField(readJson, "statusSnapshot");
5540
- return {
5541
- provider: "codewith",
5542
- agentId: stringField(agent, "agentId"),
5543
- status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
5544
- summary: stringField(statusSnapshot, "summary"),
5545
- statusReason: stringField(agent, "statusReason"),
5546
- threadId: stringField(agent, "threadId"),
5547
- rolloutPath: stringField(agent, "rolloutPath"),
5548
- pid: numberField(agent, "pid"),
5549
- lastEventSeq: codewithAgentLastEventSeq(readJson)
5550
- };
5551
- }
5552
- function sleep(ms) {
5553
- return new Promise((resolve3) => setTimeout(resolve3, ms));
5554
- }
5555
- async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
5556
- const target = spec.codewithDurableAgent?.target;
5557
- if (!target)
5558
- throw new Error("internal error: missing codewith durable target");
5559
- const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
5560
- const idempotencyKey = codewithAgentIdempotencyKey(metadata);
5561
- const start = await spawnCapture(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
5562
- cwd: spec.cwd,
5563
- env,
5564
- timeoutMs: 30000,
5565
- maxOutputBytes
5566
- });
5567
- const stderr = new BoundedOutputBuffer(maxOutputBytes);
5568
- stderr.append(start.stderr);
5569
- if (start.error || (start.status ?? 1) !== 0) {
5570
- return failureResult(startedAt, start.error ?? `codewith agent start exited with code ${start.status ?? "unknown"}`, {
5571
- exitCode: start.status ?? undefined,
5572
- stdout: start.stdout,
5573
- stderr: stderr.value()
5574
- });
5575
- }
5576
- let startJson;
5577
- try {
5578
- startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
5579
- } catch (error) {
5580
- return failureResult(startedAt, error instanceof Error ? error.message : String(error), { stdout: start.stdout, stderr: stderr.value() });
5581
- }
5582
- const agentId = stringField(recordField(startJson, "agent"), "agentId");
5583
- if (!agentId) {
5584
- return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
5585
- }
5586
- opts.onAgentProgress?.(codewithAgentProgress(startJson));
5587
- const stopAgent = async () => {
5588
- await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
5589
- cwd: spec.cwd,
5590
- env,
5591
- timeoutMs: 15000,
5592
- maxOutputBytes
5593
- });
5594
- };
5595
- const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
5596
- let lastReadJson = startJson;
5597
- let lastLogsJson;
5598
- let lastFingerprint;
5599
- let lastProgressAt = Date.now();
5600
- const evidence = () => boundedText(codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes);
5601
- while (true) {
5602
- if (opts.signal?.aborted) {
5603
- await stopAgent();
5604
- return failureResult(startedAt, "cancelled", { stdout: evidence(), stderr: stderr.value() });
5605
- }
5606
- const read = await spawnCapture(spec.command, codewithAgentControlArgs(target, "read", agentId), {
5607
- cwd: spec.cwd,
5608
- env,
5609
- timeoutMs: 30000,
5610
- maxOutputBytes
5611
- });
5612
- stderr.append(read.stderr);
5613
- if (read.error || (read.status ?? 1) !== 0) {
5614
- return failureResult(startedAt, read.error ?? `codewith agent read exited with code ${read.status ?? "unknown"}`, {
5615
- exitCode: read.status ?? undefined,
5616
- stdout: evidence(),
5617
- stderr: stderr.value()
5618
- });
5619
- }
5620
- try {
5621
- lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
5622
- } catch (error) {
5623
- return failureResult(startedAt, error instanceof Error ? error.message : String(error), {
5624
- stdout: boundedText(read.stdout, maxOutputBytes),
5625
- stderr: stderr.value()
5626
- });
5627
- }
5628
- const status = codewithAgentStatus(lastReadJson);
5629
- const fingerprint = JSON.stringify({
5630
- status,
5631
- agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
5632
- snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
5633
- });
5634
- if (fingerprint !== lastFingerprint) {
5635
- lastFingerprint = fingerprint;
5636
- lastProgressAt = Date.now();
5637
- opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
5638
- }
5639
- if (status === "completed" || status === "failed" || status === "cancelled") {
5640
- const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
5641
- cwd: spec.cwd,
5642
- env,
5643
- timeoutMs: 30000,
5644
- maxOutputBytes
5645
- });
5646
- if (!logs.error && (logs.status ?? 1) === 0) {
5647
- try {
5648
- lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
5649
- } catch {
5650
- lastLogsJson = undefined;
5651
- }
5652
- } else {
5653
- stderr.append(logs.stderr || logs.error || "");
5654
- }
5655
- if (status === "completed") {
5656
- return successResult(startedAt, { exitCode: 0, stdout: evidence(), stderr: stderr.value() });
5657
- }
5658
- return failureResult(startedAt, stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`, { exitCode: 1, stdout: evidence(), stderr: stderr.value() });
5659
- }
5660
- if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
5661
- await stopAgent();
5662
- return timeoutResult(startedAt, `timed out after ${spec.timeoutMs}ms`, { stdout: evidence(), stderr: stderr.value() });
5663
- }
5664
- if (typeof spec.idleTimeoutMs === "number" && Date.now() - lastProgressAt >= spec.idleTimeoutMs) {
5665
- await stopAgent();
5666
- return timeoutResult(startedAt, `idle timed out after ${spec.idleTimeoutMs}ms without agent progress`, {
5667
- stdout: evidence(),
5668
- stderr: stderr.value()
5669
- });
5670
- }
5671
- await sleep(pollMs);
5672
- }
5673
- }
5674
5429
  function resolvedDirEquals(left, right) {
5675
5430
  try {
5676
5431
  return realpathSync(left) === realpathSync(right);
@@ -5904,9 +5659,6 @@ function preflightTarget(target, metadata = {}, opts = {}) {
5904
5659
  async function executeTarget(target, metadata = {}, opts = {}) {
5905
5660
  let spec = commandSpec(target, opts);
5906
5661
  const machine = resolvedMachine(opts);
5907
- if (machine && !machine.local && spec.codewithDurableAgent) {
5908
- return failureResult(nowIso(), "remote Codewith durable background-agent steps require remote status polling support; run this Codewith step locally or add remote durable readback before dispatch");
5909
- }
5910
5662
  if (machine && !machine.local) {
5911
5663
  const remoteFallbackSpec = spec.worktree?.enabled && spec.worktree.mode === "auto" ? worktreeFallbackSpec(target, opts, spec.worktree.originalCwd) : undefined;
5912
5664
  return executeRemoteSpec(spec, machine, metadata, opts, remoteFallbackSpec);
@@ -5937,9 +5689,6 @@ async function executeTarget(target, metadata = {}, opts = {}) {
5937
5689
  if (worktreeEntry?.fallbackCwd) {
5938
5690
  spec = worktreeFallbackSpec(target, opts, worktreeEntry.fallbackCwd) ?? spec;
5939
5691
  }
5940
- if (spec.codewithDurableAgent) {
5941
- return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
5942
- }
5943
5692
  const child = spawn2(spec.command, spec.args, {
5944
5693
  cwd: spec.cwd,
5945
5694
  env,
@@ -7412,7 +7161,7 @@ function realSleep(ms) {
7412
7161
  return new Promise((resolve3) => setTimeout(resolve3, ms));
7413
7162
  }
7414
7163
  async function runLoop(opts) {
7415
- const sleep2 = opts.sleep ?? realSleep;
7164
+ const sleep = opts.sleep ?? realSleep;
7416
7165
  const sliceMs = opts.sliceMs ?? 200;
7417
7166
  while (!opts.shouldStop()) {
7418
7167
  try {
@@ -7423,7 +7172,7 @@ async function runLoop(opts) {
7423
7172
  let waited = 0;
7424
7173
  while (waited < opts.intervalMs && !opts.shouldStop()) {
7425
7174
  const chunk = Math.min(sliceMs, opts.intervalMs - waited);
7426
- await sleep2(chunk);
7175
+ await sleep(chunk);
7427
7176
  waited += chunk;
7428
7177
  }
7429
7178
  }
@@ -7515,7 +7264,7 @@ function processExists(pid) {
7515
7264
  }
7516
7265
  }
7517
7266
  async function reapProcessGroups(entries, opts = {}) {
7518
- const sleep2 = opts.sleep ?? realSleep;
7267
+ const sleep = opts.sleep ?? realSleep;
7519
7268
  const graceMs = Math.max(100, opts.graceMs ?? 2000);
7520
7269
  const ownPgid = ownProcessGroupId();
7521
7270
  const targets = new Map;
@@ -7544,7 +7293,7 @@ async function reapProcessGroups(entries, opts = {}) {
7544
7293
  const steps = Math.max(1, Math.ceil(graceMs / 100));
7545
7294
  let remaining = live;
7546
7295
  for (let i = 0;i < steps && remaining.length > 0; i++) {
7547
- await sleep2(100);
7296
+ await sleep(100);
7548
7297
  remaining = remaining.filter((target) => signalReapTarget(target, 0));
7549
7298
  }
7550
7299
  for (const target of remaining) {
@@ -7586,7 +7335,7 @@ async function stopDaemon(opts = {}) {
7586
7335
  }
7587
7336
  if (!state.running || !state.pid)
7588
7337
  return { wasRunning: false, stopped: false, forced: false };
7589
- const sleep2 = opts.sleep ?? realSleep;
7338
+ const sleep = opts.sleep ?? realSleep;
7590
7339
  const store = new Store(join5(dirname4(path), "loops.db"));
7591
7340
  try {
7592
7341
  const lease = store.getDaemonLease();
@@ -7599,7 +7348,7 @@ async function stopDaemon(opts = {}) {
7599
7348
  }
7600
7349
  const steps = Math.max(1, Math.ceil((opts.timeoutMs ?? 6000) / 100));
7601
7350
  for (let i = 0;i < steps; i++) {
7602
- await sleep2(100);
7351
+ await sleep(100);
7603
7352
  if (!isAlive(state.pid, record?.startedAt)) {
7604
7353
  removePid(path);
7605
7354
  return { wasRunning: true, stopped: true, forced: false, pid: state.pid };
@@ -7608,13 +7357,13 @@ async function stopDaemon(opts = {}) {
7608
7357
  try {
7609
7358
  process.kill(state.pid, "SIGKILL");
7610
7359
  } catch {}
7611
- await sleep2(150);
7360
+ await sleep(150);
7612
7361
  removePid(path);
7613
7362
  let reapedPgids = [];
7614
7363
  if (leaseVerified && lease) {
7615
7364
  const ownedRuns = store.listRuns({ status: "running", limit: 1000 }).filter((run) => run.claimedBy !== undefined && run.claimedBy.endsWith(`:${lease.id}`));
7616
7365
  reapedPgids = await reapProcessGroups(ownedRuns.map(toReapableProcess), {
7617
- sleep: sleep2,
7366
+ sleep,
7618
7367
  graceMs: opts.reapGraceMs
7619
7368
  });
7620
7369
  }
@@ -7686,7 +7435,7 @@ async function runDaemon(opts = {}) {
7686
7435
  const leaseTtlMs = opts.leaseTtlMs ?? Math.max(60000, intervalMs * 10);
7687
7436
  const concurrency = Math.max(1, opts.concurrency ?? concurrencyFromEnv() ?? 4);
7688
7437
  const log = opts.log ?? defaultDaemonLog;
7689
- const sleep2 = opts.sleep ?? realSleep;
7438
+ const sleep = opts.sleep ?? realSleep;
7690
7439
  const lease = store.acquireDaemonLease({
7691
7440
  id: leaseId,
7692
7441
  pid: process.pid,
@@ -7750,7 +7499,7 @@ async function runDaemon(opts = {}) {
7750
7499
  const entries = recovered.map(toReapableProcess).filter((entry) => entry.pid !== undefined || entry.pgid !== undefined);
7751
7500
  if (entries.length === 0)
7752
7501
  return;
7753
- const killed = await reapProcessGroups(entries, { sleep: sleep2, graceMs: opts.reapGraceMs, log });
7502
+ const killed = await reapProcessGroups(entries, { sleep, graceMs: opts.reapGraceMs, log });
7754
7503
  if (killed.length > 0)
7755
7504
  log(`reaped orphan process groups: ${killed.join(", ")}`);
7756
7505
  };
@@ -7825,7 +7574,7 @@ async function runDaemon(opts = {}) {
7825
7574
  }
7826
7575
  await runLoop({
7827
7576
  intervalMs,
7828
- sleep: sleep2,
7577
+ sleep,
7829
7578
  shouldStop: opts.shouldStop ?? (() => stopFlag),
7830
7579
  onTickError: (err) => log(`tick error: ${err instanceof Error ? err.message : String(err)}`),
7831
7580
  tickFn: async () => {
@@ -7875,10 +7624,10 @@ async function startDaemon(opts) {
7875
7624
  stdio: ["ignore", out, out]
7876
7625
  });
7877
7626
  child.unref();
7878
- const sleep2 = opts.sleep ?? realSleep;
7627
+ const sleep = opts.sleep ?? realSleep;
7879
7628
  const deadline = Math.max(1, Math.ceil((opts.waitMs ?? 4000) / 100));
7880
7629
  for (let i = 0;i < deadline; i++) {
7881
- await sleep2(100);
7630
+ await sleep(100);
7882
7631
  const current = isDaemonRunning();
7883
7632
  if (current.running)
7884
7633
  return { started: true, alreadyRunning: false, pid: current.pid };
@@ -8003,7 +7752,7 @@ function enableStartup(result) {
8003
7752
  // package.json
8004
7753
  var package_default = {
8005
7754
  name: "@hasna/loops",
8006
- version: "0.4.8",
7755
+ version: "0.4.10",
8007
7756
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
8008
7757
  type: "module",
8009
7758
  main: "dist/index.js",