chatccc 0.2.266 → 0.2.268
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/README.md +9 -3
- package/dist/src/agent-delegate-task-rpc.js +26 -15
- package/dist/src/agent-team/application/main-agent-service.js +11 -2
- package/dist/src/agent-team/application/task-execution-service.js +34 -5
- package/dist/src/agent-team/domain/task-run.js +4 -0
- package/dist/src/agent-team/http/board-routes.js +6 -0
- package/dist/src/agent-team/infrastructure/task-execution-runtime.js +4 -0
- package/dist/src/agent-team/web/agent-team-page.js +34 -20
- package/dist/src/cards.js +3 -0
- package/dist/src/engines/engine-manager.js +7 -0
- package/dist/src/execution-transcript.js +110 -0
- package/dist/src/index.js +6 -2
- package/dist/src/orchestrator.js +192 -4
- package/dist/src/safe-maintenance.js +271 -0
- package/dist/src/session-chat-binding.js +12 -0
- package/dist/src/session.js +10 -0
- package/dist/src/stream-state.js +1 -0
- package/dist/src/web-ui.js +5 -0
- package/package.json +1 -1
package/dist/src/index.js
CHANGED
|
@@ -47,7 +47,7 @@ import { loadSessionRegistryForBinding, rebuildBindingsFromRegistry, resetState,
|
|
|
47
47
|
import { startChromeDevtoolsGuard, stopChromeDevtoolsGuard } from "./chrome-devtools-guard.js";
|
|
48
48
|
import { rebuildSessionChatsFromRegistry, setQueueConsumer, } from "./session-chat-binding.js";
|
|
49
49
|
import { fixStaleStreamStates } from "./stream-state.js";
|
|
50
|
-
import { handleCommand } from "./orchestrator.js";
|
|
50
|
+
import { configureSafeMaintenanceRuntime, handleCommand, recoverSafeMaintenanceAfterStartup, } from "./orchestrator.js";
|
|
51
51
|
import { createWechatAdapter, startWechatPlatform } from "./wechat-platform.js";
|
|
52
52
|
import { handleCodexResetCardAction } from "./codex-reset-actions.js";
|
|
53
53
|
import { resolveFeishuCardActionChatType } from "./card-action-routing.js";
|
|
@@ -103,11 +103,12 @@ function createFeishuAdapter() {
|
|
|
103
103
|
}
|
|
104
104
|
const feishuPlatform = createFeishuAdapter();
|
|
105
105
|
const wechatPlatform = createWechatAdapter();
|
|
106
|
+
configureSafeMaintenanceRuntime([feishuPlatform, wechatPlatform]);
|
|
106
107
|
setSessionPlatform(feishuPlatform);
|
|
107
108
|
configureAgentTeamMainAgent(feishuPlatform);
|
|
108
109
|
// 注册队列消费回调:session 生成完成后自动处理缓存消息
|
|
109
110
|
setQueueConsumer((platform, msg) => {
|
|
110
|
-
handleCommand(platform, msg.text, msg.chatId, msg.openId, msg.msgTimestamp, msg.chatType, msg.traceId).catch(err => console.error(`[${ts()}] Queue consume failed: ${err.message}`));
|
|
111
|
+
handleCommand(platform, msg.text, msg.chatId, msg.openId, msg.msgTimestamp, msg.chatType, msg.traceId, undefined, true).catch(err => console.error(`[${ts()}] Queue consume failed: ${err.message}`));
|
|
111
112
|
});
|
|
112
113
|
function getInnerEvent(data) {
|
|
113
114
|
return (data.event ?? data);
|
|
@@ -596,6 +597,7 @@ async function main() {
|
|
|
596
597
|
appendStartupTrace(opened ? "web-ui: opening simulate browser" : "web-ui: simulate browser unavailable", { url });
|
|
597
598
|
}
|
|
598
599
|
installShutdownHandlers(simServer, serviceLifecycle);
|
|
600
|
+
await recoverSafeMaintenanceAfterStartup();
|
|
599
601
|
return;
|
|
600
602
|
}
|
|
601
603
|
if (Number.isNaN(CHATCCC_PORT) || CHATCCC_PORT < 1 || CHATCCC_PORT > 65535) {
|
|
@@ -647,6 +649,7 @@ async function main() {
|
|
|
647
649
|
});
|
|
648
650
|
try {
|
|
649
651
|
await startConfiguredPlatforms(httpServer, { failOnFeishuError: true });
|
|
652
|
+
await recoverSafeMaintenanceAfterStartup();
|
|
650
653
|
return { ok: true };
|
|
651
654
|
}
|
|
652
655
|
catch (err) {
|
|
@@ -714,6 +717,7 @@ async function main() {
|
|
|
714
717
|
});
|
|
715
718
|
}
|
|
716
719
|
await startConfiguredPlatforms(httpServer, { failOnFeishuError: false });
|
|
720
|
+
await recoverSafeMaintenanceAfterStartup();
|
|
717
721
|
}
|
|
718
722
|
/**
|
|
719
723
|
* 生命周期健康检查发现 HTTP Server 已停止监听时,优先原地恢复同一个 Server。
|
package/dist/src/orchestrator.js
CHANGED
|
@@ -17,7 +17,7 @@ import { CLAUDE_MODEL, GIT_TIMEOUT_MS, PROJECT_ROOT, anthropicConfigDisplay, con
|
|
|
17
17
|
import { buildHelpCard, buildEffortCard, buildFastModeCard, buildModelCard, buildStatusCard, buildCdContent, buildCdCard, buildSessionsCard, buildQueuedCard, buildQueueFullCard, buildCodexUsageCard, } from "./cards.js";
|
|
18
18
|
import { formatGitResult, gitResultHeaderTemplate, runGitCommand, } from "./git-command.js";
|
|
19
19
|
import { clearSessionModelOverride, clearSessionEffortOverride, getSessionStatus, getAllSessionsStatus, initClaudeSession, lastMsgTimestamps, resumeAndPrompt, sessionInfoMap, setSessionModelOverride, setSessionEffortOverride, switchChatBinding, recordSessionRegistry, getAdapterForTool, getEffectiveModelForTool, getEffectiveEffortForTool, getEffectiveFastModeForTool, setSessionFastModeOverride, stopSession, loadSessionRegistryForBinding, removeSessionRegistryRecord, saveSessionTool, recordChatPlatform, } from "./session.js";
|
|
20
|
-
import { bindChatToSession, unbindChatFromSession, isSessionRunning, displayCards, enqueueMessage, cancelQueuedMessage, } from "./session-chat-binding.js";
|
|
20
|
+
import { bindChatToSession, unbindChatFromSession, isSessionRunning, displayCards, enqueueMessage, cancelQueuedMessage, getSessionDrainSnapshot, } from "./session-chat-binding.js";
|
|
21
21
|
import { getCodexUsageSummary, getTenantAccessToken, sendPostMessage } from "./feishu-platform.js";
|
|
22
22
|
import { getCursorUsageSummary } from "./cursor-usage.js";
|
|
23
23
|
import { getChatGptSubscriptionStatus } from "./chatgpt-subscription.js";
|
|
@@ -25,8 +25,10 @@ import { applySharedPrefix } from "./shared-prefix.js";
|
|
|
25
25
|
import { sessionChatName } from "./session-name.js";
|
|
26
26
|
import { reloadRuntimeConfig } from "./runtime-reload.js";
|
|
27
27
|
import { acquireUpdateCommandGuard } from "./update-command-guard.js";
|
|
28
|
-
import { createInternalRestartEnv } from "./startup-lifecycle.js";
|
|
28
|
+
import { createInternalRestartEnv, INTERNAL_RESTART_ENV_VAR } from "./startup-lifecycle.js";
|
|
29
29
|
import { resolveChatCccRuntimeSpawnSpec } from "./runtime-entry.js";
|
|
30
|
+
import { engineManager } from "./engines/engine-specs.js";
|
|
31
|
+
import { beginSafeMaintenanceTrackedWork, isSafeMaintenanceAdmissionClosed, safeMaintenanceCoordinator, } from "./safe-maintenance.js";
|
|
30
32
|
import { feishuP2pContactStore, isValidFeishuOpenId, } from "./agent-team/repositories/feishu-p2p-contact-store.js";
|
|
31
33
|
// ---------------------------------------------------------------------------
|
|
32
34
|
// 辅助函数
|
|
@@ -577,10 +579,11 @@ function updLog(msg) {
|
|
|
577
579
|
catch { }
|
|
578
580
|
}
|
|
579
581
|
/** 同步更新 npm 全局包并 spawn 新进程重启。不依赖 systemd 或任何服务管理器。 */
|
|
580
|
-
function syncUpdateAndRestart() {
|
|
582
|
+
function syncUpdateAndRestart(options = {}) {
|
|
581
583
|
updLog(`sync update start, pid=${process.pid}`);
|
|
582
584
|
appendStartupTrace("update: sync update start", { pid: process.pid });
|
|
583
585
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
586
|
+
let updateSucceeded = false;
|
|
584
587
|
// 1. npm update
|
|
585
588
|
updLog(`running: ${npmCmd} update -g chatccc`);
|
|
586
589
|
appendStartupTrace("update: npm update begin", { npmCmd });
|
|
@@ -590,6 +593,7 @@ function syncUpdateAndRestart() {
|
|
|
590
593
|
const elapsed = Date.now() - t0;
|
|
591
594
|
updLog(`npm update OK (${elapsed}ms): ${out.slice(0, 500)}`);
|
|
592
595
|
appendStartupTrace("update: npm update OK", { elapsedMs: elapsed, outputLen: out.length });
|
|
596
|
+
updateSucceeded = true;
|
|
593
597
|
}
|
|
594
598
|
catch (e) {
|
|
595
599
|
const elapsed = Date.now() - t0;
|
|
@@ -605,6 +609,7 @@ function syncUpdateAndRestart() {
|
|
|
605
609
|
const elapsed2 = Date.now() - t1;
|
|
606
610
|
updLog(`npm install fallback OK (${elapsed2}ms): ${out2.slice(0, 500)}`);
|
|
607
611
|
appendStartupTrace("update: npm install fallback OK", { elapsedMs: elapsed2, outputLen: out2.length });
|
|
612
|
+
updateSucceeded = true;
|
|
608
613
|
}
|
|
609
614
|
catch (e2) {
|
|
610
615
|
const elapsed2 = Date.now() - t1;
|
|
@@ -613,6 +618,11 @@ function syncUpdateAndRestart() {
|
|
|
613
618
|
appendStartupTrace("update: npm install fallback failed", { elapsedMs: elapsed2, message: err2.message });
|
|
614
619
|
}
|
|
615
620
|
}
|
|
621
|
+
if (!updateSucceeded && options.spawnOnUpdateFailure === false) {
|
|
622
|
+
updLog("safe update aborted: both npm update and fallback install failed");
|
|
623
|
+
appendStartupTrace("update: safe update aborted before restart", {});
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
616
626
|
// 2. resolve bin path
|
|
617
627
|
const npmPrefix = process.env.NPM_PREFIX || "";
|
|
618
628
|
const binName = process.platform === "win32" ? "chatccc.cmd" : "chatccc";
|
|
@@ -771,15 +781,106 @@ export async function decideRestartParentExit(child, timeoutMs, pollMs = 500, tr
|
|
|
771
781
|
trace("restart: child alive after window, parent exiting", { childPid: child.pid });
|
|
772
782
|
return true;
|
|
773
783
|
}
|
|
784
|
+
const safeMaintenancePlatforms = new Map();
|
|
785
|
+
export function configureSafeMaintenanceRuntime(platforms) {
|
|
786
|
+
safeMaintenancePlatforms.clear();
|
|
787
|
+
for (const platform of platforms) {
|
|
788
|
+
if (platform.kind)
|
|
789
|
+
safeMaintenancePlatforms.set(platform.kind, platform);
|
|
790
|
+
}
|
|
791
|
+
safeMaintenanceCoordinator.configure({
|
|
792
|
+
getSnapshot() {
|
|
793
|
+
const sessions = getSessionDrainSnapshot();
|
|
794
|
+
return {
|
|
795
|
+
...sessions,
|
|
796
|
+
activeEngineIds: engineManager.getActiveInstallIds(),
|
|
797
|
+
activeWorkLabels: [],
|
|
798
|
+
};
|
|
799
|
+
},
|
|
800
|
+
execute(kind) {
|
|
801
|
+
return kind === "update" ? executeSafeUpdate() : executeSafeRestart();
|
|
802
|
+
},
|
|
803
|
+
async notify(requester, message) {
|
|
804
|
+
const platform = safeMaintenancePlatforms.get(requester.platform);
|
|
805
|
+
if (!platform)
|
|
806
|
+
throw new Error(`Unavailable platform: ${requester.platform}`);
|
|
807
|
+
await platform.sendText(requester.chatId, message);
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
export function recoverSafeMaintenanceAfterStartup() {
|
|
812
|
+
return safeMaintenanceCoordinator.recoverAfterStartup(process.env[INTERNAL_RESTART_ENV_VAR] === "1");
|
|
813
|
+
}
|
|
814
|
+
async function executeSafeRestart() {
|
|
815
|
+
fileLog.flush();
|
|
816
|
+
appendStartupTrace("safe-maintenance: restart spawn begin", { fromPid: process.pid });
|
|
817
|
+
const child = spawnRestartChild();
|
|
818
|
+
child.unref();
|
|
819
|
+
const shouldExit = await decideRestartParentExit(child, RESTART_CHILD_READY_MS);
|
|
820
|
+
if (!shouldExit)
|
|
821
|
+
return false;
|
|
822
|
+
appendStartupTrace("safe-maintenance: restart parent exit", { childPid: child.pid });
|
|
823
|
+
process.exit(0);
|
|
824
|
+
}
|
|
825
|
+
async function executeSafeUpdate() {
|
|
826
|
+
fileLog.flush();
|
|
827
|
+
appendStartupTrace("safe-maintenance: update begin", { fromPid: process.pid });
|
|
828
|
+
const child = syncUpdateAndRestart({ spawnOnUpdateFailure: false });
|
|
829
|
+
if (!child)
|
|
830
|
+
return false;
|
|
831
|
+
child.unref();
|
|
832
|
+
const shouldExit = await decideRestartParentExit(child, RESTART_CHILD_READY_MS);
|
|
833
|
+
if (!shouldExit)
|
|
834
|
+
return false;
|
|
835
|
+
appendStartupTrace("safe-maintenance: update parent exit", { childPid: child.pid });
|
|
836
|
+
process.exit(0);
|
|
837
|
+
}
|
|
838
|
+
function safeMaintenanceRequester(platform, chatId, openId) {
|
|
839
|
+
return { platform: platform.kind ?? "feishu", chatId, openId };
|
|
840
|
+
}
|
|
841
|
+
function safeMaintenancePhaseLabel(phase) {
|
|
842
|
+
const labels = {
|
|
843
|
+
draining: "等待现有任务结束",
|
|
844
|
+
executing: "正在执行维护",
|
|
845
|
+
completed: "已完成",
|
|
846
|
+
failed: "执行失败",
|
|
847
|
+
};
|
|
848
|
+
return labels[phase] ?? phase;
|
|
849
|
+
}
|
|
850
|
+
async function safeMaintenanceStatusText() {
|
|
851
|
+
const status = await safeMaintenanceCoordinator.status();
|
|
852
|
+
if (!status.job)
|
|
853
|
+
return "当前没有安全维护预约。";
|
|
854
|
+
const snapshot = status.snapshot;
|
|
855
|
+
return [
|
|
856
|
+
`安全维护:${status.job.kind === "update" ? "更新并重启" : "重启"}`,
|
|
857
|
+
`状态:${safeMaintenancePhaseLabel(status.job.phase)}`,
|
|
858
|
+
`执行中/收尾会话:${snapshot.activeSessionIds.length}`,
|
|
859
|
+
`已接受的缓存消息:${snapshot.queuedSessionIds.length}`,
|
|
860
|
+
`依赖安装任务:${snapshot.activeEngineIds.length}`,
|
|
861
|
+
`其他处理中入口:${Math.max(0, snapshot.activeWorkLabels.length - 1)}`,
|
|
862
|
+
...(status.job.lastError ? [`错误:${status.job.lastError}`] : []),
|
|
863
|
+
].join("\n");
|
|
864
|
+
}
|
|
774
865
|
// ---------------------------------------------------------------------------
|
|
775
866
|
// handleCommand — 平台无关的命令分发
|
|
776
867
|
// ---------------------------------------------------------------------------
|
|
777
|
-
export async function handleCommand(platform, text, chatId, openId, msgTimestamp, chatType = "group", traceId, commandId) {
|
|
868
|
+
export async function handleCommand(platform, text, chatId, openId, msgTimestamp, chatType = "group", traceId, commandId, acceptedBeforeSafeMaintenance = false) {
|
|
869
|
+
const release = beginSafeMaintenanceTrackedWork(acceptedBeforeSafeMaintenance ? "accepted-queued-message" : "chat-command");
|
|
870
|
+
try {
|
|
871
|
+
await handleCommandInternal(platform, text, chatId, openId, msgTimestamp, chatType, traceId, commandId, acceptedBeforeSafeMaintenance);
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
release();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
async function handleCommandInternal(platform, text, chatId, openId, msgTimestamp, chatType = "group", traceId, commandId, acceptedBeforeSafeMaintenance = false) {
|
|
778
878
|
const tid = traceId ?? makeTraceId();
|
|
779
879
|
const sharedPrefix = applySharedPrefix(text);
|
|
780
880
|
const promptText = sharedPrefix.text;
|
|
781
881
|
text = sharedPrefix.body;
|
|
782
882
|
const textLower = text.toLowerCase();
|
|
883
|
+
const normalizedCommandText = textLower.trim().replace(/\s+/g, " ");
|
|
783
884
|
const isCommandText = !sharedPrefix.matched && textLower.startsWith("/");
|
|
784
885
|
recordChatPlatform(chatId, platform);
|
|
785
886
|
if (platform.kind === "feishu" && chatType === "p2p" && isValidFeishuOpenId(openId)) {
|
|
@@ -788,6 +889,77 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
788
889
|
console.error(`[${ts()}] [AGENT-TEAM] Failed to remember Feishu private contact: ${err.message}`);
|
|
789
890
|
});
|
|
790
891
|
}
|
|
892
|
+
if (isCommandText && normalizedCommandText === "/safestatus") {
|
|
893
|
+
logTrace(tid, "BRANCH", { cmd: "/safestatus" });
|
|
894
|
+
await platform.sendText(chatId, await safeMaintenanceStatusText()).catch(() => { });
|
|
895
|
+
logTrace(tid, "DONE", { outcome: "safe_maintenance_status" });
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (isCommandText && normalizedCommandText === "/cancelsf") {
|
|
899
|
+
logTrace(tid, "BRANCH", { cmd: "/cancelsf" });
|
|
900
|
+
const canceled = await safeMaintenanceCoordinator.cancel();
|
|
901
|
+
if (!canceled) {
|
|
902
|
+
await platform.sendText(chatId, "当前没有可取消的安全维护预约;已经开始执行的维护也不能取消。").catch(() => { });
|
|
903
|
+
}
|
|
904
|
+
logTrace(tid, "DONE", { outcome: canceled ? "safe_maintenance_canceled" : "safe_maintenance_cancel_unavailable" });
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (isCommandText && normalizedCommandText === "/restart safe") {
|
|
908
|
+
logTrace(tid, "BRANCH", { cmd: "/restart safe" });
|
|
909
|
+
try {
|
|
910
|
+
const job = await safeMaintenanceCoordinator.schedule("restart", safeMaintenanceRequester(platform, chatId, openId));
|
|
911
|
+
await platform.sendText(chatId, job.phase === "executing"
|
|
912
|
+
? "安全维护已经开始执行,无法重复预约。"
|
|
913
|
+
: job.kind === "update"
|
|
914
|
+
? "已经存在优先级更高的安全更新预约,将继续等待现有任务完成。发送 /safestatus 查看状态,/cancelsf 取消预约。"
|
|
915
|
+
: "已预约安全重启。现有会话、缓存消息和依赖安装会先自然完成;现在起不再接受新的普通任务。发送 /safestatus 查看状态,/cancelsf 取消预约。").catch(() => { });
|
|
916
|
+
logTrace(tid, "DONE", { outcome: "safe_restart_scheduled", jobId: job.jobId });
|
|
917
|
+
}
|
|
918
|
+
catch (error) {
|
|
919
|
+
await platform.sendText(chatId, `安全重启预约失败:${error.message}`).catch(() => { });
|
|
920
|
+
logTrace(tid, "DONE", { outcome: "safe_restart_schedule_failed", error: error.message });
|
|
921
|
+
}
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (isCommandText && normalizedCommandText === "/update safe") {
|
|
925
|
+
logTrace(tid, "BRANCH", { cmd: "/update safe" });
|
|
926
|
+
const isGlobal = isRunningFromGlobalNpm();
|
|
927
|
+
if (!isGlobal) {
|
|
928
|
+
await platform.sendText(chatId, "当前进程非 npm 全局安装,无法使用 /update safe。请通过 npm install -g chatccc 安装后使用。").catch(() => { });
|
|
929
|
+
logTrace(tid, "DONE", { outcome: "safe_update_not_global" });
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const updateGuard = acquireUpdateCommandGuard({ commandId });
|
|
933
|
+
if (!updateGuard.allowed) {
|
|
934
|
+
if (updateGuard.reason !== "duplicate_id") {
|
|
935
|
+
await platform.sendText(chatId, "无法写入更新保护状态。为避免连续更新和重启,本次 /update safe 未预约。").catch(() => { });
|
|
936
|
+
}
|
|
937
|
+
logTrace(tid, "DONE", { outcome: updateGuard.reason === "duplicate_id" ? "safe_update_duplicate_id" : "safe_update_guard_failed" });
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
const job = await safeMaintenanceCoordinator.schedule("update", safeMaintenanceRequester(platform, chatId, openId));
|
|
942
|
+
await platform.sendText(chatId, job.phase === "executing"
|
|
943
|
+
? "安全维护已经开始执行,无法重复预约。"
|
|
944
|
+
: "已预约安全更新。现有会话、缓存消息和依赖安装会先自然完成;现在起不再接受新的普通任务。发送 /safestatus 查看状态,/cancelsf 取消预约。").catch(() => { });
|
|
945
|
+
logTrace(tid, "DONE", { outcome: "safe_update_scheduled", jobId: job.jobId });
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
await platform.sendText(chatId, `安全更新预约失败:${error.message}`).catch(() => { });
|
|
949
|
+
logTrace(tid, "DONE", { outcome: "safe_update_schedule_failed", error: error.message });
|
|
950
|
+
}
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const maintenanceAllowedCommands = new Set([
|
|
954
|
+
"/stop", "/cancel", "/state", "/sessions", "/usage", "/safestatus", "/cancelsf", "/restart", "/update", "/reload", "/help",
|
|
955
|
+
]);
|
|
956
|
+
if (isSafeMaintenanceAdmissionClosed()
|
|
957
|
+
&& !acceptedBeforeSafeMaintenance
|
|
958
|
+
&& (!isCommandText || !maintenanceAllowedCommands.has(normalizedCommandText))) {
|
|
959
|
+
await platform.sendText(chatId, "ChatCCC 正在等待安全重启或更新,当前不接受新的任务。请在维护完成后重新发送;可用 /safestatus 查看状态。").catch(() => { });
|
|
960
|
+
logTrace(tid, "DONE", { outcome: "safe_maintenance_admission_closed" });
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
791
963
|
if (isCommandText && textLower === "/reload") {
|
|
792
964
|
logTrace(tid, "BRANCH", { cmd: "/reload" });
|
|
793
965
|
try {
|
|
@@ -808,6 +980,14 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
808
980
|
}
|
|
809
981
|
if (isCommandText && textLower === "/restart") {
|
|
810
982
|
logTrace(tid, "BRANCH", { cmd: "/restart" });
|
|
983
|
+
const safeStatus = await safeMaintenanceCoordinator.status();
|
|
984
|
+
if (safeStatus.job?.phase === "executing") {
|
|
985
|
+
await platform.sendText(chatId, "安全维护已经开始执行,请勿重复重启。").catch(() => { });
|
|
986
|
+
logTrace(tid, "DONE", { outcome: "restart_blocked_by_executing_safe_maintenance" });
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (safeStatus.job?.phase === "draining")
|
|
990
|
+
await safeMaintenanceCoordinator.cancel(false);
|
|
811
991
|
await platform.sendText(chatId, "重启中...请几秒后发消息唤醒我").catch(() => { });
|
|
812
992
|
logTrace(tid, "DONE", { outcome: "restart" });
|
|
813
993
|
appendStartupTrace("restart: spawn begin", { fromPid: process.pid });
|
|
@@ -825,6 +1005,12 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
825
1005
|
}
|
|
826
1006
|
if (isCommandText && textLower === "/update") {
|
|
827
1007
|
logTrace(tid, "BRANCH", { cmd: "/update" });
|
|
1008
|
+
const safeStatus = await safeMaintenanceCoordinator.status();
|
|
1009
|
+
if (safeStatus.job?.phase === "executing") {
|
|
1010
|
+
await platform.sendText(chatId, "安全维护已经开始执行,请勿重复更新。").catch(() => { });
|
|
1011
|
+
logTrace(tid, "DONE", { outcome: "update_blocked_by_executing_safe_maintenance" });
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
828
1014
|
const isGlobal = isRunningFromGlobalNpm();
|
|
829
1015
|
appendStartupTrace("update: command received", { isGlobal, chatId });
|
|
830
1016
|
if (!isGlobal) {
|
|
@@ -851,6 +1037,8 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
851
1037
|
logTrace(tid, "DONE", { outcome: "update_guard_write_failed" });
|
|
852
1038
|
return;
|
|
853
1039
|
}
|
|
1040
|
+
if (safeStatus.job?.phase === "draining")
|
|
1041
|
+
await safeMaintenanceCoordinator.cancel(false);
|
|
854
1042
|
await platform.sendText(chatId, "正在更新并重启,请稍候...").catch(() => { });
|
|
855
1043
|
logTrace(tid, "DONE", { outcome: "update" });
|
|
856
1044
|
appendStartupTrace("update: sync update begin", { fromPid: process.pid });
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { USER_DATA_DIR } from "./config.js";
|
|
5
|
+
export const SAFE_MAINTENANCE_FILE = join(USER_DATA_DIR, "state", "safe-maintenance.json");
|
|
6
|
+
export const SAFE_MAINTENANCE_STABLE_IDLE_MS = 1_000;
|
|
7
|
+
const EMPTY_SNAPSHOT = {
|
|
8
|
+
activeSessionIds: [],
|
|
9
|
+
queuedSessionIds: [],
|
|
10
|
+
activeEngineIds: [],
|
|
11
|
+
activeWorkLabels: [],
|
|
12
|
+
};
|
|
13
|
+
export class SafeMaintenanceCoordinator {
|
|
14
|
+
filePath;
|
|
15
|
+
now;
|
|
16
|
+
idFactory;
|
|
17
|
+
stableIdleMs;
|
|
18
|
+
pollMs;
|
|
19
|
+
autoPoll;
|
|
20
|
+
trackedWork = new Map();
|
|
21
|
+
runtime = null;
|
|
22
|
+
job;
|
|
23
|
+
snapshot = EMPTY_SNAPSHOT;
|
|
24
|
+
stableSince = null;
|
|
25
|
+
timer = null;
|
|
26
|
+
tickRunning = false;
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.filePath = options.filePath ?? SAFE_MAINTENANCE_FILE;
|
|
29
|
+
this.now = options.now ?? (() => new Date());
|
|
30
|
+
this.idFactory = options.idFactory ?? randomUUID;
|
|
31
|
+
this.stableIdleMs = options.stableIdleMs ?? SAFE_MAINTENANCE_STABLE_IDLE_MS;
|
|
32
|
+
this.pollMs = options.pollMs ?? 500;
|
|
33
|
+
this.autoPoll = options.autoPoll ?? true;
|
|
34
|
+
this.job = readJob(this.filePath);
|
|
35
|
+
}
|
|
36
|
+
configure(runtime) {
|
|
37
|
+
this.runtime = runtime;
|
|
38
|
+
}
|
|
39
|
+
isAdmissionClosed() {
|
|
40
|
+
return this.job?.phase === "draining" || this.job?.phase === "executing";
|
|
41
|
+
}
|
|
42
|
+
beginTrackedWork(label) {
|
|
43
|
+
const id = this.idFactory();
|
|
44
|
+
this.trackedWork.set(id, label);
|
|
45
|
+
let released = false;
|
|
46
|
+
return () => {
|
|
47
|
+
if (released)
|
|
48
|
+
return;
|
|
49
|
+
released = true;
|
|
50
|
+
this.trackedWork.delete(id);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async schedule(kind, requester) {
|
|
54
|
+
const now = this.now().toISOString();
|
|
55
|
+
if (this.job?.phase === "executing")
|
|
56
|
+
return structuredClone(this.job);
|
|
57
|
+
const previous = this.job;
|
|
58
|
+
if (this.job?.phase === "draining") {
|
|
59
|
+
const requesters = addRequester(this.job.requesters, requester);
|
|
60
|
+
this.job = {
|
|
61
|
+
...this.job,
|
|
62
|
+
kind: this.job.kind === "update" || kind === "update" ? "update" : "restart",
|
|
63
|
+
requesters,
|
|
64
|
+
updatedAt: now,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.job = {
|
|
69
|
+
schemaVersion: 1,
|
|
70
|
+
jobId: this.idFactory(),
|
|
71
|
+
kind,
|
|
72
|
+
phase: "draining",
|
|
73
|
+
requestedAt: now,
|
|
74
|
+
updatedAt: now,
|
|
75
|
+
requesters: [requester],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
this.stableSince = null;
|
|
79
|
+
try {
|
|
80
|
+
writeJob(this.filePath, this.job);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
this.job = previous;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
this.startPolling();
|
|
87
|
+
return structuredClone(this.job);
|
|
88
|
+
}
|
|
89
|
+
async cancel(notify = true) {
|
|
90
|
+
if (this.job?.phase !== "draining")
|
|
91
|
+
return false;
|
|
92
|
+
const requesters = this.job.requesters;
|
|
93
|
+
removeJob(this.filePath);
|
|
94
|
+
this.job = null;
|
|
95
|
+
this.snapshot = EMPTY_SNAPSHOT;
|
|
96
|
+
this.stableSince = null;
|
|
97
|
+
this.stopPolling();
|
|
98
|
+
if (notify)
|
|
99
|
+
await this.notifyAll(requesters, "已取消安全维护预约,ChatCCC 恢复接受新任务。");
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
async status() {
|
|
103
|
+
if (this.runtime && this.isAdmissionClosed())
|
|
104
|
+
this.snapshot = await this.collectSnapshot();
|
|
105
|
+
return {
|
|
106
|
+
job: this.job ? structuredClone(this.job) : null,
|
|
107
|
+
snapshot: structuredClone(this.snapshot),
|
|
108
|
+
waitingCount: snapshotCount(this.snapshot),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async tick() {
|
|
112
|
+
if (this.tickRunning || this.job?.phase !== "draining" || !this.runtime)
|
|
113
|
+
return;
|
|
114
|
+
this.tickRunning = true;
|
|
115
|
+
try {
|
|
116
|
+
this.snapshot = await this.collectSnapshot();
|
|
117
|
+
if (snapshotCount(this.snapshot) > 0) {
|
|
118
|
+
this.stableSince = null;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const nowMs = this.now().getTime();
|
|
122
|
+
if (this.stableSince === null) {
|
|
123
|
+
this.stableSince = nowMs;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (nowMs - this.stableSince < this.stableIdleMs)
|
|
127
|
+
return;
|
|
128
|
+
await this.executeCurrentJob();
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
this.tickRunning = false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async recoverAfterStartup(internalRestart) {
|
|
135
|
+
if (!this.job || !this.runtime)
|
|
136
|
+
return;
|
|
137
|
+
if (this.job.phase === "draining") {
|
|
138
|
+
this.startPolling();
|
|
139
|
+
await this.notifyAll(this.job.requesters, "ChatCCC 已恢复未完成的安全维护预约,继续等待现有任务结束。");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (this.job.phase !== "executing")
|
|
143
|
+
return;
|
|
144
|
+
if (internalRestart) {
|
|
145
|
+
const completed = { ...this.job, phase: "completed", updatedAt: this.now().toISOString() };
|
|
146
|
+
writeJob(this.filePath, completed);
|
|
147
|
+
this.job = completed;
|
|
148
|
+
await this.notifyAll(this.job.requesters, this.job.kind === "update" ? "ChatCCC 已安全更新并重新启动。" : "ChatCCC 已安全重新启动。");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const message = "安全维护执行期间进程意外退出;为避免重启循环,未自动重试。";
|
|
152
|
+
const failed = {
|
|
153
|
+
...this.job,
|
|
154
|
+
phase: "failed",
|
|
155
|
+
updatedAt: this.now().toISOString(),
|
|
156
|
+
lastError: message,
|
|
157
|
+
};
|
|
158
|
+
writeJob(this.filePath, failed);
|
|
159
|
+
this.job = failed;
|
|
160
|
+
await this.notifyAll(this.job.requesters, message);
|
|
161
|
+
}
|
|
162
|
+
async collectSnapshot() {
|
|
163
|
+
const external = await this.runtime.getSnapshot();
|
|
164
|
+
return {
|
|
165
|
+
activeSessionIds: [...external.activeSessionIds],
|
|
166
|
+
queuedSessionIds: [...external.queuedSessionIds],
|
|
167
|
+
activeEngineIds: [...external.activeEngineIds],
|
|
168
|
+
activeWorkLabels: [...external.activeWorkLabels, ...this.trackedWork.values()],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
async executeCurrentJob() {
|
|
172
|
+
if (!this.job || !this.runtime)
|
|
173
|
+
return;
|
|
174
|
+
const executing = { ...this.job, phase: "executing", updatedAt: this.now().toISOString() };
|
|
175
|
+
writeJob(this.filePath, executing);
|
|
176
|
+
this.job = executing;
|
|
177
|
+
this.stopPolling();
|
|
178
|
+
const label = this.job.kind === "update" ? "更新并重启" : "重启";
|
|
179
|
+
await this.notifyAll(this.job.requesters, `现有任务已全部完成,开始安全${label}。`);
|
|
180
|
+
const started = await this.runtime.execute(this.job.kind).catch(async (error) => {
|
|
181
|
+
await this.markFailed(error instanceof Error ? error.message : String(error));
|
|
182
|
+
return false;
|
|
183
|
+
});
|
|
184
|
+
if (!started && this.job?.phase === "executing") {
|
|
185
|
+
await this.markFailed(`安全${label}未能启动,当前进程将继续提供服务。`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async markFailed(message) {
|
|
189
|
+
if (!this.job)
|
|
190
|
+
return;
|
|
191
|
+
const failed = {
|
|
192
|
+
...this.job,
|
|
193
|
+
phase: "failed",
|
|
194
|
+
updatedAt: this.now().toISOString(),
|
|
195
|
+
lastError: message,
|
|
196
|
+
};
|
|
197
|
+
writeJob(this.filePath, failed);
|
|
198
|
+
this.job = failed;
|
|
199
|
+
await this.notifyAll(this.job.requesters, message);
|
|
200
|
+
}
|
|
201
|
+
startPolling() {
|
|
202
|
+
if (!this.autoPoll || this.timer || this.job?.phase !== "draining")
|
|
203
|
+
return;
|
|
204
|
+
this.timer = setInterval(() => { void this.tick(); }, this.pollMs);
|
|
205
|
+
this.timer.unref?.();
|
|
206
|
+
}
|
|
207
|
+
stopPolling() {
|
|
208
|
+
if (!this.timer)
|
|
209
|
+
return;
|
|
210
|
+
clearInterval(this.timer);
|
|
211
|
+
this.timer = null;
|
|
212
|
+
}
|
|
213
|
+
async notifyAll(requesters, message) {
|
|
214
|
+
if (!this.runtime)
|
|
215
|
+
return;
|
|
216
|
+
await Promise.all(requesters.map((requester) => this.runtime.notify(requester, message).catch(() => { })));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function snapshotCount(snapshot) {
|
|
220
|
+
return snapshot.activeSessionIds.length
|
|
221
|
+
+ snapshot.queuedSessionIds.length
|
|
222
|
+
+ snapshot.activeEngineIds.length
|
|
223
|
+
+ snapshot.activeWorkLabels.length;
|
|
224
|
+
}
|
|
225
|
+
function addRequester(requesters, requester) {
|
|
226
|
+
if (requesters.some((item) => item.platform === requester.platform && item.chatId === requester.chatId))
|
|
227
|
+
return requesters;
|
|
228
|
+
return [...requesters, requester];
|
|
229
|
+
}
|
|
230
|
+
function readJob(filePath) {
|
|
231
|
+
if (!existsSync(filePath))
|
|
232
|
+
return null;
|
|
233
|
+
try {
|
|
234
|
+
const value = JSON.parse(readFileSync(filePath, "utf8"));
|
|
235
|
+
if (value.schemaVersion !== 1 || !value.jobId || !Array.isArray(value.requesters))
|
|
236
|
+
return null;
|
|
237
|
+
if (!["restart", "update"].includes(value.kind) || !["draining", "executing", "completed", "failed"].includes(value.phase))
|
|
238
|
+
return null;
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function writeJob(filePath, job) {
|
|
246
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
247
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
248
|
+
try {
|
|
249
|
+
writeFileSync(temporary, `${JSON.stringify(job, null, 2)}\n`, "utf8");
|
|
250
|
+
renameSync(temporary, filePath);
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
rmSync(temporary, { force: true });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function removeJob(filePath) {
|
|
257
|
+
rmSync(filePath, { force: true });
|
|
258
|
+
}
|
|
259
|
+
export let safeMaintenanceCoordinator = new SafeMaintenanceCoordinator();
|
|
260
|
+
export function _setSafeMaintenanceCoordinatorForTest(coordinator) {
|
|
261
|
+
safeMaintenanceCoordinator = coordinator;
|
|
262
|
+
}
|
|
263
|
+
export function _resetSafeMaintenanceCoordinatorForTest() {
|
|
264
|
+
safeMaintenanceCoordinator = new SafeMaintenanceCoordinator();
|
|
265
|
+
}
|
|
266
|
+
export function isSafeMaintenanceAdmissionClosed() {
|
|
267
|
+
return safeMaintenanceCoordinator.isAdmissionClosed();
|
|
268
|
+
}
|
|
269
|
+
export function beginSafeMaintenanceTrackedWork(label) {
|
|
270
|
+
return safeMaintenanceCoordinator.beginTrackedWork(label);
|
|
271
|
+
}
|
|
@@ -133,6 +133,18 @@ export function setUnifiedDisplayLoopHandle(h) {
|
|
|
133
133
|
unifiedDisplayLoopHandle = h;
|
|
134
134
|
}
|
|
135
135
|
export const queuedMessages = new Map();
|
|
136
|
+
/** Includes prompt execution, finalization, auto-recovery reservations, and accepted queues. */
|
|
137
|
+
export function getSessionDrainSnapshot() {
|
|
138
|
+
const active = new Set([
|
|
139
|
+
...activePrompts.keys(),
|
|
140
|
+
...finalizingSessions,
|
|
141
|
+
...autoRecoveryReservations,
|
|
142
|
+
]);
|
|
143
|
+
return {
|
|
144
|
+
activeSessionIds: [...active].sort(),
|
|
145
|
+
queuedSessionIds: [...queuedMessages.keys()].sort(),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
136
148
|
export function enqueueMessage(sessionId, msg) {
|
|
137
149
|
if (queuedMessages.has(sessionId))
|
|
138
150
|
return false;
|
package/dist/src/session.js
CHANGED
|
@@ -6,6 +6,7 @@ import { progressView } from "./progress/view.js";
|
|
|
6
6
|
import { createAgentActivityTracker, formatAgentActivityTitle, updateAgentActivity, } from "./agent-activity.js";
|
|
7
7
|
import { simplifyToolUse, simplifyToolResult } from "./simplify.js";
|
|
8
8
|
import { logTrace } from "./trace.js";
|
|
9
|
+
import { appendExecutionTranscriptBlock, } from "./execution-transcript.js";
|
|
9
10
|
import { createClaudeAdapter } from "./adapters/claude-adapter.js";
|
|
10
11
|
import { createCursorAdapter } from "./adapters/cursor-adapter.js";
|
|
11
12
|
import { createCodexAdapter } from "./adapters/codex-adapter.js";
|
|
@@ -698,6 +699,11 @@ export function pickFinalReply(state) {
|
|
|
698
699
|
return state.finalCompleteText || state.finalText;
|
|
699
700
|
}
|
|
700
701
|
export function accumulateBlockContent(block, state, toolCallMap) {
|
|
702
|
+
if (state.transcript !== undefined || (block.type !== "agent_progress" && block.type !== "text_reset")) {
|
|
703
|
+
const transcriptState = { transcript: state.transcript ?? [] };
|
|
704
|
+
appendExecutionTranscriptBlock(block, transcriptState);
|
|
705
|
+
state.transcript = transcriptState.transcript;
|
|
706
|
+
}
|
|
701
707
|
switch (block.type) {
|
|
702
708
|
case "thinking":
|
|
703
709
|
state.chunkCount++;
|
|
@@ -1145,6 +1151,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1145
1151
|
refreshBusySessionAvatar(sessionId, tool, platform).catch(() => { });
|
|
1146
1152
|
const state = {
|
|
1147
1153
|
accumulatedContent: "",
|
|
1154
|
+
transcript: [],
|
|
1148
1155
|
finalText: "",
|
|
1149
1156
|
finalCompleteText: "",
|
|
1150
1157
|
chunkCount: 0,
|
|
@@ -1196,6 +1203,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1196
1203
|
status: "auto_ended",
|
|
1197
1204
|
accumulatedContent: state.accumulatedContent,
|
|
1198
1205
|
finalReply: pickFinalReply(state).trim(),
|
|
1206
|
+
transcript: state.transcript,
|
|
1199
1207
|
activity: activityTracker.activity,
|
|
1200
1208
|
chunkCount: state.chunkCount,
|
|
1201
1209
|
turnCount: nextTurnCount,
|
|
@@ -1308,6 +1316,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1308
1316
|
status: "running",
|
|
1309
1317
|
accumulatedContent: state.accumulatedContent,
|
|
1310
1318
|
finalReply: pickFinalReply(state),
|
|
1319
|
+
transcript: state.transcript,
|
|
1311
1320
|
activity: activityTracker.activity,
|
|
1312
1321
|
chunkCount: state.chunkCount,
|
|
1313
1322
|
turnCount: nextTurnCount,
|
|
@@ -1406,6 +1415,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1406
1415
|
status: finalStatus,
|
|
1407
1416
|
accumulatedContent: state.accumulatedContent,
|
|
1408
1417
|
finalReply: finalReplyToWrite,
|
|
1418
|
+
transcript: state.transcript,
|
|
1409
1419
|
activity: activityTracker.activity,
|
|
1410
1420
|
chunkCount: state.chunkCount,
|
|
1411
1421
|
turnCount: nextTurnCount,
|