bosun 0.41.6 → 0.41.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.
package/config/config.mjs CHANGED
@@ -1217,7 +1217,10 @@ export function loadConfig(argv = process.argv, options = {}) {
1217
1217
  {
1218
1218
  const selPath = selectedRepository?.path || "";
1219
1219
  const selHasGit = selPath && existsSync(resolve(selPath, ".git"));
1220
- repoRoot = (selHasGit ? selPath : null) || getFallbackRepoRoot();
1220
+ repoRoot =
1221
+ explicitRepoRoot ||
1222
+ (selHasGit ? selPath : null) ||
1223
+ getFallbackRepoRoot();
1221
1224
  }
1222
1225
 
1223
1226
  if (
package/infra/monitor.mjs CHANGED
@@ -15152,10 +15152,23 @@ if (isExecutorDisabled()) {
15152
15152
  "[monitor] task-executor lifecycle delegation enabled — finalization/recovery handled by workflow replacement",
15153
15153
  );
15154
15154
  }
15155
+ const workflowRunsDir =
15156
+ config?.configDir &&
15157
+ String(config?.activeWorkspace || process.env.BOSUN_WORKSPACE || "").trim()
15158
+ ? resolve(
15159
+ config.configDir,
15160
+ "workspaces",
15161
+ String(config?.activeWorkspace || process.env.BOSUN_WORKSPACE || "").trim(),
15162
+ String(repoSlug || "").split("/").filter(Boolean).pop() || "bosun",
15163
+ ".bosun",
15164
+ "workflow-runs",
15165
+ )
15166
+ : null;
15155
15167
  const execOpts = {
15156
15168
  ...internalExecutorConfig,
15157
15169
  repoRoot,
15158
15170
  repoSlug,
15171
+ workflowRunsDir,
15159
15172
  agentPrompts,
15160
15173
  workflowOwnsTaskLifecycle: workflowOwnsTaskExecutorLifecycle,
15161
15174
  sendTelegram:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.41.6",
3
+ "version": "0.41.8",
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",
@@ -122,6 +122,7 @@ const CLAIM_CONFLICT_COMMENT_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes
122
122
  const REPO_AREA_SLOW_MERGE_LATENCY_MS = 4 * 60 * 60 * 1000;
123
123
  const REPO_AREA_VERY_SLOW_MERGE_LATENCY_MS = 8 * 60 * 60 * 1000;
124
124
  const REPO_AREA_CONTENTION_EVENT_LIMIT = 60;
125
+ const WORKFLOW_ACTIVE_RUNS_INDEX = "_active-runs.json";
125
126
  const FATAL_CLAIM_RENEW_ERRORS = new Set([
126
127
  "task_claimed_by_different_instance",
127
128
  "claim_token_mismatch",
@@ -2313,6 +2314,7 @@ class TaskExecutor {
2313
2314
  activeWorkspace: "",
2314
2315
  branchRouting: null,
2315
2316
  defaultTargetBranch: null,
2317
+ workflowRunsDir: null,
2316
2318
  onTaskStarted: null,
2317
2319
  onTaskCompleted: null,
2318
2320
  onTaskFailed: null,
@@ -2356,6 +2358,11 @@ class TaskExecutor {
2356
2358
  merged.branchRouting?.defaultBranch ||
2357
2359
  process.env.VK_TARGET_BRANCH ||
2358
2360
  "origin/main";
2361
+ this.workflowRunsDir =
2362
+ typeof merged.workflowRunsDir === "string" &&
2363
+ String(merged.workflowRunsDir).trim()
2364
+ ? resolve(String(merged.workflowRunsDir))
2365
+ : null;
2359
2366
  this.onTaskStarted = merged.onTaskStarted;
2360
2367
  this.onTaskCompleted = merged.onTaskCompleted;
2361
2368
  this.onTaskFailed = merged.onTaskFailed;
@@ -3919,6 +3926,9 @@ class TaskExecutor {
3919
3926
  .map((entry) => String(entry?.taskKey || "").trim())
3920
3927
  .filter(Boolean),
3921
3928
  );
3929
+ const activeWorkflowTaskIds = this.workflowOwnsTaskLifecycle
3930
+ ? this._readActiveWorkflowTaskIds()
3931
+ : new Set();
3922
3932
 
3923
3933
  const available = Math.max(0, this.maxParallel - this._activeSlots.size);
3924
3934
  if (available === 0) return;
@@ -4048,14 +4058,14 @@ class TaskExecutor {
4048
4058
  }
4049
4059
  const isFreshEnough =
4050
4060
  ageMs === 0 || ageMs <= INPROGRESS_RECOVERY_MAX_AGE_MS;
4061
+ const hasWorkflowRun = activeWorkflowTaskIds.has(id);
4051
4062
 
4052
- // In workflow-owned mode, executor thread presence is not a reliable
4053
- // liveness signal because workflow nodes can be actively running before
4054
- // any executor thread key is observable. Resetting "fresh" in-progress
4055
- // tasks here causes live runs to churn todo↔inprogress. Keep fresh tasks
4056
- // in-progress and let stale/unstarted guards above handle true stranding.
4063
+ // In workflow-owned mode, the authoritative liveness signals are the
4064
+ // persisted workflow active-runs index, then any live executor thread,
4065
+ // then the shared-state owner. If none of those exist, the task is
4066
+ // ownerless and must be reset even when still "fresh".
4057
4067
  if (this.workflowOwnsTaskLifecycle) {
4058
- if (hasThread) {
4068
+ if (hasWorkflowRun || hasThread) {
4059
4069
  skippedForActiveClaim++;
4060
4070
  continue;
4061
4071
  }
@@ -4080,10 +4090,25 @@ class TaskExecutor {
4080
4090
  resetToTodo++;
4081
4091
  continue;
4082
4092
  }
4083
- if (isFreshEnough) {
4084
- skippedForActiveClaim++;
4085
- continue;
4093
+ try {
4094
+ await transitionTaskStatus(id, "todo", {
4095
+ source: "task-executor-recovery-missing-workflow-run",
4096
+ });
4097
+ } catch {
4098
+ /* best effort */
4086
4099
  }
4100
+ try {
4101
+ transitionInternalTaskStatus(
4102
+ id,
4103
+ "todo",
4104
+ "task-executor-recovery-missing-workflow-run",
4105
+ );
4106
+ } catch {
4107
+ /* best effort */
4108
+ }
4109
+ this._removeRuntimeSlot(id);
4110
+ resetToTodo++;
4111
+ continue;
4087
4112
  }
4088
4113
 
4089
4114
  if (hasThread || isFreshEnough) {
@@ -4154,6 +4179,52 @@ class TaskExecutor {
4154
4179
  }
4155
4180
  }
4156
4181
 
4182
+ _readActiveWorkflowTaskIds() {
4183
+ const taskIds = new Set();
4184
+ if (!this.workflowRunsDir) return taskIds;
4185
+ const activeRunsPath = resolve(this.workflowRunsDir, WORKFLOW_ACTIVE_RUNS_INDEX);
4186
+ if (!existsSync(activeRunsPath)) return taskIds;
4187
+ let activeRuns = [];
4188
+ try {
4189
+ const parsed = JSON.parse(readFileSync(activeRunsPath, "utf8"));
4190
+ activeRuns = Array.isArray(parsed)
4191
+ ? parsed
4192
+ : Array.isArray(parsed?.runs)
4193
+ ? parsed.runs
4194
+ : [];
4195
+ } catch {
4196
+ return taskIds;
4197
+ }
4198
+ for (const entry of activeRuns) {
4199
+ const directTaskId = normalizeTaskIdKey(
4200
+ entry?.taskId || entry?.activeTaskId,
4201
+ );
4202
+ if (directTaskId) {
4203
+ taskIds.add(directTaskId);
4204
+ continue;
4205
+ }
4206
+ const runId = String(entry?.runId || "").trim();
4207
+ if (!runId) continue;
4208
+ const detailPath = resolve(this.workflowRunsDir, `${runId}.json`);
4209
+ if (!existsSync(detailPath)) continue;
4210
+ try {
4211
+ const detail = JSON.parse(readFileSync(detailPath, "utf8"));
4212
+ const detailTaskId = normalizeTaskIdKey(
4213
+ detail?.data?.taskId ||
4214
+ detail?.data?.activeTaskId ||
4215
+ detail?.inputData?.taskId ||
4216
+ detail?.inputData?.activeTaskId,
4217
+ );
4218
+ if (detailTaskId) {
4219
+ taskIds.add(detailTaskId);
4220
+ }
4221
+ } catch {
4222
+ /* best effort */
4223
+ }
4224
+ }
4225
+ return taskIds;
4226
+ }
4227
+
4157
4228
  /**
4158
4229
  * Returns the current executor status for monitoring / Telegram.
4159
4230
  * @returns {Object}