bosun 0.36.2 → 0.36.4

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.
Files changed (57) hide show
  1. package/agent-prompts.mjs +95 -0
  2. package/analyze-agent-work-helpers.mjs +308 -0
  3. package/analyze-agent-work.mjs +926 -0
  4. package/autofix.mjs +2 -0
  5. package/bosun.schema.json +101 -3
  6. package/codex-shell.mjs +85 -10
  7. package/desktop/main.mjs +871 -48
  8. package/desktop/preload.mjs +54 -1
  9. package/desktop-shortcut.mjs +90 -11
  10. package/git-editor-fix.mjs +273 -0
  11. package/mcp-registry.mjs +579 -0
  12. package/meeting-workflow-service.mjs +631 -0
  13. package/monitor.mjs +18 -103
  14. package/package.json +21 -2
  15. package/primary-agent.mjs +32 -12
  16. package/session-tracker.mjs +68 -0
  17. package/setup-web-server.mjs +20 -10
  18. package/setup.mjs +376 -83
  19. package/startup-service.mjs +51 -6
  20. package/stream-resilience.mjs +17 -7
  21. package/ui/app.js +164 -4
  22. package/ui/components/agent-selector.js +145 -1
  23. package/ui/components/chat-view.js +161 -15
  24. package/ui/components/session-list.js +2 -2
  25. package/ui/components/shared.js +188 -15
  26. package/ui/modules/icons.js +13 -0
  27. package/ui/modules/utils.js +44 -0
  28. package/ui/modules/voice-client-sdk.js +733 -0
  29. package/ui/modules/voice-overlay.js +128 -15
  30. package/ui/modules/voice.js +15 -6
  31. package/ui/setup.html +281 -81
  32. package/ui/styles/components.css +99 -3
  33. package/ui/styles/sessions.css +122 -14
  34. package/ui/styles.css +14 -0
  35. package/ui/tabs/agents.js +1 -1
  36. package/ui/tabs/chat.js +123 -14
  37. package/ui/tabs/control.js +16 -22
  38. package/ui/tabs/dashboard.js +85 -8
  39. package/ui/tabs/library.js +113 -17
  40. package/ui/tabs/settings.js +116 -2
  41. package/ui/tabs/tasks.js +388 -39
  42. package/ui/tabs/telemetry.js +0 -1
  43. package/ui/tabs/workflows.js +4 -0
  44. package/ui-server.mjs +400 -22
  45. package/update-check.mjs +41 -13
  46. package/voice-action-dispatcher.mjs +844 -0
  47. package/voice-agents-sdk.mjs +664 -0
  48. package/voice-auth-manager.mjs +164 -0
  49. package/voice-relay.mjs +1194 -0
  50. package/voice-tools.mjs +914 -0
  51. package/workflow-templates/agents.mjs +6 -2
  52. package/workflow-templates/github.mjs +154 -12
  53. package/workflow-templates.mjs +3 -0
  54. package/github-reconciler.mjs +0 -506
  55. package/merge-strategy.mjs +0 -1210
  56. package/pr-cleanup-daemon.mjs +0 -992
  57. 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
- import { PRCleanupDaemon } from "./pr-cleanup-daemon.mjs";
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
- import {
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
- import {
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 && failedRepoCount >= repoCount) {
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 (${failedRepoCount}/${repoCount})`,
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: Codex-powered merge decision analysis
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: () => prCleanupDaemon,
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 Cleanup Daemon ──────────────────────────────────────────────────
16283
- // Automatically resolves PR conflicts and CI failures every 30 minutes
16284
- if (config.prCleanupEnabled !== false) {
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.2",
3
+ "version": "0.36.4",
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 -e \"const fs=require('fs'),path=require('path');const files=fs.readdirSync('.').filter(f=>f.endsWith('.mjs'));let fail=0;for(const f of files){try{require('child_process').execSync('node --check '+f,{stdio:'pipe'});}catch(e){console.error('Syntax error: '+f);console.error(e.stderr.toString());fail=1;}}if(fail)process.exit(1);console.log('Syntax OK: '+files.length+' files checked');\"",
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,11 @@
213
222
  "vibe-kanban-wrapper.mjs",
214
223
  "vk-error-resolver.mjs",
215
224
  "vk-log-stream.mjs",
225
+ "voice-auth-manager.mjs",
226
+ "voice-action-dispatcher.mjs",
227
+ "voice-agents-sdk.mjs",
228
+ "voice-relay.mjs",
229
+ "voice-tools.mjs",
216
230
  "workspace-manager.mjs",
217
231
  "workspace-monitor.mjs",
218
232
  "workspace-reaper.mjs",
@@ -247,12 +261,17 @@
247
261
  "@anthropic-ai/claude-agent-sdk": "latest",
248
262
  "@github/copilot-sdk": "latest",
249
263
  "@google/genai": "latest",
264
+ "@modelcontextprotocol/sdk": "^1.26.0",
265
+ "@openai/agents": "^0.5.2",
250
266
  "@openai/codex-sdk": "latest",
251
267
  "@opencode-ai/sdk": "latest",
252
268
  "@preact/signals": "1.3.1",
253
269
  "@whiskeysockets/baileys": "^7.0.0-rc.9",
254
270
  "ajv": "^8.18.0",
255
271
  "es-module-shims": "1.10.0",
272
+ "express": "^5.1.0",
273
+ "express-rate-limit": "^8.0.0",
274
+ "hono": "^4.7.0",
256
275
  "htm": "3.1.1",
257
276
  "preact": "10.25.4",
258
277
  "qrcode-terminal": "^0.12.0",
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, { persistent: true });
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, { persistent: true });
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, { persistent: true });
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, { persistent: true });
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
  }
@@ -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
  */
@@ -436,7 +436,7 @@ function buildStableSetupDefaults({
436
436
  workflowMaxConcurrentBranches: 8,
437
437
  voiceEnabled: true,
438
438
  voiceProvider: "auto",
439
- voiceModel: "gpt-4o-realtime-preview-2024-12-17",
439
+ voiceModel: "gpt-realtime-1.5",
440
440
  voiceVisionModel: "gpt-4.1-mini",
441
441
  voiceId: "alloy",
442
442
  voiceTurnDetection: "server_vad",
@@ -445,7 +445,7 @@ function buildStableSetupDefaults({
445
445
  openaiRealtimeApiKey: "",
446
446
  azureOpenaiRealtimeEndpoint: "",
447
447
  azureOpenaiRealtimeApiKey: "",
448
- azureOpenaiRealtimeDeployment: "gpt-4o-realtime-preview",
448
+ azureOpenaiRealtimeDeployment: "gpt-realtime-1.5",
449
449
  copilotEnableAllMcpTools: false,
450
450
  // Backward-compatible fields consumed by older setup UI revisions.
451
451
  distribution: "primary-only",
@@ -545,17 +545,26 @@ function applyTelegramMiniAppSetupEnv(envMap, env, sourceEnv = process.env) {
545
545
  const tunnelRaw =
546
546
  env?.telegramUiTunnel ||
547
547
  env?.TELEGRAM_UI_TUNNEL ||
548
- sourceEnv.TELEGRAM_UI_TUNNEL ||
549
- "named";
550
- envMap.TELEGRAM_UI_TUNNEL = String(tunnelRaw).trim() || "named";
548
+ sourceEnv.TELEGRAM_UI_TUNNEL;
549
+ // Default to "quick" when no named-tunnel credentials are configured so the
550
+ // UI starts successfully out-of-the-box without --setup.
551
+ const hasNamedCreds = !!(
552
+ (env?.CLOUDFLARE_TUNNEL_NAME || sourceEnv.CLOUDFLARE_TUNNEL_NAME) &&
553
+ (env?.CLOUDFLARE_TUNNEL_CREDENTIALS || sourceEnv.CLOUDFLARE_TUNNEL_CREDENTIALS)
554
+ );
555
+ const tunnelDefault = hasNamedCreds ? "named" : "quick";
556
+ envMap.TELEGRAM_UI_TUNNEL = String(tunnelRaw || tunnelDefault).trim() || tunnelDefault;
551
557
 
552
558
  const quickFallbackRaw =
553
559
  env?.telegramUiAllowQuickTunnelFallback ??
554
560
  env?.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK ??
555
561
  sourceEnv.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK;
562
+ // Default true so named-tunnel failures fall back gracefully rather than
563
+ // silently killing the Web UI. --setup sets this to false when credentials
564
+ // are provided and the tunnel is known-good.
556
565
  envMap.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK = toBooleanEnvString(
557
566
  quickFallbackRaw,
558
- false,
567
+ true,
559
568
  );
560
569
 
561
570
  const fallbackAuthRaw =
@@ -855,8 +864,8 @@ function applyNonBlockingSetupEnvDefaults(envMap, env = {}, sourceEnv = process.
855
864
  env.VOICE_MODEL,
856
865
  envMap.VOICE_MODEL,
857
866
  sourceEnv.VOICE_MODEL,
858
- ) || "gpt-4o-realtime-preview-2024-12-17",
859
- ).trim() || "gpt-4o-realtime-preview-2024-12-17";
867
+ ) || "gpt-realtime-1.5",
868
+ ).trim() || "gpt-realtime-1.5";
860
869
  envMap.VOICE_VISION_MODEL = String(
861
870
  pickNonEmptyValue(
862
871
  env.voiceVisionModel,
@@ -944,8 +953,8 @@ function applyNonBlockingSetupEnvDefaults(envMap, env = {}, sourceEnv = process.
944
953
  env.AZURE_OPENAI_REALTIME_DEPLOYMENT,
945
954
  envMap.AZURE_OPENAI_REALTIME_DEPLOYMENT,
946
955
  sourceEnv.AZURE_OPENAI_REALTIME_DEPLOYMENT,
947
- ) || "gpt-4o-realtime-preview",
948
- ).trim() || "gpt-4o-realtime-preview";
956
+ ) || "gpt-realtime-1.5",
957
+ ).trim() || "gpt-realtime-1.5";
949
958
 
950
959
  envMap.CONTAINER_ENABLED = toBooleanEnvString(
951
960
  pickNonEmptyValue(env.containerEnabled, envMap.CONTAINER_ENABLED, sourceEnv.CONTAINER_ENABLED),
@@ -1734,6 +1743,7 @@ function handleApply(body) {
1734
1743
  if (configJson.projectRequirementsProfile) config.projectRequirementsProfile = configJson.projectRequirementsProfile;
1735
1744
  if (configJson.internalReplenish) config.internalReplenish = configJson.internalReplenish;
1736
1745
  if (configJson.kanban) config.kanban = configJson.kanban;
1746
+ if (configJson.voice && typeof configJson.voice === "object") config.voice = configJson.voice;
1737
1747
 
1738
1748
  const workspaceConfig = resolveSetupWorkspaceAndRepoConfig(
1739
1749
  existingConfig,