@kenkaiiii/ggcoder 5.16.0 → 5.18.0
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 +2 -2
- package/dist/app-sidecar.js +303 -88
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +24 -2
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-session-review-coverage.test.d.ts +2 -0
- package/dist/core/agent-session-review-coverage.test.d.ts.map +1 -0
- package/dist/core/agent-session-review-coverage.test.js +57 -0
- package/dist/core/agent-session-review-coverage.test.js.map +1 -0
- package/dist/core/agent-session-tool-result-policy.test.d.ts +2 -0
- package/dist/core/agent-session-tool-result-policy.test.d.ts.map +1 -0
- package/dist/core/agent-session-tool-result-policy.test.js +12 -0
- package/dist/core/agent-session-tool-result-policy.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +19 -12
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +185 -66
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-providers.js +1 -1
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +2 -1
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/event-bus.d.ts +2 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/ideal-review.d.ts +27 -0
- package/dist/core/ideal-review.d.ts.map +1 -1
- package/dist/core/ideal-review.js +64 -0
- package/dist/core/ideal-review.js.map +1 -1
- package/dist/core/ideal-review.test.js +34 -1
- package/dist/core/ideal-review.test.js.map +1 -1
- package/dist/core/lsp/client.d.ts +3 -0
- package/dist/core/lsp/client.d.ts.map +1 -1
- package/dist/core/lsp/client.js +20 -0
- package/dist/core/lsp/client.js.map +1 -1
- package/dist/core/lsp/manager.d.ts +32 -9
- package/dist/core/lsp/manager.d.ts.map +1 -1
- package/dist/core/lsp/manager.js +89 -37
- package/dist/core/lsp/manager.js.map +1 -1
- package/dist/core/lsp/manager.test.js +56 -4
- package/dist/core/lsp/manager.test.js.map +1 -1
- package/dist/core/project-discovery.d.ts +4 -11
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +33 -30
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/project-discovery.test.js +148 -0
- package/dist/core/project-discovery.test.js.map +1 -1
- package/dist/core/run-lifecycle.d.ts +40 -0
- package/dist/core/run-lifecycle.d.ts.map +1 -0
- package/dist/core/run-lifecycle.js +95 -0
- package/dist/core/run-lifecycle.js.map +1 -0
- package/dist/core/run-lifecycle.test.d.ts +2 -0
- package/dist/core/run-lifecycle.test.d.ts.map +1 -0
- package/dist/core/run-lifecycle.test.js +64 -0
- package/dist/core/run-lifecycle.test.js.map +1 -0
- package/dist/core/session-compaction.d.ts +3 -0
- package/dist/core/session-compaction.d.ts.map +1 -1
- package/dist/core/session-compaction.js +14 -1
- package/dist/core/session-compaction.js.map +1 -1
- package/dist/core/session-compaction.test.js +5 -0
- package/dist/core/session-compaction.test.js.map +1 -1
- package/dist/core/session-manager.d.ts +33 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +73 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +97 -2
- package/dist/core/session-manager.test.js.map +1 -1
- package/dist/core/session-preview.d.ts +11 -0
- package/dist/core/session-preview.d.ts.map +1 -0
- package/dist/core/session-preview.js +50 -0
- package/dist/core/session-preview.js.map +1 -0
- package/dist/core/subagent-manager.d.ts +42 -7
- package/dist/core/subagent-manager.d.ts.map +1 -1
- package/dist/core/subagent-manager.js +346 -30
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-manager.test.js +195 -3
- package/dist/core/subagent-manager.test.js.map +1 -1
- package/dist/core/subagent-store.d.ts +20 -0
- package/dist/core/subagent-store.d.ts.map +1 -0
- package/dist/core/subagent-store.js +177 -0
- package/dist/core/subagent-store.js.map +1 -0
- package/dist/core/subagent-store.test.d.ts +2 -0
- package/dist/core/subagent-store.test.d.ts.map +1 -0
- package/dist/core/subagent-store.test.js +94 -0
- package/dist/core/subagent-store.test.js.map +1 -0
- package/dist/modes/agent-home-mode.d.ts.map +1 -1
- package/dist/modes/agent-home-mode.js +10 -3
- package/dist/modes/agent-home-mode.js.map +1 -1
- package/dist/modes/serve-mode.d.ts.map +1 -1
- package/dist/modes/serve-mode.js +10 -3
- package/dist/modes/serve-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts +2 -0
- package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
- package/dist/modes/subagent-worker-mode.js +13 -3
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/tools/subagent-shared.d.ts +8 -0
- package/dist/tools/subagent-shared.d.ts.map +1 -1
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/subagent.d.ts +3 -8
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +10 -8
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/subagent.test.js +17 -1
- package/dist/tools/subagent.test.js.map +1 -1
- package/dist/tools/tasks.d.ts +1 -1
- package/dist/tools/web-fetch.d.ts +1 -1
- package/dist/ui/App.d.ts +7 -3
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +66 -83
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/Footer.d.ts +1 -0
- package/dist/ui/components/Footer.d.ts.map +1 -1
- package/dist/ui/components/Footer.js +3 -3
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/components/Footer.test.d.ts +2 -0
- package/dist/ui/components/Footer.test.d.ts.map +1 -0
- package/dist/ui/components/Footer.test.js +16 -0
- package/dist/ui/components/Footer.test.js.map +1 -0
- package/dist/ui/components/SubAgentPanel.d.ts +2 -0
- package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
- package/dist/ui/components/SubAgentPanel.js +4 -2
- package/dist/ui/components/SubAgentPanel.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +8 -3
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +64 -8
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/hooks/useSessionPersistence.d.ts +4 -2
- package/dist/ui/hooks/useSessionPersistence.d.ts.map +1 -1
- package/dist/ui/hooks/useSessionPersistence.js +10 -1
- package/dist/ui/hooks/useSessionPersistence.js.map +1 -1
- package/dist/ui/hooks/useTerminalTitle.d.ts +3 -3
- package/dist/ui/hooks/useTerminalTitle.d.ts.map +1 -1
- package/dist/ui/hooks/useTerminalTitle.js +5 -9
- package/dist/ui/hooks/useTerminalTitle.js.map +1 -1
- package/dist/ui/login.d.ts.map +1 -1
- package/dist/ui/login.js +5 -1
- package/dist/ui/login.js.map +1 -1
- package/dist/ui/render.d.ts +7 -2
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +4 -4
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/render.test.js +10 -1
- package/dist/ui/render.test.js.map +1 -1
- package/dist/ui/terminal-history.js +6 -2
- package/dist/ui/terminal-history.js.map +1 -1
- package/dist/ui/terminal-history.test.js +2 -2
- package/dist/ui/terminal-history.test.js.map +1 -1
- package/dist/ui/tui-history-parity.test.js +8 -1
- package/dist/ui/tui-history-parity.test.js.map +1 -1
- package/dist/utils/git.d.ts +2 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +12 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/git.test.d.ts +2 -0
- package/dist/utils/git.test.d.ts.map +1 -0
- package/dist/utils/git.test.js +36 -0
- package/dist/utils/git.test.js.map +1 -0
- package/package.json +4 -4
- package/dist/utils/session-title.d.ts +0 -19
- package/dist/utils/session-title.d.ts.map +0 -1
- package/dist/utils/session-title.js +0 -82
- package/dist/utils/session-title.js.map +0 -1
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ Switch mid-conversation with `/model`. Not locked to anyone.
|
|
|
71
71
|
|---|---|---|
|
|
72
72
|
| **Anthropic** | Claude Opus 4.8, Sonnet 5, Haiku 4.5 | OAuth |
|
|
73
73
|
| **OpenAI** | GPT-5.5, GPT-5.5 Pro, GPT-5.4, GPT-5.3 Codex | OAuth |
|
|
74
|
-
| **Moonshot** | Kimi K2.7 | API key |
|
|
74
|
+
| **Moonshot** | Kimi K3, Kimi K2.7 Code | OAuth or API key |
|
|
75
75
|
| **Z.AI (GLM)** | GLM-5.1, GLM-4.7, GLM-4.7 Flash | API key |
|
|
76
76
|
| **MiniMax** | MiniMax M3 (image + video) | API key |
|
|
77
77
|
| **Xiaomi (MiMo)** | MiMo-V2.5-Pro, MiMo-V2.5-Pro-UltraSpeed, MiMo-V2.5 (image + video) | API key |
|
|
@@ -80,7 +80,7 @@ Switch mid-conversation with `/model`. Not locked to anyone.
|
|
|
80
80
|
|
|
81
81
|
The same conversation, the same tools, the same project context — only the model changes. Use a strong reasoning model when you need it, swap to a fast cheap one for grunt work, never restart your session.
|
|
82
82
|
|
|
83
|
-
**Attachments.** Drag, paste, or type a path to attach images and video in the chat input. Video is sent natively to models that support it (Gemini 3.x, Kimi K2.7, MiniMax M3, MiMo-V2.5); for other models the video is saved to a temp file and the model is told to inspect it with ffmpeg or its own tools.
|
|
83
|
+
**Attachments.** Drag, paste, or type a path to attach images and video in the chat input. Video is sent natively to models that support it (Gemini 3.x, Kimi K3/K2.7 Code, MiniMax M3, MiMo-V2.5); for other models the video is saved to a temp file and the model is told to inspect it with ffmpeg or its own tools.
|
|
84
84
|
|
|
85
85
|
---
|
|
86
86
|
|
package/dist/app-sidecar.js
CHANGED
|
@@ -18,10 +18,11 @@ import os from "node:os";
|
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
20
|
import { parseArgs } from "node:util";
|
|
21
|
-
import { formatError } from "@kenkaiiii/gg-ai";
|
|
21
|
+
import { environmentSecrets, formatError, redactValue, } from "@kenkaiiii/gg-ai";
|
|
22
22
|
import { runJsonMode } from "./modes/json-mode.js";
|
|
23
23
|
import { runSubagentWorkerMode } from "./modes/subagent-worker-mode.js";
|
|
24
24
|
import { AgentSession } from "./core/agent-session.js";
|
|
25
|
+
import { RunLifecycle } from "./core/run-lifecycle.js";
|
|
25
26
|
import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, sessionsDirForChatAgent, switchChatAgent, } from "./chat-agents/index.js";
|
|
26
27
|
import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
|
|
27
28
|
import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
|
|
@@ -43,7 +44,8 @@ import { ensureAppDirs, loadSavedSettings } from "./config.js";
|
|
|
43
44
|
import { SettingsManager } from "./core/settings-manager.js";
|
|
44
45
|
import { getModel, getMaxThinkingLevel, getContextWindow, MODELS } from "./core/model-registry.js";
|
|
45
46
|
import { resolveStartOrFallback } from "./core/resolve-start.js";
|
|
46
|
-
import { getGitBranch, isGitRepo } from "./utils/git.js";
|
|
47
|
+
import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
|
|
48
|
+
import { extractPlanSteps } from "./utils/plan-steps.js";
|
|
47
49
|
import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSupported, } from "./core/thinking-level.js";
|
|
48
50
|
import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
|
|
49
51
|
import { loadCustomCommands } from "./core/custom-commands.js";
|
|
@@ -929,7 +931,8 @@ async function createSession(deps, opts) {
|
|
|
929
931
|
const clients = new Set();
|
|
930
932
|
let clientSeq = 0;
|
|
931
933
|
function broadcast(type, data) {
|
|
932
|
-
const
|
|
934
|
+
const safePayload = redactValue({ type, data }, { secrets: environmentSecrets(process.env) });
|
|
935
|
+
const frame = `data: ${JSON.stringify(safePayload)}\n\n`;
|
|
933
936
|
for (const c of clients)
|
|
934
937
|
c.res.write(frame);
|
|
935
938
|
}
|
|
@@ -1033,7 +1036,9 @@ async function createSession(deps, opts) {
|
|
|
1033
1036
|
...baseSessionOptions,
|
|
1034
1037
|
// Plan mode belongs only to the coding agent.
|
|
1035
1038
|
onEnterPlan: async (reason) => {
|
|
1039
|
+
deactivateApprovedPlan();
|
|
1036
1040
|
await session.setPlanMode(true);
|
|
1041
|
+
broadcast("plan_progress", { total: 0, completed: [] });
|
|
1037
1042
|
broadcast("plan_enter", { reason: reason ?? "" });
|
|
1038
1043
|
void session.persistAppMarker("plan", { reason: reason ?? "" }).catch(() => { });
|
|
1039
1044
|
},
|
|
@@ -1063,11 +1068,17 @@ async function createSession(deps, opts) {
|
|
|
1063
1068
|
}
|
|
1064
1069
|
}
|
|
1065
1070
|
log("INFO", "app-sidecar", "session ready", { provider, model, mode, chatAgent, cwd });
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1071
|
+
// Workspace extras (context window, git status, background tasks). Git state
|
|
1072
|
+
// is resolved once at startup and refreshed after every run; the context
|
|
1068
1073
|
// window follows the active model.
|
|
1069
|
-
|
|
1070
|
-
|
|
1074
|
+
const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount] = await Promise.all([
|
|
1075
|
+
getGitBranch(cwd).catch(() => null),
|
|
1076
|
+
isGitRepo(cwd).catch(() => false),
|
|
1077
|
+
getGitDirtyFileCount(cwd).catch(() => 0),
|
|
1078
|
+
]);
|
|
1079
|
+
let gitBranch = initialGitBranch;
|
|
1080
|
+
let gitIsRepo = initialGitIsRepo;
|
|
1081
|
+
let gitDirtyFileCount = initialDirtyFileCount;
|
|
1071
1082
|
function currentContextWindow() {
|
|
1072
1083
|
const st = session.getState();
|
|
1073
1084
|
return getContextWindow(st.model, { provider: st.provider, accountId: st.accountId });
|
|
@@ -1079,6 +1090,7 @@ async function createSession(deps, opts) {
|
|
|
1079
1090
|
contextWindow: currentContextWindow(),
|
|
1080
1091
|
gitBranch,
|
|
1081
1092
|
isGitRepo: gitIsRepo,
|
|
1093
|
+
gitDirtyFileCount,
|
|
1082
1094
|
tasks: session.listBackgroundProcesses(),
|
|
1083
1095
|
};
|
|
1084
1096
|
}
|
|
@@ -1088,8 +1100,93 @@ async function createSession(deps, opts) {
|
|
|
1088
1100
|
// failures (e.g. repeated invalid-argument errors) instead of leaving the
|
|
1089
1101
|
// fatal-abort path with no forensic trail.
|
|
1090
1102
|
const toolCallNames = new Map();
|
|
1103
|
+
// Approved-plan progress belongs beside the plan file, not in the webview.
|
|
1104
|
+
// The implementation can rewrite/expand that file mid-run, so a step count
|
|
1105
|
+
// frozen at approval time becomes dishonest (the exact stale-total bug the
|
|
1106
|
+
// CLI already fixed). Re-read the live file after tools/markers, retain the
|
|
1107
|
+
// last valid step section during transient edits, and send one authoritative
|
|
1108
|
+
// snapshot to the app.
|
|
1109
|
+
let approvedPlanPath = null;
|
|
1110
|
+
let approvedPlanTotal = 0;
|
|
1111
|
+
let approvedPlanMarkers = new Set();
|
|
1112
|
+
let approvedPlanGeneration = 0;
|
|
1113
|
+
let planMarkerTail = "";
|
|
1114
|
+
let planProgressSync = Promise.resolve(false);
|
|
1115
|
+
function planProgressPayload() {
|
|
1116
|
+
const completed = [...approvedPlanMarkers]
|
|
1117
|
+
.filter((step) => step >= 1 && step <= approvedPlanTotal)
|
|
1118
|
+
.sort((a, b) => a - b);
|
|
1119
|
+
return { total: approvedPlanTotal, completed };
|
|
1120
|
+
}
|
|
1121
|
+
async function syncApprovedPlanProgress(generation) {
|
|
1122
|
+
const planPath = approvedPlanPath;
|
|
1123
|
+
if (planPath === null || generation !== approvedPlanGeneration)
|
|
1124
|
+
return false;
|
|
1125
|
+
const content = await fs.readFile(planPath, "utf-8").catch(() => null);
|
|
1126
|
+
if (approvedPlanPath !== planPath || generation !== approvedPlanGeneration)
|
|
1127
|
+
return false;
|
|
1128
|
+
if (content !== null) {
|
|
1129
|
+
const freshTotal = extractPlanSteps(content).length;
|
|
1130
|
+
// During an in-place rewrite the step section can briefly disappear.
|
|
1131
|
+
// Keep the last real total instead of flashing 0 or declaring completion.
|
|
1132
|
+
if (freshTotal > 0 || approvedPlanTotal === 0)
|
|
1133
|
+
approvedPlanTotal = freshTotal;
|
|
1134
|
+
}
|
|
1135
|
+
broadcast("plan_progress", planProgressPayload());
|
|
1136
|
+
return (approvedPlanTotal > 0 &&
|
|
1137
|
+
Array.from({ length: approvedPlanTotal }, (_, index) => index + 1).every((step) => approvedPlanMarkers.has(step)));
|
|
1138
|
+
}
|
|
1139
|
+
function queueApprovedPlanProgressSync() {
|
|
1140
|
+
const generation = approvedPlanGeneration;
|
|
1141
|
+
planProgressSync = planProgressSync
|
|
1142
|
+
.catch(() => false)
|
|
1143
|
+
.then(() => syncApprovedPlanProgress(generation))
|
|
1144
|
+
.catch((error) => {
|
|
1145
|
+
log("WARN", "app-sidecar", "plan progress refresh failed", {
|
|
1146
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1147
|
+
});
|
|
1148
|
+
return false;
|
|
1149
|
+
});
|
|
1150
|
+
return planProgressSync;
|
|
1151
|
+
}
|
|
1152
|
+
async function activateApprovedPlan(planPath) {
|
|
1153
|
+
deactivateApprovedPlan();
|
|
1154
|
+
await session.setApprovedPlan(planPath);
|
|
1155
|
+
if (!planPath)
|
|
1156
|
+
return 0;
|
|
1157
|
+
approvedPlanPath = planPath;
|
|
1158
|
+
await queueApprovedPlanProgressSync();
|
|
1159
|
+
return approvedPlanTotal;
|
|
1160
|
+
}
|
|
1161
|
+
function deactivateApprovedPlan() {
|
|
1162
|
+
approvedPlanGeneration++;
|
|
1163
|
+
approvedPlanPath = null;
|
|
1164
|
+
approvedPlanTotal = 0;
|
|
1165
|
+
approvedPlanMarkers = new Set();
|
|
1166
|
+
planMarkerTail = "";
|
|
1167
|
+
planProgressSync = Promise.resolve(false);
|
|
1168
|
+
}
|
|
1169
|
+
function recordApprovedPlanMarkers(text) {
|
|
1170
|
+
if (approvedPlanPath === null || !text)
|
|
1171
|
+
return;
|
|
1172
|
+
const candidate = planMarkerTail + text;
|
|
1173
|
+
let changed = false;
|
|
1174
|
+
for (const match of candidate.matchAll(/\[DONE:(\d+)\]/gi)) {
|
|
1175
|
+
const step = Number.parseInt(match[1], 10);
|
|
1176
|
+
if (step >= 1 && !approvedPlanMarkers.has(step)) {
|
|
1177
|
+
approvedPlanMarkers.add(step);
|
|
1178
|
+
changed = true;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
planMarkerTail = candidate.slice(-32);
|
|
1182
|
+
if (changed)
|
|
1183
|
+
void queueApprovedPlanProgressSync();
|
|
1184
|
+
}
|
|
1091
1185
|
// Forward every relevant bus event to the webview.
|
|
1092
|
-
session.eventBus.on("text_delta", (d) =>
|
|
1186
|
+
session.eventBus.on("text_delta", (d) => {
|
|
1187
|
+
broadcast("text_delta", d);
|
|
1188
|
+
recordApprovedPlanMarkers(d.text);
|
|
1189
|
+
});
|
|
1093
1190
|
session.eventBus.on("thinking_delta", (d) => broadcast("thinking_delta", d));
|
|
1094
1191
|
session.eventBus.on("tool_call_start", (d) => {
|
|
1095
1192
|
toolCallNames.set(d.toolCallId, d.name);
|
|
@@ -1106,6 +1203,11 @@ async function createSession(deps, opts) {
|
|
|
1106
1203
|
...(d.isError ? { result: d.result.slice(0, 500) } : {}),
|
|
1107
1204
|
});
|
|
1108
1205
|
broadcast("tool_call_end", d);
|
|
1206
|
+
// Any tool can mutate the approved plan (including bash), so refresh after
|
|
1207
|
+
// every completed call while tracking is active. The file is tiny and this
|
|
1208
|
+
// keeps the displayed total aligned before the next completion marker.
|
|
1209
|
+
if (approvedPlanPath !== null)
|
|
1210
|
+
void queueApprovedPlanProgressSync();
|
|
1109
1211
|
});
|
|
1110
1212
|
// Native server tools (e.g. Anthropic web_search) do NOT end the turn — text
|
|
1111
1213
|
// streams before and after them in the SAME turn. The webview must reset its
|
|
@@ -1123,7 +1225,13 @@ async function createSession(deps, opts) {
|
|
|
1123
1225
|
session.eventBus.on("compaction_start", (d) => broadcast("compaction_start", d));
|
|
1124
1226
|
session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
|
|
1125
1227
|
let running = false;
|
|
1126
|
-
|
|
1228
|
+
const runLifecycle = new RunLifecycle((runState) => {
|
|
1229
|
+
running = runState !== "idle";
|
|
1230
|
+
if (runState === "cancelling")
|
|
1231
|
+
broadcast("run_cancelling", { runState });
|
|
1232
|
+
});
|
|
1233
|
+
const cancelledRunEndGenerations = new Set();
|
|
1234
|
+
let pendingCancelDrain = null;
|
|
1127
1235
|
// Bumped by /cancel — a run whose cancel generation changed mid-flight was
|
|
1128
1236
|
// canceled and earns no XP.
|
|
1129
1237
|
let cancelGeneration = 0;
|
|
@@ -1145,6 +1253,7 @@ async function createSession(deps, opts) {
|
|
|
1145
1253
|
let autopilotCancelled = false;
|
|
1146
1254
|
// Hard cap on review→prompt→review rounds per user turn (loop safety).
|
|
1147
1255
|
const MAX_AUTOPILOT_ROUNDS = 3;
|
|
1256
|
+
const CANCEL_TIMEOUT_MS = 5_000;
|
|
1148
1257
|
// Prompt bodies Autopilot Ken injected into the BUILD session this
|
|
1149
1258
|
// conversation. Passed into every Ken digest so injected prompts render as
|
|
1150
1259
|
// "Ken autopilot (injected)" instead of `**User:**` — otherwise multi-round
|
|
@@ -1337,74 +1446,114 @@ async function createSession(deps, opts) {
|
|
|
1337
1446
|
});
|
|
1338
1447
|
return ken;
|
|
1339
1448
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1449
|
+
function abortOwnedWork() {
|
|
1450
|
+
cancelGeneration++;
|
|
1451
|
+
abort.abort();
|
|
1452
|
+
// Stop a run-all sweep and every async child through AgentSession's signal.
|
|
1453
|
+
taskRunAll = false;
|
|
1454
|
+
autopilotCancelled = true;
|
|
1455
|
+
kenAutoAbort.abort();
|
|
1456
|
+
}
|
|
1457
|
+
function installFreshRunControllers() {
|
|
1458
|
+
abort = new AbortController();
|
|
1459
|
+
session.setSignal(abort.signal);
|
|
1460
|
+
kenAutoAbort = new AbortController();
|
|
1461
|
+
kenAutoSession?.setSignal(kenAutoAbort.signal);
|
|
1353
1462
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1463
|
+
function finishOwnedGeneration(generation, emitCancelledFallback) {
|
|
1464
|
+
const cancelled = runLifecycle.isCancellationRequested(generation);
|
|
1465
|
+
const settlement = runLifecycle.settle(generation);
|
|
1466
|
+
if (!settlement.settled)
|
|
1467
|
+
return cancelled;
|
|
1468
|
+
// A replacement signal is safe only after the provider-backed owner settled.
|
|
1469
|
+
installFreshRunControllers();
|
|
1470
|
+
if (cancelled && emitCancelledFallback && !cancelledRunEndGenerations.has(generation)) {
|
|
1471
|
+
cancelledRunEndGenerations.add(generation);
|
|
1472
|
+
broadcast("run_end", { cancelled: true, runState: runLifecycle.state });
|
|
1473
|
+
}
|
|
1474
|
+
return cancelled;
|
|
1475
|
+
}
|
|
1476
|
+
// Core provider-run bracket. Standalone runs own a lifecycle generation;
|
|
1477
|
+
// injected autopilot runs share the cycle's outer generation.
|
|
1358
1478
|
async function runAgent(label, run) {
|
|
1359
|
-
|
|
1479
|
+
const ownsGeneration = !runLifecycle.running;
|
|
1480
|
+
const generation = ownsGeneration
|
|
1481
|
+
? runLifecycle.begin(abortOwnedWork).generation
|
|
1482
|
+
: runLifecycle.generation;
|
|
1483
|
+
if (ownsGeneration)
|
|
1484
|
+
pendingCancelDrain = null;
|
|
1360
1485
|
// Progress (Ranks): completed, non-canceled runs with ≥1 assistant turn earn
|
|
1361
1486
|
// XP — prompt + any commits authored during the run window.
|
|
1362
1487
|
const runStartedAt = Date.now();
|
|
1363
1488
|
const cancelGenAtStart = cancelGeneration;
|
|
1364
1489
|
const assistantsBeforeRun = countAssistantMessages(session.getMessages());
|
|
1365
1490
|
let runSucceeded = false;
|
|
1366
|
-
broadcast("run_start", { text: label });
|
|
1491
|
+
broadcast("run_start", { text: label, runState: runLifecycle.state });
|
|
1367
1492
|
try {
|
|
1368
|
-
|
|
1493
|
+
if (!runLifecycle.isCancellationRequested(generation))
|
|
1494
|
+
await run();
|
|
1369
1495
|
runSucceeded = true;
|
|
1370
1496
|
}
|
|
1371
1497
|
catch (err) {
|
|
1372
|
-
|
|
1498
|
+
if (!runLifecycle.isCancellationRequested(generation)) {
|
|
1499
|
+
broadcastError("error", "run failed", err);
|
|
1500
|
+
}
|
|
1373
1501
|
}
|
|
1374
1502
|
finally {
|
|
1375
|
-
|
|
1503
|
+
const cancelled = runLifecycle.isCancellationRequested(generation);
|
|
1376
1504
|
if (runSucceeded &&
|
|
1505
|
+
!cancelled &&
|
|
1377
1506
|
cancelGeneration === cancelGenAtStart &&
|
|
1378
1507
|
countAssistantMessages(session.getMessages()) > assistantsBeforeRun) {
|
|
1379
1508
|
// Fire-and-forget — XP must never delay or break run teardown.
|
|
1380
1509
|
void progress.awardRun(cwd, runStartedAt, opts.id);
|
|
1381
1510
|
}
|
|
1382
|
-
// A run may have switched branches
|
|
1383
|
-
// background tasks
|
|
1384
|
-
gitBranch = await
|
|
1385
|
-
|
|
1386
|
-
|
|
1511
|
+
// A run may have switched branches, changed files, or spawned/finished
|
|
1512
|
+
// background tasks. Refresh the workspace extras once it settles.
|
|
1513
|
+
[gitBranch, gitIsRepo, gitDirtyFileCount] = await Promise.all([
|
|
1514
|
+
getGitBranch(cwd).catch(() => gitBranch),
|
|
1515
|
+
isGitRepo(cwd).catch(() => gitIsRepo),
|
|
1516
|
+
getGitDirtyFileCount(cwd).catch(() => gitDirtyFileCount),
|
|
1517
|
+
]);
|
|
1518
|
+
// Serialize behind any marker/tool-triggered refresh so the terminal
|
|
1519
|
+
// progress snapshot uses the live plan file. Once every canonical step
|
|
1520
|
+
// is complete, remove the approved plan from future system prompts and
|
|
1521
|
+
// clear the widget before run_end paints the idle activity bar.
|
|
1522
|
+
if (runSucceeded &&
|
|
1523
|
+
!cancelled &&
|
|
1524
|
+
approvedPlanPath !== null &&
|
|
1525
|
+
(await queueApprovedPlanProgressSync())) {
|
|
1526
|
+
try {
|
|
1527
|
+
await session.setApprovedPlan(undefined);
|
|
1528
|
+
deactivateApprovedPlan();
|
|
1529
|
+
broadcast("plan_progress", { total: 0, completed: [] });
|
|
1530
|
+
}
|
|
1531
|
+
catch (error) {
|
|
1532
|
+
// Keep tracking when prompt cleanup fails; hiding the widget here
|
|
1533
|
+
// would claim completion while the approved-plan contract remained.
|
|
1534
|
+
log("WARN", "app-sidecar", "completed plan cleanup failed", {
|
|
1535
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
if (ownsGeneration)
|
|
1540
|
+
finishOwnedGeneration(generation, false);
|
|
1541
|
+
// A cancelled injected run is still owned by the surrounding autopilot
|
|
1542
|
+
// cycle; its outer finalizer emits the one terminal cancelled run_end.
|
|
1543
|
+
if (!(cancelled && !ownsGeneration)) {
|
|
1544
|
+
if (cancelled)
|
|
1545
|
+
cancelledRunEndGenerations.add(generation);
|
|
1546
|
+
broadcast("run_end", {
|
|
1547
|
+
...(cancelled ? { cancelled: true } : {}),
|
|
1548
|
+
runState: runLifecycle.state,
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1387
1551
|
// Autopilot's review loop is driven explicitly from POST /prompt (see
|
|
1388
|
-
// runAutopilotCycle), NOT from this shared finally — that keeps
|
|
1389
|
-
//
|
|
1390
|
-
// the loop through the same bracket.
|
|
1391
|
-
// The agent may have marked project tasks done during the run — prune the
|
|
1392
|
-
// completed ones so they drop out of the Tasks modal automatically (users
|
|
1393
|
-
// never have to delete finished tasks by hand).
|
|
1552
|
+
// runAutopilotCycle), NOT from this shared finally — that keeps injected
|
|
1553
|
+
// runs from recursively entering the same review loop.
|
|
1394
1554
|
broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
|
|
1395
|
-
// Queue drains into the run as steering, so it's empty by run_end —
|
|
1396
|
-
// sync the webview indicator.
|
|
1397
1555
|
broadcast("queued", { count: session.getQueuedCount() });
|
|
1398
1556
|
broadcast("extras", footerExtras());
|
|
1399
|
-
// Generate a session title once, after the first run, for the title bar
|
|
1400
|
-
// (best-effort, async — don't block the response).
|
|
1401
|
-
if (!titleGenerated) {
|
|
1402
|
-
titleGenerated = true;
|
|
1403
|
-
void session.generateTitle().then((title) => {
|
|
1404
|
-
if (title)
|
|
1405
|
-
broadcast("session_title", { title });
|
|
1406
|
-
});
|
|
1407
|
-
}
|
|
1408
1557
|
}
|
|
1409
1558
|
}
|
|
1410
1559
|
// ── Autopilot orchestration ─────────────────────────────────
|
|
@@ -1430,7 +1579,8 @@ async function createSession(deps, opts) {
|
|
|
1430
1579
|
return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
|
|
1431
1580
|
}
|
|
1432
1581
|
catch (err) {
|
|
1433
|
-
|
|
1582
|
+
if (!autopilotCancelled)
|
|
1583
|
+
broadcastError("autopilot_error", "autopilot review failed", err);
|
|
1434
1584
|
return null;
|
|
1435
1585
|
}
|
|
1436
1586
|
finally {
|
|
@@ -1505,6 +1655,8 @@ async function createSession(deps, opts) {
|
|
|
1505
1655
|
async function runAutopilotCycle(originalRequest) {
|
|
1506
1656
|
if (!autopilot || autopilotCancelled)
|
|
1507
1657
|
return;
|
|
1658
|
+
const generation = runLifecycle.begin(abortOwnedWork).generation;
|
|
1659
|
+
pendingCancelDrain = null;
|
|
1508
1660
|
autopilotActive = true;
|
|
1509
1661
|
// Generation captured by the last plan review; acceptPlan re-checks it so
|
|
1510
1662
|
// a user Accept/Reject landing mid-review always wins.
|
|
@@ -1532,22 +1684,22 @@ async function createSession(deps, opts) {
|
|
|
1532
1684
|
if (pendingPlanPath === null || planGeneration !== planGenAtReview)
|
|
1533
1685
|
return false;
|
|
1534
1686
|
const planPath = pendingPlanPath;
|
|
1687
|
+
let planTotal;
|
|
1535
1688
|
try {
|
|
1536
|
-
await session.newSession();
|
|
1689
|
+
await session.newSession(true);
|
|
1537
1690
|
injectedAutopilotPrompts = [];
|
|
1538
|
-
|
|
1539
|
-
await session.setApprovedPlan(planPath);
|
|
1691
|
+
planTotal = await activateApprovedPlan(planPath);
|
|
1540
1692
|
}
|
|
1541
1693
|
catch (err) {
|
|
1542
1694
|
broadcastError("autopilot_error", "autopilot plan accept failed", err);
|
|
1543
1695
|
return false;
|
|
1544
1696
|
}
|
|
1545
1697
|
clearPendingPlan();
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
1548
|
-
// session_reset clears it — accepted must land first.
|
|
1698
|
+
// Keep the approval marker ahead of the reset, then seed the reset
|
|
1699
|
+
// with the sidecar's canonical count from the actual plan file.
|
|
1549
1700
|
broadcast("autopilot_plan_accepted", {});
|
|
1550
|
-
broadcast("session_reset", {});
|
|
1701
|
+
broadcast("session_reset", { planTotal });
|
|
1702
|
+
broadcast("plan_progress", planProgressPayload());
|
|
1551
1703
|
// Persisted into the NEW session so a resume shows the marker.
|
|
1552
1704
|
void session.persistAutopilotMarker("plan_approved");
|
|
1553
1705
|
return true;
|
|
@@ -1619,6 +1771,8 @@ async function createSession(deps, opts) {
|
|
|
1619
1771
|
}
|
|
1620
1772
|
finally {
|
|
1621
1773
|
autopilotActive = false;
|
|
1774
|
+
finishOwnedGeneration(generation, true);
|
|
1775
|
+
queueMicrotask(() => void runStrandedQueue());
|
|
1622
1776
|
}
|
|
1623
1777
|
}
|
|
1624
1778
|
// ── Stranded-queue drain ───────────────────────────────
|
|
@@ -1703,9 +1857,9 @@ async function createSession(deps, opts) {
|
|
|
1703
1857
|
return false;
|
|
1704
1858
|
// Fresh session per task so one task's context never bleeds into the next.
|
|
1705
1859
|
await session.newSession();
|
|
1860
|
+
deactivateApprovedPlan();
|
|
1706
1861
|
injectedAutopilotPrompts = [];
|
|
1707
1862
|
clearPendingPlan();
|
|
1708
|
-
titleGenerated = false;
|
|
1709
1863
|
broadcast("session_reset", {});
|
|
1710
1864
|
markTaskInProgress(cwd, task.id);
|
|
1711
1865
|
broadcast("tasks_list", { tasks: loadTasksSync(cwd) });
|
|
@@ -1786,6 +1940,30 @@ async function createSession(deps, opts) {
|
|
|
1786
1940
|
tasksPoll.unref?.();
|
|
1787
1941
|
};
|
|
1788
1942
|
scheduleTasksPoll(1500);
|
|
1943
|
+
// Files can change outside the agent (editor saves, terminal commits), so keep
|
|
1944
|
+
// the dirty count current while idle. Branch/repo state already refreshes after
|
|
1945
|
+
// agent runs; polling only the count avoids spawning three git processes per tick.
|
|
1946
|
+
let gitPoll;
|
|
1947
|
+
let gitPollStopped = false;
|
|
1948
|
+
const scheduleGitPoll = (delay) => {
|
|
1949
|
+
if (gitPollStopped)
|
|
1950
|
+
return;
|
|
1951
|
+
gitPoll = setTimeout(() => {
|
|
1952
|
+
void getGitDirtyFileCount(cwd)
|
|
1953
|
+
.catch(() => gitDirtyFileCount)
|
|
1954
|
+
.then((nextDirtyFileCount) => {
|
|
1955
|
+
if (gitPollStopped)
|
|
1956
|
+
return;
|
|
1957
|
+
if (nextDirtyFileCount !== gitDirtyFileCount) {
|
|
1958
|
+
gitDirtyFileCount = nextDirtyFileCount;
|
|
1959
|
+
broadcast("extras", footerExtras());
|
|
1960
|
+
}
|
|
1961
|
+
scheduleGitPoll(5000);
|
|
1962
|
+
});
|
|
1963
|
+
}, delay);
|
|
1964
|
+
gitPoll.unref?.();
|
|
1965
|
+
};
|
|
1966
|
+
scheduleGitPoll(5000);
|
|
1789
1967
|
function readBody(req) {
|
|
1790
1968
|
return new Promise((resolve, reject) => {
|
|
1791
1969
|
const chunks = [];
|
|
@@ -1795,7 +1973,7 @@ async function createSession(deps, opts) {
|
|
|
1795
1973
|
});
|
|
1796
1974
|
}
|
|
1797
1975
|
function json(res, status, body) {
|
|
1798
|
-
const payload = JSON.stringify(body);
|
|
1976
|
+
const payload = JSON.stringify(redactValue(body, { secrets: environmentSecrets(process.env) }));
|
|
1799
1977
|
res.writeHead(status, {
|
|
1800
1978
|
"content-type": "application/json",
|
|
1801
1979
|
"access-control-allow-origin": "*",
|
|
@@ -1811,6 +1989,7 @@ async function createSession(deps, opts) {
|
|
|
1811
1989
|
mode,
|
|
1812
1990
|
chatAgent,
|
|
1813
1991
|
running,
|
|
1992
|
+
runState: runLifecycle.state,
|
|
1814
1993
|
ready: true,
|
|
1815
1994
|
thinkingLevel: session.getThinkingLevel() ?? null,
|
|
1816
1995
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
@@ -1883,6 +2062,7 @@ async function createSession(deps, opts) {
|
|
|
1883
2062
|
mode,
|
|
1884
2063
|
chatAgent,
|
|
1885
2064
|
running,
|
|
2065
|
+
runState: runLifecycle.state,
|
|
1886
2066
|
thinkingLevel: session.getThinkingLevel() ?? null,
|
|
1887
2067
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
1888
2068
|
supportsVideo: getModel(st.model)?.supportsVideo ?? false,
|
|
@@ -2378,6 +2558,13 @@ async function createSession(deps, opts) {
|
|
|
2378
2558
|
json(res, 400, { error: "empty prompt" });
|
|
2379
2559
|
return;
|
|
2380
2560
|
}
|
|
2561
|
+
if (runLifecycle.running && runLifecycle.isCancellationRequested(runLifecycle.generation)) {
|
|
2562
|
+
json(res, 409, {
|
|
2563
|
+
error: runLifecycle.state === "cancelling" ? "run_cancelling" : "cancel_failed",
|
|
2564
|
+
runState: runLifecycle.state,
|
|
2565
|
+
});
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2381
2568
|
if (running || autopilotActive) {
|
|
2382
2569
|
// Queue prompts as mid-run steering (mirrors the CLI). Also queue while
|
|
2383
2570
|
// an autopilot cycle is active but between injected runs (build idle,
|
|
@@ -2866,26 +3053,50 @@ async function createSession(deps, opts) {
|
|
|
2866
3053
|
return;
|
|
2867
3054
|
}
|
|
2868
3055
|
if (method === "POST" && url === "/cancel") {
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
3056
|
+
void (async () => {
|
|
3057
|
+
// Even between task runs, cancellation stops the sweep. Active provider
|
|
3058
|
+
// ownership invokes the full abort hook exactly once through lifecycle.
|
|
3059
|
+
taskRunAll = false;
|
|
3060
|
+
autopilotCancelled = true;
|
|
3061
|
+
if (!runLifecycle.running) {
|
|
3062
|
+
kenAutoAbort.abort();
|
|
3063
|
+
kenAutoAbort = new AbortController();
|
|
3064
|
+
kenAutoSession?.setSignal(kenAutoAbort.signal);
|
|
3065
|
+
}
|
|
3066
|
+
const generation = runLifecycle.generation;
|
|
3067
|
+
if (!pendingCancelDrain || pendingCancelDrain.generation !== generation) {
|
|
3068
|
+
pendingCancelDrain = { generation, text: session.drainQueue() };
|
|
3069
|
+
broadcast("queued", { count: 0 });
|
|
3070
|
+
}
|
|
3071
|
+
const result = await runLifecycle.cancel(CANCEL_TIMEOUT_MS);
|
|
3072
|
+
const drained = pendingCancelDrain.text;
|
|
3073
|
+
if (result.status === "failed") {
|
|
3074
|
+
broadcast("cancel_failed", {
|
|
3075
|
+
error: "cancel_failed",
|
|
3076
|
+
reason: result.reason,
|
|
3077
|
+
runState: runLifecycle.state,
|
|
3078
|
+
});
|
|
3079
|
+
json(res, 504, {
|
|
3080
|
+
error: "cancel_failed",
|
|
3081
|
+
reason: result.reason,
|
|
3082
|
+
runState: runLifecycle.state,
|
|
3083
|
+
drained,
|
|
3084
|
+
});
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
json(res, 200, {
|
|
3088
|
+
cancelled: result.status === "cancelled",
|
|
3089
|
+
runState: runLifecycle.state,
|
|
3090
|
+
drained,
|
|
3091
|
+
});
|
|
3092
|
+
})().catch((error) => {
|
|
3093
|
+
broadcast("cancel_failed", { error: "cancel_failed", runState: runLifecycle.state });
|
|
3094
|
+
json(res, 500, {
|
|
3095
|
+
error: "cancel_failed",
|
|
3096
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3097
|
+
runState: runLifecycle.state,
|
|
3098
|
+
});
|
|
3099
|
+
});
|
|
2889
3100
|
return;
|
|
2890
3101
|
}
|
|
2891
3102
|
if (method === "POST" && url === "/new-session") {
|
|
@@ -2899,6 +3110,7 @@ async function createSession(deps, opts) {
|
|
|
2899
3110
|
if (mode === "chat") {
|
|
2900
3111
|
await session.persistAppMarker("agent_handoff", { chatAgent });
|
|
2901
3112
|
}
|
|
3113
|
+
deactivateApprovedPlan();
|
|
2902
3114
|
injectedAutopilotPrompts = [];
|
|
2903
3115
|
clearPendingPlan();
|
|
2904
3116
|
broadcast("session_reset", {});
|
|
@@ -2951,12 +3163,12 @@ async function createSession(deps, opts) {
|
|
|
2951
3163
|
broadcast("autopilot_ignored", {});
|
|
2952
3164
|
}
|
|
2953
3165
|
try {
|
|
2954
|
-
await session.newSession();
|
|
3166
|
+
await session.newSession(true);
|
|
2955
3167
|
injectedAutopilotPrompts = [];
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
broadcast("
|
|
2959
|
-
json(res, 200, { ok: true });
|
|
3168
|
+
const planTotal = await activateApprovedPlan(planPath);
|
|
3169
|
+
broadcast("session_reset", { planTotal });
|
|
3170
|
+
broadcast("plan_progress", planProgressPayload());
|
|
3171
|
+
json(res, 200, { ok: true, planTotal });
|
|
2960
3172
|
}
|
|
2961
3173
|
catch (err) {
|
|
2962
3174
|
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -3384,6 +3596,9 @@ async function createSession(deps, opts) {
|
|
|
3384
3596
|
tasksPollStopped = true;
|
|
3385
3597
|
if (tasksPoll)
|
|
3386
3598
|
clearTimeout(tasksPoll);
|
|
3599
|
+
gitPollStopped = true;
|
|
3600
|
+
if (gitPoll)
|
|
3601
|
+
clearTimeout(gitPoll);
|
|
3387
3602
|
// Stop the Telegram serve loop + dispose its per-chat sessions.
|
|
3388
3603
|
if (serveController)
|
|
3389
3604
|
await serveController.stop().catch(() => { });
|