@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.
- package/CHANGELOG.md +24 -0
- package/README.md +4 -4
- package/dist/api/index.js +1 -1
- package/dist/cli/index.js +226 -368
- package/dist/daemon/index.js +76 -327
- package/dist/index.js +63 -314
- package/dist/lib/agent-adapter.d.ts +1 -1
- package/dist/lib/mode.js +1 -1
- package/dist/lib/route/pr-review.d.ts +18 -1
- package/dist/lib/storage/index.js +40 -22
- package/dist/lib/storage/sqlite.js +40 -22
- package/dist/lib/store.js +40 -22
- package/dist/mcp/index.js +63 -314
- package/dist/runner/index.js +33 -303
- package/dist/sdk/index.js +63 -314
- package/docs/USAGE.md +4 -4
- package/package.json +1 -1
package/dist/daemon/index.js
CHANGED
|
@@ -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(
|
|
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(
|
|
81
|
+
return join(homeDir(), ".config", "systemd", "user", "loops-daemon.service");
|
|
78
82
|
}
|
|
79
83
|
function launchdPlistPath() {
|
|
80
|
-
return join(
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 :
|
|
3225
|
-
$stderr: progress.stderr === undefined ? null :
|
|
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:
|
|
3287
|
-
$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:
|
|
3666
|
-
$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:
|
|
4064
|
-
$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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|