@love-moon/conductor-cli 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @love-moon/conductor-cli
2
2
 
3
+ ## 0.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 39a49fc: fix: reclaim orphaned chat-web browser and cap chat-web task lifetime
8
+
9
+ chat-web persists one Chromium profile per provider, guarded by a per-profile
10
+ SingletonLock. A task whose browser was not cleaned up (e.g. the ai-sdk worker
11
+ was SIGKILLed) left an orphaned Chromium holding that lock, so the next task for
12
+ the same provider failed to launch with `Opening in existing browser session`.
13
+
14
+ - chat-web now reclaims stale/orphaned profile locks before launching (kills an
15
+ orphan whose owner process is gone, clears dead locks) and refuses with a
16
+ clear `ProfileLockedError` when a genuine live chat still holds the profile.
17
+ - The ai-sdk worker now closes its session (and browser) on SIGTERM/SIGINT and
18
+ bounds the close so it can't hang, preventing browser leaks on shutdown.
19
+ - conductor fire caps a chat-web task's active lifetime (default 24h,
20
+ `CONDUCTOR_CHATWEB_MAX_ACTIVE_MS`) and auto-stops it as
21
+ `KILLED / max_active_duration`; chat history is preserved.
22
+
23
+ - Updated dependencies [39a49fc]
24
+ - @love-moon/ai-sdk@0.5.1
25
+ - @love-moon/conductor-sdk@0.5.1
26
+
3
27
  ## 0.5.0
4
28
 
5
29
  ### Patch Changes
@@ -246,6 +246,18 @@ const DEFAULT_POLL_INTERVAL_MS = parseInt(
246
246
  const DEFAULT_ERROR_LOOP_WINDOW_MS = 2 * 60 * 1000;
247
247
  const DEFAULT_ERROR_LOOP_BACKOFF_MS = 3 * 60 * 1000;
248
248
  const DEFAULT_ERROR_LOOP_THRESHOLD = 3;
249
+ // Runtime backend tokens (first word of the resolved command line) that mean
250
+ // "this task drives a chat-web Chromium browser". Mirrors ai-sdk's chat-web
251
+ // aliases; user-facing aliases like `web-chatgpt` resolve to one of these.
252
+ const CHAT_WEB_RUNTIME_BACKEND_TOKENS = new Set(["chat-web", "chatweb", "chat_web", "web-chat"]);
253
+ // Max active lifetime for a chat-web task before it is auto-stopped (default
254
+ // 24h). Bounded to [1min, 7d]; override via CONDUCTOR_CHATWEB_MAX_ACTIVE_MS.
255
+ const CHAT_WEB_MAX_ACTIVE_MS = getBoundedEnvInt(
256
+ "CONDUCTOR_CHATWEB_MAX_ACTIVE_MS",
257
+ 24 * 60 * 60 * 1000,
258
+ 60_000,
259
+ 7 * 24 * 60 * 60 * 1000,
260
+ );
249
261
  const SESSION_BOOTSTRAP_LOCK_TIMEOUT_MS = 15_000;
250
262
  const SESSION_BOOTSTRAP_LOCK_RETRY_MS = 50;
251
263
  const FIRE_WATCHDOG_INTERVAL_MS = getBoundedEnvInt(
@@ -866,6 +878,18 @@ async function main() {
866
878
  env: process.env,
867
879
  });
868
880
 
881
+ // chat-web tasks drive a real Chromium browser that holds a per-profile
882
+ // singleton lock for as long as the task lives. A task that never ends
883
+ // pins that browser indefinitely (and blocks other chats for the same
884
+ // provider), so we cap its active lifetime and auto-stop it when exceeded.
885
+ // The conversation itself is preserved (it lives in the provider account
886
+ // and the persisted profile; closing the browser does not delete it).
887
+ const runtimeBackendToken = String(sessionCommandLine || "")
888
+ .trim()
889
+ .split(/\s+/)[0]
890
+ ?.toLowerCase() || "";
891
+ const isChatWebTask = CHAT_WEB_RUNTIME_BACKEND_TOKENS.has(runtimeBackendToken);
892
+
869
893
  log(`Using backend: ${cliArgs.backend}`);
870
894
 
871
895
  try {
@@ -880,6 +904,7 @@ async function main() {
880
904
 
881
905
  const signals = new AbortController();
882
906
  let shutdownSignal = null;
907
+ let autoStopReason = null;
883
908
  let backendShutdownRequested = false;
884
909
  const requestBackendShutdown = (source) => {
885
910
  if (backendShutdownRequested) {
@@ -911,6 +936,27 @@ async function main() {
911
936
  process.on("SIGINT", onSigint);
912
937
  process.on("SIGTERM", onSigterm);
913
938
 
939
+ // Cap the active lifetime of chat-web tasks (default 24h). On expiry we
940
+ // stop the task gracefully — abort the runner and close the backend
941
+ // session so the Chromium browser is released — and mark it KILLED with a
942
+ // max_active_duration reason. Chat history is retained on the provider side.
943
+ let maxActiveTimer = null;
944
+ if (isChatWebTask) {
945
+ maxActiveTimer = setTimeout(() => {
946
+ log(
947
+ `chat-web task exceeded max active duration (${CHAT_WEB_MAX_ACTIVE_MS}ms); ` +
948
+ `auto-stopping task (chat history is preserved).`,
949
+ );
950
+ autoStopReason = "max_active_duration";
951
+ fireShuttingDown = true;
952
+ signals.abort();
953
+ requestBackendShutdown("max_active_duration");
954
+ }, CHAT_WEB_MAX_ACTIVE_MS);
955
+ if (typeof maxActiveTimer.unref === "function") {
956
+ maxActiveTimer.unref();
957
+ }
958
+ }
959
+
914
960
  if (shouldFireReportTaskStatus({ launchedByDaemon, phase: "running" })) {
915
961
  try {
916
962
  await conductor.sendTaskStatus(taskContext.taskId, {
@@ -1030,7 +1076,12 @@ async function main() {
1030
1076
  // SDK durable outbox would retry forever, preventing the process from
1031
1077
  // exiting.
1032
1078
  const taskDeletedByUser = remoteStopReason === "deleted_by_user";
1033
- const finalStatus = shutdownSignal
1079
+ const finalStatus = autoStopReason
1080
+ ? {
1081
+ status: "KILLED",
1082
+ summary: `${autoStopReason}: chat-web task exceeded its max active duration`,
1083
+ }
1084
+ : shutdownSignal
1034
1085
  ? {
1035
1086
  status: "KILLED",
1036
1087
  summary: `terminated by ${shutdownSignal}`,
@@ -1079,6 +1130,9 @@ async function main() {
1079
1130
  } finally {
1080
1131
  process.off("SIGINT", onSigint);
1081
1132
  process.off("SIGTERM", onSigterm);
1133
+ if (maxActiveTimer) {
1134
+ clearTimeout(maxActiveTimer);
1135
+ }
1082
1136
  if (shutdownSignal === "SIGINT") {
1083
1137
  process.exitCode = 130;
1084
1138
  } else if (shutdownSignal === "SIGTERM") {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.5.0",
4
- "gitCommitId": "7f50aa7",
3
+ "version": "0.5.1",
4
+ "gitCommitId": "119eab6",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -23,8 +23,8 @@
23
23
  "test": "node --test test/*.test.js"
24
24
  },
25
25
  "dependencies": {
26
- "@love-moon/ai-sdk": "0.5.0",
27
- "@love-moon/conductor-sdk": "0.5.0",
26
+ "@love-moon/ai-sdk": "0.5.1",
27
+ "@love-moon/conductor-sdk": "0.5.1",
28
28
  "@github/copilot-sdk": "^0.3.0",
29
29
  "chrome-launcher": "^1.2.1",
30
30
  "chrome-remote-interface": "^0.33.0",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "optionalDependencies": {
39
39
  "@roamhq/wrtc": "^0.10.0",
40
- "@love-moon/chat-web": "0.5.0"
40
+ "@love-moon/chat-web": "0.5.1"
41
41
  },
42
42
  "pnpm": {
43
43
  "onlyBuiltDependencies": [
package/src/daemon.js CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  listAdvertisedBackends,
29
29
  resolveConfiguredRuntimeBackend,
30
30
  isBuiltInRuntimeBackend,
31
+ isCommandOptionalBuiltInRuntimeBackend,
31
32
  isRuntimeSupportedBackend,
32
33
  normalizeRuntimeBackendAlias,
33
34
  normalizeRuntimeBackendName,
@@ -5055,7 +5056,8 @@ export function startDaemon(config = {}, deps = {}) {
5055
5056
  const isAllowedExternalBackend =
5056
5057
  !isBuiltInRuntimeBackend(effectiveBackend) &&
5057
5058
  await isRuntimeSupportedBackend(effectiveBackend, { configFilePath: config.CONFIG_FILE });
5058
- if (!isAdvertisedBackend || (!hasConfiguredEntry && !isAllowedExternalBackend)) {
5059
+ const isCommandOptionalBuiltIn = isCommandOptionalBuiltInRuntimeBackend(effectiveBackend);
5060
+ if (!isAdvertisedBackend || (!hasConfiguredEntry && !isAllowedExternalBackend && !isCommandOptionalBuiltIn)) {
5059
5061
  logError(`Unsupported backend: ${selectedBackend}. Supported: ${SUPPORTED_BACKENDS.join(", ")}`);
5060
5062
  sendAgentCommandAck({
5061
5063
  requestId,
@@ -5526,7 +5528,8 @@ export function startDaemon(config = {}, deps = {}) {
5526
5528
  const isAllowedExternalBackend =
5527
5529
  !isBuiltInRuntimeBackend(effectiveBackend) &&
5528
5530
  await isRuntimeSupportedBackend(effectiveBackend, { configFilePath: config.CONFIG_FILE });
5529
- if (!isAdvertisedBackend || (!hasConfiguredEntry && !isAllowedExternalBackend)) {
5531
+ const isCommandOptionalBuiltIn = isCommandOptionalBuiltInRuntimeBackend(effectiveBackend);
5532
+ if (!isAdvertisedBackend || (!hasConfiguredEntry && !isAllowedExternalBackend && !isCommandOptionalBuiltIn)) {
5530
5533
  reportRestartFailure({
5531
5534
  taskId: normalizedTargetTaskId,
5532
5535
  projectId: normalizedProjectId,