@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/README.md +4 -4
- package/dist/cli/index.js +525 -82
- package/dist/daemon/index.js +394 -16
- package/dist/index.js +387 -9
- package/dist/lib/store.d.ts +10 -0
- package/dist/lib/store.js +90 -1
- package/dist/mcp/index.js +387 -9
- package/dist/sdk/index.js +386 -8
- package/docs/USAGE.md +4 -4
- package/package.json +1 -1
package/dist/daemon/index.js
CHANGED
|
@@ -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", "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|