opencara 0.25.0 → 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 +123 -36
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -499,6 +499,7 @@ function ensureConfigDir() {
499
499
  fs.mkdirSync(dir, { recursive: true });
500
500
  }
501
501
  var DEFAULT_MAX_DIFF_SIZE_KB = 100;
502
+ var DEFAULT_MAX_SUMMARY_INPUT_KB = 500;
502
503
  var DEFAULT_MAX_CONSECUTIVE_ERRORS = 10;
503
504
  var DEFAULT_MAX_REPO_SIZE_MB = 100;
504
505
  var DEFAULT_COMMAND_TEST_TIMEOUT_MS = 1e4;
@@ -705,6 +706,12 @@ function validateConfigData(data, envPlatformUrl) {
705
706
  );
706
707
  overrides.maxDiffSizeKb = DEFAULT_MAX_DIFF_SIZE_KB;
707
708
  }
709
+ if (typeof data.max_summary_input_kb === "number" && data.max_summary_input_kb <= 0) {
710
+ console.warn(
711
+ `\u26A0 Config warning: max_summary_input_kb must be > 0, got ${data.max_summary_input_kb}, using default (${DEFAULT_MAX_SUMMARY_INPUT_KB})`
712
+ );
713
+ overrides.maxSummaryInputKb = DEFAULT_MAX_SUMMARY_INPUT_KB;
714
+ }
708
715
  if (typeof data.max_consecutive_errors === "number" && data.max_consecutive_errors <= 0) {
709
716
  console.warn(
710
717
  `\u26A0 Config warning: max_consecutive_errors must be > 0, got ${data.max_consecutive_errors}, using default (${DEFAULT_MAX_CONSECUTIVE_ERRORS})`
@@ -745,6 +752,7 @@ function loadConfig() {
745
752
  platformUrl: envPlatformUrl || DEFAULT_PLATFORM_URL,
746
753
  authFile: null,
747
754
  maxDiffSizeKb: DEFAULT_MAX_DIFF_SIZE_KB,
755
+ maxSummaryInputKb: DEFAULT_MAX_SUMMARY_INPUT_KB,
748
756
  maxConsecutiveErrors: DEFAULT_MAX_CONSECUTIVE_ERRORS,
749
757
  maxRepoSizeMb: DEFAULT_MAX_REPO_SIZE_MB,
750
758
  codebaseDir: null,
@@ -804,6 +812,7 @@ function loadConfig() {
804
812
  platformUrl: envPlatformUrl || (typeof data.platform_url === "string" ? data.platform_url : DEFAULT_PLATFORM_URL),
805
813
  authFile: typeof data.auth_file === "string" && data.auth_file.trim() ? resolveFilePath(data.auth_file) : null,
806
814
  maxDiffSizeKb: overrides.maxDiffSizeKb ?? (typeof data.max_diff_size_kb === "number" ? data.max_diff_size_kb : DEFAULT_MAX_DIFF_SIZE_KB),
815
+ maxSummaryInputKb: overrides.maxSummaryInputKb ?? (typeof data.max_summary_input_kb === "number" ? data.max_summary_input_kb : DEFAULT_MAX_SUMMARY_INPUT_KB),
807
816
  maxConsecutiveErrors: overrides.maxConsecutiveErrors ?? (typeof data.max_consecutive_errors === "number" ? data.max_consecutive_errors : DEFAULT_MAX_CONSECUTIVE_ERRORS),
808
817
  maxRepoSizeMb: overrides.maxRepoSizeMb ?? (typeof data.max_repo_size_mb === "number" ? data.max_repo_size_mb : DEFAULT_MAX_REPO_SIZE_MB),
809
818
  codebaseDir: typeof data.codebase_dir === "string" ? data.codebase_dir : null,
@@ -905,7 +914,27 @@ var ToolTimeoutError = class extends Error {
905
914
  var SIGKILL_GRACE_MS = 5e3;
906
915
  var MIN_PARTIAL_RESULT_LENGTH = 50;
907
916
  var STDOUT_LIVENESS_TIMEOUT_MS = 3e5;
917
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 6e4;
908
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
+ }
909
938
  function validateCommandBinary(commandTemplate) {
910
939
  const { command } = parseCommandTemplate(commandTemplate);
911
940
  if (path3.isAbsolute(command)) {
@@ -1001,7 +1030,7 @@ function parseTokenUsage(stdout, stderr) {
1001
1030
  const estimated = estimateTokens(stdout);
1002
1031
  return { tokens: estimated, parsed: false, input: 0, output: estimated };
1003
1032
  }
1004
- function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd, livenessTimeoutMs) {
1033
+ function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd, livenessTimeoutMs, heartbeat) {
1005
1034
  const promptViaArg = commandTemplate.includes("${PROMPT}");
1006
1035
  const allVars = { ...vars, PROMPT: prompt2 };
1007
1036
  if (cwd && !allVars["CODEBASE_DIR"]) {
@@ -1045,6 +1074,7 @@ function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd, liv
1045
1074
  }
1046
1075
  }, effectiveLivenessMs);
1047
1076
  }
1077
+ const stopHeartbeat = startHeartbeatTimer(heartbeat, () => settled);
1048
1078
  child.stdout?.on("data", (chunk) => {
1049
1079
  stdout += chunk.toString();
1050
1080
  if (livenessTimer) {
@@ -1073,6 +1103,7 @@ function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd, liv
1073
1103
  clearTimeout(timer);
1074
1104
  if (livenessTimer) clearTimeout(livenessTimer);
1075
1105
  if (sigkillTimer) clearTimeout(sigkillTimer);
1106
+ stopHeartbeat();
1076
1107
  if (onAbort && signal) {
1077
1108
  signal.removeEventListener("abort", onAbort);
1078
1109
  }
@@ -1688,7 +1719,8 @@ ${userMessage}`;
1688
1719
  abortController.signal,
1689
1720
  void 0,
1690
1721
  deps.codebaseDir ?? void 0,
1691
- deps.livenessTimeoutMs
1722
+ deps.livenessTimeoutMs,
1723
+ deps.heartbeat
1692
1724
  );
1693
1725
  const { verdict, review } = extractVerdict(result.stdout);
1694
1726
  const inputTokens = result.tokensParsed ? 0 : estimateTokens(fullPrompt);
@@ -2613,7 +2645,7 @@ function sleep(ms, signal) {
2613
2645
 
2614
2646
  // src/summary.ts
2615
2647
  var TIMEOUT_SAFETY_MARGIN_MS2 = 3e4;
2616
- var MAX_INPUT_SIZE_BYTES = 200 * 1024;
2648
+ var MAX_INPUT_SIZE_BYTES = 500 * 1024;
2617
2649
  var InputTooLargeError = class extends Error {
2618
2650
  constructor(message) {
2619
2651
  super(message);
@@ -2663,9 +2695,10 @@ function calculateInputSize(prompt2, reviews, diffContent, contextBlock) {
2663
2695
  }
2664
2696
  async function executeSummary(req, deps, runTool = executeTool) {
2665
2697
  const inputSize = calculateInputSize(req.prompt, req.reviews, req.diffContent, req.contextBlock);
2666
- if (inputSize > MAX_INPUT_SIZE_BYTES) {
2698
+ const maxInputBytes = deps.maxSummaryInputKb !== void 0 ? deps.maxSummaryInputKb * 1024 : MAX_INPUT_SIZE_BYTES;
2699
+ if (inputSize > maxInputBytes) {
2667
2700
  throw new InputTooLargeError(
2668
- `Summary input too large (${Math.round(inputSize / 1024)}KB > ${Math.round(MAX_INPUT_SIZE_BYTES / 1024)}KB limit)`
2701
+ `Summary input too large (${Math.round(inputSize / 1024)}KB > ${Math.round(maxInputBytes / 1024)}KB limit)`
2669
2702
  );
2670
2703
  }
2671
2704
  const timeoutMs = req.timeout * 1e3;
@@ -2695,7 +2728,8 @@ ${userMessage}`;
2695
2728
  abortController.signal,
2696
2729
  void 0,
2697
2730
  deps.codebaseDir ?? void 0,
2698
- deps.livenessTimeoutMs
2731
+ deps.livenessTimeoutMs,
2732
+ deps.heartbeat
2699
2733
  );
2700
2734
  const { verdict, review } = extractVerdict(result.stdout);
2701
2735
  const flaggedReviews = extractFlaggedReviews(result.stdout);
@@ -4036,7 +4070,7 @@ function createPR(worktreePath, issueNumber, issueTitle, summary, branchName) {
4036
4070
  function isAgenticCommand(commandTemplate) {
4037
4071
  return commandTemplate.includes("${PROMPT}") && !commandTemplate.includes("--print");
4038
4072
  }
4039
- function executeAgentic(commandTemplate, prompt2, timeoutMs, cwd, signal) {
4073
+ function executeAgentic(commandTemplate, prompt2, timeoutMs, cwd, signal, heartbeat) {
4040
4074
  const allVars = { PROMPT: prompt2, CODEBASE_DIR: cwd };
4041
4075
  const { command, args } = parseCommandTemplate(commandTemplate, allVars);
4042
4076
  return new Promise((resolve2, reject) => {
@@ -4057,6 +4091,7 @@ function executeAgentic(commandTemplate, prompt2, timeoutMs, cwd, signal) {
4057
4091
  }, 5e3);
4058
4092
  }
4059
4093
  }, timeoutMs);
4094
+ const stopHeartbeat = startHeartbeatTimer(heartbeat, () => settled);
4060
4095
  let onAbort;
4061
4096
  if (signal) {
4062
4097
  onAbort = () => {
@@ -4064,16 +4099,19 @@ function executeAgentic(commandTemplate, prompt2, timeoutMs, cwd, signal) {
4064
4099
  };
4065
4100
  signal.addEventListener("abort", onAbort, { once: true });
4066
4101
  }
4067
- child.on("error", (err) => {
4102
+ function agenticCleanup() {
4068
4103
  clearTimeout(timer);
4104
+ stopHeartbeat();
4069
4105
  if (onAbort && signal) signal.removeEventListener("abort", onAbort);
4106
+ }
4107
+ child.on("error", (err) => {
4108
+ agenticCleanup();
4070
4109
  if (settled) return;
4071
4110
  settled = true;
4072
4111
  reject(err);
4073
4112
  });
4074
4113
  child.on("close", (code, sig) => {
4075
- clearTimeout(timer);
4076
- if (onAbort && signal) signal.removeEventListener("abort", onAbort);
4114
+ agenticCleanup();
4077
4115
  if (settled) return;
4078
4116
  settled = true;
4079
4117
  if (sig === "SIGTERM" || sig === "SIGKILL") {
@@ -4097,7 +4135,8 @@ async function executeImplement(task, worktreePath, deps, timeoutSeconds, signal
4097
4135
  prompt2,
4098
4136
  effectiveTimeout,
4099
4137
  worktreePath,
4100
- signal
4138
+ signal,
4139
+ deps.heartbeat
4101
4140
  );
4102
4141
  return {
4103
4142
  output: {
@@ -4116,7 +4155,9 @@ async function executeImplement(task, worktreePath, deps, timeoutSeconds, signal
4116
4155
  effectiveTimeout,
4117
4156
  signal,
4118
4157
  void 0,
4119
- worktreePath
4158
+ worktreePath,
4159
+ void 0,
4160
+ deps.heartbeat
4120
4161
  );
4121
4162
  const output = parseImplementOutput(result.stdout);
4122
4163
  const inputTokens = result.tokensParsed ? 0 : estimateTokens(prompt2);
@@ -4338,7 +4379,9 @@ async function executeFix(task, diffContent, deps, timeoutSeconds, worktreePath,
4338
4379
  effectiveTimeout,
4339
4380
  signal,
4340
4381
  void 0,
4341
- worktreePath
4382
+ worktreePath,
4383
+ void 0,
4384
+ deps.heartbeat
4342
4385
  );
4343
4386
  const inputTokens = result.tokensParsed ? 0 : estimateTokens(prompt2);
4344
4387
  const detail = result.tokenDetail;
@@ -5057,6 +5100,49 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
5057
5100
  await sleep2(pollIntervalMs, signal);
5058
5101
  }
5059
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
+ }
5060
5146
  async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, agentInfo, logger, agentSession, routerRelay, signal, cleanupTracker, verbose) {
5061
5147
  const { task_id, owner, repo, pr_number, diff_url, timeout_seconds, prompt: prompt2, role, base_ref } = task;
5062
5148
  const { log, logError, logWarn } = logger;
@@ -5201,12 +5287,14 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
5201
5287
  log(" (suspicious prompt report not sent \u2014 endpoint not available)");
5202
5288
  }
5203
5289
  }
5290
+ const heartbeat = isLongRunningRole(role) ? createHeartbeatControl(client, task_id, agentId, role, logger) : void 0;
5204
5291
  try {
5205
5292
  if (isImplementRole(role)) {
5206
5293
  const codebaseDir = reviewDeps.codebaseDir || path9.join(CONFIG_DIR, "repos");
5207
5294
  const implementDeps = {
5208
5295
  commandTemplate: reviewDeps.commandTemplate,
5209
- codebaseDir
5296
+ codebaseDir,
5297
+ heartbeat
5210
5298
  };
5211
5299
  const implementResult = await executeImplementTask(
5212
5300
  client,
@@ -5240,7 +5328,8 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
5240
5328
  throw new Error("Fix task requires a codebase worktree but checkout failed");
5241
5329
  }
5242
5330
  const fixDeps = {
5243
- commandTemplate: reviewDeps.commandTemplate
5331
+ commandTemplate: reviewDeps.commandTemplate,
5332
+ heartbeat
5244
5333
  };
5245
5334
  const fixResult = await executeFixTask(
5246
5335
  client,
@@ -5354,6 +5443,7 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
5354
5443
  { execGh: defaultExecGh }
5355
5444
  );
5356
5445
  } else if (role === "summary" && "reviews" in claimResponse && claimResponse.reviews) {
5446
+ const summaryDeps = { ...taskReviewDeps, heartbeat };
5357
5447
  await executeSummaryTask(
5358
5448
  client,
5359
5449
  agentId,
@@ -5365,7 +5455,7 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
5365
5455
  prompt2,
5366
5456
  timeout_seconds,
5367
5457
  claimResponse.reviews,
5368
- taskReviewDeps,
5458
+ summaryDeps,
5369
5459
  consumptionDeps,
5370
5460
  logger,
5371
5461
  agentInfo,
@@ -5375,6 +5465,7 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
5375
5465
  verbose
5376
5466
  );
5377
5467
  } else {
5468
+ const reviewDepsWithHeartbeat = { ...taskReviewDeps, heartbeat };
5378
5469
  await executeReviewTask(
5379
5470
  client,
5380
5471
  agentId,
@@ -5385,7 +5476,7 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
5385
5476
  diffContent,
5386
5477
  prompt2,
5387
5478
  timeout_seconds,
5388
- taskReviewDeps,
5479
+ reviewDepsWithHeartbeat,
5389
5480
  consumptionDeps,
5390
5481
  logger,
5391
5482
  agentInfo,
@@ -5478,11 +5569,9 @@ async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber,
5478
5569
  diffContent,
5479
5570
  contextBlock
5480
5571
  });
5481
- const response = await routerRelay.sendPrompt(
5482
- "review_request",
5483
- taskId,
5484
- fullPrompt,
5485
- timeoutSeconds
5572
+ const response = await runWithHeartbeat(
5573
+ reviewDeps.heartbeat,
5574
+ () => routerRelay.sendPrompt("review_request", taskId, fullPrompt, timeoutSeconds)
5486
5575
  );
5487
5576
  const parsed = routerRelay.parseReviewResponse(response);
5488
5577
  reviewText = parsed.review;
@@ -5577,11 +5666,9 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
5577
5666
  diffContent,
5578
5667
  contextBlock
5579
5668
  });
5580
- const response = await routerRelay.sendPrompt(
5581
- "review_request",
5582
- taskId,
5583
- fullPrompt,
5584
- timeoutSeconds
5669
+ const response = await runWithHeartbeat(
5670
+ reviewDeps.heartbeat,
5671
+ () => routerRelay.sendPrompt("review_request", taskId, fullPrompt, timeoutSeconds)
5585
5672
  );
5586
5673
  const parsed = routerRelay.parseReviewResponse(response);
5587
5674
  reviewText = parsed.review;
@@ -5680,11 +5767,9 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
5680
5767
  diffContent,
5681
5768
  contextBlock
5682
5769
  });
5683
- const response = await routerRelay.sendPrompt(
5684
- "summary_request",
5685
- taskId,
5686
- fullPrompt,
5687
- timeoutSeconds
5770
+ const response = await runWithHeartbeat(
5771
+ reviewDeps.heartbeat,
5772
+ () => routerRelay.sendPrompt("summary_request", taskId, fullPrompt, timeoutSeconds)
5688
5773
  );
5689
5774
  const parsed = extractVerdict(response);
5690
5775
  summaryText = parsed.review;
@@ -5793,7 +5878,7 @@ function sleep2(ms, signal) {
5793
5878
  async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumptionDeps, options) {
5794
5879
  const client = new ApiClient(platformUrl, {
5795
5880
  authToken: options?.authToken,
5796
- cliVersion: "0.25.0",
5881
+ cliVersion: "0.25.2",
5797
5882
  versionOverride: options?.versionOverride,
5798
5883
  onTokenRefresh: options?.onTokenRefresh
5799
5884
  });
@@ -6158,7 +6243,7 @@ async function startBatchAgents(config, agents, pollIntervalMs, oauthToken, opti
6158
6243
  const { versionOverride, verbose, instancesOverride, agentOwner, userOrgs } = options;
6159
6244
  const client = new ApiClient(config.platformUrl, {
6160
6245
  authToken: oauthToken,
6161
- cliVersion: "0.25.0",
6246
+ cliVersion: "0.25.2",
6162
6247
  versionOverride,
6163
6248
  onTokenRefresh: () => getValidToken(config.platformUrl, { configPath: config.authFile })
6164
6249
  });
@@ -6364,6 +6449,7 @@ async function startAgentRouter() {
6364
6449
  const reviewDeps = {
6365
6450
  commandTemplate: commandTemplate ?? "",
6366
6451
  maxDiffSizeKb: config.maxDiffSizeKb,
6452
+ maxSummaryInputKb: config.maxSummaryInputKb,
6367
6453
  maxRepoSizeMb: config.maxRepoSizeMb,
6368
6454
  codebaseDir,
6369
6455
  livenessTimeoutMs: agentConfig?.livenessTimeout != null ? agentConfig.livenessTimeout * 1e3 : void 0
@@ -6430,6 +6516,7 @@ function startAgentByIndex(config, agentIndex, pollIntervalMs, oauthToken, versi
6430
6516
  const reviewDeps = {
6431
6517
  commandTemplate,
6432
6518
  maxDiffSizeKb: config.maxDiffSizeKb,
6519
+ maxSummaryInputKb: config.maxSummaryInputKb,
6433
6520
  maxRepoSizeMb: config.maxRepoSizeMb,
6434
6521
  codebaseDir,
6435
6522
  livenessTimeoutMs: agentConfig?.livenessTimeout != null ? agentConfig.livenessTimeout * 1e3 : void 0
@@ -6507,7 +6594,7 @@ agentCommand.command("start").description("Start agents in polling mode").option
6507
6594
  }
6508
6595
  config = loadConfig();
6509
6596
  }
6510
- console.log(formatVersionBanner("0.25.0", "a984ae2"));
6597
+ console.log(formatVersionBanner("0.25.2", "c374877"));
6511
6598
  if (config.agents && config.agents.length > 0) {
6512
6599
  const toolEntries = config.agents.map((a) => ({
6513
6600
  tool: a.tool,
@@ -7329,7 +7416,7 @@ var statusCommand = new Command4("status").description("Show agent config, conne
7329
7416
  });
7330
7417
 
7331
7418
  // src/index.ts
7332
- var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version(`${"0.25.0"} (${"a984ae2"})`);
7419
+ var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version(`${"0.25.2"} (${"c374877"})`);
7333
7420
  program.addCommand(agentCommand);
7334
7421
  program.addCommand(authCommand());
7335
7422
  program.addCommand(dedupCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.25.0",
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",