@prestyj/cli 5.6.0 → 5.8.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 +369 -92
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +5 -2
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/shared.d.ts.map +1 -1
- package/dist/cli/shared.js +2 -0
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +33 -3
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +7 -0
- package/dist/config.test.js.map +1 -1
- package/dist/core/agent-session-compaction.test.js +521 -10
- package/dist/core/agent-session-compaction.test.js.map +1 -1
- package/dist/core/agent-session-memory-tail.test.js +1 -1
- package/dist/core/agent-session-memory-tail.test.js.map +1 -1
- package/dist/core/agent-session-queue.test.js +1 -1
- package/dist/core/agent-session-queue.test.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 +83 -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 +25 -0
- package/dist/core/agent-session-tool-result-policy.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +51 -12
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +415 -106
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-providers.d.ts.map +1 -1
- package/dist/core/auth-providers.js +15 -8
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/compaction/active-context.d.ts +17 -0
- package/dist/core/compaction/active-context.d.ts.map +1 -0
- package/dist/core/compaction/active-context.js +20 -0
- package/dist/core/compaction/active-context.js.map +1 -0
- package/dist/core/compaction/active-context.test.d.ts +2 -0
- package/dist/core/compaction/active-context.test.d.ts.map +1 -0
- package/dist/core/compaction/active-context.test.js +36 -0
- package/dist/core/compaction/active-context.test.js.map +1 -0
- package/dist/core/compaction/compactor.d.ts +19 -18
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +64 -42
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +122 -65
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/compaction/token-estimator.d.ts +25 -1
- package/dist/core/compaction/token-estimator.d.ts.map +1 -1
- package/dist/core/compaction/token-estimator.js +86 -1
- package/dist/core/compaction/token-estimator.js.map +1 -1
- package/dist/core/compaction/token-estimator.test.js +147 -1
- package/dist/core/compaction/token-estimator.test.js.map +1 -1
- package/dist/core/compaction/tool-result-pruner.d.ts +37 -0
- package/dist/core/compaction/tool-result-pruner.d.ts.map +1 -0
- package/dist/core/compaction/tool-result-pruner.js +103 -0
- package/dist/core/compaction/tool-result-pruner.js.map +1 -0
- package/dist/core/compaction/tool-result-pruner.test.d.ts +2 -0
- package/dist/core/compaction/tool-result-pruner.test.d.ts.map +1 -0
- package/dist/core/compaction/tool-result-pruner.test.js +148 -0
- package/dist/core/compaction/tool-result-pruner.test.js.map +1 -0
- 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 +33 -0
- package/dist/core/ideal-review.d.ts.map +1 -1
- package/dist/core/ideal-review.js +78 -0
- package/dist/core/ideal-review.js.map +1 -1
- package/dist/core/ideal-review.test.js +43 -1
- package/dist/core/ideal-review.test.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.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 +6 -9
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +35 -28
- 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/resolve-start.test.js +1 -0
- package/dist/core/resolve-start.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/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +3 -2
- package/dist/core/settings-manager.js.map +1 -1
- 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 +201 -5
- 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/bash.d.ts +9 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +28 -18
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/bash.test.d.ts +2 -0
- package/dist/tools/bash.test.d.ts.map +1 -0
- package/dist/tools/bash.test.js +56 -0
- package/dist/tools/bash.test.js.map +1 -0
- package/dist/tools/goals.d.ts +3 -3
- package/dist/tools/overflow.d.ts +12 -2
- package/dist/tools/overflow.d.ts.map +1 -1
- package/dist/tools/overflow.js +49 -4
- package/dist/tools/overflow.js.map +1 -1
- package/dist/tools/overflow.test.d.ts +2 -0
- package/dist/tools/overflow.test.d.ts.map +1 -0
- package/dist/tools/overflow.test.js +62 -0
- package/dist/tools/overflow.test.js.map +1 -0
- 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 +2 -2
- 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 +68 -98
- 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/ModelSelector.d.ts.map +1 -1
- package/dist/ui/components/ModelSelector.js +1 -0
- package/dist/ui/components/ModelSelector.js.map +1 -1
- 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 +9 -6
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +68 -8
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.test.js +31 -0
- package/dist/ui/hooks/useAgentLoop.test.js.map +1 -1
- package/dist/ui/hooks/useContextCompaction.d.ts +5 -8
- package/dist/ui/hooks/useContextCompaction.d.ts.map +1 -1
- package/dist/ui/hooks/useContextCompaction.js +77 -16
- package/dist/ui/hooks/useContextCompaction.js.map +1 -1
- package/dist/ui/hooks/useContextCompaction.test.d.ts +2 -0
- package/dist/ui/hooks/useContextCompaction.test.d.ts.map +1 -0
- package/dist/ui/hooks/useContextCompaction.test.js +135 -0
- package/dist/ui/hooks/useContextCompaction.test.js.map +1 -0
- 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 +7 -2
- 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 +5 -5
- 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/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 "@prestyj/ai";
|
|
21
|
+
import { environmentSecrets, formatError, redactValue } from "@prestyj/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";
|
|
@@ -33,6 +34,7 @@ import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-c
|
|
|
33
34
|
import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
|
|
34
35
|
import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
|
|
35
36
|
import { AuthStorage } from "./core/auth-storage.js";
|
|
37
|
+
import { cleanupToolOutputs } from "./tools/overflow.js";
|
|
36
38
|
import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, } from "@prestyj/core";
|
|
37
39
|
import { loginAnthropic } from "./core/oauth/anthropic.js";
|
|
38
40
|
import { loginOpenAI } from "./core/oauth/openai.js";
|
|
@@ -43,7 +45,8 @@ import { ensureAppDirs, loadSavedSettings } from "./config.js";
|
|
|
43
45
|
import { SettingsManager } from "./core/settings-manager.js";
|
|
44
46
|
import { getModel, getMaxThinkingLevel, getContextWindow, MODELS } from "./core/model-registry.js";
|
|
45
47
|
import { resolveStartOrFallback } from "./core/resolve-start.js";
|
|
46
|
-
import { getGitBranch, isGitRepo } from "./utils/git.js";
|
|
48
|
+
import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
|
|
49
|
+
import { extractPlanSteps } from "./utils/plan-steps.js";
|
|
47
50
|
import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSupported, } from "./core/thinking-level.js";
|
|
48
51
|
import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
|
|
49
52
|
import { loadCustomCommands } from "./core/custom-commands.js";
|
|
@@ -62,16 +65,20 @@ import { awardPrompt, awardCommits } from "./core/progress/engine.js";
|
|
|
62
65
|
import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
|
|
63
66
|
import { rebuildFromSessions } from "./core/progress/rebuild.js";
|
|
64
67
|
const ALL_PROVIDERS = [
|
|
68
|
+
// US
|
|
65
69
|
"anthropic",
|
|
66
|
-
"xiaomi",
|
|
67
70
|
"openai",
|
|
68
71
|
"gemini",
|
|
69
|
-
"
|
|
72
|
+
"xai",
|
|
73
|
+
// China
|
|
70
74
|
"moonshot",
|
|
75
|
+
"glm",
|
|
71
76
|
"minimax",
|
|
77
|
+
"xiaomi",
|
|
72
78
|
"deepseek",
|
|
73
|
-
|
|
79
|
+
// Japan, then provider-agnostic gateway last
|
|
74
80
|
"sakana",
|
|
81
|
+
"openrouter",
|
|
75
82
|
];
|
|
76
83
|
function appSettingsFile() {
|
|
77
84
|
return path.join(os.homedir(), ".ezcoder", "ezcoder-app.json");
|
|
@@ -504,6 +511,9 @@ async function main() {
|
|
|
504
511
|
// before anything spawns (bash tool, background tasks, LSP, git helpers all
|
|
505
512
|
// inherit it). Best-effort — never blocks startup beyond its internal cap.
|
|
506
513
|
await enrichProcessPath();
|
|
514
|
+
// Sweep recoverable full tool outputs (~/.ezcoder/tool-output/) older than 48h.
|
|
515
|
+
// Fire-and-forget: cleanup must never delay or break startup.
|
|
516
|
+
void cleanupToolOutputs().catch(() => { });
|
|
507
517
|
const auth = new AuthStorage(paths.authFile);
|
|
508
518
|
await auth.load();
|
|
509
519
|
// Every window's session lives here as an in-process object, keyed by the id
|
|
@@ -534,6 +544,11 @@ async function main() {
|
|
|
534
544
|
});
|
|
535
545
|
const usageCache = new Map();
|
|
536
546
|
const usageRequests = new Map();
|
|
547
|
+
// 429 backoff: when the provider rate-limits the usage endpoint, hold the
|
|
548
|
+
// error result until this timestamp instead of re-polling (and re-logging a
|
|
549
|
+
// WARN) every 60s — the old cadence hammered a limited endpoint for hours.
|
|
550
|
+
const usageRateLimitedUntil = new Map();
|
|
551
|
+
const USAGE_RATE_LIMIT_BACKOFF_MS = 5 * 60_000;
|
|
537
552
|
async function fetchUsageProvider(provider) {
|
|
538
553
|
const displayName = provider === "anthropic" ? "Anthropic" : "Codex";
|
|
539
554
|
if (!(await auth.hasProviderAuth(provider))) {
|
|
@@ -542,14 +557,24 @@ async function main() {
|
|
|
542
557
|
try {
|
|
543
558
|
let credentials = await auth.resolveCredentials(provider);
|
|
544
559
|
try {
|
|
545
|
-
|
|
560
|
+
const snapshot = {
|
|
561
|
+
...(await fetchSubscriptionUsage(provider, credentials)),
|
|
562
|
+
connected: true,
|
|
563
|
+
};
|
|
564
|
+
usageRateLimitedUntil.delete(provider);
|
|
565
|
+
return snapshot;
|
|
546
566
|
}
|
|
547
567
|
catch (error) {
|
|
548
568
|
// A provider can revoke an access token before its stored expiry. Refresh
|
|
549
569
|
// once on 401, matching inference auth recovery, then retry the usage call.
|
|
550
570
|
if (error instanceof SubscriptionUsageError && error.status === 401) {
|
|
551
571
|
credentials = await auth.resolveCredentials(provider, { forceRefresh: true });
|
|
552
|
-
|
|
572
|
+
const snapshot = {
|
|
573
|
+
...(await fetchSubscriptionUsage(provider, credentials)),
|
|
574
|
+
connected: true,
|
|
575
|
+
};
|
|
576
|
+
usageRateLimitedUntil.delete(provider);
|
|
577
|
+
return snapshot;
|
|
553
578
|
}
|
|
554
579
|
throw error;
|
|
555
580
|
}
|
|
@@ -557,6 +582,9 @@ async function main() {
|
|
|
557
582
|
catch (error) {
|
|
558
583
|
const message = error instanceof Error ? error.message : String(error);
|
|
559
584
|
log("WARN", "app-sidecar", "subscription usage fetch failed", { provider, message });
|
|
585
|
+
if (error instanceof SubscriptionUsageError && error.status === 429) {
|
|
586
|
+
usageRateLimitedUntil.set(provider, Date.now() + USAGE_RATE_LIMIT_BACKOFF_MS);
|
|
587
|
+
}
|
|
560
588
|
const connected = await auth.hasProviderAuth(provider);
|
|
561
589
|
return {
|
|
562
590
|
provider,
|
|
@@ -585,9 +613,12 @@ async function main() {
|
|
|
585
613
|
const missingReset = result.connected &&
|
|
586
614
|
result.windows.length > 0 &&
|
|
587
615
|
result.windows.some((window) => window.resetsAt === undefined);
|
|
616
|
+
const rateLimitedUntil = usageRateLimitedUntil.get(provider) ?? 0;
|
|
588
617
|
usageCache.set(provider, {
|
|
589
618
|
result,
|
|
590
|
-
expiresAt: Date.now()
|
|
619
|
+
expiresAt: rateLimitedUntil > Date.now()
|
|
620
|
+
? rateLimitedUntil
|
|
621
|
+
: Date.now() + (missingReset ? 10_000 : 60_000),
|
|
591
622
|
});
|
|
592
623
|
return result;
|
|
593
624
|
}
|
|
@@ -929,7 +960,8 @@ async function createSession(deps, opts) {
|
|
|
929
960
|
const clients = new Set();
|
|
930
961
|
let clientSeq = 0;
|
|
931
962
|
function broadcast(type, data) {
|
|
932
|
-
const
|
|
963
|
+
const safePayload = redactValue({ type, data }, { secrets: environmentSecrets(process.env) });
|
|
964
|
+
const frame = `data: ${JSON.stringify(safePayload)}\n\n`;
|
|
933
965
|
for (const c of clients)
|
|
934
966
|
c.res.write(frame);
|
|
935
967
|
}
|
|
@@ -1009,6 +1041,11 @@ async function createSession(deps, opts) {
|
|
|
1009
1041
|
signal: abort.signal,
|
|
1010
1042
|
// Keep MCP startup off the readiness path in both modes.
|
|
1011
1043
|
backgroundMcpConnect: true,
|
|
1044
|
+
// Keep restore-time auto-compaction off the readiness path too: its summary
|
|
1045
|
+
// LLM call (30s timeout) used to freeze waitForReady — and with it the whole
|
|
1046
|
+
// window (project picker, session list) — whenever a resumed session was
|
|
1047
|
+
// over the context threshold. First prompt compacts instead, with UI events.
|
|
1048
|
+
deferLoadCompaction: true,
|
|
1012
1049
|
};
|
|
1013
1050
|
let session;
|
|
1014
1051
|
if (mode === "chat") {
|
|
@@ -1036,7 +1073,9 @@ async function createSession(deps, opts) {
|
|
|
1036
1073
|
// Unattended task runs cannot approve plans; implement directly instead.
|
|
1037
1074
|
if (taskRunActive)
|
|
1038
1075
|
return false;
|
|
1076
|
+
deactivateApprovedPlan();
|
|
1039
1077
|
await session.setPlanMode(true);
|
|
1078
|
+
broadcast("plan_progress", { total: 0, completed: [] });
|
|
1040
1079
|
broadcast("plan_enter", { reason: reason ?? "" });
|
|
1041
1080
|
void session.persistAppMarker("plan", { reason: reason ?? "" }).catch(() => { });
|
|
1042
1081
|
},
|
|
@@ -1071,11 +1110,17 @@ async function createSession(deps, opts) {
|
|
|
1071
1110
|
}
|
|
1072
1111
|
}
|
|
1073
1112
|
log("INFO", "app-sidecar", "session ready", { provider, model, mode, chatAgent, cwd });
|
|
1074
|
-
//
|
|
1075
|
-
//
|
|
1113
|
+
// Workspace extras (context window, git status, background tasks). Git state
|
|
1114
|
+
// is resolved once at startup and refreshed after every run; the context
|
|
1076
1115
|
// window follows the active model.
|
|
1077
|
-
|
|
1078
|
-
|
|
1116
|
+
const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount] = await Promise.all([
|
|
1117
|
+
getGitBranch(cwd).catch(() => null),
|
|
1118
|
+
isGitRepo(cwd).catch(() => false),
|
|
1119
|
+
getGitDirtyFileCount(cwd).catch(() => 0),
|
|
1120
|
+
]);
|
|
1121
|
+
let gitBranch = initialGitBranch;
|
|
1122
|
+
let gitIsRepo = initialGitIsRepo;
|
|
1123
|
+
let gitDirtyFileCount = initialDirtyFileCount;
|
|
1079
1124
|
function currentContextWindow() {
|
|
1080
1125
|
const st = session.getState();
|
|
1081
1126
|
return getContextWindow(st.model, { provider: st.provider, accountId: st.accountId });
|
|
@@ -1087,6 +1132,7 @@ async function createSession(deps, opts) {
|
|
|
1087
1132
|
contextWindow: currentContextWindow(),
|
|
1088
1133
|
gitBranch,
|
|
1089
1134
|
isGitRepo: gitIsRepo,
|
|
1135
|
+
gitDirtyFileCount,
|
|
1090
1136
|
tasks: session.listBackgroundProcesses(),
|
|
1091
1137
|
};
|
|
1092
1138
|
}
|
|
@@ -1096,8 +1142,93 @@ async function createSession(deps, opts) {
|
|
|
1096
1142
|
// failures (e.g. repeated invalid-argument errors) instead of leaving the
|
|
1097
1143
|
// fatal-abort path with no forensic trail.
|
|
1098
1144
|
const toolCallNames = new Map();
|
|
1145
|
+
// Approved-plan progress belongs beside the plan file, not in the webview.
|
|
1146
|
+
// The implementation can rewrite/expand that file mid-run, so a step count
|
|
1147
|
+
// frozen at approval time becomes dishonest (the exact stale-total bug the
|
|
1148
|
+
// CLI already fixed). Re-read the live file after tools/markers, retain the
|
|
1149
|
+
// last valid step section during transient edits, and send one authoritative
|
|
1150
|
+
// snapshot to the app.
|
|
1151
|
+
let approvedPlanPath = null;
|
|
1152
|
+
let approvedPlanTotal = 0;
|
|
1153
|
+
let approvedPlanMarkers = new Set();
|
|
1154
|
+
let approvedPlanGeneration = 0;
|
|
1155
|
+
let planMarkerTail = "";
|
|
1156
|
+
let planProgressSync = Promise.resolve(false);
|
|
1157
|
+
function planProgressPayload() {
|
|
1158
|
+
const completed = [...approvedPlanMarkers]
|
|
1159
|
+
.filter((step) => step >= 1 && step <= approvedPlanTotal)
|
|
1160
|
+
.sort((a, b) => a - b);
|
|
1161
|
+
return { total: approvedPlanTotal, completed };
|
|
1162
|
+
}
|
|
1163
|
+
async function syncApprovedPlanProgress(generation) {
|
|
1164
|
+
const planPath = approvedPlanPath;
|
|
1165
|
+
if (planPath === null || generation !== approvedPlanGeneration)
|
|
1166
|
+
return false;
|
|
1167
|
+
const content = await fs.readFile(planPath, "utf-8").catch(() => null);
|
|
1168
|
+
if (approvedPlanPath !== planPath || generation !== approvedPlanGeneration)
|
|
1169
|
+
return false;
|
|
1170
|
+
if (content !== null) {
|
|
1171
|
+
const freshTotal = extractPlanSteps(content).length;
|
|
1172
|
+
// During an in-place rewrite the step section can briefly disappear.
|
|
1173
|
+
// Keep the last real total instead of flashing 0 or declaring completion.
|
|
1174
|
+
if (freshTotal > 0 || approvedPlanTotal === 0)
|
|
1175
|
+
approvedPlanTotal = freshTotal;
|
|
1176
|
+
}
|
|
1177
|
+
broadcast("plan_progress", planProgressPayload());
|
|
1178
|
+
return (approvedPlanTotal > 0 &&
|
|
1179
|
+
Array.from({ length: approvedPlanTotal }, (_, index) => index + 1).every((step) => approvedPlanMarkers.has(step)));
|
|
1180
|
+
}
|
|
1181
|
+
function queueApprovedPlanProgressSync() {
|
|
1182
|
+
const generation = approvedPlanGeneration;
|
|
1183
|
+
planProgressSync = planProgressSync
|
|
1184
|
+
.catch(() => false)
|
|
1185
|
+
.then(() => syncApprovedPlanProgress(generation))
|
|
1186
|
+
.catch((error) => {
|
|
1187
|
+
log("WARN", "app-sidecar", "plan progress refresh failed", {
|
|
1188
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1189
|
+
});
|
|
1190
|
+
return false;
|
|
1191
|
+
});
|
|
1192
|
+
return planProgressSync;
|
|
1193
|
+
}
|
|
1194
|
+
async function activateApprovedPlan(planPath) {
|
|
1195
|
+
deactivateApprovedPlan();
|
|
1196
|
+
await session.setApprovedPlan(planPath);
|
|
1197
|
+
if (!planPath)
|
|
1198
|
+
return 0;
|
|
1199
|
+
approvedPlanPath = planPath;
|
|
1200
|
+
await queueApprovedPlanProgressSync();
|
|
1201
|
+
return approvedPlanTotal;
|
|
1202
|
+
}
|
|
1203
|
+
function deactivateApprovedPlan() {
|
|
1204
|
+
approvedPlanGeneration++;
|
|
1205
|
+
approvedPlanPath = null;
|
|
1206
|
+
approvedPlanTotal = 0;
|
|
1207
|
+
approvedPlanMarkers = new Set();
|
|
1208
|
+
planMarkerTail = "";
|
|
1209
|
+
planProgressSync = Promise.resolve(false);
|
|
1210
|
+
}
|
|
1211
|
+
function recordApprovedPlanMarkers(text) {
|
|
1212
|
+
if (approvedPlanPath === null || !text)
|
|
1213
|
+
return;
|
|
1214
|
+
const candidate = planMarkerTail + text;
|
|
1215
|
+
let changed = false;
|
|
1216
|
+
for (const match of candidate.matchAll(/\[DONE:(\d+)\]/gi)) {
|
|
1217
|
+
const step = Number.parseInt(match[1], 10);
|
|
1218
|
+
if (step >= 1 && !approvedPlanMarkers.has(step)) {
|
|
1219
|
+
approvedPlanMarkers.add(step);
|
|
1220
|
+
changed = true;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
planMarkerTail = candidate.slice(-32);
|
|
1224
|
+
if (changed)
|
|
1225
|
+
void queueApprovedPlanProgressSync();
|
|
1226
|
+
}
|
|
1099
1227
|
// Forward every relevant bus event to the webview.
|
|
1100
|
-
session.eventBus.on("text_delta", (d) =>
|
|
1228
|
+
session.eventBus.on("text_delta", (d) => {
|
|
1229
|
+
broadcast("text_delta", d);
|
|
1230
|
+
recordApprovedPlanMarkers(d.text);
|
|
1231
|
+
});
|
|
1101
1232
|
session.eventBus.on("thinking_delta", (d) => broadcast("thinking_delta", d));
|
|
1102
1233
|
session.eventBus.on("tool_call_start", (d) => {
|
|
1103
1234
|
toolCallNames.set(d.toolCallId, d.name);
|
|
@@ -1120,6 +1251,11 @@ async function createSession(deps, opts) {
|
|
|
1120
1251
|
if (name === "tasks") {
|
|
1121
1252
|
broadcast("tasks_list", { tasks: loadTasksSync(cwd) });
|
|
1122
1253
|
}
|
|
1254
|
+
// Any tool can mutate the approved plan (including bash), so refresh after
|
|
1255
|
+
// every completed call while tracking is active. The file is tiny and this
|
|
1256
|
+
// keeps the displayed total aligned before the next completion marker.
|
|
1257
|
+
if (approvedPlanPath !== null)
|
|
1258
|
+
void queueApprovedPlanProgressSync();
|
|
1123
1259
|
});
|
|
1124
1260
|
// Native server tools (e.g. Anthropic web_search) do NOT end the turn — text
|
|
1125
1261
|
// streams before and after them in the SAME turn. The webview must reset its
|
|
@@ -1137,15 +1273,23 @@ async function createSession(deps, opts) {
|
|
|
1137
1273
|
session.eventBus.on("compaction_start", (d) => broadcast("compaction_start", d));
|
|
1138
1274
|
session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
|
|
1139
1275
|
let running = false;
|
|
1140
|
-
|
|
1276
|
+
const runLifecycle = new RunLifecycle((runState) => {
|
|
1277
|
+
running = runState !== "idle";
|
|
1278
|
+
if (runState === "cancelling")
|
|
1279
|
+
broadcast("run_cancelling", { runState });
|
|
1280
|
+
});
|
|
1281
|
+
const cancelledRunEndGenerations = new Set();
|
|
1282
|
+
let pendingCancelDrain = null;
|
|
1141
1283
|
// Bumped by /cancel — a run whose cancel generation changed mid-flight was
|
|
1142
1284
|
// canceled and earns no XP.
|
|
1143
1285
|
let cancelGeneration = 0;
|
|
1144
1286
|
// Autopilot (auto-review) toggle for THIS window's project. Loaded from
|
|
1145
1287
|
// ezcoder-app.json on boot; flipped via POST /autopilot. When on, POST /prompt runs
|
|
1146
1288
|
// runAutopilotCycle after the user's turn settles — Nolan auto-reviews the work
|
|
1147
|
-
// and drives the review→prompt→review loop.
|
|
1289
|
+
// and drives the review→prompt→review loop. Nolan is the sole verification
|
|
1290
|
+
// owner in this mode, so suppress the build session's redundant Ideal hook.
|
|
1148
1291
|
let autopilot = mode === "code" && (await loadAutopilot(cwd));
|
|
1292
|
+
session.setIdealReviewSuppressed(autopilot);
|
|
1149
1293
|
// True while an autopilot review is in flight (used to defer nolanAuto model
|
|
1150
1294
|
// switches, like nolanRunning does for chat Nolan, and to drive the spinner).
|
|
1151
1295
|
let autopilotReviewing = false;
|
|
@@ -1159,6 +1303,7 @@ async function createSession(deps, opts) {
|
|
|
1159
1303
|
let autopilotCancelled = false;
|
|
1160
1304
|
// Hard cap on review→prompt→review rounds per user turn (loop safety).
|
|
1161
1305
|
const MAX_AUTOPILOT_ROUNDS = 3;
|
|
1306
|
+
const CANCEL_TIMEOUT_MS = 5_000;
|
|
1162
1307
|
// Prompt bodies Autopilot Nolan injected into the BUILD session this
|
|
1163
1308
|
// conversation. Passed into every Nolan digest so injected prompts render as
|
|
1164
1309
|
// "Nolan autopilot (injected)" instead of `**User:**` — otherwise multi-round
|
|
@@ -1341,6 +1486,9 @@ async function createSession(deps, opts) {
|
|
|
1341
1486
|
// (often >5 min) regardless of the user's global speedProfile pick.
|
|
1342
1487
|
forceLongCacheRetention: true,
|
|
1343
1488
|
});
|
|
1489
|
+
// Nolan is already the independent autopilot reviewer; recursively running
|
|
1490
|
+
// his own Ideal self-review adds latency and can corrupt the verdict shape.
|
|
1491
|
+
ken.setIdealReviewSuppressed(true);
|
|
1344
1492
|
await ken.initialize();
|
|
1345
1493
|
// Deliberately no bus bridge: the review is silent. Errors surface via the
|
|
1346
1494
|
// runAutopilotReview try/catch as autopilot_error frames.
|
|
@@ -1351,53 +1499,108 @@ async function createSession(deps, opts) {
|
|
|
1351
1499
|
});
|
|
1352
1500
|
return ken;
|
|
1353
1501
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1502
|
+
function abortOwnedWork() {
|
|
1503
|
+
cancelGeneration++;
|
|
1504
|
+
abort.abort();
|
|
1505
|
+
// Stop a run-all sweep and every async child through AgentSession's signal.
|
|
1506
|
+
taskRunAll = false;
|
|
1507
|
+
autopilotCancelled = true;
|
|
1508
|
+
nolanAutoAbort.abort();
|
|
1509
|
+
}
|
|
1510
|
+
function installFreshRunControllers() {
|
|
1511
|
+
abort = new AbortController();
|
|
1512
|
+
session.setSignal(abort.signal);
|
|
1513
|
+
nolanAutoAbort = new AbortController();
|
|
1514
|
+
nolanAutoSession?.setSignal(nolanAutoAbort.signal);
|
|
1367
1515
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1516
|
+
function finishOwnedGeneration(generation, emitCancelledFallback) {
|
|
1517
|
+
const cancelled = runLifecycle.isCancellationRequested(generation);
|
|
1518
|
+
const settlement = runLifecycle.settle(generation);
|
|
1519
|
+
if (!settlement.settled)
|
|
1520
|
+
return cancelled;
|
|
1521
|
+
// A replacement signal is safe only after the provider-backed owner settled.
|
|
1522
|
+
installFreshRunControllers();
|
|
1523
|
+
if (cancelled && emitCancelledFallback && !cancelledRunEndGenerations.has(generation)) {
|
|
1524
|
+
cancelledRunEndGenerations.add(generation);
|
|
1525
|
+
broadcast("run_end", { cancelled: true, runState: runLifecycle.state });
|
|
1526
|
+
}
|
|
1527
|
+
return cancelled;
|
|
1528
|
+
}
|
|
1529
|
+
// Core provider-run bracket. Standalone runs own a lifecycle generation;
|
|
1530
|
+
// injected autopilot runs share the cycle's outer generation.
|
|
1372
1531
|
async function runAgent(label, run) {
|
|
1373
|
-
|
|
1532
|
+
const ownsGeneration = !runLifecycle.running;
|
|
1533
|
+
const generation = ownsGeneration
|
|
1534
|
+
? runLifecycle.begin(abortOwnedWork).generation
|
|
1535
|
+
: runLifecycle.generation;
|
|
1536
|
+
if (ownsGeneration)
|
|
1537
|
+
pendingCancelDrain = null;
|
|
1374
1538
|
// Progress (Ranks): completed, non-canceled runs with ≥1 assistant turn earn
|
|
1375
1539
|
// XP — prompt + any commits authored during the run window.
|
|
1376
1540
|
const runStartedAt = Date.now();
|
|
1377
1541
|
const cancelGenAtStart = cancelGeneration;
|
|
1378
1542
|
const assistantsBeforeRun = countAssistantMessages(session.getMessages());
|
|
1379
1543
|
let runSucceeded = false;
|
|
1380
|
-
broadcast("run_start", { text: label });
|
|
1544
|
+
broadcast("run_start", { text: label, runState: runLifecycle.state });
|
|
1381
1545
|
try {
|
|
1382
|
-
|
|
1546
|
+
if (!runLifecycle.isCancellationRequested(generation))
|
|
1547
|
+
await run();
|
|
1383
1548
|
runSucceeded = true;
|
|
1384
1549
|
}
|
|
1385
1550
|
catch (err) {
|
|
1386
|
-
|
|
1551
|
+
if (!runLifecycle.isCancellationRequested(generation)) {
|
|
1552
|
+
broadcastError("error", "run failed", err);
|
|
1553
|
+
}
|
|
1387
1554
|
}
|
|
1388
1555
|
finally {
|
|
1389
|
-
|
|
1556
|
+
const cancelled = runLifecycle.isCancellationRequested(generation);
|
|
1390
1557
|
if (runSucceeded &&
|
|
1558
|
+
!cancelled &&
|
|
1391
1559
|
cancelGeneration === cancelGenAtStart &&
|
|
1392
1560
|
countAssistantMessages(session.getMessages()) > assistantsBeforeRun) {
|
|
1393
1561
|
// Fire-and-forget — XP must never delay or break run teardown.
|
|
1394
1562
|
void progress.awardRun(cwd, runStartedAt, opts.id);
|
|
1395
1563
|
}
|
|
1396
|
-
// A run may have switched branches
|
|
1397
|
-
// background tasks
|
|
1398
|
-
gitBranch = await
|
|
1399
|
-
|
|
1400
|
-
|
|
1564
|
+
// A run may have switched branches, changed files, or spawned/finished
|
|
1565
|
+
// background tasks. Refresh the workspace extras once it settles.
|
|
1566
|
+
[gitBranch, gitIsRepo, gitDirtyFileCount] = await Promise.all([
|
|
1567
|
+
getGitBranch(cwd).catch(() => gitBranch),
|
|
1568
|
+
isGitRepo(cwd).catch(() => gitIsRepo),
|
|
1569
|
+
getGitDirtyFileCount(cwd).catch(() => gitDirtyFileCount),
|
|
1570
|
+
]);
|
|
1571
|
+
// Serialize behind any marker/tool-triggered refresh so the terminal
|
|
1572
|
+
// progress snapshot uses the live plan file. Once every canonical step
|
|
1573
|
+
// is complete, remove the approved plan from future system prompts and
|
|
1574
|
+
// clear the widget before run_end paints the idle activity bar.
|
|
1575
|
+
if (runSucceeded &&
|
|
1576
|
+
!cancelled &&
|
|
1577
|
+
approvedPlanPath !== null &&
|
|
1578
|
+
(await queueApprovedPlanProgressSync())) {
|
|
1579
|
+
try {
|
|
1580
|
+
await session.setApprovedPlan(undefined);
|
|
1581
|
+
deactivateApprovedPlan();
|
|
1582
|
+
broadcast("plan_progress", { total: 0, completed: [] });
|
|
1583
|
+
}
|
|
1584
|
+
catch (error) {
|
|
1585
|
+
// Keep tracking when prompt cleanup fails; hiding the widget here
|
|
1586
|
+
// would claim completion while the approved-plan contract remained.
|
|
1587
|
+
log("WARN", "app-sidecar", "completed plan cleanup failed", {
|
|
1588
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
if (ownsGeneration)
|
|
1593
|
+
finishOwnedGeneration(generation, false);
|
|
1594
|
+
// A cancelled injected run is still owned by the surrounding autopilot
|
|
1595
|
+
// cycle; its outer finalizer emits the one terminal cancelled run_end.
|
|
1596
|
+
if (!(cancelled && !ownsGeneration)) {
|
|
1597
|
+
if (cancelled)
|
|
1598
|
+
cancelledRunEndGenerations.add(generation);
|
|
1599
|
+
broadcast("run_end", {
|
|
1600
|
+
...(cancelled ? { cancelled: true } : {}),
|
|
1601
|
+
runState: runLifecycle.state,
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1401
1604
|
// Autopilot's review loop is driven explicitly from POST /prompt (see
|
|
1402
1605
|
// runAutopilotCycle), NOT from this shared finally — that keeps the
|
|
1403
1606
|
// injected EZ Coder runs this cycle triggers from recursively re-entering
|
|
@@ -1406,19 +1609,8 @@ async function createSession(deps, opts) {
|
|
|
1406
1609
|
// completed ones so they drop out of the Tasks modal automatically (users
|
|
1407
1610
|
// never have to delete finished tasks by hand).
|
|
1408
1611
|
broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
|
|
1409
|
-
// Queue drains into the run as steering, so it's empty by run_end —
|
|
1410
|
-
// sync the webview indicator.
|
|
1411
1612
|
broadcast("queued", { count: session.getQueuedCount() });
|
|
1412
1613
|
broadcast("extras", footerExtras());
|
|
1413
|
-
// Generate a session title once, after the first run, for the title bar
|
|
1414
|
-
// (best-effort, async — don't block the response).
|
|
1415
|
-
if (!titleGenerated) {
|
|
1416
|
-
titleGenerated = true;
|
|
1417
|
-
void session.generateTitle().then((title) => {
|
|
1418
|
-
if (title)
|
|
1419
|
-
broadcast("session_title", { title });
|
|
1420
|
-
});
|
|
1421
|
-
}
|
|
1422
1614
|
}
|
|
1423
1615
|
}
|
|
1424
1616
|
// ── Autopilot orchestration ─────────────────────────────────
|
|
@@ -1444,7 +1636,8 @@ async function createSession(deps, opts) {
|
|
|
1444
1636
|
return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
|
|
1445
1637
|
}
|
|
1446
1638
|
catch (err) {
|
|
1447
|
-
|
|
1639
|
+
if (!autopilotCancelled)
|
|
1640
|
+
broadcastError("autopilot_error", "autopilot review failed", err);
|
|
1448
1641
|
return null;
|
|
1449
1642
|
}
|
|
1450
1643
|
finally {
|
|
@@ -1519,7 +1712,10 @@ async function createSession(deps, opts) {
|
|
|
1519
1712
|
async function runAutopilotCycle(originalRequest) {
|
|
1520
1713
|
if (!autopilot || autopilotCancelled)
|
|
1521
1714
|
return;
|
|
1715
|
+
const generation = runLifecycle.begin(abortOwnedWork).generation;
|
|
1716
|
+
pendingCancelDrain = null;
|
|
1522
1717
|
autopilotActive = true;
|
|
1718
|
+
session.setIdealReviewSuppressed(true);
|
|
1523
1719
|
// Generation captured by the last plan review; acceptPlan re-checks it so
|
|
1524
1720
|
// a user Accept/Reject landing mid-review always wins.
|
|
1525
1721
|
let planGenAtReview = -1;
|
|
@@ -1546,22 +1742,22 @@ async function createSession(deps, opts) {
|
|
|
1546
1742
|
if (pendingPlanPath === null || planGeneration !== planGenAtReview)
|
|
1547
1743
|
return false;
|
|
1548
1744
|
const planPath = pendingPlanPath;
|
|
1745
|
+
let planTotal;
|
|
1549
1746
|
try {
|
|
1550
|
-
await session.newSession();
|
|
1747
|
+
await session.newSession(true);
|
|
1551
1748
|
injectedAutopilotPrompts = [];
|
|
1552
|
-
|
|
1553
|
-
await session.setApprovedPlan(planPath);
|
|
1749
|
+
planTotal = await activateApprovedPlan(planPath);
|
|
1554
1750
|
}
|
|
1555
1751
|
catch (err) {
|
|
1556
1752
|
broadcastError("autopilot_error", "autopilot plan accept failed", err);
|
|
1557
1753
|
return false;
|
|
1558
1754
|
}
|
|
1559
1755
|
clearPendingPlan();
|
|
1560
|
-
//
|
|
1561
|
-
//
|
|
1562
|
-
// session_reset clears it — accepted must land first.
|
|
1756
|
+
// Keep the approval marker ahead of the reset, then seed the reset
|
|
1757
|
+
// with the sidecar's canonical count from the actual plan file.
|
|
1563
1758
|
broadcast("autopilot_plan_accepted", {});
|
|
1564
|
-
broadcast("session_reset", {});
|
|
1759
|
+
broadcast("session_reset", { planTotal });
|
|
1760
|
+
broadcast("plan_progress", planProgressPayload());
|
|
1565
1761
|
// Persisted into the NEW session so a resume shows the marker.
|
|
1566
1762
|
void session.persistAutopilotMarker("plan_approved");
|
|
1567
1763
|
return true;
|
|
@@ -1633,6 +1829,9 @@ async function createSession(deps, opts) {
|
|
|
1633
1829
|
}
|
|
1634
1830
|
finally {
|
|
1635
1831
|
autopilotActive = false;
|
|
1832
|
+
session.setIdealReviewSuppressed(autopilot);
|
|
1833
|
+
finishOwnedGeneration(generation, true);
|
|
1834
|
+
queueMicrotask(() => void runStrandedQueue());
|
|
1636
1835
|
}
|
|
1637
1836
|
}
|
|
1638
1837
|
// ── Stranded-queue drain ───────────────────────────────
|
|
@@ -1730,9 +1929,9 @@ async function createSession(deps, opts) {
|
|
|
1730
1929
|
async function runTaskByIdInner(task) {
|
|
1731
1930
|
// Fresh session per task so one task's context never bleeds into the next.
|
|
1732
1931
|
await session.newSession();
|
|
1932
|
+
deactivateApprovedPlan();
|
|
1733
1933
|
injectedAutopilotPrompts = [];
|
|
1734
1934
|
clearPendingPlan();
|
|
1735
|
-
titleGenerated = false;
|
|
1736
1935
|
broadcast("session_reset", {});
|
|
1737
1936
|
markTaskInProgress(cwd, task.id);
|
|
1738
1937
|
broadcast("tasks_list", { tasks: loadTasksSync(cwd) });
|
|
@@ -1816,6 +2015,30 @@ async function createSession(deps, opts) {
|
|
|
1816
2015
|
tasksPoll.unref?.();
|
|
1817
2016
|
};
|
|
1818
2017
|
scheduleTasksPoll(1500);
|
|
2018
|
+
// Files can change outside the agent (editor saves, terminal commits), so keep
|
|
2019
|
+
// the dirty count current while idle. Branch/repo state already refreshes after
|
|
2020
|
+
// agent runs; polling only the count avoids spawning three git processes per tick.
|
|
2021
|
+
let gitPoll;
|
|
2022
|
+
let gitPollStopped = false;
|
|
2023
|
+
const scheduleGitPoll = (delay) => {
|
|
2024
|
+
if (gitPollStopped)
|
|
2025
|
+
return;
|
|
2026
|
+
gitPoll = setTimeout(() => {
|
|
2027
|
+
void getGitDirtyFileCount(cwd)
|
|
2028
|
+
.catch(() => gitDirtyFileCount)
|
|
2029
|
+
.then((nextDirtyFileCount) => {
|
|
2030
|
+
if (gitPollStopped)
|
|
2031
|
+
return;
|
|
2032
|
+
if (nextDirtyFileCount !== gitDirtyFileCount) {
|
|
2033
|
+
gitDirtyFileCount = nextDirtyFileCount;
|
|
2034
|
+
broadcast("extras", footerExtras());
|
|
2035
|
+
}
|
|
2036
|
+
scheduleGitPoll(5000);
|
|
2037
|
+
});
|
|
2038
|
+
}, delay);
|
|
2039
|
+
gitPoll.unref?.();
|
|
2040
|
+
};
|
|
2041
|
+
scheduleGitPoll(5000);
|
|
1819
2042
|
function readBody(req) {
|
|
1820
2043
|
return new Promise((resolve, reject) => {
|
|
1821
2044
|
const chunks = [];
|
|
@@ -1825,7 +2048,7 @@ async function createSession(deps, opts) {
|
|
|
1825
2048
|
});
|
|
1826
2049
|
}
|
|
1827
2050
|
function json(res, status, body) {
|
|
1828
|
-
const payload = JSON.stringify(body);
|
|
2051
|
+
const payload = JSON.stringify(redactValue(body, { secrets: environmentSecrets(process.env) }));
|
|
1829
2052
|
res.writeHead(status, {
|
|
1830
2053
|
"content-type": "application/json",
|
|
1831
2054
|
"access-control-allow-origin": "*",
|
|
@@ -1841,6 +2064,7 @@ async function createSession(deps, opts) {
|
|
|
1841
2064
|
mode,
|
|
1842
2065
|
chatAgent,
|
|
1843
2066
|
running,
|
|
2067
|
+
runState: runLifecycle.state,
|
|
1844
2068
|
ready: true,
|
|
1845
2069
|
thinkingLevel: session.getThinkingLevel() ?? null,
|
|
1846
2070
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
@@ -1913,6 +2137,7 @@ async function createSession(deps, opts) {
|
|
|
1913
2137
|
mode,
|
|
1914
2138
|
chatAgent,
|
|
1915
2139
|
running,
|
|
2140
|
+
runState: runLifecycle.state,
|
|
1916
2141
|
thinkingLevel: session.getThinkingLevel() ?? null,
|
|
1917
2142
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
1918
2143
|
supportsVideo: getModel(st.model)?.supportsVideo ?? false,
|
|
@@ -2087,6 +2312,20 @@ async function createSession(deps, opts) {
|
|
|
2087
2312
|
void (async () => {
|
|
2088
2313
|
const commandCandidates = [...PROMPT_COMMANDS, ...(await loadCustomCommands(cwd))];
|
|
2089
2314
|
const messages = session.getMessages();
|
|
2315
|
+
// An Ideal hook is injected immediately after the assistant's candidate
|
|
2316
|
+
// no-tool response. That response is review scratch, not a user-visible
|
|
2317
|
+
// final answer. Live SSE drops it when the hook event arrives; mark the
|
|
2318
|
+
// same assistant message here so resumed history stays identical.
|
|
2319
|
+
const hiddenIdealDrafts = new Set();
|
|
2320
|
+
for (let i = 0; i < messages.length - 1; i++) {
|
|
2321
|
+
const draft = messages[i];
|
|
2322
|
+
const hookPrompt = messages[i + 1];
|
|
2323
|
+
if (draft?.role !== "assistant" || hookPrompt?.role !== "user")
|
|
2324
|
+
continue;
|
|
2325
|
+
const restored = restoreUserRow(hookPrompt.content);
|
|
2326
|
+
if (detectHookKind(restored.text) === "ideal")
|
|
2327
|
+
hiddenIdealDrafts.add(draft);
|
|
2328
|
+
}
|
|
2090
2329
|
// Pre-index tool results by toolCallId so we can pair tool calls with
|
|
2091
2330
|
// their results (for sub-agent status + image extraction).
|
|
2092
2331
|
const toolResultMap = new Map();
|
|
@@ -2304,10 +2543,11 @@ async function createSession(deps, opts) {
|
|
|
2304
2543
|
}
|
|
2305
2544
|
}
|
|
2306
2545
|
}
|
|
2307
|
-
else {
|
|
2546
|
+
else if (!hiddenIdealDrafts.has(msg)) {
|
|
2308
2547
|
// Assistant: one wire row per persisted text block — live streaming
|
|
2309
2548
|
// splits bubbles at server_tool_call boundaries, and the persisted
|
|
2310
|
-
// content keeps those blocks separate.
|
|
2549
|
+
// content keeps those blocks separate. Ideal-review candidate drafts
|
|
2550
|
+
// are intentionally omitted to match the live pre-final hook flow.
|
|
2311
2551
|
for (const blockText of restoreAssistantTexts(msg.content)) {
|
|
2312
2552
|
history.push({
|
|
2313
2553
|
role: "assistant",
|
|
@@ -2409,6 +2649,13 @@ async function createSession(deps, opts) {
|
|
|
2409
2649
|
json(res, 400, { error: "empty prompt" });
|
|
2410
2650
|
return;
|
|
2411
2651
|
}
|
|
2652
|
+
if (runLifecycle.running && runLifecycle.isCancellationRequested(runLifecycle.generation)) {
|
|
2653
|
+
json(res, 409, {
|
|
2654
|
+
error: runLifecycle.state === "cancelling" ? "run_cancelling" : "cancel_failed",
|
|
2655
|
+
runState: runLifecycle.state,
|
|
2656
|
+
});
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2412
2659
|
if (running || autopilotActive) {
|
|
2413
2660
|
// Queue prompts as mid-run steering (mirrors the CLI). Also queue while
|
|
2414
2661
|
// an autopilot cycle is active but between injected runs (build idle,
|
|
@@ -2580,6 +2827,9 @@ async function createSession(deps, opts) {
|
|
|
2580
2827
|
return;
|
|
2581
2828
|
}
|
|
2582
2829
|
autopilot = enabled;
|
|
2830
|
+
// A toggle-off during an active cycle takes effect after Nolan finishes;
|
|
2831
|
+
// until then, injected build runs must not re-enable Ideal self-review.
|
|
2832
|
+
session.setIdealReviewSuppressed(enabled || autopilotActive);
|
|
2583
2833
|
await saveAutopilot(cwd, enabled);
|
|
2584
2834
|
log("INFO", "app-sidecar", "autopilot toggled", { enabled: String(enabled) });
|
|
2585
2835
|
broadcast("autopilot", { autopilot: enabled });
|
|
@@ -2982,27 +3232,50 @@ async function createSession(deps, opts) {
|
|
|
2982
3232
|
return;
|
|
2983
3233
|
}
|
|
2984
3234
|
if (method === "POST" && url === "/cancel") {
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3235
|
+
void (async () => {
|
|
3236
|
+
// Even between task runs, cancellation stops the sweep. Active provider
|
|
3237
|
+
// ownership invokes the full abort hook exactly once through lifecycle.
|
|
3238
|
+
taskRunAll = false;
|
|
3239
|
+
autopilotCancelled = true;
|
|
3240
|
+
if (!runLifecycle.running) {
|
|
3241
|
+
nolanAutoAbort.abort();
|
|
3242
|
+
nolanAutoAbort = new AbortController();
|
|
3243
|
+
nolanAutoSession?.setSignal(nolanAutoAbort.signal);
|
|
3244
|
+
}
|
|
3245
|
+
const generation = runLifecycle.generation;
|
|
3246
|
+
if (!pendingCancelDrain || pendingCancelDrain.generation !== generation) {
|
|
3247
|
+
pendingCancelDrain = { generation, text: session.drainQueue() };
|
|
3248
|
+
broadcast("queued", { count: 0 });
|
|
3249
|
+
}
|
|
3250
|
+
const result = await runLifecycle.cancel(CANCEL_TIMEOUT_MS);
|
|
3251
|
+
const drained = pendingCancelDrain.text;
|
|
3252
|
+
if (result.status === "failed") {
|
|
3253
|
+
broadcast("cancel_failed", {
|
|
3254
|
+
error: "cancel_failed",
|
|
3255
|
+
reason: result.reason,
|
|
3256
|
+
runState: runLifecycle.state,
|
|
3257
|
+
});
|
|
3258
|
+
json(res, 504, {
|
|
3259
|
+
error: "cancel_failed",
|
|
3260
|
+
reason: result.reason,
|
|
3261
|
+
runState: runLifecycle.state,
|
|
3262
|
+
drained,
|
|
3263
|
+
});
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
json(res, 200, {
|
|
3267
|
+
cancelled: result.status === "cancelled",
|
|
3268
|
+
runState: runLifecycle.state,
|
|
3269
|
+
drained,
|
|
3270
|
+
});
|
|
3271
|
+
})().catch((error) => {
|
|
3272
|
+
broadcast("cancel_failed", { error: "cancel_failed", runState: runLifecycle.state });
|
|
3273
|
+
json(res, 500, {
|
|
3274
|
+
error: "cancel_failed",
|
|
3275
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3276
|
+
runState: runLifecycle.state,
|
|
3277
|
+
});
|
|
3278
|
+
});
|
|
3006
3279
|
return;
|
|
3007
3280
|
}
|
|
3008
3281
|
if (method === "POST" && url === "/new-session") {
|
|
@@ -3016,6 +3289,7 @@ async function createSession(deps, opts) {
|
|
|
3016
3289
|
if (mode === "chat") {
|
|
3017
3290
|
await session.persistAppMarker("agent_handoff", { chatAgent });
|
|
3018
3291
|
}
|
|
3292
|
+
deactivateApprovedPlan();
|
|
3019
3293
|
injectedAutopilotPrompts = [];
|
|
3020
3294
|
clearPendingPlan();
|
|
3021
3295
|
broadcast("session_reset", {});
|
|
@@ -3068,12 +3342,12 @@ async function createSession(deps, opts) {
|
|
|
3068
3342
|
broadcast("autopilot_ignored", {});
|
|
3069
3343
|
}
|
|
3070
3344
|
try {
|
|
3071
|
-
await session.newSession();
|
|
3345
|
+
await session.newSession(true);
|
|
3072
3346
|
injectedAutopilotPrompts = [];
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
broadcast("
|
|
3076
|
-
json(res, 200, { ok: true });
|
|
3347
|
+
const planTotal = await activateApprovedPlan(planPath);
|
|
3348
|
+
broadcast("session_reset", { planTotal });
|
|
3349
|
+
broadcast("plan_progress", planProgressPayload());
|
|
3350
|
+
json(res, 200, { ok: true, planTotal });
|
|
3077
3351
|
}
|
|
3078
3352
|
catch (err) {
|
|
3079
3353
|
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -3501,6 +3775,9 @@ async function createSession(deps, opts) {
|
|
|
3501
3775
|
tasksPollStopped = true;
|
|
3502
3776
|
if (tasksPoll)
|
|
3503
3777
|
clearTimeout(tasksPoll);
|
|
3778
|
+
gitPollStopped = true;
|
|
3779
|
+
if (gitPoll)
|
|
3780
|
+
clearTimeout(gitPoll);
|
|
3504
3781
|
// Stop the Telegram serve loop + dispose its per-chat sessions.
|
|
3505
3782
|
if (serveController)
|
|
3506
3783
|
await serveController.stop().catch(() => { });
|