bosun 0.36.2 → 0.36.3
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/analyze-agent-work-helpers.mjs +308 -0
- package/analyze-agent-work.mjs +926 -0
- package/autofix.mjs +2 -0
- package/codex-shell.mjs +85 -10
- package/git-editor-fix.mjs +273 -0
- package/mcp-registry.mjs +579 -0
- package/meeting-workflow-service.mjs +631 -0
- package/monitor.mjs +18 -103
- package/package.json +13 -2
- package/primary-agent.mjs +32 -12
- package/session-tracker.mjs +68 -0
- package/stream-resilience.mjs +17 -7
- package/ui/app.js +19 -4
- package/ui/components/chat-view.js +108 -5
- package/ui/components/session-list.js +1 -1
- package/ui/components/shared.js +188 -15
- package/ui/modules/icons.js +13 -0
- package/ui/modules/utils.js +44 -0
- package/ui/modules/voice.js +15 -6
- package/ui/styles/components.css +99 -3
- package/ui/styles/sessions.css +84 -12
- package/ui/tabs/chat.js +5 -1
- package/ui/tabs/control.js +16 -22
- package/ui/tabs/dashboard.js +85 -8
- package/ui/tabs/library.js +113 -17
- package/ui/tabs/settings.js +116 -2
- package/ui/tabs/tasks.js +388 -39
- package/ui/tabs/telemetry.js +0 -1
- package/ui/tabs/workflows.js +4 -0
- package/ui-server.mjs +193 -19
- package/update-check.mjs +41 -13
- package/voice-relay.mjs +816 -0
- package/voice-tools.mjs +679 -0
- package/workflow-templates/agents.mjs +6 -2
- package/workflow-templates/github.mjs +154 -12
- package/workflow-templates.mjs +3 -0
- package/github-reconciler.mjs +0 -506
- package/merge-strategy.mjs +0 -1210
- package/pr-cleanup-daemon.mjs +0 -992
- package/workspace-reaper.mjs +0 -405
package/monitor.mjs
CHANGED
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
generateWeeklyAgentWorkReport,
|
|
60
60
|
shouldSendWeeklyReport,
|
|
61
61
|
} from "./agent-work-report.mjs";
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
import {
|
|
64
64
|
execPrimaryPrompt,
|
|
65
65
|
initPrimaryAgent,
|
|
@@ -96,11 +96,7 @@ import {
|
|
|
96
96
|
} from "./container-runner.mjs";
|
|
97
97
|
import { ensureCodexConfig, printConfigSummary } from "./codex-config.mjs";
|
|
98
98
|
import { RestartController } from "./restart-controller.mjs";
|
|
99
|
-
|
|
100
|
-
analyzeMergeStrategy,
|
|
101
|
-
executeDecision,
|
|
102
|
-
resetMergeStrategyDedup,
|
|
103
|
-
} from "./merge-strategy.mjs";
|
|
99
|
+
|
|
104
100
|
import { assessTask, quickAssess } from "./task-assessment.mjs";
|
|
105
101
|
import {
|
|
106
102
|
getBosunCoAuthorTrailer,
|
|
@@ -227,10 +223,7 @@ import { createErrorDetector } from "./error-detector.mjs";
|
|
|
227
223
|
import { createAgentSupervisor } from "./agent-supervisor.mjs";
|
|
228
224
|
import { getSessionTracker } from "./session-tracker.mjs";
|
|
229
225
|
import { pullWorkspaceRepos } from "./workspace-manager.mjs";
|
|
230
|
-
|
|
231
|
-
startGitHubReconciler,
|
|
232
|
-
stopGitHubReconciler,
|
|
233
|
-
} from "./github-reconciler.mjs";
|
|
226
|
+
|
|
234
227
|
import {
|
|
235
228
|
getKanbanBackendName,
|
|
236
229
|
setKanbanBackend,
|
|
@@ -1249,6 +1242,8 @@ function isBenignWorkspaceSyncFailure(errorText) {
|
|
|
1249
1242
|
let workspaceCount = 0;
|
|
1250
1243
|
let repoCount = 0;
|
|
1251
1244
|
let failedRepoCount = 0;
|
|
1245
|
+
let nonBenignFailedRepoCount = 0;
|
|
1246
|
+
let benignFailedRepoCount = 0;
|
|
1252
1247
|
let workspaceExceptionCount = 0;
|
|
1253
1248
|
try {
|
|
1254
1249
|
for (const wsId of workspaceIds) {
|
|
@@ -1262,6 +1257,8 @@ function isBenignWorkspaceSyncFailure(errorText) {
|
|
|
1262
1257
|
if (failed.length > 0) {
|
|
1263
1258
|
const benignFailed = failed.filter((r) => isBenignWorkspaceSyncFailure(r?.error));
|
|
1264
1259
|
const nonBenignFailed = failed.filter((r) => !isBenignWorkspaceSyncFailure(r?.error));
|
|
1260
|
+
benignFailedRepoCount += benignFailed.length;
|
|
1261
|
+
nonBenignFailedRepoCount += nonBenignFailed.length;
|
|
1265
1262
|
if (nonBenignFailed.length === 0) {
|
|
1266
1263
|
clearWorkspaceSyncWarnForWorkspace(wsId);
|
|
1267
1264
|
console.log(
|
|
@@ -1304,6 +1301,7 @@ function isBenignWorkspaceSyncFailure(errorText) {
|
|
|
1304
1301
|
}
|
|
1305
1302
|
} catch (err) {
|
|
1306
1303
|
failedRepoCount += 1;
|
|
1304
|
+
nonBenignFailedRepoCount += 1;
|
|
1307
1305
|
workspaceExceptionCount += 1;
|
|
1308
1306
|
const errText = formatMonitorError(err).replace(/\s+/g, " ").trim();
|
|
1309
1307
|
const errSnippet = (errText || "unknown error").slice(0, 180);
|
|
@@ -1318,10 +1316,10 @@ function isBenignWorkspaceSyncFailure(errorText) {
|
|
|
1318
1316
|
}
|
|
1319
1317
|
} finally {
|
|
1320
1318
|
const durationMs = Date.now() - runStartedAt;
|
|
1321
|
-
const summary = `[monitor] workspace sync: cycle complete (${workspaceCount} workspace(s), ${repoCount} repo(s), ${failedRepoCount} failure(s), ${workspaceExceptionCount} exception(s), ${Math.round(durationMs / 1000)}s)`;
|
|
1322
|
-
if (repoCount > 0 &&
|
|
1319
|
+
const summary = `[monitor] workspace sync: cycle complete (${workspaceCount} workspace(s), ${repoCount} repo(s), ${failedRepoCount} failure(s), ${nonBenignFailedRepoCount} non-benign, ${benignFailedRepoCount} benign, ${workspaceExceptionCount} exception(s), ${Math.round(durationMs / 1000)}s)`;
|
|
1320
|
+
if (repoCount > 0 && nonBenignFailedRepoCount >= repoCount) {
|
|
1323
1321
|
console.warn(
|
|
1324
|
-
`[monitor] workspace sync: all repos failed this cycle (${
|
|
1322
|
+
`[monitor] workspace sync: all repos failed this cycle (${nonBenignFailedRepoCount}/${repoCount})`,
|
|
1325
1323
|
);
|
|
1326
1324
|
}
|
|
1327
1325
|
if (workspaceExceptionCount > 0) {
|
|
@@ -1976,11 +1974,7 @@ if (primaryAgentReady) {
|
|
|
1976
1974
|
void initPrimaryAgent(primaryAgentName);
|
|
1977
1975
|
}
|
|
1978
1976
|
|
|
1979
|
-
// Merge strategy:
|
|
1980
|
-
// Enabled by default unless CODEX_ANALYZE_MERGE_STRATEGY=false
|
|
1981
|
-
const codexAnalyzeMergeStrategy =
|
|
1982
|
-
agentPoolEnabled &&
|
|
1983
|
-
(process.env.CODEX_ANALYZE_MERGE_STRATEGY || "").toLowerCase() !== "false";
|
|
1977
|
+
// Merge strategy: now handled by PR_MERGE_STRATEGY workflow template
|
|
1984
1978
|
const mergeStrategyMode = String(
|
|
1985
1979
|
process.env.MERGE_STRATEGY_MODE || "smart",
|
|
1986
1980
|
).toLowerCase();
|
|
@@ -2439,9 +2433,6 @@ function restartSelf(reason) {
|
|
|
2439
2433
|
vkLogStream.stop();
|
|
2440
2434
|
vkLogStream = null;
|
|
2441
2435
|
}
|
|
2442
|
-
if (prCleanupDaemon) {
|
|
2443
|
-
prCleanupDaemon.stop();
|
|
2444
|
-
}
|
|
2445
2436
|
const shutdownPromises = [];
|
|
2446
2437
|
if (agentEndpoint) {
|
|
2447
2438
|
shutdownPromises.push(
|
|
@@ -2515,6 +2506,7 @@ function detectChangedFiles(repoRootPath) {
|
|
|
2515
2506
|
cwd: repoRootPath,
|
|
2516
2507
|
encoding: "utf8",
|
|
2517
2508
|
timeout: 10_000,
|
|
2509
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2518
2510
|
});
|
|
2519
2511
|
return output
|
|
2520
2512
|
.split(/\r?\n/)
|
|
@@ -2532,6 +2524,7 @@ function getChangeSummary(repoRootPath, files) {
|
|
|
2532
2524
|
cwd: repoRootPath,
|
|
2533
2525
|
encoding: "utf8",
|
|
2534
2526
|
timeout: 10_000,
|
|
2527
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2535
2528
|
});
|
|
2536
2529
|
return diff.trim() || files.join(", ");
|
|
2537
2530
|
} catch {
|
|
@@ -3536,9 +3529,6 @@ function restartVibeKanbanProcess() {
|
|
|
3536
3529
|
vkLogStream.stop();
|
|
3537
3530
|
vkLogStream = null;
|
|
3538
3531
|
}
|
|
3539
|
-
if (prCleanupDaemon) {
|
|
3540
|
-
prCleanupDaemon.stop();
|
|
3541
|
-
}
|
|
3542
3532
|
// Just kill the process — the exit handler will auto-restart it
|
|
3543
3533
|
if (vibeKanbanProcess && !vibeKanbanProcess.killed) {
|
|
3544
3534
|
try {
|
|
@@ -14125,9 +14115,6 @@ function selfRestartForSourceChange(
|
|
|
14125
14115
|
vkLogStream.stop();
|
|
14126
14116
|
vkLogStream = null;
|
|
14127
14117
|
}
|
|
14128
|
-
if (prCleanupDaemon) {
|
|
14129
|
-
prCleanupDaemon.stop();
|
|
14130
|
-
}
|
|
14131
14118
|
// ── Agent isolation: by default, do NOT stop internal executor on self-restart ──
|
|
14132
14119
|
// Task agents run as in-process SDK async iterators. Stopping the executor
|
|
14133
14120
|
// during a normal restart is unnecessary because process.exit(75) kills them.
|
|
@@ -15409,8 +15396,6 @@ let agentEndpoint = null;
|
|
|
15409
15396
|
let agentEventBus = null;
|
|
15410
15397
|
/** @type {import("./review-agent.mjs").ReviewAgent|null} */
|
|
15411
15398
|
let reviewAgent = null;
|
|
15412
|
-
/** @type {Map<string, import("./merge-strategy.mjs").MergeContext>} */
|
|
15413
|
-
const pendingMergeStrategyByTask = new Map();
|
|
15414
15399
|
/** @type {Map<string, { approved: boolean, reviewedAt: string }>} */
|
|
15415
15400
|
const reviewGateResults = new Map();
|
|
15416
15401
|
/** @type {import("./sync-engine.mjs").SyncEngine|null} */
|
|
@@ -15419,53 +15404,6 @@ let syncEngine = null;
|
|
|
15419
15404
|
let errorDetector = null;
|
|
15420
15405
|
/** @type {import("./agent-supervisor.mjs").AgentSupervisor|null} */
|
|
15421
15406
|
let agentSupervisor = null;
|
|
15422
|
-
/** @type {import("./pr-cleanup-daemon.mjs").PRCleanupDaemon|null} */
|
|
15423
|
-
let prCleanupDaemon = null;
|
|
15424
|
-
/** @type {import("./github-reconciler.mjs").GitHubReconciler|null} */
|
|
15425
|
-
let ghReconciler = null;
|
|
15426
|
-
|
|
15427
|
-
function restartGitHubReconciler() {
|
|
15428
|
-
if (isWorkflowReplacingModule("github-reconciler.mjs")) {
|
|
15429
|
-
console.log("[monitor] skipping legacy GitHub reconciler — handled by workflow");
|
|
15430
|
-
return;
|
|
15431
|
-
}
|
|
15432
|
-
try {
|
|
15433
|
-
stopGitHubReconciler();
|
|
15434
|
-
ghReconciler = null;
|
|
15435
|
-
} catch {
|
|
15436
|
-
/* best effort */
|
|
15437
|
-
}
|
|
15438
|
-
|
|
15439
|
-
const activeKanbanBackend = getActiveKanbanBackend();
|
|
15440
|
-
if (activeKanbanBackend !== "github") {
|
|
15441
|
-
return;
|
|
15442
|
-
}
|
|
15443
|
-
if (!githubReconcile?.enabled) {
|
|
15444
|
-
return;
|
|
15445
|
-
}
|
|
15446
|
-
const repo =
|
|
15447
|
-
process.env.GITHUB_REPOSITORY ||
|
|
15448
|
-
(process.env.GITHUB_REPO_OWNER && process.env.GITHUB_REPO_NAME
|
|
15449
|
-
? `${process.env.GITHUB_REPO_OWNER}/${process.env.GITHUB_REPO_NAME}`
|
|
15450
|
-
: "") ||
|
|
15451
|
-
repoSlug ||
|
|
15452
|
-
"unknown/unknown";
|
|
15453
|
-
if (!repo || repo === "unknown/unknown") {
|
|
15454
|
-
console.warn("[gh-reconciler] disabled — missing repo slug");
|
|
15455
|
-
return;
|
|
15456
|
-
}
|
|
15457
|
-
|
|
15458
|
-
ghReconciler = startGitHubReconciler({
|
|
15459
|
-
repoSlug: repo,
|
|
15460
|
-
intervalMs: githubReconcile.intervalMs,
|
|
15461
|
-
mergedLookbackHours: githubReconcile.mergedLookbackHours,
|
|
15462
|
-
trackingLabels: githubReconcile.trackingLabels,
|
|
15463
|
-
sendTelegram:
|
|
15464
|
-
telegramToken && telegramChatId
|
|
15465
|
-
? (msg) => void sendTelegramMessage(msg)
|
|
15466
|
-
: null,
|
|
15467
|
-
});
|
|
15468
|
-
}
|
|
15469
15407
|
|
|
15470
15408
|
if (!isMonitorTestRuntime) {
|
|
15471
15409
|
if (workflowAutomationEnabled) {
|
|
@@ -16148,7 +16086,6 @@ startAgentWorkAnalyzer();
|
|
|
16148
16086
|
startAgentAlertTailer();
|
|
16149
16087
|
startMonitorMonitorSupervisor();
|
|
16150
16088
|
startTaskPlannerStatusLoop();
|
|
16151
|
-
restartGitHubReconciler();
|
|
16152
16089
|
|
|
16153
16090
|
// ── Two-way Telegram :workflow: primary agent ────────────────────────────────────────
|
|
16154
16091
|
injectMonitorFunctions({
|
|
@@ -16177,7 +16114,7 @@ injectMonitorFunctions({
|
|
|
16177
16114
|
getReviewAgentEnabled: () => isReviewAgentEnabled(),
|
|
16178
16115
|
getSyncEngine: () => syncEngine,
|
|
16179
16116
|
getErrorDetector: () => errorDetector,
|
|
16180
|
-
getPrCleanupDaemon: () =>
|
|
16117
|
+
getPrCleanupDaemon: () => null,
|
|
16181
16118
|
getWorkspaceMonitor: () => workspaceMonitor,
|
|
16182
16119
|
getMonitorMonitorStatus: () => getMonitorMonitorStatusSnapshot(),
|
|
16183
16120
|
getTaskStoreStats: () => {
|
|
@@ -16279,31 +16216,9 @@ if (isContainerEnabled()) {
|
|
|
16279
16216
|
}
|
|
16280
16217
|
}
|
|
16281
16218
|
|
|
16282
|
-
// ── Start PR
|
|
16283
|
-
//
|
|
16284
|
-
|
|
16285
|
-
if (isWorkflowReplacingModule("pr-cleanup-daemon.mjs")) {
|
|
16286
|
-
console.log("[monitor] skipping legacy PR cleanup daemon — handled by workflow");
|
|
16287
|
-
} else {
|
|
16288
|
-
const prRepoRoot = effectiveRepoRoot || repoRoot || process.cwd();
|
|
16289
|
-
const flowGateControlsMerges =
|
|
16290
|
-
isFlowPrimaryEnabled() && isFlowReviewGateEnabled();
|
|
16291
|
-
console.log(`[monitor] Starting PR cleanup daemon (repoRoot: ${prRepoRoot})...`);
|
|
16292
|
-
if (flowGateControlsMerges) {
|
|
16293
|
-
console.log(
|
|
16294
|
-
"[monitor] Flow review gate is active — PR cleanup daemon auto-merge is disabled",
|
|
16295
|
-
);
|
|
16296
|
-
}
|
|
16297
|
-
prCleanupDaemon = new PRCleanupDaemon({
|
|
16298
|
-
intervalMs: 30 * 60 * 1000, // 30 minutes
|
|
16299
|
-
maxConcurrentCleanups: 3,
|
|
16300
|
-
dryRun: false,
|
|
16301
|
-
autoMerge: !flowGateControlsMerges,
|
|
16302
|
-
repoRoot: prRepoRoot,
|
|
16303
|
-
});
|
|
16304
|
-
prCleanupDaemon.start();
|
|
16305
|
-
}
|
|
16306
|
-
}
|
|
16219
|
+
// ── Start PR Watchdog & Kanban Sync handled by workflow templates ────────────
|
|
16220
|
+
// PR conflict resolution, CI repair, and GitHub↔kanban sync are now managed
|
|
16221
|
+
// by the BOSUN_PR_WATCHDOG_TEMPLATE and GITHUB_KANBAN_SYNC_TEMPLATE workflows.
|
|
16307
16222
|
} else {
|
|
16308
16223
|
console.log(
|
|
16309
16224
|
"[monitor] test runtime detected (VITEST/NODE_ENV=test) — runtime services disabled",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.3",
|
|
4
4
|
"description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, 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",
|
|
@@ -95,9 +95,12 @@
|
|
|
95
95
|
"build": "node vendor-sync.mjs",
|
|
96
96
|
"build:docs": "node build-docs.mjs",
|
|
97
97
|
"shared-workspaces": "node shared-workspace-cli.mjs",
|
|
98
|
-
"syntax:check": "node -
|
|
98
|
+
"syntax:check": "node --experimental-vm-modules --no-warnings=ExperimentalWarning syntax-check.mjs",
|
|
99
99
|
"pretest": "npm run syntax:check",
|
|
100
100
|
"test": "vitest run --config vitest.config.mjs",
|
|
101
|
+
"test:vitest": "vitest run --config vitest.config.mjs",
|
|
102
|
+
"test:node": "node --test tests/*.node.test.mjs",
|
|
103
|
+
"test:all": "npm run test:vitest && npm run test:node",
|
|
101
104
|
"test:voice-provider-smoke": "vitest run --config vitest.config.mjs tests/voice-provider-smoke.test.mjs",
|
|
102
105
|
"check:native-call-parity": "vitest run --config vitest.config.mjs tests/voice-provider-smoke.test.mjs tests/native-call-parity-checklist.test.mjs",
|
|
103
106
|
"test:watch": "vitest",
|
|
@@ -128,6 +131,8 @@
|
|
|
128
131
|
"agent-prompts.mjs",
|
|
129
132
|
"agent-sdk.mjs",
|
|
130
133
|
"agent-work-report.mjs",
|
|
134
|
+
"analyze-agent-work-helpers.mjs",
|
|
135
|
+
"analyze-agent-work.mjs",
|
|
131
136
|
"anomaly-detector.mjs",
|
|
132
137
|
"autofix.mjs",
|
|
133
138
|
"claude-shell.mjs",
|
|
@@ -159,11 +164,14 @@
|
|
|
159
164
|
"github-reconciler.mjs",
|
|
160
165
|
"get-telegram-chat-id.mjs",
|
|
161
166
|
"git-commit-helpers.mjs",
|
|
167
|
+
"git-editor-fix.mjs",
|
|
162
168
|
"git-safety.mjs",
|
|
163
169
|
"kanban-adapter.mjs",
|
|
164
170
|
"lib/logger.mjs",
|
|
165
171
|
"library-manager.mjs",
|
|
166
172
|
"maintenance.mjs",
|
|
173
|
+
"mcp-registry.mjs",
|
|
174
|
+
"meeting-workflow-service.mjs",
|
|
167
175
|
"merge-strategy.mjs",
|
|
168
176
|
"monitor.mjs",
|
|
169
177
|
"opencode-shell.mjs",
|
|
@@ -191,6 +199,7 @@
|
|
|
191
199
|
"sync-engine.mjs",
|
|
192
200
|
"task-archiver.mjs",
|
|
193
201
|
"task-assessment.mjs",
|
|
202
|
+
"task-cli.mjs",
|
|
194
203
|
"task-complexity.mjs",
|
|
195
204
|
"task-claims.mjs",
|
|
196
205
|
"task-context.mjs",
|
|
@@ -213,6 +222,8 @@
|
|
|
213
222
|
"vibe-kanban-wrapper.mjs",
|
|
214
223
|
"vk-error-resolver.mjs",
|
|
215
224
|
"vk-log-stream.mjs",
|
|
225
|
+
"voice-relay.mjs",
|
|
226
|
+
"voice-tools.mjs",
|
|
216
227
|
"workspace-manager.mjs",
|
|
217
228
|
"workspace-monitor.mjs",
|
|
218
229
|
"workspace-reaper.mjs",
|
package/primary-agent.mjs
CHANGED
|
@@ -148,14 +148,18 @@ const ADAPTERS = {
|
|
|
148
148
|
* Forward an SDK-native command to the Codex shell.
|
|
149
149
|
* /clear is handled specially as a reset; others are sent as user input.
|
|
150
150
|
*/
|
|
151
|
-
execSdkCommand: async (command, args) => {
|
|
151
|
+
execSdkCommand: async (command, args, options = {}) => {
|
|
152
152
|
const cmd = command.startsWith("/") ? command : `/${command}`;
|
|
153
153
|
if (cmd === "/clear") {
|
|
154
154
|
await resetThread();
|
|
155
155
|
return "Session cleared.";
|
|
156
156
|
}
|
|
157
157
|
const fullCmd = args ? `${cmd} ${args}` : cmd;
|
|
158
|
-
return execCodexPrompt(fullCmd, {
|
|
158
|
+
return execCodexPrompt(fullCmd, {
|
|
159
|
+
persistent: true,
|
|
160
|
+
cwd: options.cwd,
|
|
161
|
+
sessionId: options.sessionId || null,
|
|
162
|
+
});
|
|
159
163
|
},
|
|
160
164
|
},
|
|
161
165
|
"copilot-sdk": {
|
|
@@ -169,14 +173,18 @@ const ADAPTERS = {
|
|
|
169
173
|
reset: resetCopilotSession,
|
|
170
174
|
init: async () => initCopilotShell(),
|
|
171
175
|
sdkCommands: ["/status", "/model", "/clear"],
|
|
172
|
-
execSdkCommand: async (command, args) => {
|
|
176
|
+
execSdkCommand: async (command, args, options = {}) => {
|
|
173
177
|
const cmd = command.startsWith("/") ? command : `/${command}`;
|
|
174
178
|
if (cmd === "/clear") {
|
|
175
179
|
await resetCopilotSession();
|
|
176
180
|
return "Session cleared.";
|
|
177
181
|
}
|
|
178
182
|
const fullCmd = args ? `${cmd} ${args}` : cmd;
|
|
179
|
-
return execCopilotPrompt(fullCmd, {
|
|
183
|
+
return execCopilotPrompt(fullCmd, {
|
|
184
|
+
persistent: true,
|
|
185
|
+
cwd: options.cwd,
|
|
186
|
+
sessionId: options.sessionId || null,
|
|
187
|
+
});
|
|
180
188
|
},
|
|
181
189
|
},
|
|
182
190
|
"claude-sdk": {
|
|
@@ -193,14 +201,17 @@ const ADAPTERS = {
|
|
|
193
201
|
return true;
|
|
194
202
|
},
|
|
195
203
|
sdkCommands: ["/compact", "/status", "/model", "/clear"],
|
|
196
|
-
execSdkCommand: async (command, args) => {
|
|
204
|
+
execSdkCommand: async (command, args, options = {}) => {
|
|
197
205
|
const cmd = command.startsWith("/") ? command : `/${command}`;
|
|
198
206
|
if (cmd === "/clear") {
|
|
199
207
|
await resetClaudeSession();
|
|
200
208
|
return "Session cleared.";
|
|
201
209
|
}
|
|
202
210
|
const fullCmd = args ? `${cmd} ${args}` : cmd;
|
|
203
|
-
return execClaudePrompt(fullCmd, {
|
|
211
|
+
return execClaudePrompt(fullCmd, {
|
|
212
|
+
cwd: options.cwd,
|
|
213
|
+
sessionId: options.sessionId || null,
|
|
214
|
+
});
|
|
204
215
|
},
|
|
205
216
|
},
|
|
206
217
|
"gemini-sdk": {
|
|
@@ -218,14 +229,18 @@ const ADAPTERS = {
|
|
|
218
229
|
switchSession: switchGeminiSession,
|
|
219
230
|
createSession: createGeminiSession,
|
|
220
231
|
sdkCommands: ["/status", "/model", "/clear"],
|
|
221
|
-
execSdkCommand: async (command, args) => {
|
|
232
|
+
execSdkCommand: async (command, args, options = {}) => {
|
|
222
233
|
const cmd = command.startsWith("/") ? command : `/${command}`;
|
|
223
234
|
if (cmd === "/clear") {
|
|
224
235
|
await resetGeminiSession();
|
|
225
236
|
return "Session cleared.";
|
|
226
237
|
}
|
|
227
238
|
const fullCmd = args ? `${cmd} ${args}` : cmd;
|
|
228
|
-
return execGeminiPrompt(fullCmd, {
|
|
239
|
+
return execGeminiPrompt(fullCmd, {
|
|
240
|
+
persistent: true,
|
|
241
|
+
cwd: options.cwd,
|
|
242
|
+
sessionId: options.sessionId || null,
|
|
243
|
+
});
|
|
229
244
|
},
|
|
230
245
|
},
|
|
231
246
|
"opencode-sdk": {
|
|
@@ -246,14 +261,18 @@ const ADAPTERS = {
|
|
|
246
261
|
switchSession: switchOpencodeSession,
|
|
247
262
|
createSession: createOpencodeSession,
|
|
248
263
|
sdkCommands: ["/status", "/model", "/sessions", "/clear"],
|
|
249
|
-
execSdkCommand: async (command, args) => {
|
|
264
|
+
execSdkCommand: async (command, args, options = {}) => {
|
|
250
265
|
const cmd = command.startsWith("/") ? command : `/${command}`;
|
|
251
266
|
if (cmd === "/clear") {
|
|
252
267
|
await resetOpencodeSession();
|
|
253
268
|
return "Session cleared.";
|
|
254
269
|
}
|
|
255
270
|
const fullCmd = args ? `${cmd} ${args}` : cmd;
|
|
256
|
-
return execOpencodePrompt(fullCmd, {
|
|
271
|
+
return execOpencodePrompt(fullCmd, {
|
|
272
|
+
persistent: true,
|
|
273
|
+
cwd: options.cwd,
|
|
274
|
+
sessionId: options.sessionId || null,
|
|
275
|
+
});
|
|
257
276
|
},
|
|
258
277
|
},
|
|
259
278
|
};
|
|
@@ -959,9 +978,10 @@ export function getSdkCommands(adapterName) {
|
|
|
959
978
|
* @param {string} command — e.g. "/compact", "/model"
|
|
960
979
|
* @param {string} [args] — optional arguments string
|
|
961
980
|
* @param {string} [adapterName] — target adapter (defaults to active)
|
|
981
|
+
* @param {object} [options] — execution overrides (e.g. cwd/sessionId)
|
|
962
982
|
* @returns {Promise<string|object>}
|
|
963
983
|
*/
|
|
964
|
-
export async function execSdkCommand(command, args = "", adapterName) {
|
|
984
|
+
export async function execSdkCommand(command, args = "", adapterName, options = {}) {
|
|
965
985
|
const adapter = adapterName ? ADAPTERS[adapterName] : activeAdapter;
|
|
966
986
|
if (!adapter) {
|
|
967
987
|
throw new Error(`Unknown adapter: ${adapterName || "(none)"}`);
|
|
@@ -973,5 +993,5 @@ export async function execSdkCommand(command, args = "", adapterName) {
|
|
|
973
993
|
if (typeof adapter.execSdkCommand !== "function") {
|
|
974
994
|
throw new Error(`Adapter ${adapter.name} does not support SDK commands.`);
|
|
975
995
|
}
|
|
976
|
-
return adapter.execSdkCommand(cmd, args);
|
|
996
|
+
return adapter.execSdkCommand(cmd, args, options);
|
|
977
997
|
}
|
package/session-tracker.mjs
CHANGED
|
@@ -236,6 +236,7 @@ export class SessionTracker {
|
|
|
236
236
|
// Direct message format (role/content)
|
|
237
237
|
if (event && event.role && event.content !== undefined) {
|
|
238
238
|
const msg = {
|
|
239
|
+
id: event.id || `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
239
240
|
role: event.role,
|
|
240
241
|
content: String(event.content).slice(0, MAX_MESSAGE_CHARS),
|
|
241
242
|
timestamp: event.timestamp || new Date().toISOString(),
|
|
@@ -577,6 +578,73 @@ export class SessionTracker {
|
|
|
577
578
|
this.#markDirty(sessionId);
|
|
578
579
|
}
|
|
579
580
|
|
|
581
|
+
/**
|
|
582
|
+
* Edit a previously recorded user message in-place.
|
|
583
|
+
* @param {string} sessionId
|
|
584
|
+
* @param {Object} payload
|
|
585
|
+
* @param {string} [payload.messageId]
|
|
586
|
+
* @param {string} [payload.timestamp]
|
|
587
|
+
* @param {string} [payload.previousContent]
|
|
588
|
+
* @param {string} payload.content
|
|
589
|
+
* @returns {{ok:boolean,error?:string,message?:object,index?:number}}
|
|
590
|
+
*/
|
|
591
|
+
editUserMessage(sessionId, payload = {}) {
|
|
592
|
+
const session = this.#sessions.get(sessionId);
|
|
593
|
+
if (!session) return { ok: false, error: "Session not found" };
|
|
594
|
+
|
|
595
|
+
const nextContent = String(payload?.content || "").trim();
|
|
596
|
+
if (!nextContent) return { ok: false, error: "content is required" };
|
|
597
|
+
|
|
598
|
+
const messageId = String(payload?.messageId || "").trim();
|
|
599
|
+
const timestamp = String(payload?.timestamp || "").trim();
|
|
600
|
+
const previousContent = payload?.previousContent != null
|
|
601
|
+
? String(payload.previousContent)
|
|
602
|
+
: "";
|
|
603
|
+
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
604
|
+
|
|
605
|
+
let idx = -1;
|
|
606
|
+
if (messageId) {
|
|
607
|
+
idx = messages.findIndex((msg) => String(msg?.id || "") === messageId);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (idx < 0 && timestamp) {
|
|
611
|
+
idx = messages.findIndex((msg) => {
|
|
612
|
+
if (String(msg?.role || "").toLowerCase() !== "user") return false;
|
|
613
|
+
if (String(msg?.timestamp || "") !== timestamp) return false;
|
|
614
|
+
if (!previousContent) return true;
|
|
615
|
+
return String(msg?.content || "") === previousContent;
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (idx < 0 && previousContent) {
|
|
620
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
621
|
+
const msg = messages[i];
|
|
622
|
+
if (String(msg?.role || "").toLowerCase() !== "user") continue;
|
|
623
|
+
if (String(msg?.content || "") === previousContent) {
|
|
624
|
+
idx = i;
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (idx < 0) return { ok: false, error: "Message not found" };
|
|
631
|
+
|
|
632
|
+
const target = messages[idx];
|
|
633
|
+
if (String(target?.role || "").toLowerCase() !== "user") {
|
|
634
|
+
return { ok: false, error: "Only user messages can be edited" };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
target.id = target.id || `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
638
|
+
target.content = nextContent.slice(0, MAX_MESSAGE_CHARS);
|
|
639
|
+
target.edited = true;
|
|
640
|
+
target.editedAt = new Date().toISOString();
|
|
641
|
+
session.lastActivityAt = Date.now();
|
|
642
|
+
session.lastActiveAt = new Date().toISOString();
|
|
643
|
+
this.#markDirty(sessionId);
|
|
644
|
+
|
|
645
|
+
return { ok: true, message: { ...target }, index: idx };
|
|
646
|
+
}
|
|
647
|
+
|
|
580
648
|
/**
|
|
581
649
|
* Flush all dirty sessions to disk immediately.
|
|
582
650
|
*/
|
package/stream-resilience.mjs
CHANGED
|
@@ -46,9 +46,9 @@ const streamConfig = readInternalExecutorStreamConfig();
|
|
|
46
46
|
export const MAX_STREAM_RETRIES = parseNumericSetting({
|
|
47
47
|
envKey: "INTERNAL_EXECUTOR_STREAM_MAX_RETRIES",
|
|
48
48
|
configValue: streamConfig.maxRetries,
|
|
49
|
-
fallback:
|
|
49
|
+
fallback: 8,
|
|
50
50
|
min: 1,
|
|
51
|
-
max:
|
|
51
|
+
max: 20,
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
/** Base backoff in ms. Doubles per attempt: 2 s → 4 s → 8 s → 16 s → 32 s. */
|
|
@@ -62,7 +62,7 @@ const STREAM_RETRY_BASE_MS = parseNumericSetting({
|
|
|
62
62
|
const STREAM_RETRY_MAX_MS = parseNumericSetting({
|
|
63
63
|
envKey: "INTERNAL_EXECUTOR_STREAM_RETRY_MAX_MS",
|
|
64
64
|
configValue: streamConfig.retryMaxMs,
|
|
65
|
-
fallback:
|
|
65
|
+
fallback: 60_000,
|
|
66
66
|
min: STREAM_RETRY_BASE_MS,
|
|
67
67
|
max: 300_000,
|
|
68
68
|
});
|
|
@@ -111,23 +111,33 @@ export function isTransientStreamError(err) {
|
|
|
111
111
|
msg.includes("service_unavailable") ||
|
|
112
112
|
msg.includes("529") || // Azure overloaded
|
|
113
113
|
msg.includes("rate_limit_exceeded") ||
|
|
114
|
-
msg.includes("overloaded_error") // Anthropic overloaded
|
|
114
|
+
msg.includes("overloaded_error") || // Anthropic overloaded
|
|
115
|
+
// ── Azure / Foundry specific ────────────────────────────────────────────
|
|
116
|
+
msg.includes("reconnecting") ||
|
|
117
|
+
msg.includes("upstream connect error") ||
|
|
118
|
+
msg.includes("no healthy upstream") ||
|
|
119
|
+
msg.includes("gateway timeout") ||
|
|
120
|
+
msg.includes("model is currently overloaded") ||
|
|
121
|
+
msg.includes("the server had an error") ||
|
|
122
|
+
msg.includes("an error occurred during streaming")
|
|
115
123
|
);
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
/**
|
|
119
|
-
* Exponential backoff delay for stream retries, with ±
|
|
127
|
+
* Exponential backoff delay for stream retries, with ±25% jitter.
|
|
120
128
|
*
|
|
121
129
|
* attempt 0 → ~2 s
|
|
122
130
|
* attempt 1 → ~4 s
|
|
123
131
|
* attempt 2 → ~8 s
|
|
124
132
|
* attempt 3 → ~16 s
|
|
125
|
-
* attempt 4 → ~32 s
|
|
133
|
+
* attempt 4 → ~32 s
|
|
134
|
+
* attempt 5+ → ~60 s (capped)
|
|
126
135
|
*
|
|
127
136
|
* @param {number} attempt zero-based retry index
|
|
128
137
|
* @returns {number} delay in milliseconds
|
|
129
138
|
*/
|
|
130
139
|
export function streamRetryDelay(attempt) {
|
|
131
140
|
const base = Math.min(STREAM_RETRY_BASE_MS * 2 ** attempt, STREAM_RETRY_MAX_MS);
|
|
132
|
-
|
|
141
|
+
// ±25% jitter to avoid thundering herd on Azure reconnect
|
|
142
|
+
return base + (Math.random() - 0.5) * 0.5 * base;
|
|
133
143
|
}
|
package/ui/app.js
CHANGED
|
@@ -665,7 +665,7 @@ function SidebarNav({ collapsed = false, onToggle }) {
|
|
|
665
665
|
<button
|
|
666
666
|
key=${tab.id}
|
|
667
667
|
class="sidebar-nav-item ${isActive ? "active" : ""} ${isChild ? "sidebar-nav-child" : ""}"
|
|
668
|
-
style
|
|
668
|
+
style="position:relative"
|
|
669
669
|
aria-label=${tab.label}
|
|
670
670
|
aria-current=${isActive ? "page" : null}
|
|
671
671
|
title=${collapsed ? tab.label : undefined}
|
|
@@ -962,7 +962,7 @@ function InspectorPanel({ onResizeStart, onResizeReset, showResizer }) {
|
|
|
962
962
|
* Bottom Navigation
|
|
963
963
|
* ═══════════════════════════════════════════════ */
|
|
964
964
|
const PRIMARY_NAV_TABS = ["dashboard", "chat", "tasks", "agents"];
|
|
965
|
-
const MORE_NAV_TABS = ["control", "infra", "logs", "library", "workflows", "settings"];
|
|
965
|
+
const MORE_NAV_TABS = ["control", "infra", "logs", "telemetry", "library", "workflows", "settings"];
|
|
966
966
|
|
|
967
967
|
function getTabsById(ids) {
|
|
968
968
|
return ids
|
|
@@ -2068,6 +2068,21 @@ function App() {
|
|
|
2068
2068
|
}
|
|
2069
2069
|
|
|
2070
2070
|
/* ─── Mount ─── */
|
|
2071
|
-
const
|
|
2072
|
-
|
|
2071
|
+
const mountRoot = () => document.getElementById("app");
|
|
2072
|
+
const mountApp = () => {
|
|
2073
|
+
const root = mountRoot();
|
|
2074
|
+
if (!root) return;
|
|
2075
|
+
preactRender(html`<${App} />`, root);
|
|
2076
|
+
};
|
|
2077
|
+
const remountApp = () => {
|
|
2078
|
+
const root = mountRoot();
|
|
2079
|
+
if (!root) return;
|
|
2080
|
+
try {
|
|
2081
|
+
preactRender(null, root);
|
|
2082
|
+
} catch {
|
|
2083
|
+
root.replaceChildren();
|
|
2084
|
+
}
|
|
2085
|
+
preactRender(html`<${App} />`, root);
|
|
2086
|
+
};
|
|
2087
|
+
globalThis.__veRemountApp = remountApp;
|
|
2073
2088
|
mountApp();
|