bosun 0.40.15 → 0.40.17

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.
@@ -40,6 +40,8 @@
40
40
  */
41
41
 
42
42
  import { resolve, dirname } from "node:path";
43
+ import { existsSync, readFileSync } from "node:fs";
44
+ import { homedir } from "node:os";
43
45
  import { fileURLToPath } from "node:url";
44
46
  import { loadConfig } from "../config/config.mjs";
45
47
  import { resolveRepoRoot, resolveAgentRepoRoot } from "../config/repo-root.mjs";
@@ -566,16 +568,28 @@ function hasSdkPrerequisites(name, runtimeEnv = process.env) {
566
568
  }
567
569
 
568
570
  if (name === "codex") {
569
- // Codex needs an OpenAI API key (or Azure key, or profile-specific key)
571
+ // Codex needs an OpenAI API key (or Azure key, or profile-specific key),
572
+ // OR a valid ~/.codex/config.toml where an env_key reference is satisfied.
570
573
  const hasKey =
571
574
  runtimeEnv.OPENAI_API_KEY ||
572
575
  runtimeEnv.AZURE_OPENAI_API_KEY ||
573
576
  runtimeEnv.CODEX_MODEL_PROFILE_XL_API_KEY ||
574
577
  runtimeEnv.CODEX_MODEL_PROFILE_M_API_KEY;
575
- if (!hasKey) {
576
- return { ok: false, reason: "no API key (OPENAI_API_KEY / AZURE_OPENAI_API_KEY)" };
578
+ if (hasKey) return { ok: true, reason: null };
579
+ // Check ~/.codex/config.toml Codex CLI SDK reads auth env_key refs from there
580
+ try {
581
+ const configToml = resolve(homedir(), ".codex", "config.toml");
582
+ if (existsSync(configToml)) {
583
+ const tomlText = readFileSync(configToml, "utf8");
584
+ // Extract all env_key = "VAR_NAME" entries and check if any are set
585
+ for (const match of tomlText.matchAll(/env_key\s*=\s*"([^"]+)"/g)) {
586
+ if (runtimeEnv[match[1]]) return { ok: true, reason: null };
587
+ }
588
+ }
589
+ } catch {
590
+ // best effort — fall through to failure
577
591
  }
578
- return { ok: true, reason: null };
592
+ return { ok: false, reason: "no API key (OPENAI_API_KEY / AZURE_OPENAI_API_KEY) and no satisfied env_key in ~/.codex/config.toml" };
579
593
  }
580
594
  if (name === "copilot") {
581
595
  // Copilot auth can come from multiple sources (OAuth manager, gh auth,
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.40.15",
3
+ "version": "0.40.17",
4
4
  "description": "Bosun Autonomous Engineering — manages AI agent executors with failover, extremely powerful workflow builder, and a massive amount of included default workflow templates for autonomous engineering, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/task/task-cli.mjs CHANGED
File without changes
@@ -822,7 +822,7 @@ const MAX_IDLE_CONTINUES = 5;
822
822
  const WATCHDOG_WARMUP_MS = 5 * 60_000; // 5 minutes warmup
823
823
  const SHARED_STATE_ENABLED = process.env.SHARED_STATE_ENABLED !== "false";
824
824
  const SHARED_STATE_STALE_THRESHOLD_MS =
825
- Number(process.env.SHARED_STATE_STALE_THRESHOLD_MS) || 300_000;
825
+ Number(process.env.SHARED_STATE_STALE_THRESHOLD_MS) || 600_000;
826
826
  const NO_COMMIT_STATE_FILE = resolve(
827
827
  dirname(fileURLToPath(import.meta.url)),
828
828
  "..",
@@ -3900,9 +3900,12 @@ class TaskExecutor {
3900
3900
  const ownerId = sharedState?.ownerId || null;
3901
3901
  const heartbeat =
3902
3902
  sharedState?.ownerHeartbeat || sharedState?.heartbeat || null;
3903
+ // Any non-stale owner (workflow run or executor instance) should
3904
+ // block recovery re-dispatch. Removing the ownerId !== instanceId
3905
+ // guard ensures workflow-owned tasks (wf-<uuid> owners) are also
3906
+ // protected when action.claim_task IS used.
3903
3907
  if (
3904
3908
  ownerId &&
3905
- ownerId !== this._instanceId &&
3906
3909
  !isSharedHeartbeatStale(heartbeat, SHARED_STATE_STALE_THRESHOLD_MS)
3907
3910
  ) {
3908
3911
  skippedForActiveClaim++;
@@ -3977,6 +3980,43 @@ class TaskExecutor {
3977
3980
  }
3978
3981
  const isFreshEnough =
3979
3982
  ageMs === 0 || ageMs <= INPROGRESS_RECOVERY_MAX_AGE_MS;
3983
+
3984
+ // In workflow-owned mode, calling executeTask() fires task.assigned
3985
+ // which launches a new workflow run. If one already exists (evidenced by
3986
+ // an alive agent thread), that creates two competing runs → owner_mismatch.
3987
+ // trigger.task_assigned workflows don't call action.claim_task, so ownerId
3988
+ // is null and the shared-state guard above cannot protect against this.
3989
+ // • Active thread → workflow is still managing it; leave it alone.
3990
+ // • No active thread but fresh → agent died; reset to todo so
3991
+ // trigger.task_available re-dispatches cleanly, without double-dispatch.
3992
+ if (this.workflowOwnsTaskLifecycle) {
3993
+ if (hasThread) {
3994
+ skippedForActiveClaim++;
3995
+ continue;
3996
+ }
3997
+ if (isFreshEnough) {
3998
+ try {
3999
+ await transitionTaskStatus(id, "todo", {
4000
+ source: "task-executor-recovery-workflow-owned",
4001
+ });
4002
+ } catch {
4003
+ /* best effort */
4004
+ }
4005
+ try {
4006
+ transitionInternalTaskStatus(
4007
+ id,
4008
+ "todo",
4009
+ "task-executor-recovery-workflow-owned",
4010
+ );
4011
+ } catch {
4012
+ /* best effort */
4013
+ }
4014
+ this._removeRuntimeSlot(id);
4015
+ resetToTodo++;
4016
+ continue;
4017
+ }
4018
+ }
4019
+
3980
4020
  if (hasThread || isFreshEnough) {
3981
4021
  if (!internalTask) {
3982
4022
  try {
File without changes
File without changes
@@ -0,0 +1 @@
1
+ /* backup of original chat-view.js before MUI transformation */
@@ -0,0 +1 @@
1
+ /* backup - original infra.js before MUI transformation */
File without changes