diff --git a/.planning/2026-08-21-disable-task-board/findings.md b/.planning/2026-08-21-disable-task-board/findings.md new file mode 100644 index 0000000..d0c5f78 --- /dev/null +++ b/.planning/2026-08-21-disable-task-board/findings.md @@ -0,0 +1,6 @@ +# 临时禁用任务看板发现 + +- 页面入口位于 `public/index.html` 的 `task-board-open`,当前会话跟踪入口为 `task-tracking-control`。 +- 服务端自动状态链路有两层:`dispatchTaskBoardLifecycle()` 与 `enqueueTaskStatusClassification()`。 +- 任务 MCP 动态定义由 `taskBoardMcpToolDefinitionsForSession()` 返回;功能关闭时应返回空数组。 +- 最小改法是使用显式常量门禁,不删除实现,便于重构后重新开放。 diff --git a/.planning/2026-08-21-disable-task-board/progress.md b/.planning/2026-08-21-disable-task-board/progress.md new file mode 100644 index 0000000..2a49587 --- /dev/null +++ b/.planning/2026-08-21-disable-task-board/progress.md @@ -0,0 +1,9 @@ +# 临时禁用任务看板进度 + +- 2026-08-21:codebase-memory 索引 ready;已定位页面入口、任务跟踪控件、生命周期 Hook、分类 Hook 和任务 MCP 定义入口。 +- 2026-08-21:计划审查补充了关闭态真实 MCP、旧 WebSocket 客户端、Hook no-op、历史数据不变和重启失败处理的验收标准。 +- 2026-08-21:默认关闭门禁已覆盖页面入口、跟踪控件、任务 MCP、WebSocket、生命周期与自动分类;关闭态专项真实跑完一轮 Codex App 消息后历史 taskTracking 完全不变,退出码 0。 +- 2026-08-21:语法、service、lifecycle、integration 均退出 0;完整回归首次因旧 usage feature 静态字面量断言退出 1,已做最小契约修正。 +- 2026-08-21:完整回归修正后退出 0,`git diff --check` 退出 0;进入重启会话门禁检查。 +- 2026-08-21:重启门禁确认只有当前对话运行;`ccweb` 已重启并在线,PM2 环境确认 `CC_WEB_TASK_BOARD_ENABLED=0`。 +- 2026-08-21:CentOS 7 baseline 单文件包重新构建成功;六项语法检查、MCP initialize 和 tar 内容校验均退出 0,准备提交并推送全部修改。 diff --git a/.planning/2026-08-21-disable-task-board/task_plan.md b/.planning/2026-08-21-disable-task-board/task_plan.md new file mode 100644 index 0000000..4d90dff --- /dev/null +++ b/.planning/2026-08-21-disable-task-board/task_plan.md @@ -0,0 +1,36 @@ +# 临时禁用任务看板 + +## 目标 + +在任务看板重构期间,通过明确的功能门禁禁用页面入口、任务跟踪控件、任务 MCP 暴露和服务端自动 Hook;保留现有实现与数据。完成后重启 ccweb、重新构建 baseline 发布包并推送。 + +## 阶段 + +- [x] 1. 定位并固定页面入口与 Hook 门禁点 +- [x] 2. 写入临时功能门禁与回归契约 +- [x] 3. 运行最小验证 +- [x] 4. 检查会话门禁并重启 ccweb +- [x] 5. 重新构建并验证 CentOS 7 baseline 包 +- [x] 6. 提交全部修改并推送 origin/main + +## 约束 + +- 不删除看板实现、历史状态或配置。 +- 页面不显示看板按钮与加入看板控件。 +- 服务端不执行任务生命周期和自动分类 Hook。 +- 模型不再看到任务状态 MCP 工具。 +- 重启前必须确认除当前对话外没有 running 会话。 + +## 完成标准 + +- 默认页面不显示任务看板按钮,当前会话不显示加入/移出看板控件。 +- 默认真实 MCP `tools/list` 不包含任何 `ccweb_task_*`;过期客户端直接调用更新也被拒绝。 +- 默认 WebSocket 看板查询与写入被拒绝,新建会话不能通过遗留参数加入看板。 +- 用户消息、轮次完成和运行事件不会进入生命周期或自动分类 Hook。 +- 隔离测试前后既有 session 的 `taskTracking` 内容与版本不变。 +- 若重启门禁不满足则停止在重启前;满足后执行 `pm2 restart ccweb --update-env`。 +- 发布包固定为 `dist-exe/cc-web-bun-linux-x64-baseline.tar.gz`,完成语法、MCP initialize、tar 校验后提交并推送 `origin/main`。 + +## 错误记录 + +- 完整回归首次退出 1:认证 feature 对象新增 `taskBoard` 后,usage statistics 旧静态断言仍匹配单行旧字面量;已更新为分别检查两个 feature 字段。 diff --git a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz index 5ba1177..65dbe6e 100644 Binary files a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz and b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz differ diff --git a/public/app.js b/public/app.js index 43078d1..d0cc6ca 100644 --- a/public/app.js +++ b/public/app.js @@ -305,6 +305,7 @@ let usageDashboardRequestSeq = 0; const taskBoardViewState = { open: false, + enabled: false, savedFocus: null, }; let taskBoardController = null; @@ -4587,6 +4588,16 @@ } } + function setTaskBoardFeatureEnabled(enabled) { + taskBoardViewState.enabled = enabled === true; + if (taskBoardOpen) taskBoardOpen.hidden = !taskBoardViewState.enabled; + if (taskTrackingControl) taskTrackingControl.hidden = true; + if (!taskBoardViewState.enabled) { + if (taskBoardViewState.open) closeTaskBoard({ restoreFocus: false }); + taskTrackingController?.setState({ sessionId: '', disabled: true }); + } + } + function setUsageDashboardState(state, message = '') { const loading = state === 'loading'; usageDashboardState.loading = loading; @@ -5417,7 +5428,7 @@ } function openTaskBoard() { - if (!taskBoardPanel || !taskBoardRoot || !window.CcwebTaskBoard) return; + if (!taskBoardViewState.enabled || !taskBoardPanel || !taskBoardRoot || !window.CcwebTaskBoard) return; if (usageDashboardState.open) closeUsageDashboard({ restoreFocus: false }); if (advancedSessionSearchState.open) closeAdvancedSessionSearch({ restoreFocus: false }); taskBoardViewState.savedFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null; @@ -5444,6 +5455,11 @@ function syncTaskTrackingControl(snapshot = null) { if (!taskTrackingControl || !window.CcwebTaskBoard) return; + if (!taskBoardViewState.enabled) { + taskTrackingControl.hidden = true; + taskTrackingController?.setState({ sessionId: '', disabled: true }); + return; + } const sessionId = snapshot?.sessionId || currentSessionId || ''; const tracking = snapshot?.taskTracking || {}; if (!sessionId) { @@ -6969,6 +6985,7 @@ loginOverlay.hidden = true; app.hidden = false; setUsageDashboardFeatureEnabled(msg.features?.usageStatistics === true); + setTaskBoardFeatureEnabled(msg.features?.taskBoard === true); const flushedSessionSwitch = flushPendingSessionSwitch(); const flushedSessionResume = flushedSessionSwitch ? false : flushPendingSessionResume(); if (!flushedSessionSwitch && @@ -6996,6 +7013,7 @@ authToken = null; wsAuthenticated = false; setUsageDashboardFeatureEnabled(false); + setTaskBoardFeatureEnabled(false); localStorage.removeItem('cc-web-token'); document.dispatchEvent(new CustomEvent('cc-web-auth-failed')); loginOverlay.hidden = false; @@ -11515,6 +11533,8 @@ setUsageDashboardFeatureEnabled(false); } + setTaskBoardFeatureEnabled(false); + if (taskBoardOpen && taskBoardPanel && taskBoardRoot) { taskBoardOpen.addEventListener('click', openTaskBoard); taskBoardClose?.addEventListener('click', () => closeTaskBoard()); diff --git a/public/index.html b/public/index.html index 3a23a18..bdec15d 100644 --- a/public/index.html +++ b/public/index.html @@ -75,7 +75,7 @@ - diff --git a/scripts/regression.js b/scripts/regression.js index d499b6d..a2acdfa 100644 --- a/scripts/regression.js +++ b/scripts/regression.js @@ -813,6 +813,13 @@ function assertTaskBoardIntegrationContract() { /@media \(prefers-reduced-motion: reduce\)[\s\S]*?\.ccweb-task-board__card\[data-runtime-state='running'\]::after\s*\{[^}]*animation:\s*none;/.test(taskBoardStyleSource), 'Wasteland edge tracer should retain a static reduced-motion treatment' ); + assert( + serverSource.includes("const TASK_BOARD_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.CC_WEB_TASK_BOARD_ENABLED || ''));"), + 'Task board must have an explicit opt-in runtime gate during refactor' + ); + assert(indexSource.includes('id="task-board-open"') && /id="task-board-open"[^>]*hidden/.test(indexSource), 'Task board page entry should be hidden by default'); + assert(frontendSource.includes('setTaskBoardFeatureEnabled(msg.features?.taskBoard === true)'), 'Frontend task board entry should follow the server feature gate'); + assert(serverSource.includes("return mcpToolError('task_board_disabled'"), 'Direct task update calls must fail closed while disabled'); } function assertFrontendGenerationControlsContract() { @@ -4243,6 +4250,7 @@ async function runCodexAppRuntimeImageSteerRegression() { CC_WEB_CONFIG_DIR: configDir, CC_WEB_SESSIONS_DIR: sessionsDir, CC_WEB_LOGS_DIR: logsDir, + CC_WEB_TASK_BOARD_ENABLED: '1', HOME: homeDir, CLAUDE_PATH: MOCK_CLAUDE, CODEX_PATH: MOCK_CODEX_APP_SERVER, @@ -4377,6 +4385,7 @@ async function runCodexAppStaleRunningRegression(options = {}) { CC_WEB_CONFIG_DIR: configDir, CC_WEB_SESSIONS_DIR: sessionsDir, CC_WEB_LOGS_DIR: logsDir, + CC_WEB_TASK_BOARD_ENABLED: '1', HOME: homeDir, CLAUDE_PATH: MOCK_CLAUDE, CODEX_PATH: MOCK_CODEX_APP_SERVER, @@ -5370,7 +5379,8 @@ function assertUsageStatisticsContract() { assert(serverSource.includes("case 'usage_stats_query':") && serverSource.includes("type: 'usage_stats_result'") && serverSource.includes("type: 'usage_stats_error'") - && serverSource.includes('features: { usageStatistics: USAGE_STATISTICS_ENABLED }'), + && serverSource.includes('usageStatistics: USAGE_STATISTICS_ENABLED') + && serverSource.includes('taskBoard: TASK_BOARD_ENABLED'), 'Server should expose the versioned usage query protocol and authenticated feature flag'); assert(!queryHandler.includes('sendSessionList') && !queryHandler.includes('broadcastSessionList'), 'Usage queries must not refresh or replace session_list'); @@ -6325,6 +6335,7 @@ async function main() { CC_WEB_CONFIG_DIR: configDir, CC_WEB_SESSIONS_DIR: sessionsDir, CC_WEB_LOGS_DIR: logsDir, + CC_WEB_TASK_BOARD_ENABLED: '1', HOME: homeDir, CLAUDE_PATH: MOCK_CLAUDE, CODEX_PATH: MOCK_CODEX_APP_SERVER, diff --git a/scripts/task-board-disabled-unit.js b/scripts/task-board-disabled-unit.js new file mode 100644 index 0000000..bd63db9 --- /dev/null +++ b/scripts/task-board-disabled-unit.js @@ -0,0 +1,194 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const http = require('node:http'); +const net = require('node:net'); +const os = require('node:os'); +const path = require('node:path'); +const { spawn } = require('node:child_process'); +const WebSocket = require('ws'); + +const ROOT = path.resolve(__dirname, '..'); +const SERVER = path.join(ROOT, 'server.js'); + +function freePort() { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.once('error', reject); + server.listen(0, '127.0.0.1', () => { + const port = server.address().port; + server.close(() => resolve(port)); + }); + }); +} + +function waitForPort(port) { + const started = Date.now(); + return new Promise((resolve, reject) => { + const check = () => { + const socket = net.createConnection({ host: '127.0.0.1', port }); + socket.once('connect', () => { socket.destroy(); resolve(); }); + socket.once('error', () => { + if (Date.now() - started > 8_000) reject(new Error('等待关闭态测试服务超时')); + else setTimeout(check, 40); + }); + }; + check(); + }); +} + +function callMcp(port, token, sourceSessionId, method, params = {}) { + return new Promise((resolve, reject) => { + const body = JSON.stringify({ jsonrpc: '2.0', id: `disabled-${Date.now()}`, method, params }); + const request = http.request({ + host: '127.0.0.1', + port, + path: `/api/internal/mcp/stream?sourceSessionId=${encodeURIComponent(sourceSessionId)}`, + method: 'POST', + headers: { + 'content-type': 'application/json', + 'content-length': Buffer.byteLength(body), + 'x-cc-web-mcp-token': token, + }, + }, (response) => { + let text = ''; + response.setEncoding('utf8'); + response.on('data', (chunk) => { text += chunk; }); + response.on('end', () => resolve({ status: response.statusCode, body: JSON.parse(text || '{}') })); + }); + request.once('error', reject); + request.end(body); + }); +} + +function connect(port, password) { + return new Promise((resolve, reject) => { + const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`); + const messages = []; + ws.on('open', () => ws.send(JSON.stringify({ type: 'auth', password }))); + ws.on('message', (raw) => { + const message = JSON.parse(String(raw)); + messages.push(message); + if (message.type === 'auth_result') { + if (message.success) resolve({ ws, messages, auth: message }); + else reject(new Error('关闭态测试认证失败')); + } + }); + ws.once('error', reject); + }); +} + +function next(messages, predicate) { + return new Promise((resolve, reject) => { + const started = Date.now(); + const timer = setInterval(() => { + const index = messages.findIndex(predicate); + if (index >= 0) { + clearInterval(timer); + resolve(messages.splice(index, 1)[0]); + } else if (Date.now() - started > 5_000) { + clearInterval(timer); + reject(new Error(`关闭态 WS 响应超时: ${messages.map((item) => item.type).join(',')}`)); + } + }, 25); + }); +} + +async function main() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-task-board-disabled-')); + const configDir = path.join(root, 'config'); + const sessionsDir = path.join(root, 'sessions'); + const logsDir = path.join(root, 'logs'); + [configDir, sessionsDir, logsDir].forEach((dir) => fs.mkdirSync(dir, { recursive: true })); + const sessionId = 'disabled-existing-task'; + const sessionPath = path.join(sessionsDir, `${sessionId}.json`); + const before = { + id: sessionId, + title: '重构前历史任务', + agent: 'codexapp', + cwd: root, + created: '2026-08-21T00:00:00.000Z', + updated: '2026-08-21T00:00:00.000Z', + messages: [], + taskTracking: { + enabled: true, + statusId: 'in_progress', + source: 'user', + version: 7, + archivedAt: '2026-08-20T23:59:00.000Z', + archivedBy: 'user', + }, + }; + fs.writeFileSync(sessionPath, JSON.stringify(before, null, 2)); + const port = await freePort(); + const password = 'Disabled!234'; + const token = 'DisabledMcp!234'; + const child = spawn(process.execPath, [SERVER], { + cwd: ROOT, + env: { + ...process.env, + PORT: String(port), + CC_WEB_PASSWORD: password, + CC_WEB_INTERNAL_MCP_TOKEN: token, + CC_WEB_CONFIG_DIR: configDir, + CC_WEB_SESSIONS_DIR: sessionsDir, + CC_WEB_LOGS_DIR: logsDir, + CC_WEB_TASK_BOARD_ENABLED: '0', + HOME: root, + CODEX_PATH: path.join(ROOT, 'scripts', 'mock-codex-app-server.js'), + }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + try { + await waitForPort(port); + const html = await (await fetch(`http://127.0.0.1:${port}/`)).text(); + assert.match(html, /id="task-board-open"[^>]*hidden/); + assert.match(html, /id="task-tracking-control"[^>]*hidden/); + + const tools = await callMcp(port, token, sessionId, 'tools/list'); + assert.equal(tools.status, 200); + assert.deepEqual( + (tools.body.result?.tools || []).map((tool) => tool.name).filter((name) => name.startsWith('ccweb_task_')), + [], + ); + const update = await callMcp(port, token, sessionId, 'tools/call', { + name: 'ccweb_task_update', + arguments: { statusId: 'completed' }, + }); + assert.equal(update.body.result?.structuredContent?.code, 'task_board_disabled'); + + const client = await connect(port, password); + client.ws.send(JSON.stringify({ type: 'task_board_query', requestId: 'disabled-query' })); + const query = await next(client.messages, (message) => message.type === 'task_board_result' && message.requestId === 'disabled-query'); + assert.equal(query.code, 'task_board_disabled'); + client.ws.send(JSON.stringify({ type: 'new_session', requestId: 'disabled-new', taskTrackingEnabled: true })); + const created = await next(client.messages, (message) => message.type === 'session_info' && message.requestId === 'disabled-new'); + assert.equal(created.taskTracking.enabled, false); + + client.ws.send(JSON.stringify({ + type: 'message', + text: '关闭态普通轮次不得触发任务状态 Hook', + sessionId, + mode: 'yolo', + agent: 'codexapp', + })); + await next(client.messages, (message) => message.type === 'done' && message.sessionId === sessionId); + await new Promise((resolve) => setTimeout(resolve, 100)); + client.ws.close(); + + assert.deepEqual(JSON.parse(fs.readFileSync(sessionPath, 'utf8')).taskTracking, before.taskTracking); + assert.equal(client.auth.features.taskBoard, false); + console.log('Task board disabled checks passed.'); + } finally { + if (child.exitCode === null && child.signalCode === null) { + child.kill('SIGTERM'); + await new Promise((resolve) => child.once('exit', resolve)); + } + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/task-board-integration.js b/scripts/task-board-integration.js index 3a29522..1367f5e 100644 --- a/scripts/task-board-integration.js +++ b/scripts/task-board-integration.js @@ -344,6 +344,7 @@ async function main() { CC_WEB_CONFIG_DIR: configDir, CC_WEB_SESSIONS_DIR: sessionsDir, CC_WEB_LOGS_DIR: logsDir, + CC_WEB_TASK_BOARD_ENABLED: '1', HOME: homeDir, CLAUDE_PATH: MOCK_CLAUDE, CODEX_PATH: MOCK_CODEX_APP_SERVER, diff --git a/server.js b/server.js index 4fd4bee..2fadb50 100644 --- a/server.js +++ b/server.js @@ -173,6 +173,7 @@ const CODEX_APP_CCWEB_MCP_TRANSPORT = normalizeCodexAppCcwebMcpTransport(process const CODEX_APP_WORKER_DISABLED = /^(0|false|no|off)$/i.test(String(process.env.CC_WEB_CODEX_APP_WORKER || '')); const CODEX_APP_WORKER_ENABLED = !CODEX_APP_WORKER_DISABLED; const USAGE_STATISTICS_ENABLED = !/^(0|false|no|off)$/i.test(String(process.env.CC_WEB_USAGE_STATISTICS || '')); +const TASK_BOARD_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.CC_WEB_TASK_BOARD_ENABLED || '')); const CODEX_APP_PROCESS_ENV_STRIP_KEYS = [ 'CC_WEB_MCP_URL', 'CC_WEB_MCP_TOKEN', @@ -2780,6 +2781,7 @@ function listComposerMcpItems(options = {}) { const seen = new Set(); const push = (item) => { if (!item?.server && !item?.name) return; + if (!TASK_BOARD_ENABLED && String(item.name || '').startsWith('ccweb_task_')) return; const key = `${item.kind || ''}:${item.server || ''}:${item.name || item.label || item.insertion || ''}`; if (seen.has(key)) return; seen.add(key); @@ -4574,7 +4576,7 @@ function createProductionTaskBoardService() { taskBoardService = createProductionTaskBoardService(); taskBoardMcpHandlers = createTaskBoardMcpHandlers(taskBoardService); -taskBoardLifecycle = createTaskBoardLifecycle(taskBoardService, { +taskBoardLifecycle = TASK_BOARD_ENABLED ? createTaskBoardLifecycle(taskBoardService, { isTrackingEnabled(sessionId) { try { return taskBoardService.getTask(sessionId).taskTracking.enabled; @@ -4585,8 +4587,8 @@ taskBoardLifecycle = createTaskBoardLifecycle(taskBoardService, { logger(level, event, data) { plog(String(level || '').toUpperCase() === 'ERROR' ? 'ERROR' : 'WARN', event, data); }, -}); -taskStatusClassifier = createTaskStatusClassifier({ +}) : null; +taskStatusClassifier = TASK_BOARD_ENABLED ? createTaskStatusClassifier({ taskBoardService, loadSession, resolveRuntime: resolveTaskStatusClassifierRuntime, @@ -4600,10 +4602,10 @@ taskStatusClassifier = createTaskStatusClassifier({ }); broadcastSessionList(); }, -}); +}) : null; function enqueueTaskStatusClassification(sessionId, event = {}) { - if (!taskStatusClassifier || !sessionId) return Promise.resolve(null); + if (!TASK_BOARD_ENABLED || !taskStatusClassifier || !sessionId) return Promise.resolve(null); return taskStatusClassifier.enqueue(sessionId, event).catch((error) => { plog('WARN', 'task_status_classification_enqueue_failed', { sessionId: String(sessionId).slice(0, 8), @@ -4615,7 +4617,7 @@ function enqueueTaskStatusClassification(sessionId, event = {}) { } function taskBoardMcpToolDefinitionsForSession(sessionId) { - if (!taskBoardService || !sessionId) return []; + if (!TASK_BOARD_ENABLED || !taskBoardService || !sessionId) return []; try { return createTaskBoardMcpToolDefinitions(taskBoardService, { sourceSessionId: sanitizeId(sessionId), @@ -5320,7 +5322,7 @@ function broadcastTaskBoardEvent(eventName, subject = null, extra = {}) { } function dispatchTaskBoardLifecycle(sessionId, event = {}) { - if (!taskBoardLifecycle || !sessionId || event.trackingEnabled === false) return Promise.resolve(null); + if (!TASK_BOARD_ENABLED || !taskBoardLifecycle || !sessionId || event.trackingEnabled === false) return Promise.resolve(null); return taskBoardLifecycle.handleEvent(sessionId, event).then((result) => { if (result?.changed && result.task) { broadcastTaskBoardEvent(`lifecycle:${event.type || result.eventType || 'updated'}`, result.task, { @@ -5349,6 +5351,14 @@ function sendTaskBoardWsResult(ws, request, payload = {}) { } function handleTaskBoardWsMessage(ws, message = {}) { + if (!TASK_BOARD_ENABLED) { + sendTaskBoardWsResult(ws, message, { + ok: false, + code: 'task_board_disabled', + message: '任务看板正在重构,暂不可用。', + }); + return; + } const actor = { source: 'user', id: 'ccweb-ui' }; try { switch (message.type) { @@ -6577,6 +6587,9 @@ function callInternalMcpTool(tool, args, sourceSessionId, sourceHopCount) { case 'ccweb_prompt_user': return createCcwebPromptUser(args, sourceSessionId); case 'ccweb_task_update': { + if (!TASK_BOARD_ENABLED) { + return mcpToolError('task_board_disabled', '任务看板正在重构,暂不可用。'); + } const result = taskBoardMcpHandlers.ccweb_task_update(args, { sourceSessionId, actorId: `mcp:${String(sourceSessionId || '').slice(0, 32)}`, @@ -7717,7 +7730,10 @@ wss.on('connection', (ws, req) => { token: authToken, mustChangePassword: !!authConfig.mustChange, frontendAssetVersion: computeFrontendAssetVersion(), - features: { usageStatistics: USAGE_STATISTICS_ENABLED }, + features: { + usageStatistics: USAGE_STATISTICS_ENABLED, + taskBoard: TASK_BOARD_ENABLED, + }, }); sendSessionList(ws); } else { @@ -9232,7 +9248,7 @@ function handleNewSession(ws, msg) { }); } let { session } = result; - if (request.taskTrackingEnabled === true) { + if (TASK_BOARD_ENABLED && request.taskTrackingEnabled === true) { try { const trackedTask = taskBoardService.setTracking(session.id, true, { source: 'user',