opencara 0.23.6 → 0.23.8

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 +101 -39
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -714,6 +714,20 @@ function parseAgents(data) {
714
714
  agent.maxTasksPerDay = v;
715
715
  }
716
716
  }
717
+ if (typeof obj.liveness_timeout === "number") {
718
+ if (obj.liveness_timeout === 0) {
719
+ agent.livenessTimeout = 0;
720
+ } else {
721
+ const v = parsePositiveInt(obj.liveness_timeout);
722
+ if (v === null) {
723
+ console.warn(
724
+ `\u26A0 Config warning: agents[${i}].liveness_timeout must be a non-negative integer (seconds), got ${obj.liveness_timeout}. Value ignored.`
725
+ );
726
+ } else {
727
+ agent.livenessTimeout = v;
728
+ }
729
+ }
730
+ }
717
731
  const repoConfig = parseRepoConfig(obj, i);
718
732
  if (repoConfig) agent.repos = repoConfig;
719
733
  const synthesizeRepoConfig = parseRepoConfig(obj, i, "synthesize_repos");
@@ -1750,6 +1764,7 @@ var ToolTimeoutError = class extends Error {
1750
1764
  };
1751
1765
  var SIGKILL_GRACE_MS = 5e3;
1752
1766
  var MIN_PARTIAL_RESULT_LENGTH = 50;
1767
+ var STDOUT_LIVENESS_TIMEOUT_MS = 3e5;
1753
1768
  var MAX_STDERR_LENGTH = 1e3;
1754
1769
  function validateCommandBinary(commandTemplate) {
1755
1770
  const { command } = parseCommandTemplate(commandTemplate);
@@ -1846,7 +1861,7 @@ function parseTokenUsage(stdout, stderr) {
1846
1861
  const estimated = estimateTokens(stdout);
1847
1862
  return { tokens: estimated, parsed: false, input: 0, output: estimated };
1848
1863
  }
1849
- function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd) {
1864
+ function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd, livenessTimeoutMs) {
1850
1865
  const promptViaArg = commandTemplate.includes("${PROMPT}");
1851
1866
  const allVars = { ...vars, PROMPT: prompt2 };
1852
1867
  if (cwd && !allVars["CODEBASE_DIR"]) {
@@ -1866,7 +1881,12 @@ function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd) {
1866
1881
  let stderr = "";
1867
1882
  let settled = false;
1868
1883
  let sigkillTimer;
1884
+ let killedByLiveness = false;
1885
+ const effectiveLivenessMs = livenessTimeoutMs === void 0 ? STDOUT_LIVENESS_TIMEOUT_MS : livenessTimeoutMs;
1886
+ let killScheduled = false;
1869
1887
  function scheduleKillEscalation() {
1888
+ if (killScheduled) return;
1889
+ killScheduled = true;
1870
1890
  child.kill("SIGTERM");
1871
1891
  if (sigkillTimer) clearTimeout(sigkillTimer);
1872
1892
  sigkillTimer = setTimeout(() => {
@@ -1876,8 +1896,26 @@ function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd) {
1876
1896
  }, SIGKILL_GRACE_MS);
1877
1897
  }
1878
1898
  const timer = setTimeout(scheduleKillEscalation, timeoutMs);
1899
+ let livenessTimer;
1900
+ if (effectiveLivenessMs > 0) {
1901
+ livenessTimer = setTimeout(() => {
1902
+ if (!settled) {
1903
+ killedByLiveness = true;
1904
+ scheduleKillEscalation();
1905
+ }
1906
+ }, effectiveLivenessMs);
1907
+ }
1879
1908
  child.stdout?.on("data", (chunk) => {
1880
1909
  stdout += chunk.toString();
1910
+ if (livenessTimer) {
1911
+ clearTimeout(livenessTimer);
1912
+ livenessTimer = setTimeout(() => {
1913
+ if (!settled) {
1914
+ killedByLiveness = true;
1915
+ scheduleKillEscalation();
1916
+ }
1917
+ }, effectiveLivenessMs);
1918
+ }
1881
1919
  });
1882
1920
  child.stderr?.on("data", (chunk) => {
1883
1921
  stderr += chunk.toString();
@@ -1893,6 +1931,7 @@ function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd) {
1893
1931
  }
1894
1932
  function cleanup() {
1895
1933
  clearTimeout(timer);
1934
+ if (livenessTimer) clearTimeout(livenessTimer);
1896
1935
  if (sigkillTimer) clearTimeout(sigkillTimer);
1897
1936
  if (onAbort && signal) {
1898
1937
  signal.removeEventListener("abort", onAbort);
@@ -1917,11 +1956,19 @@ function executeTool(commandTemplate, prompt2, timeoutMs, signal, vars, cwd) {
1917
1956
  return;
1918
1957
  }
1919
1958
  if (sig === "SIGTERM" || sig === "SIGKILL") {
1920
- reject(
1921
- new ToolTimeoutError(
1922
- `Tool "${command}" timed out after ${Math.round(timeoutMs / 1e3)}s`
1923
- )
1924
- );
1959
+ if (killedByLiveness) {
1960
+ reject(
1961
+ new ToolTimeoutError(
1962
+ `Tool "${command}" killed: no stdout for ${Math.round(effectiveLivenessMs / 1e3)}s (process may be stuck)`
1963
+ )
1964
+ );
1965
+ } else {
1966
+ reject(
1967
+ new ToolTimeoutError(
1968
+ `Tool "${command}" timed out after ${Math.round(timeoutMs / 1e3)}s`
1969
+ )
1970
+ );
1971
+ }
1925
1972
  return;
1926
1973
  }
1927
1974
  if (code !== 0) {
@@ -2500,7 +2547,8 @@ ${userMessage}`;
2500
2547
  effectiveTimeout,
2501
2548
  abortController.signal,
2502
2549
  void 0,
2503
- deps.codebaseDir ?? void 0
2550
+ deps.codebaseDir ?? void 0,
2551
+ deps.livenessTimeoutMs
2504
2552
  );
2505
2553
  const { verdict, review } = extractVerdict(result.stdout);
2506
2554
  const inputTokens = result.tokensParsed ? 0 : estimateTokens(fullPrompt);
@@ -2615,7 +2663,8 @@ ${userMessage}`;
2615
2663
  effectiveTimeout,
2616
2664
  abortController.signal,
2617
2665
  void 0,
2618
- deps.codebaseDir ?? void 0
2666
+ deps.codebaseDir ?? void 0,
2667
+ deps.livenessTimeoutMs
2619
2668
  );
2620
2669
  const { verdict, review } = extractVerdict(result.stdout);
2621
2670
  const flaggedReviews = extractFlaggedReviews(result.stdout);
@@ -5663,7 +5712,7 @@ function sleep2(ms, signal) {
5663
5712
  async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumptionDeps, options) {
5664
5713
  const client = new ApiClient(platformUrl, {
5665
5714
  authToken: options?.authToken,
5666
- cliVersion: "0.23.6",
5715
+ cliVersion: "0.23.8",
5667
5716
  versionOverride: options?.versionOverride,
5668
5717
  onTokenRefresh: options?.onTokenRefresh
5669
5718
  });
@@ -5770,6 +5819,8 @@ async function batchPollLoop(client, agentStates, options) {
5770
5819
  let consecutiveAuthErrors = 0;
5771
5820
  let consecutiveErrors = 0;
5772
5821
  let pollCycleCount = 0;
5822
+ const busyAgents = /* @__PURE__ */ new Set();
5823
+ const inflightPromises = /* @__PURE__ */ new Set();
5773
5824
  while (!signal?.aborted) {
5774
5825
  if (accessibleRepos && githubToken && recheckInterval > 0 && pollCycleCount > 0 && pollCycleCount % recheckInterval === 0) {
5775
5826
  const allRepos = extractRepoUrls(
@@ -5826,13 +5877,17 @@ async function batchPollLoop(client, agentStates, options) {
5826
5877
  break;
5827
5878
  }
5828
5879
  try {
5829
- const descriptors = agentStates.map((s) => s.descriptor);
5880
+ const availableStates = agentStates.filter((s) => !busyAgents.has(s));
5881
+ if (availableStates.length === 0) {
5882
+ await sleep2(pollIntervalMs, signal);
5883
+ continue;
5884
+ }
5885
+ const descriptors = availableStates.map((s) => s.descriptor);
5830
5886
  const request = buildBatchPollRequest(descriptors);
5831
5887
  const response = await client.post("/api/tasks/poll/batch", request);
5832
5888
  consecutiveAuthErrors = 0;
5833
5889
  consecutiveErrors = 0;
5834
- const handlePromises = [];
5835
- for (const state of agentStates) {
5890
+ for (const state of availableStates) {
5836
5891
  const agentName = state.descriptor.name;
5837
5892
  const pollResponse = response.assignments[agentName];
5838
5893
  if (!pollResponse || pollResponse.tasks.length === 0) continue;
@@ -5846,8 +5901,9 @@ async function batchPollLoop(client, agentStates, options) {
5846
5901
  );
5847
5902
  const task = eligible[0];
5848
5903
  if (!task) continue;
5849
- handlePromises.push(
5850
- (async () => {
5904
+ busyAgents.add(state);
5905
+ const p = (async () => {
5906
+ try {
5851
5907
  const result = await handleTask(
5852
5908
  client,
5853
5909
  state.descriptor.agentId,
@@ -5876,28 +5932,27 @@ async function batchPollLoop(client, agentStates, options) {
5876
5932
  );
5877
5933
  }
5878
5934
  }
5879
- })()
5880
- );
5881
- }
5882
- if (handlePromises.length > 0) {
5883
- const results = await Promise.allSettled(handlePromises);
5884
- for (const r of results) {
5885
- if (r.status === "rejected") {
5886
- logError(`${icons.error} Task handler failed: ${r.reason}`);
5935
+ } catch (err) {
5936
+ logError(`${icons.error} Task handler failed: ${err.message}`);
5887
5937
  consecutiveErrors++;
5938
+ } finally {
5939
+ busyAgents.delete(state);
5940
+ if (state.cleanupTracker) {
5941
+ try {
5942
+ const swept = await state.cleanupTracker.sweep(cleanupWorktree);
5943
+ if (swept > 0) {
5944
+ state.logger.log(
5945
+ `${icons.info} Cleaned up ${swept} stale codebase director${swept === 1 ? "y" : "ies"}`
5946
+ );
5947
+ }
5948
+ } catch {
5949
+ }
5950
+ }
5888
5951
  }
5889
- }
5952
+ })();
5953
+ inflightPromises.add(p);
5954
+ void p.finally(() => inflightPromises.delete(p));
5890
5955
  }
5891
- await Promise.allSettled(
5892
- agentStates.filter((state) => state.cleanupTracker).map(async (state) => {
5893
- const swept = await state.cleanupTracker.sweep(cleanupWorktree);
5894
- if (swept > 0) {
5895
- state.logger.log(
5896
- `${icons.info} Cleaned up ${swept} stale codebase director${swept === 1 ? "y" : "ies"}`
5897
- );
5898
- }
5899
- })
5900
- );
5901
5956
  } catch (err) {
5902
5957
  if (signal?.aborted) break;
5903
5958
  if (err instanceof UpgradeRequiredError) {
@@ -5944,12 +5999,16 @@ async function batchPollLoop(client, agentStates, options) {
5944
5999
  }
5945
6000
  await sleep2(pollIntervalMs, signal);
5946
6001
  }
6002
+ if (inflightPromises.size > 0) {
6003
+ log(`${icons.info} Waiting for ${inflightPromises.size} in-flight task(s) to complete...`);
6004
+ await Promise.allSettled([...inflightPromises]);
6005
+ }
5947
6006
  }
5948
6007
  async function startBatchAgents(config, agents, pollIntervalMs, oauthToken, options) {
5949
6008
  const { versionOverride, verbose, instancesOverride, agentOwner, userOrgs } = options;
5950
6009
  const client = new ApiClient(config.platformUrl, {
5951
6010
  authToken: oauthToken,
5952
- cliVersion: "0.23.6",
6011
+ cliVersion: "0.23.8",
5953
6012
  versionOverride,
5954
6013
  onTokenRefresh: () => getValidToken(config.platformUrl, { configPath: config.authFile })
5955
6014
  });
@@ -5995,7 +6054,8 @@ async function startBatchAgents(config, agents, pollIntervalMs, oauthToken, opti
5995
6054
  commandTemplate,
5996
6055
  maxDiffSizeKb: config.maxDiffSizeKb,
5997
6056
  maxRepoSizeMb: config.maxRepoSizeMb,
5998
- codebaseDir
6057
+ codebaseDir,
6058
+ livenessTimeoutMs: agentConfig.livenessTimeout != null ? agentConfig.livenessTimeout * 1e3 : void 0
5999
6059
  };
6000
6060
  const session = createSessionTracker();
6001
6061
  const usageTracker = new UsageTracker();
@@ -6151,7 +6211,8 @@ async function startAgentRouter() {
6151
6211
  commandTemplate: commandTemplate ?? "",
6152
6212
  maxDiffSizeKb: config.maxDiffSizeKb,
6153
6213
  maxRepoSizeMb: config.maxRepoSizeMb,
6154
- codebaseDir
6214
+ codebaseDir,
6215
+ livenessTimeoutMs: agentConfig?.livenessTimeout != null ? agentConfig.livenessTimeout * 1e3 : void 0
6155
6216
  };
6156
6217
  const session = createSessionTracker();
6157
6218
  const usageTracker = new UsageTracker();
@@ -6218,7 +6279,8 @@ function startAgentByIndex(config, agentIndex, pollIntervalMs, oauthToken, versi
6218
6279
  commandTemplate,
6219
6280
  maxDiffSizeKb: config.maxDiffSizeKb,
6220
6281
  maxRepoSizeMb: config.maxRepoSizeMb,
6221
- codebaseDir
6282
+ codebaseDir,
6283
+ livenessTimeoutMs: agentConfig?.livenessTimeout != null ? agentConfig.livenessTimeout * 1e3 : void 0
6222
6284
  };
6223
6285
  const model = agentConfig?.model ?? "unknown";
6224
6286
  const tool = agentConfig?.tool ?? "unknown";
@@ -6292,7 +6354,7 @@ agentCommand.command("start").description("Start agents in polling mode").option
6292
6354
  }
6293
6355
  config = loadConfig();
6294
6356
  }
6295
- console.log(formatVersionBanner("0.23.6", "9696efa"));
6357
+ console.log(formatVersionBanner("0.23.8", "4d448ed"));
6296
6358
  if (config.agents && config.agents.length > 0) {
6297
6359
  const toolEntries = config.agents.map((a) => ({
6298
6360
  tool: a.tool,
@@ -7115,7 +7177,7 @@ var statusCommand = new Command4("status").description("Show agent config, conne
7115
7177
  });
7116
7178
 
7117
7179
  // src/index.ts
7118
- var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version(`${"0.23.6"} (${"9696efa"})`);
7180
+ var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version(`${"0.23.8"} (${"4d448ed"})`);
7119
7181
  program.addCommand(agentCommand);
7120
7182
  program.addCommand(authCommand());
7121
7183
  program.addCommand(dedupCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.23.6",
3
+ "version": "0.23.8",
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",