@hasna/loops 0.4.7 → 0.4.9
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/CHANGELOG.md +34 -0
- package/README.md +4 -4
- package/dist/api/index.js +1 -1
- package/dist/cli/index.js +97 -353
- package/dist/daemon/index.js +25 -313
- package/dist/index.js +26 -308
- package/dist/lib/agent-adapter.d.ts +1 -1
- package/dist/lib/mode.js +1 -1
- package/dist/lib/route/pr-review.d.ts +11 -1
- package/dist/lib/storage/index.js +10 -11
- package/dist/lib/storage/sqlite.js +10 -11
- package/dist/lib/store.js +10 -11
- package/dist/mcp/index.js +12 -300
- package/dist/runner/index.js +12 -300
- package/dist/sdk/index.js +12 -300
- package/docs/USAGE.md +4 -4
- package/package.json +1 -1
package/dist/daemon/index.js
CHANGED
|
@@ -581,7 +581,7 @@ import { isAbsolute, resolve } from "path";
|
|
|
581
581
|
|
|
582
582
|
// src/lib/agent-adapter.ts
|
|
583
583
|
import { spawn } from "child_process";
|
|
584
|
-
var
|
|
584
|
+
var UNSAFE_CODEWITH_EXEC_EXTRA_ARGS = new Set([
|
|
585
585
|
"e",
|
|
586
586
|
"exec",
|
|
587
587
|
"agent",
|
|
@@ -630,12 +630,12 @@ function validateAgentOptions(target, label, capabilities) {
|
|
|
630
630
|
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
631
631
|
}
|
|
632
632
|
if (provider === "codewith" && target.agent !== undefined) {
|
|
633
|
-
throw new Error(`${label}.agent is not supported for provider codewith
|
|
633
|
+
throw new Error(`${label}.agent is not supported for provider codewith`);
|
|
634
634
|
}
|
|
635
635
|
if (provider === "codewith") {
|
|
636
|
-
const unsafe = target.extraArgs?.find((arg) =>
|
|
636
|
+
const unsafe = target.extraArgs?.find((arg) => UNSAFE_CODEWITH_EXEC_EXTRA_ARGS.has(arg));
|
|
637
637
|
if (unsafe) {
|
|
638
|
-
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith
|
|
638
|
+
throw new Error(`${label}.extraArgs cannot include ${unsafe}; codewith exec launch flags are managed by the adapter`);
|
|
639
639
|
}
|
|
640
640
|
}
|
|
641
641
|
if (target.addDirs?.length && !["codewith", "codex"].includes(provider)) {
|
|
@@ -718,7 +718,10 @@ function buildAgentInvocation(target) {
|
|
|
718
718
|
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
719
719
|
if (target.variant)
|
|
720
720
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
721
|
-
|
|
721
|
+
const sandbox = codewithLikeSandbox(target);
|
|
722
|
+
if (sandbox === "workspace-write")
|
|
723
|
+
args.push("-c", "sandbox_workspace_write.network_access=true");
|
|
724
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", sandbox, "--skip-git-repo-check");
|
|
722
725
|
if (target.cwd)
|
|
723
726
|
args.push("--cd", target.cwd);
|
|
724
727
|
for (const dir of target.addDirs ?? [])
|
|
@@ -726,11 +729,7 @@ function buildAgentInvocation(target) {
|
|
|
726
729
|
if (target.model)
|
|
727
730
|
args.push("--model", target.model);
|
|
728
731
|
args.push(...target.extraArgs ?? []);
|
|
729
|
-
|
|
730
|
-
if (target.cwd)
|
|
731
|
-
args.push("--cwd", target.cwd);
|
|
732
|
-
args.push(target.prompt);
|
|
733
|
-
return { command: "codewith", args };
|
|
732
|
+
return { command: "codewith", args, stdin: target.prompt };
|
|
734
733
|
}
|
|
735
734
|
case "codex": {
|
|
736
735
|
if (target.variant)
|
|
@@ -783,7 +782,7 @@ function adapterFor(provider, capabilities) {
|
|
|
783
782
|
var PROVIDER_ADAPTERS = {
|
|
784
783
|
claude: adapterFor("claude", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
785
784
|
cursor: adapterFor("cursor", { sandbox: CURSOR_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
786
|
-
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable:
|
|
785
|
+
codewith: adapterFor("codewith", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
787
786
|
codex: adapterFor("codex", { sandbox: CODEX_LIKE_SANDBOXES, durable: false, remote: true, promptChannel: "stdin" }),
|
|
788
787
|
aicopilot: adapterFor("aicopilot", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" }),
|
|
789
788
|
opencode: adapterFor("opencode", { sandbox: [], durable: false, remote: true, promptChannel: "stdin" })
|
|
@@ -5063,11 +5062,6 @@ var TRANSPORT_ENV_KEYS = new Set([
|
|
|
5063
5062
|
"USER",
|
|
5064
5063
|
"XDG_RUNTIME_DIR"
|
|
5065
5064
|
]);
|
|
5066
|
-
function boundedText(text, maxBytes) {
|
|
5067
|
-
const buffer = new BoundedOutputBuffer(maxBytes);
|
|
5068
|
-
buffer.append(text);
|
|
5069
|
-
return buffer.value();
|
|
5070
|
-
}
|
|
5071
5065
|
function buildResult(status, startedAt, fields = {}) {
|
|
5072
5066
|
const finishedAt = fields.finishedAt ?? nowIso();
|
|
5073
5067
|
return {
|
|
@@ -5131,21 +5125,6 @@ function metadataEnv(metadata) {
|
|
|
5131
5125
|
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
5132
5126
|
return env;
|
|
5133
5127
|
}
|
|
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
5128
|
function allowlistEnv(allowlist) {
|
|
5150
5129
|
const env = {};
|
|
5151
5130
|
if (allowlist?.tools?.length)
|
|
@@ -5200,8 +5179,7 @@ function commandSpec(target, opts) {
|
|
|
5200
5179
|
preflightAnyOf: invocation.preflightAnyOf,
|
|
5201
5180
|
stdin: invocation.stdin,
|
|
5202
5181
|
allowlist: agentTarget.allowlist,
|
|
5203
|
-
worktree: agentTarget.worktree
|
|
5204
|
-
codewithDurableAgent: adapter.capabilities.durable ? { target: agentTarget } : undefined
|
|
5182
|
+
worktree: agentTarget.worktree
|
|
5205
5183
|
};
|
|
5206
5184
|
}
|
|
5207
5185
|
function composeExecutionEnv(spec, metadata, opts, accountEnv) {
|
|
@@ -5411,266 +5389,6 @@ async function preflightNativeAuthProfile(spec, env) {
|
|
|
5411
5389
|
const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
|
|
5412
5390
|
assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
|
|
5413
5391
|
}
|
|
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
5392
|
function resolvedDirEquals(left, right) {
|
|
5675
5393
|
try {
|
|
5676
5394
|
return realpathSync(left) === realpathSync(right);
|
|
@@ -5904,9 +5622,6 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
5904
5622
|
async function executeTarget(target, metadata = {}, opts = {}) {
|
|
5905
5623
|
let spec = commandSpec(target, opts);
|
|
5906
5624
|
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
5625
|
if (machine && !machine.local) {
|
|
5911
5626
|
const remoteFallbackSpec = spec.worktree?.enabled && spec.worktree.mode === "auto" ? worktreeFallbackSpec(target, opts, spec.worktree.originalCwd) : undefined;
|
|
5912
5627
|
return executeRemoteSpec(spec, machine, metadata, opts, remoteFallbackSpec);
|
|
@@ -5937,9 +5652,6 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
5937
5652
|
if (worktreeEntry?.fallbackCwd) {
|
|
5938
5653
|
spec = worktreeFallbackSpec(target, opts, worktreeEntry.fallbackCwd) ?? spec;
|
|
5939
5654
|
}
|
|
5940
|
-
if (spec.codewithDurableAgent) {
|
|
5941
|
-
return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
|
|
5942
|
-
}
|
|
5943
5655
|
const child = spawn2(spec.command, spec.args, {
|
|
5944
5656
|
cwd: spec.cwd,
|
|
5945
5657
|
env,
|
|
@@ -7412,7 +7124,7 @@ function realSleep(ms) {
|
|
|
7412
7124
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
7413
7125
|
}
|
|
7414
7126
|
async function runLoop(opts) {
|
|
7415
|
-
const
|
|
7127
|
+
const sleep = opts.sleep ?? realSleep;
|
|
7416
7128
|
const sliceMs = opts.sliceMs ?? 200;
|
|
7417
7129
|
while (!opts.shouldStop()) {
|
|
7418
7130
|
try {
|
|
@@ -7423,7 +7135,7 @@ async function runLoop(opts) {
|
|
|
7423
7135
|
let waited = 0;
|
|
7424
7136
|
while (waited < opts.intervalMs && !opts.shouldStop()) {
|
|
7425
7137
|
const chunk = Math.min(sliceMs, opts.intervalMs - waited);
|
|
7426
|
-
await
|
|
7138
|
+
await sleep(chunk);
|
|
7427
7139
|
waited += chunk;
|
|
7428
7140
|
}
|
|
7429
7141
|
}
|
|
@@ -7515,7 +7227,7 @@ function processExists(pid) {
|
|
|
7515
7227
|
}
|
|
7516
7228
|
}
|
|
7517
7229
|
async function reapProcessGroups(entries, opts = {}) {
|
|
7518
|
-
const
|
|
7230
|
+
const sleep = opts.sleep ?? realSleep;
|
|
7519
7231
|
const graceMs = Math.max(100, opts.graceMs ?? 2000);
|
|
7520
7232
|
const ownPgid = ownProcessGroupId();
|
|
7521
7233
|
const targets = new Map;
|
|
@@ -7544,7 +7256,7 @@ async function reapProcessGroups(entries, opts = {}) {
|
|
|
7544
7256
|
const steps = Math.max(1, Math.ceil(graceMs / 100));
|
|
7545
7257
|
let remaining = live;
|
|
7546
7258
|
for (let i = 0;i < steps && remaining.length > 0; i++) {
|
|
7547
|
-
await
|
|
7259
|
+
await sleep(100);
|
|
7548
7260
|
remaining = remaining.filter((target) => signalReapTarget(target, 0));
|
|
7549
7261
|
}
|
|
7550
7262
|
for (const target of remaining) {
|
|
@@ -7586,7 +7298,7 @@ async function stopDaemon(opts = {}) {
|
|
|
7586
7298
|
}
|
|
7587
7299
|
if (!state.running || !state.pid)
|
|
7588
7300
|
return { wasRunning: false, stopped: false, forced: false };
|
|
7589
|
-
const
|
|
7301
|
+
const sleep = opts.sleep ?? realSleep;
|
|
7590
7302
|
const store = new Store(join5(dirname4(path), "loops.db"));
|
|
7591
7303
|
try {
|
|
7592
7304
|
const lease = store.getDaemonLease();
|
|
@@ -7599,7 +7311,7 @@ async function stopDaemon(opts = {}) {
|
|
|
7599
7311
|
}
|
|
7600
7312
|
const steps = Math.max(1, Math.ceil((opts.timeoutMs ?? 6000) / 100));
|
|
7601
7313
|
for (let i = 0;i < steps; i++) {
|
|
7602
|
-
await
|
|
7314
|
+
await sleep(100);
|
|
7603
7315
|
if (!isAlive(state.pid, record?.startedAt)) {
|
|
7604
7316
|
removePid(path);
|
|
7605
7317
|
return { wasRunning: true, stopped: true, forced: false, pid: state.pid };
|
|
@@ -7608,13 +7320,13 @@ async function stopDaemon(opts = {}) {
|
|
|
7608
7320
|
try {
|
|
7609
7321
|
process.kill(state.pid, "SIGKILL");
|
|
7610
7322
|
} catch {}
|
|
7611
|
-
await
|
|
7323
|
+
await sleep(150);
|
|
7612
7324
|
removePid(path);
|
|
7613
7325
|
let reapedPgids = [];
|
|
7614
7326
|
if (leaseVerified && lease) {
|
|
7615
7327
|
const ownedRuns = store.listRuns({ status: "running", limit: 1000 }).filter((run) => run.claimedBy !== undefined && run.claimedBy.endsWith(`:${lease.id}`));
|
|
7616
7328
|
reapedPgids = await reapProcessGroups(ownedRuns.map(toReapableProcess), {
|
|
7617
|
-
sleep
|
|
7329
|
+
sleep,
|
|
7618
7330
|
graceMs: opts.reapGraceMs
|
|
7619
7331
|
});
|
|
7620
7332
|
}
|
|
@@ -7686,7 +7398,7 @@ async function runDaemon(opts = {}) {
|
|
|
7686
7398
|
const leaseTtlMs = opts.leaseTtlMs ?? Math.max(60000, intervalMs * 10);
|
|
7687
7399
|
const concurrency = Math.max(1, opts.concurrency ?? concurrencyFromEnv() ?? 4);
|
|
7688
7400
|
const log = opts.log ?? defaultDaemonLog;
|
|
7689
|
-
const
|
|
7401
|
+
const sleep = opts.sleep ?? realSleep;
|
|
7690
7402
|
const lease = store.acquireDaemonLease({
|
|
7691
7403
|
id: leaseId,
|
|
7692
7404
|
pid: process.pid,
|
|
@@ -7750,7 +7462,7 @@ async function runDaemon(opts = {}) {
|
|
|
7750
7462
|
const entries = recovered.map(toReapableProcess).filter((entry) => entry.pid !== undefined || entry.pgid !== undefined);
|
|
7751
7463
|
if (entries.length === 0)
|
|
7752
7464
|
return;
|
|
7753
|
-
const killed = await reapProcessGroups(entries, { sleep
|
|
7465
|
+
const killed = await reapProcessGroups(entries, { sleep, graceMs: opts.reapGraceMs, log });
|
|
7754
7466
|
if (killed.length > 0)
|
|
7755
7467
|
log(`reaped orphan process groups: ${killed.join(", ")}`);
|
|
7756
7468
|
};
|
|
@@ -7825,7 +7537,7 @@ async function runDaemon(opts = {}) {
|
|
|
7825
7537
|
}
|
|
7826
7538
|
await runLoop({
|
|
7827
7539
|
intervalMs,
|
|
7828
|
-
sleep
|
|
7540
|
+
sleep,
|
|
7829
7541
|
shouldStop: opts.shouldStop ?? (() => stopFlag),
|
|
7830
7542
|
onTickError: (err) => log(`tick error: ${err instanceof Error ? err.message : String(err)}`),
|
|
7831
7543
|
tickFn: async () => {
|
|
@@ -7875,10 +7587,10 @@ async function startDaemon(opts) {
|
|
|
7875
7587
|
stdio: ["ignore", out, out]
|
|
7876
7588
|
});
|
|
7877
7589
|
child.unref();
|
|
7878
|
-
const
|
|
7590
|
+
const sleep = opts.sleep ?? realSleep;
|
|
7879
7591
|
const deadline = Math.max(1, Math.ceil((opts.waitMs ?? 4000) / 100));
|
|
7880
7592
|
for (let i = 0;i < deadline; i++) {
|
|
7881
|
-
await
|
|
7593
|
+
await sleep(100);
|
|
7882
7594
|
const current = isDaemonRunning();
|
|
7883
7595
|
if (current.running)
|
|
7884
7596
|
return { started: true, alreadyRunning: false, pid: current.pid };
|
|
@@ -8003,7 +7715,7 @@ function enableStartup(result) {
|
|
|
8003
7715
|
// package.json
|
|
8004
7716
|
var package_default = {
|
|
8005
7717
|
name: "@hasna/loops",
|
|
8006
|
-
version: "0.4.
|
|
7718
|
+
version: "0.4.9",
|
|
8007
7719
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
8008
7720
|
type: "module",
|
|
8009
7721
|
main: "dist/index.js",
|