@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/dist/cli/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(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,
@@ -4219,7 +4237,7 @@ class Store {
4219
4237
  // package.json
4220
4238
  var package_default = {
4221
4239
  name: "@hasna/loops",
4222
- version: "0.4.8",
4240
+ version: "0.4.10",
4223
4241
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4224
4242
  type: "module",
4225
4243
  main: "dist/index.js",
@@ -4902,11 +4920,6 @@ var TRANSPORT_ENV_KEYS = new Set([
4902
4920
  "USER",
4903
4921
  "XDG_RUNTIME_DIR"
4904
4922
  ]);
4905
- function boundedText(text, maxBytes) {
4906
- const buffer = new BoundedOutputBuffer(maxBytes);
4907
- buffer.append(text);
4908
- return buffer.value();
4909
- }
4910
4923
  function buildResult(status, startedAt, fields = {}) {
4911
4924
  const finishedAt = fields.finishedAt ?? nowIso();
4912
4925
  return {
@@ -4944,6 +4957,20 @@ function notifySpawn(pid, opts) {
4944
4957
  function shellQuote(value) {
4945
4958
  return `'${value.replace(/'/g, `'\\''`)}'`;
4946
4959
  }
4960
+ function codewithProfileCandidateFromLine(line) {
4961
+ const cols = line.trim().split(/\s+/);
4962
+ if (cols[0] === "*")
4963
+ return cols[1];
4964
+ return cols[0];
4965
+ }
4966
+ function codewithProfileForError(profile) {
4967
+ return /[\u0000-\u001F\u007F]/.test(profile) ? JSON.stringify(profile) ?? profile : profile;
4968
+ }
4969
+ function assertCodewithAuthProfileSupported(profile) {
4970
+ if (profile.includes("\x00")) {
4971
+ throw new Error(`codewith auth profile contains unsupported NUL byte: ${codewithProfileForError(profile)}`);
4972
+ }
4973
+ }
4947
4974
  function metadataEnv(metadata) {
4948
4975
  const env = {};
4949
4976
  if (metadata.loopId)
@@ -4970,21 +4997,6 @@ function metadataEnv(metadata) {
4970
4997
  env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
4971
4998
  return env;
4972
4999
  }
4973
- function codewithAgentIdempotencyKey(metadata) {
4974
- const parts = [
4975
- "openloops",
4976
- metadata.workflowRunId ? `workflow-run:${metadata.workflowRunId}` : undefined,
4977
- metadata.workflowStepId ? `step:${metadata.workflowStepId}` : undefined,
4978
- metadata.runId ? `loop-run:${metadata.runId}` : undefined,
4979
- metadata.loopId ? `loop:${metadata.loopId}` : undefined,
4980
- metadata.scheduledFor ? `scheduled:${metadata.scheduledFor}` : undefined,
4981
- metadata.goalId ? `goal:${metadata.goalId}` : undefined,
4982
- metadata.goalNodeKey ? `node:${metadata.goalNodeKey}` : undefined
4983
- ].filter(Boolean);
4984
- if (parts.length > 1)
4985
- return parts.join(":");
4986
- return `openloops:adhoc:${randomBytes2(16).toString("hex")}`;
4987
- }
4988
5000
  function allowlistEnv(allowlist) {
4989
5001
  const env = {};
4990
5002
  if (allowlist?.tools?.length)
@@ -5025,6 +5037,9 @@ function commandSpec(target, opts) {
5025
5037
  };
5026
5038
  }
5027
5039
  const agentTarget = target;
5040
+ if (agentTarget.provider === "codewith" && agentTarget.authProfile) {
5041
+ assertCodewithAuthProfileSupported(agentTarget.authProfile);
5042
+ }
5028
5043
  const adapter = providerAdapter(agentTarget.provider);
5029
5044
  const invocation = adapter.buildInvocation(agentTarget);
5030
5045
  return {
@@ -5039,8 +5054,7 @@ function commandSpec(target, opts) {
5039
5054
  preflightAnyOf: invocation.preflightAnyOf,
5040
5055
  stdin: invocation.stdin,
5041
5056
  allowlist: agentTarget.allowlist,
5042
- worktree: agentTarget.worktree,
5043
- codewithDurableAgent: adapter.capabilities.durable ? { target: agentTarget } : undefined
5057
+ worktree: agentTarget.worktree
5044
5058
  };
5045
5059
  }
5046
5060
  function composeExecutionEnv(spec, metadata, opts, accountEnv) {
@@ -5198,7 +5212,8 @@ function remotePreflightScript(spec, metadata) {
5198
5212
  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");
5199
5213
  }
5200
5214
  if (spec.nativeAuthProfile?.provider === "codewith") {
5201
- 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");
5215
+ const profileForError = codewithProfileForError(spec.nativeAuthProfile.profile);
5216
+ 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");
5202
5217
  }
5203
5218
  return lines.join(`
5204
5219
  `);
@@ -5223,9 +5238,9 @@ function assertCodewithProfileListed(profile, result) {
5223
5238
  const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
5224
5239
  throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
5225
5240
  }
5226
- const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
5241
+ const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(codewithProfileCandidateFromLine).filter(Boolean));
5227
5242
  if (!profiles.has(profile)) {
5228
- throw new Error(`codewith auth profile not found: ${profile}`);
5243
+ throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
5229
5244
  }
5230
5245
  }
5231
5246
  function preflightNativeAuthProfileSync(spec, env) {
@@ -5250,266 +5265,6 @@ async function preflightNativeAuthProfile(spec, env) {
5250
5265
  const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
5251
5266
  assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
5252
5267
  }
5253
- function codewithAgentStartArgs(target, idempotencyKey) {
5254
- const args = providerAdapter(target.provider).buildInvocation(target).args;
5255
- const startIndex = args.findIndex((arg, index) => arg === "start" && args[index - 1] === "agent");
5256
- if (startIndex === -1)
5257
- throw new Error("internal error: codewith durable agent args missing agent start");
5258
- args.splice(startIndex + 1, 0, "--idempotency-key", idempotencyKey);
5259
- return args;
5260
- }
5261
- function codewithAgentControlArgs(target, command, agentId) {
5262
- return [
5263
- ...target.authProfile ? ["--auth-profile", target.authProfile] : [],
5264
- "agent",
5265
- command,
5266
- ...command === "logs" ? ["--limit", "20"] : [],
5267
- "--json",
5268
- agentId
5269
- ];
5270
- }
5271
- function extractFirstJsonObject(text) {
5272
- let depth = 0;
5273
- let start = -1;
5274
- let inString = false;
5275
- let escaped = false;
5276
- for (let i = 0;i < text.length; i += 1) {
5277
- const ch = text[i];
5278
- if (inString) {
5279
- if (escaped)
5280
- escaped = false;
5281
- else if (ch === "\\")
5282
- escaped = true;
5283
- else if (ch === '"')
5284
- inString = false;
5285
- continue;
5286
- }
5287
- if (ch === '"') {
5288
- inString = true;
5289
- } else if (ch === "{") {
5290
- if (depth === 0)
5291
- start = i;
5292
- depth += 1;
5293
- } else if (ch === "}") {
5294
- if (depth > 0) {
5295
- depth -= 1;
5296
- if (depth === 0 && start !== -1)
5297
- return text.slice(start, i + 1);
5298
- }
5299
- }
5300
- }
5301
- return;
5302
- }
5303
- function parseJsonOutput(stdout, label) {
5304
- const raw = stdout || "{}";
5305
- const candidates = [raw.trim()];
5306
- const scanned = extractFirstJsonObject(raw);
5307
- if (scanned && scanned !== raw.trim())
5308
- candidates.push(scanned);
5309
- let lastError;
5310
- for (const candidate of candidates) {
5311
- try {
5312
- const value = JSON.parse(candidate);
5313
- if (!value || typeof value !== "object" || Array.isArray(value))
5314
- throw new Error("not an object");
5315
- return value;
5316
- } catch (error) {
5317
- lastError = error;
5318
- }
5319
- }
5320
- throw new Error(`${label} did not return JSON${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
5321
- }
5322
- function recordField(value, key) {
5323
- if (!value || typeof value !== "object" || Array.isArray(value))
5324
- return;
5325
- const field = value[key];
5326
- return field && typeof field === "object" && !Array.isArray(field) ? field : undefined;
5327
- }
5328
- function stringField(value, key) {
5329
- const field = value?.[key];
5330
- return typeof field === "string" ? field : undefined;
5331
- }
5332
- function numberField(value, key) {
5333
- const field = value?.[key];
5334
- return typeof field === "number" && Number.isFinite(field) ? field : undefined;
5335
- }
5336
- function codewithAgentStatus(readJson) {
5337
- return stringField(recordField(readJson, "agent"), "status") ?? stringField(recordField(readJson, "statusSnapshot"), "status");
5338
- }
5339
- function codewithAgentLastEventSeq(readJson) {
5340
- const agentSeq = numberField(recordField(readJson, "agent"), "lastEventSeq");
5341
- const snapshotSeq = numberField(recordField(readJson, "statusSnapshot"), "lastEventSeq");
5342
- if (agentSeq !== undefined && snapshotSeq !== undefined)
5343
- return Math.max(agentSeq, snapshotSeq);
5344
- return agentSeq ?? snapshotSeq;
5345
- }
5346
- function codewithAgentEvidence(startJson, readJson, logsJson) {
5347
- const agent = recordField(readJson, "agent") ?? recordField(startJson, "agent");
5348
- const statusSnapshot = recordField(readJson, "statusSnapshot");
5349
- const events = Array.isArray(logsJson?.data) ? logsJson.data.filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)).map((event) => ({
5350
- seq: numberField(event, "seq"),
5351
- eventType: stringField(event, "eventType"),
5352
- createdAt: numberField(event, "createdAt")
5353
- })) : undefined;
5354
- return JSON.stringify({
5355
- codewithAgent: {
5356
- agentId: stringField(agent, "agentId"),
5357
- status: stringField(agent, "status"),
5358
- desiredState: stringField(agent, "desiredState"),
5359
- statusReason: stringField(agent, "statusReason"),
5360
- threadId: stringField(agent, "threadId"),
5361
- rolloutPath: stringField(agent, "rolloutPath"),
5362
- pid: numberField(agent, "pid"),
5363
- exitCode: numberField(agent, "exitCode"),
5364
- created: typeof startJson.created === "boolean" ? startJson.created : undefined
5365
- },
5366
- statusSnapshot: statusSnapshot ? {
5367
- seq: numberField(statusSnapshot, "seq"),
5368
- status: stringField(statusSnapshot, "status"),
5369
- summary: stringField(statusSnapshot, "summary"),
5370
- pendingInteractionCount: numberField(statusSnapshot, "pendingInteractionCount"),
5371
- lastEventSeq: numberField(statusSnapshot, "lastEventSeq")
5372
- } : undefined,
5373
- events
5374
- }, null, 2);
5375
- }
5376
- function codewithAgentProgress(readJson) {
5377
- const agent = recordField(readJson, "agent");
5378
- const statusSnapshot = recordField(readJson, "statusSnapshot");
5379
- return {
5380
- provider: "codewith",
5381
- agentId: stringField(agent, "agentId"),
5382
- status: stringField(agent, "status") ?? stringField(statusSnapshot, "status"),
5383
- summary: stringField(statusSnapshot, "summary"),
5384
- statusReason: stringField(agent, "statusReason"),
5385
- threadId: stringField(agent, "threadId"),
5386
- rolloutPath: stringField(agent, "rolloutPath"),
5387
- pid: numberField(agent, "pid"),
5388
- lastEventSeq: codewithAgentLastEventSeq(readJson)
5389
- };
5390
- }
5391
- function sleep(ms) {
5392
- return new Promise((resolve3) => setTimeout(resolve3, ms));
5393
- }
5394
- async function executeCodewithDurableAgent(spec, metadata, opts, env, startedAt) {
5395
- const target = spec.codewithDurableAgent?.target;
5396
- if (!target)
5397
- throw new Error("internal error: missing codewith durable target");
5398
- const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
5399
- const idempotencyKey = codewithAgentIdempotencyKey(metadata);
5400
- const start = await spawnCapture(spec.command, codewithAgentStartArgs(target, idempotencyKey), {
5401
- cwd: spec.cwd,
5402
- env,
5403
- timeoutMs: 30000,
5404
- maxOutputBytes
5405
- });
5406
- const stderr = new BoundedOutputBuffer(maxOutputBytes);
5407
- stderr.append(start.stderr);
5408
- if (start.error || (start.status ?? 1) !== 0) {
5409
- return failureResult(startedAt, start.error ?? `codewith agent start exited with code ${start.status ?? "unknown"}`, {
5410
- exitCode: start.status ?? undefined,
5411
- stdout: start.stdout,
5412
- stderr: stderr.value()
5413
- });
5414
- }
5415
- let startJson;
5416
- try {
5417
- startJson = parseJsonOutput(start.stdout || "{}", "codewith agent start");
5418
- } catch (error) {
5419
- return failureResult(startedAt, error instanceof Error ? error.message : String(error), { stdout: start.stdout, stderr: stderr.value() });
5420
- }
5421
- const agentId = stringField(recordField(startJson, "agent"), "agentId");
5422
- if (!agentId) {
5423
- return failureResult(startedAt, "codewith agent start did not return agent.agentId", { stdout: start.stdout, stderr: stderr.value() });
5424
- }
5425
- opts.onAgentProgress?.(codewithAgentProgress(startJson));
5426
- const stopAgent = async () => {
5427
- await spawnCapture(spec.command, codewithAgentControlArgs(target, "stop", agentId), {
5428
- cwd: spec.cwd,
5429
- env,
5430
- timeoutMs: 15000,
5431
- maxOutputBytes
5432
- });
5433
- };
5434
- const pollMs = Math.max(100, Number(opts.env?.LOOPS_CODEWITH_AGENT_POLL_MS ?? process.env.LOOPS_CODEWITH_AGENT_POLL_MS ?? 2000) || 2000);
5435
- let lastReadJson = startJson;
5436
- let lastLogsJson;
5437
- let lastFingerprint;
5438
- let lastProgressAt = Date.now();
5439
- const evidence = () => boundedText(codewithAgentEvidence(startJson, lastReadJson, lastLogsJson), maxOutputBytes);
5440
- while (true) {
5441
- if (opts.signal?.aborted) {
5442
- await stopAgent();
5443
- return failureResult(startedAt, "cancelled", { stdout: evidence(), stderr: stderr.value() });
5444
- }
5445
- const read = await spawnCapture(spec.command, codewithAgentControlArgs(target, "read", agentId), {
5446
- cwd: spec.cwd,
5447
- env,
5448
- timeoutMs: 30000,
5449
- maxOutputBytes
5450
- });
5451
- stderr.append(read.stderr);
5452
- if (read.error || (read.status ?? 1) !== 0) {
5453
- return failureResult(startedAt, read.error ?? `codewith agent read exited with code ${read.status ?? "unknown"}`, {
5454
- exitCode: read.status ?? undefined,
5455
- stdout: evidence(),
5456
- stderr: stderr.value()
5457
- });
5458
- }
5459
- try {
5460
- lastReadJson = parseJsonOutput(read.stdout || "{}", "codewith agent read");
5461
- } catch (error) {
5462
- return failureResult(startedAt, error instanceof Error ? error.message : String(error), {
5463
- stdout: boundedText(read.stdout, maxOutputBytes),
5464
- stderr: stderr.value()
5465
- });
5466
- }
5467
- const status = codewithAgentStatus(lastReadJson);
5468
- const fingerprint = JSON.stringify({
5469
- status,
5470
- agentLastEventSeq: numberField(recordField(lastReadJson, "agent"), "lastEventSeq"),
5471
- snapshot: recordField(lastReadJson, "statusSnapshot") ?? null
5472
- });
5473
- if (fingerprint !== lastFingerprint) {
5474
- lastFingerprint = fingerprint;
5475
- lastProgressAt = Date.now();
5476
- opts.onAgentProgress?.(codewithAgentProgress(lastReadJson));
5477
- }
5478
- if (status === "completed" || status === "failed" || status === "cancelled") {
5479
- const logs = await spawnCapture(spec.command, codewithAgentControlArgs(target, "logs", agentId), {
5480
- cwd: spec.cwd,
5481
- env,
5482
- timeoutMs: 30000,
5483
- maxOutputBytes
5484
- });
5485
- if (!logs.error && (logs.status ?? 1) === 0) {
5486
- try {
5487
- lastLogsJson = parseJsonOutput(logs.stdout || "{}", "codewith agent logs");
5488
- } catch {
5489
- lastLogsJson = undefined;
5490
- }
5491
- } else {
5492
- stderr.append(logs.stderr || logs.error || "");
5493
- }
5494
- if (status === "completed") {
5495
- return successResult(startedAt, { exitCode: 0, stdout: evidence(), stderr: stderr.value() });
5496
- }
5497
- return failureResult(startedAt, stringField(recordField(lastReadJson, "agent"), "statusReason") ?? `codewith agent ${status}`, { exitCode: 1, stdout: evidence(), stderr: stderr.value() });
5498
- }
5499
- if (typeof spec.timeoutMs === "number" && new Date(nowIso()).getTime() - new Date(startedAt).getTime() >= spec.timeoutMs) {
5500
- await stopAgent();
5501
- return timeoutResult(startedAt, `timed out after ${spec.timeoutMs}ms`, { stdout: evidence(), stderr: stderr.value() });
5502
- }
5503
- if (typeof spec.idleTimeoutMs === "number" && Date.now() - lastProgressAt >= spec.idleTimeoutMs) {
5504
- await stopAgent();
5505
- return timeoutResult(startedAt, `idle timed out after ${spec.idleTimeoutMs}ms without agent progress`, {
5506
- stdout: evidence(),
5507
- stderr: stderr.value()
5508
- });
5509
- }
5510
- await sleep(pollMs);
5511
- }
5512
- }
5513
5268
  function resolvedDirEquals(left, right) {
5514
5269
  try {
5515
5270
  return realpathSync(left) === realpathSync(right);
@@ -5743,9 +5498,6 @@ function preflightTarget(target, metadata = {}, opts = {}) {
5743
5498
  async function executeTarget(target, metadata = {}, opts = {}) {
5744
5499
  let spec = commandSpec(target, opts);
5745
5500
  const machine = resolvedMachine(opts);
5746
- if (machine && !machine.local && spec.codewithDurableAgent) {
5747
- 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");
5748
- }
5749
5501
  if (machine && !machine.local) {
5750
5502
  const remoteFallbackSpec = spec.worktree?.enabled && spec.worktree.mode === "auto" ? worktreeFallbackSpec(target, opts, spec.worktree.originalCwd) : undefined;
5751
5503
  return executeRemoteSpec(spec, machine, metadata, opts, remoteFallbackSpec);
@@ -5776,9 +5528,6 @@ async function executeTarget(target, metadata = {}, opts = {}) {
5776
5528
  if (worktreeEntry?.fallbackCwd) {
5777
5529
  spec = worktreeFallbackSpec(target, opts, worktreeEntry.fallbackCwd) ?? spec;
5778
5530
  }
5779
- if (spec.codewithDurableAgent) {
5780
- return executeCodewithDurableAgent(spec, metadata, opts, env, startedAt);
5781
- }
5782
5531
  const child = spawn2(spec.command, spec.args, {
5783
5532
  cwd: spec.cwd,
5784
5533
  env,
@@ -7676,7 +7425,7 @@ function realSleep(ms) {
7676
7425
  return new Promise((resolve3) => setTimeout(resolve3, ms));
7677
7426
  }
7678
7427
  async function runLoop(opts) {
7679
- const sleep2 = opts.sleep ?? realSleep;
7428
+ const sleep = opts.sleep ?? realSleep;
7680
7429
  const sliceMs = opts.sliceMs ?? 200;
7681
7430
  while (!opts.shouldStop()) {
7682
7431
  try {
@@ -7687,7 +7436,7 @@ async function runLoop(opts) {
7687
7436
  let waited = 0;
7688
7437
  while (waited < opts.intervalMs && !opts.shouldStop()) {
7689
7438
  const chunk = Math.min(sliceMs, opts.intervalMs - waited);
7690
- await sleep2(chunk);
7439
+ await sleep(chunk);
7691
7440
  waited += chunk;
7692
7441
  }
7693
7442
  }
@@ -7779,7 +7528,7 @@ function processExists(pid) {
7779
7528
  }
7780
7529
  }
7781
7530
  async function reapProcessGroups(entries, opts = {}) {
7782
- const sleep2 = opts.sleep ?? realSleep;
7531
+ const sleep = opts.sleep ?? realSleep;
7783
7532
  const graceMs = Math.max(100, opts.graceMs ?? 2000);
7784
7533
  const ownPgid = ownProcessGroupId();
7785
7534
  const targets = new Map;
@@ -7808,7 +7557,7 @@ async function reapProcessGroups(entries, opts = {}) {
7808
7557
  const steps = Math.max(1, Math.ceil(graceMs / 100));
7809
7558
  let remaining = live;
7810
7559
  for (let i = 0;i < steps && remaining.length > 0; i++) {
7811
- await sleep2(100);
7560
+ await sleep(100);
7812
7561
  remaining = remaining.filter((target) => signalReapTarget(target, 0));
7813
7562
  }
7814
7563
  for (const target of remaining) {
@@ -7850,7 +7599,7 @@ async function stopDaemon(opts = {}) {
7850
7599
  }
7851
7600
  if (!state.running || !state.pid)
7852
7601
  return { wasRunning: false, stopped: false, forced: false };
7853
- const sleep2 = opts.sleep ?? realSleep;
7602
+ const sleep = opts.sleep ?? realSleep;
7854
7603
  const store = new Store(join5(dirname4(path), "loops.db"));
7855
7604
  try {
7856
7605
  const lease = store.getDaemonLease();
@@ -7863,7 +7612,7 @@ async function stopDaemon(opts = {}) {
7863
7612
  }
7864
7613
  const steps = Math.max(1, Math.ceil((opts.timeoutMs ?? 6000) / 100));
7865
7614
  for (let i = 0;i < steps; i++) {
7866
- await sleep2(100);
7615
+ await sleep(100);
7867
7616
  if (!isAlive(state.pid, record?.startedAt)) {
7868
7617
  removePid(path);
7869
7618
  return { wasRunning: true, stopped: true, forced: false, pid: state.pid };
@@ -7872,13 +7621,13 @@ async function stopDaemon(opts = {}) {
7872
7621
  try {
7873
7622
  process.kill(state.pid, "SIGKILL");
7874
7623
  } catch {}
7875
- await sleep2(150);
7624
+ await sleep(150);
7876
7625
  removePid(path);
7877
7626
  let reapedPgids = [];
7878
7627
  if (leaseVerified && lease) {
7879
7628
  const ownedRuns = store.listRuns({ status: "running", limit: 1000 }).filter((run) => run.claimedBy !== undefined && run.claimedBy.endsWith(`:${lease.id}`));
7880
7629
  reapedPgids = await reapProcessGroups(ownedRuns.map(toReapableProcess), {
7881
- sleep: sleep2,
7630
+ sleep,
7882
7631
  graceMs: opts.reapGraceMs
7883
7632
  });
7884
7633
  }
@@ -7953,7 +7702,7 @@ async function runDaemon(opts = {}) {
7953
7702
  const leaseTtlMs = opts.leaseTtlMs ?? Math.max(60000, intervalMs * 10);
7954
7703
  const concurrency = Math.max(1, opts.concurrency ?? concurrencyFromEnv() ?? 4);
7955
7704
  const log = opts.log ?? defaultDaemonLog;
7956
- const sleep2 = opts.sleep ?? realSleep;
7705
+ const sleep = opts.sleep ?? realSleep;
7957
7706
  const lease = store.acquireDaemonLease({
7958
7707
  id: leaseId,
7959
7708
  pid: process.pid,
@@ -8017,7 +7766,7 @@ async function runDaemon(opts = {}) {
8017
7766
  const entries = recovered.map(toReapableProcess).filter((entry) => entry.pid !== undefined || entry.pgid !== undefined);
8018
7767
  if (entries.length === 0)
8019
7768
  return;
8020
- const killed = await reapProcessGroups(entries, { sleep: sleep2, graceMs: opts.reapGraceMs, log });
7769
+ const killed = await reapProcessGroups(entries, { sleep, graceMs: opts.reapGraceMs, log });
8021
7770
  if (killed.length > 0)
8022
7771
  log(`reaped orphan process groups: ${killed.join(", ")}`);
8023
7772
  };
@@ -8092,7 +7841,7 @@ async function runDaemon(opts = {}) {
8092
7841
  }
8093
7842
  await runLoop({
8094
7843
  intervalMs,
8095
- sleep: sleep2,
7844
+ sleep,
8096
7845
  shouldStop: opts.shouldStop ?? (() => stopFlag),
8097
7846
  onTickError: (err) => log(`tick error: ${err instanceof Error ? err.message : String(err)}`),
8098
7847
  tickFn: async () => {
@@ -8142,10 +7891,10 @@ async function startDaemon(opts) {
8142
7891
  stdio: ["ignore", out, out]
8143
7892
  });
8144
7893
  child.unref();
8145
- const sleep2 = opts.sleep ?? realSleep;
7894
+ const sleep = opts.sleep ?? realSleep;
8146
7895
  const deadline = Math.max(1, Math.ceil((opts.waitMs ?? 4000) / 100));
8147
7896
  for (let i = 0;i < deadline; i++) {
8148
- await sleep2(100);
7897
+ await sleep(100);
8149
7898
  const current = isDaemonRunning();
8150
7899
  if (current.running)
8151
7900
  return { started: true, alreadyRunning: false, pid: current.pid };
@@ -11078,7 +10827,7 @@ function eventMetadata(event) {
11078
10827
  return metadata;
11079
10828
  return {};
11080
10829
  }
11081
- function stringField2(value) {
10830
+ function stringField(value) {
11082
10831
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
11083
10832
  }
11084
10833
  function slugSegment2(value, fallback = "event") {
@@ -11093,14 +10842,14 @@ function stableHash(parts) {
11093
10842
  }
11094
10843
  function taskEventField(data, keys) {
11095
10844
  for (const key of keys) {
11096
- const direct = stringField2(data[key]);
10845
+ const direct = stringField(data[key]);
11097
10846
  if (direct)
11098
10847
  return direct;
11099
10848
  }
11100
10849
  const task = data.task;
11101
10850
  if (task && typeof task === "object" && !Array.isArray(task)) {
11102
10851
  for (const key of keys) {
11103
- const direct = stringField2(task[key]);
10852
+ const direct = stringField(task[key]);
11104
10853
  if (direct)
11105
10854
  return direct;
11106
10855
  }
@@ -11108,7 +10857,7 @@ function taskEventField(data, keys) {
11108
10857
  const payload = data.payload;
11109
10858
  if (payload && typeof payload === "object" && !Array.isArray(payload)) {
11110
10859
  for (const key of keys) {
11111
- const direct = stringField2(payload[key]);
10860
+ const direct = stringField(payload[key]);
11112
10861
  if (direct)
11113
10862
  return direct;
11114
10863
  }
@@ -11612,6 +11361,7 @@ function permissionModeFromOpts(opts, provider) {
11612
11361
  return mode;
11613
11362
  }
11614
11363
  // src/lib/route/pr-review.ts
11364
+ import { spawnSync as spawnSync7 } from "child_process";
11615
11365
  var PR_AUTHOR_FIELDS = [
11616
11366
  "github_author",
11617
11367
  "githubAuthor",
@@ -11663,11 +11413,18 @@ var PR_REVIEW_REQUIRED_FIELDS = [
11663
11413
  "branch_protection_review_required",
11664
11414
  "branchProtectionReviewRequired"
11665
11415
  ];
11416
+ var GITHUB_USER_LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
11666
11417
  function githubLogin(value) {
11667
11418
  if (!value?.trim())
11668
11419
  return;
11669
- const login = value.trim().replace(/^@/, "");
11670
- return /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(login) ? login : undefined;
11420
+ let login = value.trim().replace(/^@/, "");
11421
+ const appActor = /^app\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))$/.exec(login);
11422
+ if (appActor)
11423
+ login = `${appActor[1]}[bot]`;
11424
+ const bot = /^([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\[bot\]$/.exec(login);
11425
+ if (bot)
11426
+ return `${bot[1]}[bot]`;
11427
+ return GITHUB_USER_LOGIN.test(login) ? login : undefined;
11671
11428
  }
11672
11429
  function githubLogins(values) {
11673
11430
  const seen = new Set;
@@ -11726,6 +11483,53 @@ function routeTextFieldValues(record, field) {
11726
11483
  }
11727
11484
  return values;
11728
11485
  }
11486
+ var PR_STATE_FIELDS = [
11487
+ "pr_state",
11488
+ "prState",
11489
+ "pull_request_state",
11490
+ "pullRequestState",
11491
+ "state",
11492
+ "pr_status",
11493
+ "prStatus"
11494
+ ];
11495
+ var CLOSED_PR_STATES = new Set(["MERGED", "CLOSED"]);
11496
+ function normalizePrState(value) {
11497
+ const trimmed = value?.trim().toUpperCase();
11498
+ return trimmed ? trimmed : undefined;
11499
+ }
11500
+ function prStateFromEvidence(records, text) {
11501
+ const field = normalizePrState(firstRouteField(records, PR_STATE_FIELDS));
11502
+ if (field)
11503
+ return field;
11504
+ const match = /\b(?:pr[_\s-]?state|pull[_\s-]?request[_\s-]?state|state)\s*[:=]\s*(MERGED|CLOSED|OPEN)\b/i.exec(text);
11505
+ return match ? match[1].toUpperCase() : undefined;
11506
+ }
11507
+ function prReferenceFrom(text) {
11508
+ const url = /github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/(\d+)/i.exec(text);
11509
+ if (url)
11510
+ return { owner: url[1], repo: url[2], number: Number(url[3]) };
11511
+ const short = /github-pr:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)#(\d+)/i.exec(text);
11512
+ if (short)
11513
+ return { owner: short[1], repo: short[2], number: Number(short[3]) };
11514
+ return;
11515
+ }
11516
+ function ghAuthorResolver(ref) {
11517
+ const result = spawnSync7("gh", ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", "author", "-q", ".author.login"], { encoding: "utf8", timeout: 20000 });
11518
+ if (result.error || result.status !== 0)
11519
+ return;
11520
+ return githubLogin((result.stdout ?? "").trim());
11521
+ }
11522
+ function ghStateResolver(ref) {
11523
+ const result = spawnSync7("gh", ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", "state,mergeStateStatus"], { encoding: "utf8", timeout: 20000 });
11524
+ if (result.error || result.status !== 0)
11525
+ return;
11526
+ try {
11527
+ const parsed = JSON.parse(result.stdout ?? "{}");
11528
+ return { state: parsed.state, mergeStateStatus: parsed.mergeStateStatus };
11529
+ } catch {
11530
+ return;
11531
+ }
11532
+ }
11729
11533
  function authorFromPrText(text) {
11730
11534
  const patterns = [
11731
11535
  /\bauthor\s+(?:is\s+also|is|=|:)\s+@?([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))/i,
@@ -11752,7 +11556,7 @@ function reviewersFromPrText(text) {
11752
11556
  }
11753
11557
  return githubLogins(reviewers);
11754
11558
  }
11755
- function prReviewRoutingDecision(data, metadata, opts) {
11559
+ function prReviewRoutingDecision(data, metadata, opts, resolveAuthor = ghAuthorResolver, resolveState = ghStateResolver) {
11756
11560
  const records = [...taskEventRecords(data, metadata), ...automationRecords(data, metadata)];
11757
11561
  const text = routeEvidenceText(records);
11758
11562
  const signals = [];
@@ -11772,7 +11576,21 @@ function prReviewRoutingDecision(data, metadata, opts) {
11772
11576
  const required = hasPrReference && (reviewRequiredByField || reviewRequiredByText || mergeBlockedByText || approvalIntent);
11773
11577
  if (!required)
11774
11578
  return { required: false, allowed: true, reviewers: [], signals };
11775
- const author = githubLogin(firstRouteField(records, PR_AUTHOR_FIELDS)) ?? authorFromPrText(text);
11579
+ const prRef = prReferenceFrom(text);
11580
+ let prState = prStateFromEvidence(records, text);
11581
+ if (!prState && prRef) {
11582
+ const live = resolveState(prRef);
11583
+ prState = normalizePrState(live?.state);
11584
+ }
11585
+ if (prState && CLOSED_PR_STATES.has(prState)) {
11586
+ return {
11587
+ required: true,
11588
+ allowed: false,
11589
+ reason: `PR is already ${prState.toLowerCase()}; skipping merge/review route (freshness gate)`,
11590
+ reviewers: [],
11591
+ signals: [...signals, "pr-not-open"]
11592
+ };
11593
+ }
11776
11594
  const reviewers = githubLogins([
11777
11595
  opts.githubReviewer,
11778
11596
  ...splitList(opts.githubReviewerPool) ?? [],
@@ -11780,6 +11598,14 @@ function prReviewRoutingDecision(data, metadata, opts) {
11780
11598
  ...PR_REVIEWER_POOL_FIELDS.flatMap((field) => routeFieldValues(records, field)),
11781
11599
  ...reviewersFromPrText(text)
11782
11600
  ]);
11601
+ let author = githubLogin(firstRouteField(records, PR_AUTHOR_FIELDS)) ?? authorFromPrText(text);
11602
+ if (!author && prRef) {
11603
+ const derived = githubLogin(resolveAuthor(prRef));
11604
+ if (derived) {
11605
+ author = derived;
11606
+ signals.push("author-derived-gh");
11607
+ }
11608
+ }
11783
11609
  const selectedReviewer = author ? reviewers.find((reviewer) => reviewer.toLowerCase() !== author.toLowerCase()) : undefined;
11784
11610
  if (!author) {
11785
11611
  return {
@@ -11821,7 +11647,7 @@ function prReviewRoutingDecision(data, metadata, opts) {
11821
11647
  }
11822
11648
  // src/lib/route/throttle.ts
11823
11649
  import { realpathSync as realpathSync2 } from "fs";
11824
- import { spawnSync as spawnSync7 } from "child_process";
11650
+ import { spawnSync as spawnSync8 } from "child_process";
11825
11651
  import { resolve as resolve6 } from "path";
11826
11652
  function routeThrottleLimitsFromOpts(opts) {
11827
11653
  return {
@@ -11843,7 +11669,7 @@ function normalizeRoutePath(value) {
11843
11669
  } catch {
11844
11670
  return canonical;
11845
11671
  }
11846
- const gitRoot = spawnSync7("git", ["-C", canonical, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
11672
+ const gitRoot = spawnSync8("git", ["-C", canonical, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
11847
11673
  if (gitRoot.status === 0 && gitRoot.stdout.trim()) {
11848
11674
  try {
11849
11675
  return realpathSync2(gitRoot.stdout.trim());
@@ -11893,7 +11719,7 @@ function routeThrottleDryRunPreview(args) {
11893
11719
  };
11894
11720
  }
11895
11721
  function isExistingGitProjectPath(path) {
11896
- const result = spawnSync7("git", ["-C", path, "rev-parse", "--is-inside-work-tree"], { encoding: "utf8" });
11722
+ const result = spawnSync8("git", ["-C", path, "rev-parse", "--is-inside-work-tree"], { encoding: "utf8" });
11897
11723
  return result.status === 0;
11898
11724
  }
11899
11725
  function validateRequiredRouteWorktreeProjectPath(opts, projectPath) {
@@ -11973,6 +11799,34 @@ var UNCLEARED_ROUTE_WORK_ITEM_STATUSES = new Set([
11973
11799
  function isUnclearedRouteWorkItem(item) {
11974
11800
  return UNCLEARED_ROUTE_WORK_ITEM_STATUSES.has(item.status);
11975
11801
  }
11802
+ var REACTIVATABLE_TERMINAL_STATUSES = new Set([
11803
+ "succeeded",
11804
+ "failed",
11805
+ "dead_letter",
11806
+ "cancelled"
11807
+ ]);
11808
+ var MAX_TODOS_TASK_ROUTE_REDISPATCHES = 8;
11809
+ function todosTaskRouteRedispatchBackoffMs(attempts) {
11810
+ const base = 2 * 60000;
11811
+ const cap = 30 * 60000;
11812
+ const exp = Math.max(0, Math.min(attempts - 1, 10));
11813
+ return Math.min(cap, base * 2 ** exp);
11814
+ }
11815
+ function reactivateStaleTodosTaskWorkItem(store, routeKey, item, now = Date.now()) {
11816
+ if (routeKey !== "todos-task")
11817
+ return;
11818
+ if (!REACTIVATABLE_TERMINAL_STATUSES.has(item.status))
11819
+ return;
11820
+ if (item.attempts >= MAX_TODOS_TASK_ROUTE_REDISPATCHES)
11821
+ return;
11822
+ const finishedAt = Date.parse(item.updatedAt);
11823
+ if (Number.isFinite(finishedAt) && now - finishedAt < todosTaskRouteRedispatchBackoffMs(item.attempts)) {
11824
+ return;
11825
+ }
11826
+ return store.requeueWorkflowWorkItem(item.id, {
11827
+ reason: `re-admitted from ${item.status}: todos task still actionable after prior run (attempt ${item.attempts + 1}/${MAX_TODOS_TASK_ROUTE_REDISPATCHES})`
11828
+ });
11829
+ }
11976
11830
  function todosTaskRouteTemplateId(opts) {
11977
11831
  const id = (opts.template ?? TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).trim();
11978
11832
  if (!TODOS_TASK_ROUTE_TEMPLATE_IDS.has(id)) {
@@ -11985,11 +11839,11 @@ async function readEventEnvelopeInput(opts = {}) {
11985
11839
  const event = JSON.parse(raw);
11986
11840
  if (!event || typeof event !== "object" || Array.isArray(event))
11987
11841
  throw new ValidationError("event JSON must be an object");
11988
- if (!stringField2(event.id))
11842
+ if (!stringField(event.id))
11989
11843
  throw new ValidationError("event.id is required");
11990
- if (!stringField2(event.type))
11844
+ if (!stringField(event.type))
11991
11845
  throw new ValidationError("event.type is required");
11992
- if (!stringField2(event.source))
11846
+ if (!stringField(event.source))
11993
11847
  throw new ValidationError("event.source is required");
11994
11848
  return event;
11995
11849
  }
@@ -12065,9 +11919,11 @@ function routeEvent(plan) {
12065
11919
  const invocation = store.createWorkflowInvocation(plan.invocationInput);
12066
11920
  const existingItem = store.findWorkflowWorkItem(plan.routeKey, idempotencyKey);
12067
11921
  if (existingItem && isUnclearedRouteWorkItem(existingItem)) {
12068
- const existingLoop = existingItem.loopId ? store.getLoop(existingItem.loopId) : undefined;
12069
- const existingWorkflow = existingItem.workflowId ? store.getWorkflow(existingItem.workflowId) : undefined;
12070
- return { kind: "deduped", existingItem, existingLoop, existingWorkflow, invocation };
11922
+ if (!reactivateStaleTodosTaskWorkItem(store, plan.routeKey, existingItem)) {
11923
+ const existingLoop = existingItem.loopId ? store.getLoop(existingItem.loopId) : undefined;
11924
+ const existingWorkflow = existingItem.workflowId ? store.getWorkflow(existingItem.workflowId) : undefined;
11925
+ return { kind: "deduped", existingItem, existingLoop, existingWorkflow, invocation };
11926
+ }
12071
11927
  }
12072
11928
  const throttle = hasThrottleLimits(plan.throttleLimits) ? routeThrottleDecision(store, { projectPath: plan.routeProjectPath, projectGroup: plan.projectGroup, limits: plan.throttleLimits }) : undefined;
12073
11929
  const workItem = store.upsertWorkflowWorkItem({
@@ -12199,10 +12055,12 @@ function routeTodosTaskEvent(event, opts) {
12199
12055
  try {
12200
12056
  const existingItem = store.findWorkflowWorkItem("todos-task", idempotencyKey);
12201
12057
  if (existingItem && isUnclearedRouteWorkItem(existingItem)) {
12202
- const existingLoop = existingItem.loopId ? store.getLoop(existingItem.loopId) : undefined;
12203
- const existingWorkflow = existingItem.workflowId ? store.getWorkflow(existingItem.workflowId) : undefined;
12204
- const existingInvocation = store.getWorkflowInvocation(existingItem.invocationId);
12205
- return dedupedRoutePrint({ event, idempotencyKey, dedupeValueExtras: {} }, { existingItem, existingLoop, existingWorkflow, invocation: existingInvocation });
12058
+ if (!reactivateStaleTodosTaskWorkItem(store, "todos-task", existingItem)) {
12059
+ const existingLoop = existingItem.loopId ? store.getLoop(existingItem.loopId) : undefined;
12060
+ const existingWorkflow = existingItem.workflowId ? store.getWorkflow(existingItem.workflowId) : undefined;
12061
+ const existingInvocation = store.getWorkflowInvocation(existingItem.invocationId);
12062
+ return dedupedRoutePrint({ event, idempotencyKey, dedupeValueExtras: {} }, { existingItem, existingLoop, existingWorkflow, invocation: existingInvocation });
12063
+ }
12206
12064
  }
12207
12065
  } finally {
12208
12066
  store.close();
@@ -12360,8 +12218,8 @@ function routeGenericEvent(event, opts) {
12360
12218
  eventId: event.id,
12361
12219
  eventType: event.type,
12362
12220
  eventSource: event.source,
12363
- eventSubject: stringField2(event.subject),
12364
- eventMessage: stringField2(event.message),
12221
+ eventSubject: stringField(event.subject),
12222
+ eventMessage: stringField(event.message),
12365
12223
  eventJson: JSON.stringify(event),
12366
12224
  projectPath,
12367
12225
  routeProjectPath,
@@ -12402,9 +12260,9 @@ function routeGenericEvent(event, opts) {
12402
12260
  },
12403
12261
  subjectRef: {
12404
12262
  kind: "event",
12405
- id: stringField2(event.subject) ?? event.id,
12263
+ id: stringField(event.subject) ?? event.id,
12406
12264
  path: routeProjectPath,
12407
- raw: { message: stringField2(event.message) }
12265
+ raw: { message: stringField(event.message) }
12408
12266
  },
12409
12267
  intent: "route",
12410
12268
  scope: {
@@ -12433,7 +12291,7 @@ function routeGenericEvent(event, opts) {
12433
12291
  invocationInput,
12434
12292
  routeProjectPath,
12435
12293
  projectGroup,
12436
- subjectRef: stringField2(event.subject) ?? event.id,
12294
+ subjectRef: stringField(event.subject) ?? event.id,
12437
12295
  loopName,
12438
12296
  loopDescription: `Run ${workflowBody.name} once for event ${event.id}; idempotency=${idempotencyKey}`,
12439
12297
  throttleLimits,
@@ -12455,14 +12313,14 @@ import { resolve as resolve8 } from "path";
12455
12313
 
12456
12314
  // src/lib/route/todos-cli.ts
12457
12315
  import { closeSync, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync9, rmSync as rmSync6 } from "fs";
12458
- import { spawnSync as spawnSync8 } from "child_process";
12316
+ import { spawnSync as spawnSync9 } from "child_process";
12459
12317
  import { join as join10 } from "path";
12460
12318
  import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
12461
12319
  function defaultLoopsProject() {
12462
12320
  return process.env.LOOPS_TASK_PROJECT || process.env.LOOPS_DATA_DIR || `${process.env.HOME || homedir5()}/.hasna/loops`;
12463
12321
  }
12464
12322
  function runLocalCommand(command, args, opts = {}) {
12465
- const result = spawnSync8(command, args, {
12323
+ const result = spawnSync9(command, args, {
12466
12324
  input: opts.input,
12467
12325
  encoding: "utf8",
12468
12326
  timeout: opts.timeoutMs ?? 30000,
@@ -12483,7 +12341,7 @@ function runLocalCommandWithStdoutFile(command, args, opts = {}) {
12483
12341
  const stdoutFd = openSync2(stdoutPath, "w");
12484
12342
  let result;
12485
12343
  try {
12486
- result = spawnSync8(command, args, {
12344
+ result = spawnSync9(command, args, {
12487
12345
  input: opts.input,
12488
12346
  encoding: "utf8",
12489
12347
  timeout: opts.timeoutMs ?? 30000,
@@ -12528,7 +12386,7 @@ function todosMutationSummary(result) {
12528
12386
  // src/lib/route/drain.ts
12529
12387
  function taskField(task, keys) {
12530
12388
  for (const key of keys) {
12531
- const value = stringField2(task[key]);
12389
+ const value = stringField(task[key]);
12532
12390
  if (value)
12533
12391
  return value;
12534
12392
  }
@@ -12553,9 +12411,9 @@ function taskListValues(task) {
12553
12411
  const taskList = objectField2(task.task_list);
12554
12412
  return [
12555
12413
  taskField(task, ["task_list_id", "taskListId"]),
12556
- stringField2(task.task_list?.id),
12557
- stringField2(task.task_list?.slug),
12558
- stringField2(taskList?.name),
12414
+ stringField(task.task_list?.id),
12415
+ stringField(task.task_list?.slug),
12416
+ stringField(taskList?.name),
12559
12417
  taskField(task, ["task_list", "taskList"])
12560
12418
  ].filter((value) => Boolean(value));
12561
12419
  }
@@ -12629,12 +12487,12 @@ function compactDrainResult(result) {
12629
12487
  kind: result.kind,
12630
12488
  taskId: event?.subject,
12631
12489
  eventId: event?.id,
12632
- idempotencyKey: stringField2(value.idempotencyKey),
12633
- reason: stringField2(value.reason) ?? throttle?.reason,
12634
- loopId: stringField2(loop?.id),
12635
- loopName: stringField2(loop?.name),
12636
- workflowId: stringField2(workflow?.id),
12637
- workflowName: stringField2(workflow?.name),
12490
+ idempotencyKey: stringField(value.idempotencyKey),
12491
+ reason: stringField(value.reason) ?? throttle?.reason,
12492
+ loopId: stringField(loop?.id),
12493
+ loopName: stringField(loop?.name),
12494
+ workflowId: stringField(workflow?.id),
12495
+ workflowName: stringField(workflow?.name),
12638
12496
  providerRouting,
12639
12497
  requeue,
12640
12498
  queuedAtSource: value.queuedAtSource,
@@ -12668,7 +12526,7 @@ function loadTodoProjectPathsFromRegistry(opts) {
12668
12526
  const includeFilters = listFromRepeatedOpts(opts.todosProjectInclude)?.map((entry) => normalizeRoutePath(entry) ?? resolve8(entry)) ?? [];
12669
12527
  const includesPrefix = normalizeRoutePath(opts.projectPathPrefix) ?? (opts.projectPathPrefix ? resolve8(opts.projectPathPrefix) : undefined);
12670
12528
  const fromProjects = projectsPayload.map((entry) => objectField2(entry)).filter((project) => Boolean(project)).map((project) => {
12671
- const path = stringField2(project.path) ?? stringField2(project.projectPath) ?? stringField2(project.project_path) ?? stringField2(project.dir) ?? stringField2(project.root) ?? stringField2(project.cwd);
12529
+ const path = stringField(project.path) ?? stringField(project.projectPath) ?? stringField(project.project_path) ?? stringField(project.dir) ?? stringField(project.root) ?? stringField(project.cwd);
12672
12530
  if (!path)
12673
12531
  return;
12674
12532
  return { path };
@@ -14063,7 +13921,7 @@ routes.command("show <id>").description("show one admission work item").action(r
14063
13921
  routes.command("requeue <id>").description("requeue a terminal admission work item for the next task/event delivery").option("--reason <text>", "operator reason recorded on the work item").action(runAction((id, opts) => {
14064
13922
  const store = new Store;
14065
13923
  try {
14066
- const reason = stringField2(opts.reason);
13924
+ const reason = stringField(opts.reason);
14067
13925
  if (!reason)
14068
13926
  throw new ValidationError("routes requeue requires --reason <text>");
14069
13927
  const item = store.requeueWorkflowWorkItem(id, { reason });