opencara 0.25.1 → 0.25.2

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.
Files changed (2) hide show
  1. package/dist/index.js +108 -33
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -914,7 +914,27 @@ var ToolTimeoutError = class extends Error {
914
914
  var SIGKILL_GRACE_MS = 5e3;
915
915
  var MIN_PARTIAL_RESULT_LENGTH = 50;
916
916
  var STDOUT_LIVENESS_TIMEOUT_MS = 3e5;
917
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 6e4;
917
918
  var MAX_STDERR_LENGTH = 1e3;
919
+ function startHeartbeatTimer(heartbeat, isSettled = () => false) {
920
+ if (!heartbeat) return () => {
921
+ };
922
+ const intervalMs = heartbeat.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
923
+ if (intervalMs <= 0) return () => {
924
+ };
925
+ const timer = setInterval(() => {
926
+ if (isSettled()) return;
927
+ try {
928
+ const r = heartbeat.callback();
929
+ if (r && typeof r.catch === "function") {
930
+ r.catch(() => {
931
+ });
932
+ }
933
+ } catch {
934
+ }
935
+ }, intervalMs);
936
+ return () => clearInterval(timer);
937
+ }
918
938
  function validateCommandBinary(commandTemplate) {
919
939
  const { command } = parseCommandTemplate(commandTemplate);
920
940
  if (path3.isAbsolute(command)) {
@@ -1010,7 +1030,7 @@ function parseTokenUsage(stdout, stderr) {
1010
1030
  const estimated = estimateTokens(stdout);
1011
1031
  return { tokens: estimated, parsed: false, input: 0, output: estimated };
1012
1032
  }
1013
- function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd, livenessTimeoutMs) {
1033
+ function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd, livenessTimeoutMs, heartbeat) {
1014
1034
  const promptViaArg = commandTemplate.includes("${PROMPT}");
1015
1035
  const allVars = { ...vars, PROMPT: prompt2 };
1016
1036
  if (cwd && !allVars["CODEBASE_DIR"]) {
@@ -1054,6 +1074,7 @@ function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd, liv
1054
1074
  }
1055
1075
  }, effectiveLivenessMs);
1056
1076
  }
1077
+ const stopHeartbeat = startHeartbeatTimer(heartbeat, () => settled);
1057
1078
  child.stdout?.on("data", (chunk) => {
1058
1079
  stdout += chunk.toString();
1059
1080
  if (livenessTimer) {
@@ -1082,6 +1103,7 @@ function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd, liv
1082
1103
  clearTimeout(timer);
1083
1104
  if (livenessTimer) clearTimeout(livenessTimer);
1084
1105
  if (sigkillTimer) clearTimeout(sigkillTimer);
1106
+ stopHeartbeat();
1085
1107
  if (onAbort && signal) {
1086
1108
  signal.removeEventListener("abort", onAbort);
1087
1109
  }
@@ -1697,7 +1719,8 @@ ${userMessage}`;
1697
1719
  abortController.signal,
1698
1720
  void 0,
1699
1721
  deps.codebaseDir ?? void 0,
1700
- deps.livenessTimeoutMs
1722
+ deps.livenessTimeoutMs,
1723
+ deps.heartbeat
1701
1724
  );
1702
1725
  const { verdict, review } = extractVerdict(result.stdout);
1703
1726
  const inputTokens = result.tokensParsed ? 0 : estimateTokens(fullPrompt);
@@ -2705,7 +2728,8 @@ ${userMessage}`;
2705
2728
  abortController.signal,
2706
2729
  void 0,
2707
2730
  deps.codebaseDir ?? void 0,
2708
- deps.livenessTimeoutMs
2731
+ deps.livenessTimeoutMs,
2732
+ deps.heartbeat
2709
2733
  );
2710
2734
  const { verdict, review } = extractVerdict(result.stdout);
2711
2735
  const flaggedReviews = extractFlaggedReviews(result.stdout);
@@ -4046,7 +4070,7 @@ function createPR(worktreePath, issueNumber, issueTitle, summary, branchName) {
4046
4070
  function isAgenticCommand(commandTemplate) {
4047
4071
  return commandTemplate.includes("${PROMPT}") && !commandTemplate.includes("--print");
4048
4072
  }
4049
- function executeAgentic(commandTemplate, prompt2, timeoutMs, cwd, signal) {
4073
+ function executeAgentic(commandTemplate, prompt2, timeoutMs, cwd, signal, heartbeat) {
4050
4074
  const allVars = { PROMPT: prompt2, CODEBASE_DIR: cwd };
4051
4075
  const { command, args } = parseCommandTemplate(commandTemplate, allVars);
4052
4076
  return new Promise((resolve2, reject) => {
@@ -4067,6 +4091,7 @@ function executeAgentic(commandTemplate, prompt2, timeoutMs, cwd, signal) {
4067
4091
  }, 5e3);
4068
4092
  }
4069
4093
  }, timeoutMs);
4094
+ const stopHeartbeat = startHeartbeatTimer(heartbeat, () => settled);
4070
4095
  let onAbort;
4071
4096
  if (signal) {
4072
4097
  onAbort = () => {
@@ -4074,16 +4099,19 @@ function executeAgentic(commandTemplate, prompt2, timeoutMs, cwd, signal) {
4074
4099
  };
4075
4100
  signal.addEventListener("abort", onAbort, { once: true });
4076
4101
  }
4077
- child.on("error", (err) => {
4102
+ function agenticCleanup() {
4078
4103
  clearTimeout(timer);
4104
+ stopHeartbeat();
4079
4105
  if (onAbort && signal) signal.removeEventListener("abort", onAbort);
4106
+ }
4107
+ child.on("error", (err) => {
4108
+ agenticCleanup();
4080
4109
  if (settled) return;
4081
4110
  settled = true;
4082
4111
  reject(err);
4083
4112
  });
4084
4113
  child.on("close", (code, sig) => {
4085
- clearTimeout(timer);
4086
- if (onAbort && signal) signal.removeEventListener("abort", onAbort);
4114
+ agenticCleanup();
4087
4115
  if (settled) return;
4088
4116
  settled = true;
4089
4117
  if (sig === "SIGTERM" || sig === "SIGKILL") {
@@ -4107,7 +4135,8 @@ async function executeImplement(task, worktreePath, deps, timeoutSeconds, signal
4107
4135
  prompt2,
4108
4136
  effectiveTimeout,
4109
4137
  worktreePath,
4110
- signal
4138
+ signal,
4139
+ deps.heartbeat
4111
4140
  );
4112
4141
  return {
4113
4142
  output: {
@@ -4126,7 +4155,9 @@ async function executeImplement(task, worktreePath, deps, timeoutSeconds, signal
4126
4155
  effectiveTimeout,
4127
4156
  signal,
4128
4157
  void 0,
4129
- worktreePath
4158
+ worktreePath,
4159
+ void 0,
4160
+ deps.heartbeat
4130
4161
  );
4131
4162
  const output = parseImplementOutput(result.stdout);
4132
4163
  const inputTokens = result.tokensParsed ? 0 : estimateTokens(prompt2);
@@ -4348,7 +4379,9 @@ async function executeFix(task, diffContent, deps, timeoutSeconds, worktreePath,
4348
4379
  effectiveTimeout,
4349
4380
  signal,
4350
4381
  void 0,
4351
- worktreePath
4382
+ worktreePath,
4383
+ void 0,
4384
+ deps.heartbeat
4352
4385
  );
4353
4386
  const inputTokens = result.tokensParsed ? 0 : estimateTokens(prompt2);
4354
4387
  const detail = result.tokenDetail;
@@ -5067,6 +5100,49 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
5067
5100
  await sleep2(pollIntervalMs, signal);
5068
5101
  }
5069
5102
  }
5103
+ function isLongRunningRole(role) {
5104
+ return isFixRole(role) || isImplementRole(role) || role === "review" || role === "summary";
5105
+ }
5106
+ async function runWithHeartbeat(heartbeat, work) {
5107
+ let done = false;
5108
+ const stop = startHeartbeatTimer(heartbeat, () => done);
5109
+ try {
5110
+ return await work();
5111
+ } finally {
5112
+ done = true;
5113
+ stop();
5114
+ }
5115
+ }
5116
+ function createHeartbeatControl(client, taskId, agentId, role, logger, intervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS) {
5117
+ let sawNotFound = false;
5118
+ let failureStreakLogged = false;
5119
+ return {
5120
+ intervalMs,
5121
+ callback: async () => {
5122
+ try {
5123
+ await client.post(`/api/tasks/${taskId}/heartbeat`, {
5124
+ agent_id: agentId,
5125
+ role
5126
+ });
5127
+ failureStreakLogged = false;
5128
+ } catch (err) {
5129
+ if (err instanceof HttpError && err.status === 404) {
5130
+ if (!sawNotFound) {
5131
+ sawNotFound = true;
5132
+ logger.log(` (heartbeat endpoint not available \u2014 old server, continuing)`);
5133
+ }
5134
+ return;
5135
+ }
5136
+ if (!failureStreakLogged) {
5137
+ failureStreakLogged = true;
5138
+ logger.logWarn(
5139
+ ` ${icons.warn} Heartbeat failed for task ${taskId}: ${err.message} (further failures suppressed until next success)`
5140
+ );
5141
+ }
5142
+ }
5143
+ }
5144
+ };
5145
+ }
5070
5146
  async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, agentInfo, logger, agentSession, routerRelay, signal, cleanupTracker, verbose) {
5071
5147
  const { task_id, owner, repo, pr_number, diff_url, timeout_seconds, prompt: prompt2, role, base_ref } = task;
5072
5148
  const { log, logError, logWarn } = logger;
@@ -5211,12 +5287,14 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
5211
5287
  log(" (suspicious prompt report not sent \u2014 endpoint not available)");
5212
5288
  }
5213
5289
  }
5290
+ const heartbeat = isLongRunningRole(role) ? createHeartbeatControl(client, task_id, agentId, role, logger) : void 0;
5214
5291
  try {
5215
5292
  if (isImplementRole(role)) {
5216
5293
  const codebaseDir = reviewDeps.codebaseDir || path9.join(CONFIG_DIR, "repos");
5217
5294
  const implementDeps = {
5218
5295
  commandTemplate: reviewDeps.commandTemplate,
5219
- codebaseDir
5296
+ codebaseDir,
5297
+ heartbeat
5220
5298
  };
5221
5299
  const implementResult = await executeImplementTask(
5222
5300
  client,
@@ -5250,7 +5328,8 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
5250
5328
  throw new Error("Fix task requires a codebase worktree but checkout failed");
5251
5329
  }
5252
5330
  const fixDeps = {
5253
- commandTemplate: reviewDeps.commandTemplate
5331
+ commandTemplate: reviewDeps.commandTemplate,
5332
+ heartbeat
5254
5333
  };
5255
5334
  const fixResult = await executeFixTask(
5256
5335
  client,
@@ -5364,6 +5443,7 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
5364
5443
  { execGh: defaultExecGh }
5365
5444
  );
5366
5445
  } else if (role === "summary" && "reviews" in claimResponse && claimResponse.reviews) {
5446
+ const summaryDeps = { ...taskReviewDeps, heartbeat };
5367
5447
  await executeSummaryTask(
5368
5448
  client,
5369
5449
  agentId,
@@ -5375,7 +5455,7 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
5375
5455
  prompt2,
5376
5456
  timeout_seconds,
5377
5457
  claimResponse.reviews,
5378
- taskReviewDeps,
5458
+ summaryDeps,
5379
5459
  consumptionDeps,
5380
5460
  logger,
5381
5461
  agentInfo,
@@ -5385,6 +5465,7 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
5385
5465
  verbose
5386
5466
  );
5387
5467
  } else {
5468
+ const reviewDepsWithHeartbeat = { ...taskReviewDeps, heartbeat };
5388
5469
  await executeReviewTask(
5389
5470
  client,
5390
5471
  agentId,
@@ -5395,7 +5476,7 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
5395
5476
  diffContent,
5396
5477
  prompt2,
5397
5478
  timeout_seconds,
5398
- taskReviewDeps,
5479
+ reviewDepsWithHeartbeat,
5399
5480
  consumptionDeps,
5400
5481
  logger,
5401
5482
  agentInfo,
@@ -5488,11 +5569,9 @@ async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber,
5488
5569
  diffContent,
5489
5570
  contextBlock
5490
5571
  });
5491
- const response = await routerRelay.sendPrompt(
5492
- "review_request",
5493
- taskId,
5494
- fullPrompt,
5495
- timeoutSeconds
5572
+ const response = await runWithHeartbeat(
5573
+ reviewDeps.heartbeat,
5574
+ () => routerRelay.sendPrompt("review_request", taskId, fullPrompt, timeoutSeconds)
5496
5575
  );
5497
5576
  const parsed = routerRelay.parseReviewResponse(response);
5498
5577
  reviewText = parsed.review;
@@ -5587,11 +5666,9 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
5587
5666
  diffContent,
5588
5667
  contextBlock
5589
5668
  });
5590
- const response = await routerRelay.sendPrompt(
5591
- "review_request",
5592
- taskId,
5593
- fullPrompt,
5594
- timeoutSeconds
5669
+ const response = await runWithHeartbeat(
5670
+ reviewDeps.heartbeat,
5671
+ () => routerRelay.sendPrompt("review_request", taskId, fullPrompt, timeoutSeconds)
5595
5672
  );
5596
5673
  const parsed = routerRelay.parseReviewResponse(response);
5597
5674
  reviewText = parsed.review;
@@ -5690,11 +5767,9 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
5690
5767
  diffContent,
5691
5768
  contextBlock
5692
5769
  });
5693
- const response = await routerRelay.sendPrompt(
5694
- "summary_request",
5695
- taskId,
5696
- fullPrompt,
5697
- timeoutSeconds
5770
+ const response = await runWithHeartbeat(
5771
+ reviewDeps.heartbeat,
5772
+ () => routerRelay.sendPrompt("summary_request", taskId, fullPrompt, timeoutSeconds)
5698
5773
  );
5699
5774
  const parsed = extractVerdict(response);
5700
5775
  summaryText = parsed.review;
@@ -5803,7 +5878,7 @@ function sleep2(ms, signal) {
5803
5878
  async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumptionDeps, options) {
5804
5879
  const client = new ApiClient(platformUrl, {
5805
5880
  authToken: options?.authToken,
5806
- cliVersion: "0.25.1",
5881
+ cliVersion: "0.25.2",
5807
5882
  versionOverride: options?.versionOverride,
5808
5883
  onTokenRefresh: options?.onTokenRefresh
5809
5884
  });
@@ -6168,7 +6243,7 @@ async function startBatchAgents(config, agents, pollIntervalMs, oauthToken, opti
6168
6243
  const { versionOverride, verbose, instancesOverride, agentOwner, userOrgs } = options;
6169
6244
  const client = new ApiClient(config.platformUrl, {
6170
6245
  authToken: oauthToken,
6171
- cliVersion: "0.25.1",
6246
+ cliVersion: "0.25.2",
6172
6247
  versionOverride,
6173
6248
  onTokenRefresh: () => getValidToken(config.platformUrl, { configPath: config.authFile })
6174
6249
  });
@@ -6519,7 +6594,7 @@ agentCommand.command("start").description("Start agents in polling mode").option
6519
6594
  }
6520
6595
  config = loadConfig();
6521
6596
  }
6522
- console.log(formatVersionBanner("0.25.1", "085f9a8"));
6597
+ console.log(formatVersionBanner("0.25.2", "c374877"));
6523
6598
  if (config.agents && config.agents.length > 0) {
6524
6599
  const toolEntries = config.agents.map((a) => ({
6525
6600
  tool: a.tool,
@@ -7341,7 +7416,7 @@ var statusCommand = new Command4("status").description("Show agent config, conne
7341
7416
  });
7342
7417
 
7343
7418
  // src/index.ts
7344
- var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version(`${"0.25.1"} (${"085f9a8"})`);
7419
+ var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version(`${"0.25.2"} (${"c374877"})`);
7345
7420
  program.addCommand(agentCommand);
7346
7421
  program.addCommand(authCommand());
7347
7422
  program.addCommand(dedupCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.25.1",
3
+ "version": "0.25.2",
4
4
  "description": "Distributed AI code review agent — poll, review, and submit PR reviews using your own AI tools",
5
5
  "type": "module",
6
6
  "license": "MIT",