bosun 0.35.0 → 0.35.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.
- package/agent-pool.mjs +59 -0
- package/claude-shell.mjs +44 -6
- package/codex-shell.mjs +68 -21
- package/copilot-shell.mjs +53 -14
- package/maintenance.mjs +49 -0
- package/monitor.mjs +51 -66
- package/package.json +2 -1
- package/stream-resilience.mjs +79 -0
- package/telegram-bot.mjs +267 -102
- package/ui/index.html +5 -5
- package/ui-server.mjs +178 -6
package/ui-server.mjs
CHANGED
|
@@ -1796,6 +1796,7 @@ const SETTINGS_SENSITIVE_KEYS = new Set([
|
|
|
1796
1796
|
|
|
1797
1797
|
const SETTINGS_KNOWN_SET = new Set(SETTINGS_KNOWN_KEYS);
|
|
1798
1798
|
let _settingsLastUpdateTime = 0;
|
|
1799
|
+
const ASYNC_UI_COMMAND_BASES = new Set(["/plan"]);
|
|
1799
1800
|
|
|
1800
1801
|
function hasSettingValue(value) {
|
|
1801
1802
|
return value !== undefined && value !== null && value !== "";
|
|
@@ -2827,6 +2828,82 @@ function textResponse(res, statusCode, body, contentType = "text/plain") {
|
|
|
2827
2828
|
res.end(body);
|
|
2828
2829
|
}
|
|
2829
2830
|
|
|
2831
|
+
function normalizeTaskStatusKey(status) {
|
|
2832
|
+
return String(status || "")
|
|
2833
|
+
.trim()
|
|
2834
|
+
.toLowerCase()
|
|
2835
|
+
.replace(/[\s_-]+/g, "");
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
function shouldRestartTaskAfterStatusUpdate(previousStatus, nextStatus) {
|
|
2839
|
+
const prev = normalizeTaskStatusKey(previousStatus);
|
|
2840
|
+
const next = normalizeTaskStatusKey(nextStatus);
|
|
2841
|
+
return prev === "inreview" && next === "inprogress";
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
async function maybeRestartTaskOnReopen({
|
|
2845
|
+
taskId,
|
|
2846
|
+
previousStatus,
|
|
2847
|
+
nextStatus,
|
|
2848
|
+
updatedTask,
|
|
2849
|
+
adapter,
|
|
2850
|
+
executor,
|
|
2851
|
+
}) {
|
|
2852
|
+
if (!taskId) {
|
|
2853
|
+
return { attempted: false, started: false, reason: "missing_task_id" };
|
|
2854
|
+
}
|
|
2855
|
+
if (!shouldRestartTaskAfterStatusUpdate(previousStatus, nextStatus)) {
|
|
2856
|
+
return { attempted: false, started: false, reason: "transition_not_eligible" };
|
|
2857
|
+
}
|
|
2858
|
+
if (!executor) {
|
|
2859
|
+
return { attempted: true, started: false, reason: "executor_unavailable" };
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
const status = executor.getStatus?.() || {};
|
|
2863
|
+
const activeSlots = Array.isArray(status?.slots) ? status.slots : [];
|
|
2864
|
+
const alreadyRunning = activeSlots.some(
|
|
2865
|
+
(slot) => String(slot?.taskId || "").trim() === String(taskId).trim(),
|
|
2866
|
+
);
|
|
2867
|
+
if (alreadyRunning) {
|
|
2868
|
+
return { attempted: true, started: false, reason: "already_running" };
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
const maxParallel = Number(status?.maxParallel || 0);
|
|
2872
|
+
const activeCount = Number(status?.activeSlots || activeSlots.length || 0);
|
|
2873
|
+
const hasFreeSlot = maxParallel <= 0 || activeCount < maxParallel;
|
|
2874
|
+
if (!hasFreeSlot) {
|
|
2875
|
+
return { attempted: true, started: false, reason: "no_free_slots" };
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
let taskToRun = updatedTask && typeof updatedTask === "object" ? updatedTask : null;
|
|
2879
|
+
if (!taskToRun?.id && typeof adapter?.getTask === "function") {
|
|
2880
|
+
try {
|
|
2881
|
+
taskToRun = await adapter.getTask(taskId);
|
|
2882
|
+
} catch (err) {
|
|
2883
|
+
return {
|
|
2884
|
+
attempted: true,
|
|
2885
|
+
started: false,
|
|
2886
|
+
reason: "task_lookup_failed",
|
|
2887
|
+
error: err?.message || String(err),
|
|
2888
|
+
};
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
if (!taskToRun) {
|
|
2892
|
+
return { attempted: true, started: false, reason: "task_not_found" };
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
executor.executeTask(taskToRun, {
|
|
2896
|
+
force: true,
|
|
2897
|
+
recoveredFromInProgress: true,
|
|
2898
|
+
}).catch((error) => {
|
|
2899
|
+
console.warn(
|
|
2900
|
+
`[telegram-ui] failed to restart reopened task ${taskId}: ${error.message}`,
|
|
2901
|
+
);
|
|
2902
|
+
});
|
|
2903
|
+
|
|
2904
|
+
return { attempted: true, started: true, reason: "restarted" };
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2830
2907
|
function parseInitData(initData) {
|
|
2831
2908
|
const params = new URLSearchParams(initData);
|
|
2832
2909
|
const data = {};
|
|
@@ -4609,6 +4686,9 @@ async function handleApi(req, res, url) {
|
|
|
4609
4686
|
return;
|
|
4610
4687
|
}
|
|
4611
4688
|
const adapter = getKanbanAdapter();
|
|
4689
|
+
const previousTask = typeof adapter.getTask === "function"
|
|
4690
|
+
? await adapter.getTask(taskId).catch(() => null)
|
|
4691
|
+
: null;
|
|
4612
4692
|
const tagsProvided = body && Object.prototype.hasOwnProperty.call(body, "tags");
|
|
4613
4693
|
const tags = tagsProvided ? normalizeTagsInput(body?.tags) : undefined;
|
|
4614
4694
|
const draftProvided = body && Object.prototype.hasOwnProperty.call(body, "draft");
|
|
@@ -4651,12 +4731,27 @@ async function handleApi(req, res, url) {
|
|
|
4651
4731
|
typeof adapter.updateTask === "function"
|
|
4652
4732
|
? await adapter.updateTask(taskId, patch)
|
|
4653
4733
|
: await adapter.updateTaskStatus(taskId, patch.status);
|
|
4654
|
-
|
|
4734
|
+
const executor = uiDeps.getInternalExecutor?.() || null;
|
|
4735
|
+
const restart = await maybeRestartTaskOnReopen({
|
|
4736
|
+
taskId,
|
|
4737
|
+
previousStatus: previousTask?.status || null,
|
|
4738
|
+
nextStatus: updated?.status || patch.status || null,
|
|
4739
|
+
updatedTask: updated,
|
|
4740
|
+
adapter,
|
|
4741
|
+
executor,
|
|
4742
|
+
});
|
|
4743
|
+
jsonResponse(res, 200, { ok: true, data: updated, restart });
|
|
4655
4744
|
broadcastUiEvent(["tasks", "overview"], "invalidate", {
|
|
4656
4745
|
reason: "task-updated",
|
|
4657
4746
|
taskId,
|
|
4658
4747
|
status: updated?.status || patch.status || null,
|
|
4659
4748
|
});
|
|
4749
|
+
if (restart?.started) {
|
|
4750
|
+
broadcastUiEvent(["tasks", "overview", "executor", "agents"], "invalidate", {
|
|
4751
|
+
reason: "task-restarted",
|
|
4752
|
+
taskId,
|
|
4753
|
+
});
|
|
4754
|
+
}
|
|
4660
4755
|
} catch (err) {
|
|
4661
4756
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
4662
4757
|
}
|
|
@@ -4672,6 +4767,9 @@ async function handleApi(req, res, url) {
|
|
|
4672
4767
|
return;
|
|
4673
4768
|
}
|
|
4674
4769
|
const adapter = getKanbanAdapter();
|
|
4770
|
+
const previousTask = typeof adapter.getTask === "function"
|
|
4771
|
+
? await adapter.getTask(taskId).catch(() => null)
|
|
4772
|
+
: null;
|
|
4675
4773
|
const tagsProvided = body && Object.prototype.hasOwnProperty.call(body, "tags");
|
|
4676
4774
|
const tags = tagsProvided ? normalizeTagsInput(body?.tags) : undefined;
|
|
4677
4775
|
const draftProvided = body && Object.prototype.hasOwnProperty.call(body, "draft");
|
|
@@ -4714,12 +4812,27 @@ async function handleApi(req, res, url) {
|
|
|
4714
4812
|
typeof adapter.updateTask === "function"
|
|
4715
4813
|
? await adapter.updateTask(taskId, patch)
|
|
4716
4814
|
: await adapter.updateTaskStatus(taskId, patch.status);
|
|
4717
|
-
|
|
4815
|
+
const executor = uiDeps.getInternalExecutor?.() || null;
|
|
4816
|
+
const restart = await maybeRestartTaskOnReopen({
|
|
4817
|
+
taskId,
|
|
4818
|
+
previousStatus: previousTask?.status || null,
|
|
4819
|
+
nextStatus: updated?.status || patch.status || null,
|
|
4820
|
+
updatedTask: updated,
|
|
4821
|
+
adapter,
|
|
4822
|
+
executor,
|
|
4823
|
+
});
|
|
4824
|
+
jsonResponse(res, 200, { ok: true, data: updated, restart });
|
|
4718
4825
|
broadcastUiEvent(["tasks", "overview"], "invalidate", {
|
|
4719
4826
|
reason: "task-edited",
|
|
4720
4827
|
taskId,
|
|
4721
4828
|
status: updated?.status || patch.status || null,
|
|
4722
4829
|
});
|
|
4830
|
+
if (restart?.started) {
|
|
4831
|
+
broadcastUiEvent(["tasks", "overview", "executor", "agents"], "invalidate", {
|
|
4832
|
+
reason: "task-restarted",
|
|
4833
|
+
taskId,
|
|
4834
|
+
});
|
|
4835
|
+
}
|
|
4723
4836
|
} catch (err) {
|
|
4724
4837
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
4725
4838
|
}
|
|
@@ -6620,8 +6733,44 @@ async function handleApi(req, res, url) {
|
|
|
6620
6733
|
}
|
|
6621
6734
|
const handler = uiDeps.handleUiCommand;
|
|
6622
6735
|
if (typeof handler === "function") {
|
|
6623
|
-
|
|
6624
|
-
|
|
6736
|
+
if (ASYNC_UI_COMMAND_BASES.has(cmdBase)) {
|
|
6737
|
+
setImmediate(() => {
|
|
6738
|
+
let pending;
|
|
6739
|
+
try {
|
|
6740
|
+
pending = Promise.resolve(handler(command));
|
|
6741
|
+
} catch (err) {
|
|
6742
|
+
console.warn(`[ui] async command failed (${command}): ${err?.message || err}`);
|
|
6743
|
+
broadcastUiEvent(["overview", "executor", "tasks"], "invalidate", {
|
|
6744
|
+
reason: "command-failed",
|
|
6745
|
+
command,
|
|
6746
|
+
});
|
|
6747
|
+
return;
|
|
6748
|
+
}
|
|
6749
|
+
pending
|
|
6750
|
+
.then(() => {
|
|
6751
|
+
broadcastUiEvent(["overview", "executor", "tasks"], "invalidate", {
|
|
6752
|
+
reason: "command-executed",
|
|
6753
|
+
command,
|
|
6754
|
+
});
|
|
6755
|
+
})
|
|
6756
|
+
.catch((err) => {
|
|
6757
|
+
console.warn(`[ui] async command failed (${command}): ${err?.message || err}`);
|
|
6758
|
+
broadcastUiEvent(["overview", "executor", "tasks"], "invalidate", {
|
|
6759
|
+
reason: "command-failed",
|
|
6760
|
+
command,
|
|
6761
|
+
});
|
|
6762
|
+
});
|
|
6763
|
+
});
|
|
6764
|
+
jsonResponse(res, 202, {
|
|
6765
|
+
ok: true,
|
|
6766
|
+
queued: true,
|
|
6767
|
+
command,
|
|
6768
|
+
message: "Command accepted and running in background.",
|
|
6769
|
+
});
|
|
6770
|
+
} else {
|
|
6771
|
+
const result = await handler(command);
|
|
6772
|
+
jsonResponse(res, 200, { ok: true, data: result || null, command });
|
|
6773
|
+
}
|
|
6625
6774
|
} else {
|
|
6626
6775
|
// No command handler wired — acknowledge and broadcast refresh
|
|
6627
6776
|
jsonResponse(res, 200, {
|
|
@@ -6632,7 +6781,7 @@ async function handleApi(req, res, url) {
|
|
|
6632
6781
|
});
|
|
6633
6782
|
}
|
|
6634
6783
|
broadcastUiEvent(["overview", "executor", "tasks"], "invalidate", {
|
|
6635
|
-
reason: "command-executed",
|
|
6784
|
+
reason: ASYNC_UI_COMMAND_BASES.has(cmdBase) ? "command-queued" : "command-executed",
|
|
6636
6785
|
command,
|
|
6637
6786
|
});
|
|
6638
6787
|
} catch (err) {
|
|
@@ -7326,7 +7475,8 @@ async function handleStatic(req, res, url) {
|
|
|
7326
7475
|
return;
|
|
7327
7476
|
}
|
|
7328
7477
|
|
|
7329
|
-
const
|
|
7478
|
+
const rawPathname = String(url.pathname || "/").trim() || "/";
|
|
7479
|
+
const pathname = rawPathname === "/" ? "/index.html" : rawPathname;
|
|
7330
7480
|
const filePath = resolve(uiRoot, `.${pathname}`);
|
|
7331
7481
|
|
|
7332
7482
|
if (!filePath.startsWith(uiRoot)) {
|
|
@@ -7335,6 +7485,28 @@ async function handleStatic(req, res, url) {
|
|
|
7335
7485
|
}
|
|
7336
7486
|
|
|
7337
7487
|
if (!existsSync(filePath)) {
|
|
7488
|
+
// SPA fallback: deep links like /tasks/123 must load index.html so the
|
|
7489
|
+
// client router can resolve the route after refresh.
|
|
7490
|
+
const looksLikeFile = /\.[a-z0-9]+$/i.test(pathname);
|
|
7491
|
+
const hasTemplateBraces = pathname.includes("{{") || pathname.includes("}}");
|
|
7492
|
+
if (!looksLikeFile || hasTemplateBraces) {
|
|
7493
|
+
const indexPath = resolve(uiRoot, "index.html");
|
|
7494
|
+
if (existsSync(indexPath)) {
|
|
7495
|
+
try {
|
|
7496
|
+
const data = await readFile(indexPath);
|
|
7497
|
+
res.writeHead(200, {
|
|
7498
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
7499
|
+
"Access-Control-Allow-Origin": "*",
|
|
7500
|
+
"Cache-Control": "no-store",
|
|
7501
|
+
});
|
|
7502
|
+
res.end(data);
|
|
7503
|
+
return;
|
|
7504
|
+
} catch (err) {
|
|
7505
|
+
textResponse(res, 500, `Failed to load /index.html: ${err.message}`);
|
|
7506
|
+
return;
|
|
7507
|
+
}
|
|
7508
|
+
}
|
|
7509
|
+
}
|
|
7338
7510
|
textResponse(res, 404, "Not Found");
|
|
7339
7511
|
return;
|
|
7340
7512
|
}
|