@prestyj/cli 5.7.0 → 5.9.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/assets/skills/evidence-led-ui/LICENSES.md +12 -0
- package/assets/skills/evidence-led-ui/SKILL.md +14 -6
- package/assets/skills/evidence-led-ui/references/anti-defaults.md +9 -1
- package/assets/skills/evidence-led-ui/references/craft-rulings.md +33 -0
- package/assets/skills/evidence-led-ui/references/production-contract.md +41 -21
- package/assets/skills/evidence-led-ui/references/provenance.md +7 -5
- package/assets/skills/evidence-led-ui/references/quality-rubric.md +10 -10
- package/dist/app-sidecar-sessions.d.ts +8 -0
- package/dist/app-sidecar-sessions.d.ts.map +1 -0
- package/dist/app-sidecar-sessions.js +31 -0
- package/dist/app-sidecar-sessions.js.map +1 -0
- package/dist/app-sidecar-sessions.test.d.ts +2 -0
- package/dist/app-sidecar-sessions.test.d.ts.map +1 -0
- package/dist/app-sidecar-sessions.test.js +83 -0
- package/dist/app-sidecar-sessions.test.js.map +1 -0
- package/dist/app-sidecar.js +488 -156
- 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 +61 -29
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -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 +540 -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.js +26 -0
- package/dist/core/agent-session-review-coverage.test.js.map +1 -1
- 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 +44 -8
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +369 -99
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agents.d.ts +1 -1
- package/dist/core/agents.d.ts.map +1 -1
- package/dist/core/agents.js +4 -1
- package/dist/core/agents.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/auth-storage.d.ts +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +1 -1
- package/dist/core/auth-storage.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 +84 -53
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +135 -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 +5 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js +6 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/goal-controller.test.js +3 -3
- package/dist/core/goal-controller.test.js.map +1 -1
- package/dist/core/ideal-review.d.ts +6 -0
- package/dist/core/ideal-review.d.ts.map +1 -1
- package/dist/core/ideal-review.js +14 -0
- package/dist/core/ideal-review.js.map +1 -1
- package/dist/core/ideal-review.test.js +10 -1
- package/dist/core/ideal-review.test.js.map +1 -1
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/input-normalization.test.d.ts +2 -0
- package/dist/core/input-normalization.test.d.ts.map +1 -0
- package/dist/core/input-normalization.test.js +75 -0
- package/dist/core/input-normalization.test.js.map +1 -0
- package/dist/core/loop-breaker.d.ts +38 -1
- package/dist/core/loop-breaker.d.ts.map +1 -1
- package/dist/core/loop-breaker.js +84 -2
- package/dist/core/loop-breaker.js.map +1 -1
- package/dist/core/loop-breaker.test.js +108 -1
- package/dist/core/loop-breaker.test.js.map +1 -1
- package/dist/core/message-images.d.ts +12 -0
- package/dist/core/message-images.d.ts.map +1 -0
- package/dist/core/message-images.js +52 -0
- package/dist/core/message-images.js.map +1 -0
- package/dist/core/message-images.test.d.ts +2 -0
- package/dist/core/message-images.test.d.ts.map +1 -0
- package/dist/core/message-images.test.js +62 -0
- package/dist/core/message-images.test.js.map +1 -0
- package/dist/core/project-discovery.d.ts +7 -10
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +125 -66
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/project-discovery.test.js +177 -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/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 +35 -8
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +264 -125
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +17 -0
- 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/session-storage.d.ts +57 -0
- package/dist/core/session-storage.d.ts.map +1 -0
- package/dist/core/session-storage.js +568 -0
- package/dist/core/session-storage.js.map +1 -0
- package/dist/core/session-storage.test.d.ts +2 -0
- package/dist/core/session-storage.test.d.ts.map +1 -0
- package/dist/core/session-storage.test.js +269 -0
- package/dist/core/session-storage.test.js.map +1 -0
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +10 -2
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/sidecar-error-reporter.d.ts +12 -0
- package/dist/core/sidecar-error-reporter.d.ts.map +1 -0
- package/dist/core/sidecar-error-reporter.js +57 -0
- package/dist/core/sidecar-error-reporter.js.map +1 -0
- package/dist/core/sidecar-error-reporter.test.d.ts +2 -0
- package/dist/core/sidecar-error-reporter.test.d.ts.map +1 -0
- package/dist/core/sidecar-error-reporter.test.js +61 -0
- package/dist/core/sidecar-error-reporter.test.js.map +1 -0
- package/dist/core/skills.d.ts +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +4 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/core/subagent-manager.d.ts +9 -4
- package/dist/core/subagent-manager.d.ts.map +1 -1
- package/dist/core/subagent-manager.js +30 -2
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-manager.test.js +47 -3
- package/dist/core/subagent-manager.test.js.map +1 -1
- package/dist/core/verify-commands.d.ts.map +1 -1
- package/dist/core/verify-commands.js +1 -6
- package/dist/core/verify-commands.js.map +1 -1
- package/dist/core/verify-commands.test.js +1 -5
- package/dist/core/verify-commands.test.js.map +1 -1
- package/dist/core/workspace-guard.d.ts +38 -0
- package/dist/core/workspace-guard.d.ts.map +1 -0
- package/dist/core/workspace-guard.js +112 -0
- package/dist/core/workspace-guard.js.map +1 -0
- package/dist/core/workspace-guard.test.d.ts +2 -0
- package/dist/core/workspace-guard.test.d.ts.map +1 -0
- package/dist/core/workspace-guard.test.js +86 -0
- package/dist/core/workspace-guard.test.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/json-mode.d.ts.map +1 -1
- package/dist/modes/json-mode.js +9 -0
- package/dist/modes/json-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
- package/dist/modes/subagent-worker-mode.js +11 -0
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/session.d.ts +4 -4
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +38 -101
- package/dist/session.js.map +1 -1
- package/dist/session.test.d.ts +2 -0
- package/dist/session.test.d.ts.map +1 -0
- package/dist/session.test.js +35 -0
- package/dist/session.test.js.map +1 -0
- package/dist/system-prompt.d.ts +12 -0
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +86 -19
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +132 -8
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/bash.d.ts +12 -2
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +65 -24
- 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 +89 -0
- package/dist/tools/bash.test.js.map +1 -0
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +7 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +7 -0
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/tools/exit-plan.d.ts.map +1 -1
- package/dist/tools/exit-plan.js +15 -4
- package/dist/tools/exit-plan.js.map +1 -1
- package/dist/tools/exit-plan.test.d.ts +2 -0
- package/dist/tools/exit-plan.test.d.ts.map +1 -0
- package/dist/tools/exit-plan.test.js +72 -0
- package/dist/tools/exit-plan.test.js.map +1 -0
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +8 -3
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/grep.test.js +6 -0
- package/dist/tools/grep.test.js.map +1 -1
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -2
- package/dist/tools/index.js.map +1 -1
- 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/screenshot.js +1 -1
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/skill.d.ts.map +1 -1
- package/dist/tools/skill.js +4 -2
- package/dist/tools/skill.js.map +1 -1
- package/dist/tools/source-path.d.ts.map +1 -1
- package/dist/tools/source-path.js +9 -0
- package/dist/tools/source-path.js.map +1 -1
- package/dist/tools/subagent-shared.d.ts +9 -1
- package/dist/tools/subagent-shared.d.ts.map +1 -1
- package/dist/tools/subagent-shared.js +4 -1
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/subagent-shared.test.js +21 -1
- package/dist/tools/subagent-shared.test.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/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +13 -2
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +7 -1
- package/dist/tools/write.js.map +1 -1
- package/dist/tools/write.test.js +12 -0
- package/dist/tools/write.test.js.map +1 -1
- package/dist/ui/App.d.ts +0 -2
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +23 -87
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/app-items.d.ts +10 -0
- package/dist/ui/app-items.d.ts.map +1 -1
- package/dist/ui/app-items.js +10 -0
- package/dist/ui/app-items.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +1 -0
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/components/Footer.test.js +1 -1
- 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 +7 -6
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +56 -11
- 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.map +1 -1
- package/dist/ui/hooks/useSessionPersistence.js +3 -0
- 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/hooks/useTranscriptHistory.d.ts +8 -5
- package/dist/ui/hooks/useTranscriptHistory.d.ts.map +1 -1
- package/dist/ui/hooks/useTranscriptHistory.js +2 -30
- package/dist/ui/hooks/useTranscriptHistory.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 +0 -2
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +0 -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/dist/utils/http-body.d.ts +19 -0
- package/dist/utils/http-body.d.ts.map +1 -0
- package/dist/utils/http-body.js +58 -0
- package/dist/utils/http-body.js.map +1 -0
- package/dist/utils/http-body.test.d.ts +2 -0
- package/dist/utils/http-body.test.d.ts.map +1 -0
- package/dist/utils/http-body.test.js +68 -0
- package/dist/utils/http-body.test.js.map +1 -0
- package/dist/utils/text.d.ts +9 -0
- package/dist/utils/text.d.ts.map +1 -0
- package/dist/utils/text.js +11 -0
- package/dist/utils/text.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
|
@@ -23,7 +23,7 @@ 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
25
|
import { RunLifecycle } from "./core/run-lifecycle.js";
|
|
26
|
-
import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId,
|
|
26
|
+
import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
|
|
27
27
|
import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
|
|
28
28
|
import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
|
|
29
29
|
import { buildNolanSystemPrompt, buildNolanAutopilotSystemPrompt } from "./core/nolan-prompt.js";
|
|
@@ -34,6 +34,8 @@ import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-c
|
|
|
34
34
|
import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
|
|
35
35
|
import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
|
|
36
36
|
import { AuthStorage } from "./core/auth-storage.js";
|
|
37
|
+
import { cleanupToolOutputs } from "./tools/overflow.js";
|
|
38
|
+
import { readCappedBody } from "./utils/http-body.js";
|
|
37
39
|
import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, } from "@prestyj/core";
|
|
38
40
|
import { loginAnthropic } from "./core/oauth/anthropic.js";
|
|
39
41
|
import { loginOpenAI } from "./core/oauth/openai.js";
|
|
@@ -42,18 +44,20 @@ import { loginKimi } from "./core/oauth/kimi.js";
|
|
|
42
44
|
import { AUTH_PROVIDERS } from "./core/auth-providers.js";
|
|
43
45
|
import { ensureAppDirs, loadSavedSettings } from "./config.js";
|
|
44
46
|
import { SettingsManager } from "./core/settings-manager.js";
|
|
45
|
-
import { getModel,
|
|
47
|
+
import { getModel, getDefaultThinkingLevel, getContextWindow, MODELS, } from "./core/model-registry.js";
|
|
46
48
|
import { resolveStartOrFallback } from "./core/resolve-start.js";
|
|
47
|
-
import { getGitBranch, isGitRepo } from "./utils/git.js";
|
|
49
|
+
import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
|
|
50
|
+
import { extractPlanSteps } from "./utils/plan-steps.js";
|
|
48
51
|
import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSupported, } from "./core/thinking-level.js";
|
|
49
52
|
import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
|
|
50
53
|
import { loadCustomCommands } from "./core/custom-commands.js";
|
|
51
|
-
import { discoverProjects
|
|
54
|
+
import { discoverProjects } from "./core/project-discovery.js";
|
|
55
|
+
import { listSidecarSessions } from "./app-sidecar-sessions.js";
|
|
52
56
|
import { loadTasksSync, saveTasksSync, pruneDoneTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/task-store.js";
|
|
53
57
|
import { initLogger, log } from "./core/logger.js";
|
|
54
58
|
import { RADIO_STATIONS, getCurrentStation, getRadioVolume, playRadio, setRadioVolume, stopRadio, } from "./core/radio.js";
|
|
55
59
|
import { enrichProcessPath } from "./core/shell-path.js";
|
|
56
|
-
import { downscaleForPreview, validateVisionImage } from "./utils/image.js";
|
|
60
|
+
import { downscaleForPreview, shrinkToFit, validateVisionImage } from "./utils/image.js";
|
|
57
61
|
import { startServeMode } from "./modes/serve-mode.js";
|
|
58
62
|
import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
|
|
59
63
|
import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, } from "./core/mcp/index.js";
|
|
@@ -62,17 +66,22 @@ import { loadProgress, peekProgress, updateProgress } from "./core/progress/stor
|
|
|
62
66
|
import { awardPrompt, awardCommits } from "./core/progress/engine.js";
|
|
63
67
|
import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
|
|
64
68
|
import { rebuildFromSessions } from "./core/progress/rebuild.js";
|
|
69
|
+
import { captureSidecarError, flushSidecarErrors, shouldCaptureToolFailure, shouldCaptureUsagePollingError, wrapSidecarHandler, } from "./core/sidecar-error-reporter.js";
|
|
65
70
|
const ALL_PROVIDERS = [
|
|
71
|
+
// US
|
|
66
72
|
"anthropic",
|
|
67
|
-
"xiaomi",
|
|
68
73
|
"openai",
|
|
69
74
|
"gemini",
|
|
70
|
-
"
|
|
75
|
+
"xai",
|
|
76
|
+
// China
|
|
71
77
|
"moonshot",
|
|
78
|
+
"glm",
|
|
72
79
|
"minimax",
|
|
80
|
+
"xiaomi",
|
|
73
81
|
"deepseek",
|
|
74
|
-
|
|
82
|
+
// Japan, then provider-agnostic gateway last
|
|
75
83
|
"sakana",
|
|
84
|
+
"openrouter",
|
|
76
85
|
];
|
|
77
86
|
function appSettingsFile() {
|
|
78
87
|
return path.join(os.homedir(), ".ezcoder", "ezcoder-app.json");
|
|
@@ -163,6 +172,7 @@ async function persistModelSelection(settingsFile, provider, model) {
|
|
|
163
172
|
await sm.set("defaultModel", model);
|
|
164
173
|
}
|
|
165
174
|
catch (err) {
|
|
175
|
+
captureSidecarError(err, "app-sidecar.settings.persist-model");
|
|
166
176
|
log("WARN", "app-sidecar", "failed to persist model selection", { err: String(err) });
|
|
167
177
|
}
|
|
168
178
|
}
|
|
@@ -179,6 +189,7 @@ async function persistThinkingLevel(settingsFile, level) {
|
|
|
179
189
|
await sm.set("thinkingLevel", level);
|
|
180
190
|
}
|
|
181
191
|
catch (err) {
|
|
192
|
+
captureSidecarError(err, "app-sidecar.settings.persist-thinking");
|
|
182
193
|
log("WARN", "app-sidecar", "failed to persist thinking level", { err: String(err) });
|
|
183
194
|
}
|
|
184
195
|
}
|
|
@@ -196,19 +207,28 @@ async function prepareAttachments(cwd, attachments) {
|
|
|
196
207
|
const fileName = `${Date.now().toString(36)}-${safe}`;
|
|
197
208
|
const filePath = path.join(dir, fileName);
|
|
198
209
|
const buf = Buffer.from(a.data, "base64");
|
|
199
|
-
// Validate image attachments before they become native image content
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
210
|
+
// Validate and cap image attachments before they become native image content
|
|
211
|
+
// blocks. Anthropic applies a 2000 px per-dimension cap once conversation
|
|
212
|
+
// history contains more than 20 images, so every attachment must be safe for
|
|
213
|
+
// later turns too. Keep the original on disk, but send the resized bytes.
|
|
214
|
+
// Corrupt or unsupported images become plain files so they cannot reject the
|
|
215
|
+
// entire provider request.
|
|
205
216
|
let prepared = { ...a };
|
|
206
217
|
if (a.kind === "image") {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
try {
|
|
219
|
+
const resized = await shrinkToFit(buf, a.mediaType);
|
|
220
|
+
const validatedType = await validateVisionImage(resized.buffer);
|
|
221
|
+
prepared = validatedType
|
|
222
|
+
? {
|
|
223
|
+
...a,
|
|
224
|
+
mediaType: validatedType,
|
|
225
|
+
data: resized.buffer.toString("base64"),
|
|
226
|
+
}
|
|
227
|
+
: { ...a, kind: "file" };
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
211
230
|
prepared = { ...a, kind: "file" };
|
|
231
|
+
}
|
|
212
232
|
}
|
|
213
233
|
try {
|
|
214
234
|
await fs.writeFile(filePath, buf);
|
|
@@ -221,6 +241,9 @@ async function prepareAttachments(cwd, attachments) {
|
|
|
221
241
|
return out;
|
|
222
242
|
}
|
|
223
243
|
const FILE_SEARCH_LIMIT = 20;
|
|
244
|
+
// Upper bound on files walked per search (baseline #8 memory cap). Far above the
|
|
245
|
+
// 20-result output limit, so relevance/recency ranking is unaffected in practice.
|
|
246
|
+
const FILE_SEARCH_SCAN_CAP = 50_000;
|
|
224
247
|
/** Score a candidate path against a lowercased query. Higher is better; a
|
|
225
248
|
* negative score means "no match". Basename hits beat path hits; prefix beats
|
|
226
249
|
* substring; shorter paths break ties. */
|
|
@@ -273,8 +296,13 @@ async function searchProjectFiles(cwd, rawQuery) {
|
|
|
273
296
|
}
|
|
274
297
|
const ig = ignore.default().add(gitignore);
|
|
275
298
|
// `stats: true` gives mtime without a second stat pass, so the empty-query
|
|
276
|
-
// "recent files" path is a single walk.
|
|
277
|
-
|
|
299
|
+
// "recent files" path is a single walk. Stream + hard scan cap (baseline #8):
|
|
300
|
+
// collecting the full result array retained ~0.9 MB per 1k files (18.5 MB at
|
|
301
|
+
// 20k), unbounded. Bail after FILE_SEARCH_SCAN_CAP entries so a giant repo
|
|
302
|
+
// can't balloon RSS; the newest/most-relevant matches still surface because
|
|
303
|
+
// the cap is far above the 20-result output limit.
|
|
304
|
+
const entries = [];
|
|
305
|
+
const scanStream = fg.default.stream("**/*", {
|
|
278
306
|
cwd,
|
|
279
307
|
dot: false,
|
|
280
308
|
onlyFiles: true,
|
|
@@ -283,6 +311,17 @@ async function searchProjectFiles(cwd, rawQuery) {
|
|
|
283
311
|
followSymbolicLinks: false,
|
|
284
312
|
stats: true,
|
|
285
313
|
});
|
|
314
|
+
for await (const entry of scanStream) {
|
|
315
|
+
entries.push(entry);
|
|
316
|
+
if (entries.length >= FILE_SEARCH_SCAN_CAP) {
|
|
317
|
+
scanStream.destroy();
|
|
318
|
+
log("DEBUG", "app-sidecar", "file search scan cap hit", {
|
|
319
|
+
cap: String(FILE_SEARCH_SCAN_CAP),
|
|
320
|
+
cwd,
|
|
321
|
+
});
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
286
325
|
const files = entries.filter((e) => !ig.ignores(e.path));
|
|
287
326
|
if (!query) {
|
|
288
327
|
return files
|
|
@@ -437,7 +476,11 @@ async function runJsonModeIfRequested() {
|
|
|
437
476
|
maxTurns: maxTurnsRaw ? parseInt(maxTurnsRaw, 10) : undefined,
|
|
438
477
|
allowedTools,
|
|
439
478
|
promptCacheKey: values["prompt-cache-key"],
|
|
440
|
-
}).catch((err) => {
|
|
479
|
+
}).catch(async (err) => {
|
|
480
|
+
captureSidecarError(err, "app-sidecar.json-mode", {
|
|
481
|
+
provider: String(values.provider ?? "anthropic"),
|
|
482
|
+
});
|
|
483
|
+
await flushSidecarErrors();
|
|
441
484
|
process.stderr.write((err instanceof Error ? err.message : String(err)) + "\n");
|
|
442
485
|
process.exit(1);
|
|
443
486
|
});
|
|
@@ -446,13 +489,8 @@ async function runJsonModeIfRequested() {
|
|
|
446
489
|
// ── Daemon-level HTTP helpers (shared by the session-management routes) ─────
|
|
447
490
|
// The per-session route table has its own local copies; these serve the
|
|
448
491
|
// daemon's own POST /session / DELETE /session routes.
|
|
449
|
-
function daemonReadBody(req) {
|
|
450
|
-
return
|
|
451
|
-
const chunks = [];
|
|
452
|
-
req.on("data", (c) => chunks.push(c));
|
|
453
|
-
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
454
|
-
req.on("error", reject);
|
|
455
|
-
});
|
|
492
|
+
function daemonReadBody(req, res) {
|
|
493
|
+
return readCappedBody(req, res);
|
|
456
494
|
}
|
|
457
495
|
function daemonJson(res, status, body) {
|
|
458
496
|
res.writeHead(status, {
|
|
@@ -505,6 +543,9 @@ async function main() {
|
|
|
505
543
|
// before anything spawns (bash tool, background tasks, LSP, git helpers all
|
|
506
544
|
// inherit it). Best-effort — never blocks startup beyond its internal cap.
|
|
507
545
|
await enrichProcessPath();
|
|
546
|
+
// Sweep recoverable full tool outputs (~/.ezcoder/tool-output/) older than 48h.
|
|
547
|
+
// Fire-and-forget: cleanup must never delay or break startup.
|
|
548
|
+
void cleanupToolOutputs().catch(() => { });
|
|
508
549
|
const auth = new AuthStorage(paths.authFile);
|
|
509
550
|
await auth.load();
|
|
510
551
|
// Every window's session lives here as an in-process object, keyed by the id
|
|
@@ -535,30 +576,63 @@ async function main() {
|
|
|
535
576
|
});
|
|
536
577
|
const usageCache = new Map();
|
|
537
578
|
const usageRequests = new Map();
|
|
579
|
+
// 429 backoff: quota endpoints are auxiliary UI data. Honor Retry-After when
|
|
580
|
+
// provided; otherwise retain the unavailable snapshot for 30 minutes. Clamp
|
|
581
|
+
// the provider value so a malformed header can neither hammer the endpoint
|
|
582
|
+
// nor suppress usage data forever.
|
|
583
|
+
const usageRateLimitedUntil = new Map();
|
|
584
|
+
const USAGE_RATE_LIMIT_FALLBACK_BACKOFF_MS = 30 * 60_000;
|
|
585
|
+
const USAGE_RATE_LIMIT_MIN_BACKOFF_MS = 60_000;
|
|
586
|
+
const USAGE_RATE_LIMIT_MAX_BACKOFF_MS = 24 * 60 * 60_000;
|
|
538
587
|
async function fetchUsageProvider(provider) {
|
|
539
|
-
const displayName = provider === "anthropic" ? "Anthropic" : "Codex";
|
|
540
|
-
|
|
588
|
+
const displayName = provider === "anthropic" ? "Anthropic" : provider === "openai" ? "Codex" : "Kimi";
|
|
589
|
+
// Kimi plan usage is tracked on the OAuth credential specifically — the
|
|
590
|
+
// Moonshot platform API key is metered per-token, not per plan window.
|
|
591
|
+
const authKey = provider === "moonshot" ? MOONSHOT_OAUTH_KEY : provider;
|
|
592
|
+
if (!(await auth.hasProviderAuth(authKey))) {
|
|
541
593
|
return { provider, displayName, connected: false, windows: [], fetchedAt: Date.now() };
|
|
542
594
|
}
|
|
543
595
|
try {
|
|
544
|
-
let credentials = await auth.resolveCredentials(
|
|
596
|
+
let credentials = await auth.resolveCredentials(authKey);
|
|
545
597
|
try {
|
|
546
|
-
|
|
598
|
+
const snapshot = {
|
|
599
|
+
...(await fetchSubscriptionUsage(provider, credentials)),
|
|
600
|
+
connected: true,
|
|
601
|
+
};
|
|
602
|
+
usageRateLimitedUntil.delete(provider);
|
|
603
|
+
return snapshot;
|
|
547
604
|
}
|
|
548
605
|
catch (error) {
|
|
549
606
|
// A provider can revoke an access token before its stored expiry. Refresh
|
|
550
607
|
// once on 401, matching inference auth recovery, then retry the usage call.
|
|
551
608
|
if (error instanceof SubscriptionUsageError && error.status === 401) {
|
|
552
|
-
credentials = await auth.resolveCredentials(
|
|
553
|
-
|
|
609
|
+
credentials = await auth.resolveCredentials(authKey, { forceRefresh: true });
|
|
610
|
+
const snapshot = {
|
|
611
|
+
...(await fetchSubscriptionUsage(provider, credentials)),
|
|
612
|
+
connected: true,
|
|
613
|
+
};
|
|
614
|
+
usageRateLimitedUntil.delete(provider);
|
|
615
|
+
return snapshot;
|
|
554
616
|
}
|
|
555
617
|
throw error;
|
|
556
618
|
}
|
|
557
619
|
}
|
|
558
620
|
catch (error) {
|
|
559
621
|
const message = error instanceof Error ? error.message : String(error);
|
|
560
|
-
|
|
561
|
-
|
|
622
|
+
if (shouldCaptureUsagePollingError(error)) {
|
|
623
|
+
captureSidecarError(error, "app-sidecar.usage.fetch", { provider });
|
|
624
|
+
}
|
|
625
|
+
let backoffMs;
|
|
626
|
+
if (error instanceof SubscriptionUsageError && error.status === 429) {
|
|
627
|
+
backoffMs = Math.min(USAGE_RATE_LIMIT_MAX_BACKOFF_MS, Math.max(USAGE_RATE_LIMIT_MIN_BACKOFF_MS, error.retryAfterMs ?? USAGE_RATE_LIMIT_FALLBACK_BACKOFF_MS));
|
|
628
|
+
usageRateLimitedUntil.set(provider, Date.now() + backoffMs);
|
|
629
|
+
}
|
|
630
|
+
log("WARN", "app-sidecar", "subscription usage fetch failed", {
|
|
631
|
+
provider,
|
|
632
|
+
message,
|
|
633
|
+
...(backoffMs !== undefined && { backoffMs: String(backoffMs) }),
|
|
634
|
+
});
|
|
635
|
+
const connected = await auth.hasProviderAuth(authKey);
|
|
562
636
|
return {
|
|
563
637
|
provider,
|
|
564
638
|
displayName,
|
|
@@ -586,9 +660,12 @@ async function main() {
|
|
|
586
660
|
const missingReset = result.connected &&
|
|
587
661
|
result.windows.length > 0 &&
|
|
588
662
|
result.windows.some((window) => window.resetsAt === undefined);
|
|
663
|
+
const rateLimitedUntil = usageRateLimitedUntil.get(provider) ?? 0;
|
|
589
664
|
usageCache.set(provider, {
|
|
590
665
|
result,
|
|
591
|
-
expiresAt: Date.now()
|
|
666
|
+
expiresAt: rateLimitedUntil > Date.now()
|
|
667
|
+
? rateLimitedUntil
|
|
668
|
+
: Date.now() + (missingReset ? 10_000 : 60_000),
|
|
592
669
|
});
|
|
593
670
|
return result;
|
|
594
671
|
}
|
|
@@ -610,7 +687,7 @@ async function main() {
|
|
|
610
687
|
return null;
|
|
611
688
|
}
|
|
612
689
|
}
|
|
613
|
-
const server = http.createServer((req, res) => {
|
|
690
|
+
const server = http.createServer(wrapSidecarHandler((req, res) => {
|
|
614
691
|
const url = req.url ?? "/";
|
|
615
692
|
const method = req.method ?? "GET";
|
|
616
693
|
// CORS preflight — the webview origin differs from 127.0.0.1.
|
|
@@ -626,7 +703,9 @@ async function main() {
|
|
|
626
703
|
// ── Daemon-level routes (session lifecycle) ──────────────────────────
|
|
627
704
|
// Create a session for a window: { mode?, cwd, sessionPath? } → { sessionId }.
|
|
628
705
|
if (method === "POST" && url === "/session") {
|
|
629
|
-
void daemonReadBody(req).then(async (raw) => {
|
|
706
|
+
void daemonReadBody(req, res).then(async (raw) => {
|
|
707
|
+
if (raw === null)
|
|
708
|
+
return;
|
|
630
709
|
let body = {};
|
|
631
710
|
try {
|
|
632
711
|
body = raw ? JSON.parse(raw) : {};
|
|
@@ -654,6 +733,7 @@ async function main() {
|
|
|
654
733
|
}
|
|
655
734
|
catch (err) {
|
|
656
735
|
const message = err instanceof Error ? err.message : String(err);
|
|
736
|
+
captureSidecarError(err, "app-sidecar.session.create");
|
|
657
737
|
log("ERROR", "app-sidecar", "session create failed", { message });
|
|
658
738
|
daemonJson(res, 500, { error: message });
|
|
659
739
|
}
|
|
@@ -684,12 +764,13 @@ async function main() {
|
|
|
684
764
|
if (method === "GET" && (url === "/usage" || url.startsWith("/usage?"))) {
|
|
685
765
|
void (async () => {
|
|
686
766
|
const provider = new URL(url, `http://${host}`).searchParams.get("provider");
|
|
687
|
-
if (provider !== "anthropic" && provider !== "openai") {
|
|
767
|
+
if (provider !== "anthropic" && provider !== "openai" && provider !== "moonshot") {
|
|
688
768
|
daemonJson(res, 400, { error: "unsupported usage provider" });
|
|
689
769
|
return;
|
|
690
770
|
}
|
|
691
771
|
daemonJson(res, 200, await subscriptionUsage(provider));
|
|
692
772
|
})().catch((error) => {
|
|
773
|
+
captureSidecarError(error, "app-sidecar.usage.request");
|
|
693
774
|
log("ERROR", "app-sidecar", "subscription usage request failed", {
|
|
694
775
|
message: error instanceof Error ? error.message : String(error),
|
|
695
776
|
});
|
|
@@ -705,7 +786,7 @@ async function main() {
|
|
|
705
786
|
return;
|
|
706
787
|
}
|
|
707
788
|
ctx.handle(req, res, url, method);
|
|
708
|
-
});
|
|
789
|
+
}, "app-sidecar.http"));
|
|
709
790
|
server.listen(port, host, () => {
|
|
710
791
|
const addr = server.address();
|
|
711
792
|
// The Rust shell reads this line to learn the daemon port.
|
|
@@ -722,6 +803,8 @@ async function main() {
|
|
|
722
803
|
// Radio playback is app-wide (one stream across all windows), so it stops
|
|
723
804
|
// at the daemon level, not per session.
|
|
724
805
|
stopRadio();
|
|
806
|
+
// Close the ~/.ezcoder progress fs.watch handle (baseline #8 leak fix).
|
|
807
|
+
progress.dispose();
|
|
725
808
|
await Promise.all([...sessions.values()].map((c) => c.dispose().catch(() => { })));
|
|
726
809
|
server.close();
|
|
727
810
|
process.exit(0);
|
|
@@ -850,6 +933,7 @@ async function createProgressManager(agentDir, broadcastAll) {
|
|
|
850
933
|
broadcastAll(buildSnapshot(updated), originId);
|
|
851
934
|
}
|
|
852
935
|
catch (err) {
|
|
936
|
+
captureSidecarError(err, "app-sidecar.progress.award");
|
|
853
937
|
log("DEBUG", "app-sidecar", "progress award failed", {
|
|
854
938
|
message: err instanceof Error ? err.message : String(err),
|
|
855
939
|
});
|
|
@@ -858,6 +942,7 @@ async function createProgressManager(agentDir, broadcastAll) {
|
|
|
858
942
|
// Watch ~/.ezcoder (dir watch survives the atomic tmp+rename) for progress.json
|
|
859
943
|
// writes from other daemon processes; debounce, reload read-only, dedupe by nonce.
|
|
860
944
|
let watchDebounce = null;
|
|
945
|
+
let progressWatcher = null;
|
|
861
946
|
try {
|
|
862
947
|
const watcher = fsWatch(agentDir, (_event, filename) => {
|
|
863
948
|
if (filename !== "progress.json")
|
|
@@ -879,13 +964,30 @@ async function createProgressManager(agentDir, broadcastAll) {
|
|
|
879
964
|
}, 150);
|
|
880
965
|
});
|
|
881
966
|
watcher.unref();
|
|
967
|
+
progressWatcher = watcher;
|
|
882
968
|
}
|
|
883
969
|
catch (err) {
|
|
970
|
+
captureSidecarError(err, "app-sidecar.progress.watch");
|
|
884
971
|
log("DEBUG", "app-sidecar", "progress watch unavailable", {
|
|
885
972
|
message: err instanceof Error ? err.message : String(err),
|
|
886
973
|
});
|
|
887
974
|
}
|
|
888
|
-
|
|
975
|
+
// Dispose closes the fs.watch handle (baseline #8: it was previously never
|
|
976
|
+
// closed — a per-daemon leak) and clears any pending debounce timer.
|
|
977
|
+
function dispose() {
|
|
978
|
+
if (watchDebounce) {
|
|
979
|
+
clearTimeout(watchDebounce);
|
|
980
|
+
watchDebounce = null;
|
|
981
|
+
}
|
|
982
|
+
try {
|
|
983
|
+
progressWatcher?.close();
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
// Already closed / never opened — nothing to do.
|
|
987
|
+
}
|
|
988
|
+
progressWatcher = null;
|
|
989
|
+
}
|
|
990
|
+
return { snapshot, awardRun, dispose };
|
|
889
991
|
}
|
|
890
992
|
/**
|
|
891
993
|
* Build one in-process agent session: its AgentSession, SSE client set, event
|
|
@@ -904,6 +1006,10 @@ async function createSession(deps, opts) {
|
|
|
904
1006
|
// parsing); the daemon owns the real listen host.
|
|
905
1007
|
const host = "127.0.0.1";
|
|
906
1008
|
const saved = loadSavedSettings(paths.settingsFile);
|
|
1009
|
+
// Native login/logout and other live sessions share auth.json. Refresh the
|
|
1010
|
+
// daemon-level snapshot before choosing this session's provider so a project
|
|
1011
|
+
// never boots against credentials that were just replaced or disconnected.
|
|
1012
|
+
await auth.load();
|
|
907
1013
|
// Per-project model/thinking prefs win over the shared global settings.json:
|
|
908
1014
|
// each window (one project cwd) restores its own selection instead of every
|
|
909
1015
|
// window reading the same single global slot that the last writer clobbered
|
|
@@ -922,9 +1028,14 @@ async function createSession(deps, opts) {
|
|
|
922
1028
|
});
|
|
923
1029
|
}
|
|
924
1030
|
// Per-project thinking prefs win over the global settings.json fallback.
|
|
1031
|
+
// With no saved level, the default follows the active credential's endpoint:
|
|
1032
|
+
// Kimi K3 on the OAuth coding endpoint starts at its declared default (high),
|
|
1033
|
+
// matching the official kimi-code CLI's plan-usage profile.
|
|
925
1034
|
const thinkEnabled = projectPrefs?.thinkingEnabled ?? saved.thinkingEnabled;
|
|
926
1035
|
const thinkingLevel = thinkEnabled
|
|
927
|
-
? (projectPrefs?.thinkingLevel ??
|
|
1036
|
+
? (projectPrefs?.thinkingLevel ??
|
|
1037
|
+
saved.thinkingLevel ??
|
|
1038
|
+
getDefaultThinkingLevel(model, { baseUrl: auth.getStoredBaseUrl(provider) }))
|
|
928
1039
|
: undefined;
|
|
929
1040
|
// ── SSE fan-out (declared before the session so plan callbacks can use it) ─
|
|
930
1041
|
const clients = new Set();
|
|
@@ -971,6 +1082,11 @@ async function createSession(deps, opts) {
|
|
|
971
1082
|
const f = formatError(err);
|
|
972
1083
|
const message = f.message ? desktopGuidance(f.message) : undefined;
|
|
973
1084
|
const guidance = desktopGuidance(f.guidance);
|
|
1085
|
+
captureSidecarError(err, `app-sidecar.${logLabel.replaceAll(" ", "-")}`, {
|
|
1086
|
+
scope: type,
|
|
1087
|
+
...(f.provider ? { provider: f.provider } : {}),
|
|
1088
|
+
...(f.statusCode != null ? { status: String(f.statusCode) } : {}),
|
|
1089
|
+
});
|
|
974
1090
|
log("ERROR", "app-sidecar", logLabel, {
|
|
975
1091
|
headline: f.headline,
|
|
976
1092
|
source: f.source,
|
|
@@ -1011,6 +1127,11 @@ async function createSession(deps, opts) {
|
|
|
1011
1127
|
signal: abort.signal,
|
|
1012
1128
|
// Keep MCP startup off the readiness path in both modes.
|
|
1013
1129
|
backgroundMcpConnect: true,
|
|
1130
|
+
// Keep restore-time auto-compaction off the readiness path too: its summary
|
|
1131
|
+
// LLM call (30s timeout) used to freeze waitForReady — and with it the whole
|
|
1132
|
+
// window (project picker, session list) — whenever a resumed session was
|
|
1133
|
+
// over the context threshold. First prompt compacts instead, with UI events.
|
|
1134
|
+
deferLoadCompaction: true,
|
|
1014
1135
|
};
|
|
1015
1136
|
let session;
|
|
1016
1137
|
if (mode === "chat") {
|
|
@@ -1023,6 +1144,7 @@ async function createSession(deps, opts) {
|
|
|
1023
1144
|
chatAgent = nextAgent;
|
|
1024
1145
|
broadcast("chat_agent_change", { chatAgent: nextAgent });
|
|
1025
1146
|
await session.persistAppMarker("agent_handoff", { chatAgent: nextAgent }).catch((error) => {
|
|
1147
|
+
captureSidecarError(error, "app-sidecar.chat-agent.persist-handoff");
|
|
1026
1148
|
log("WARN", "app-sidecar", "agent handoff marker persist failed", {
|
|
1027
1149
|
message: error instanceof Error ? error.message : String(error),
|
|
1028
1150
|
});
|
|
@@ -1038,7 +1160,9 @@ async function createSession(deps, opts) {
|
|
|
1038
1160
|
// Unattended task runs cannot approve plans; implement directly instead.
|
|
1039
1161
|
if (taskRunActive)
|
|
1040
1162
|
return false;
|
|
1163
|
+
deactivateApprovedPlan();
|
|
1041
1164
|
await session.setPlanMode(true);
|
|
1165
|
+
broadcast("plan_progress", { total: 0, completed: [] });
|
|
1042
1166
|
broadcast("plan_enter", { reason: reason ?? "" });
|
|
1043
1167
|
void session.persistAppMarker("plan", { reason: reason ?? "" }).catch(() => { });
|
|
1044
1168
|
},
|
|
@@ -1073,11 +1197,17 @@ async function createSession(deps, opts) {
|
|
|
1073
1197
|
}
|
|
1074
1198
|
}
|
|
1075
1199
|
log("INFO", "app-sidecar", "session ready", { provider, model, mode, chatAgent, cwd });
|
|
1076
|
-
//
|
|
1077
|
-
//
|
|
1200
|
+
// Workspace extras (context window, git status, background tasks). Git state
|
|
1201
|
+
// is resolved once at startup and refreshed after every run; the context
|
|
1078
1202
|
// window follows the active model.
|
|
1079
|
-
|
|
1080
|
-
|
|
1203
|
+
const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount] = await Promise.all([
|
|
1204
|
+
getGitBranch(cwd).catch(() => null),
|
|
1205
|
+
isGitRepo(cwd).catch(() => false),
|
|
1206
|
+
getGitDirtyFileCount(cwd).catch(() => 0),
|
|
1207
|
+
]);
|
|
1208
|
+
let gitBranch = initialGitBranch;
|
|
1209
|
+
let gitIsRepo = initialGitIsRepo;
|
|
1210
|
+
let gitDirtyFileCount = initialDirtyFileCount;
|
|
1081
1211
|
function currentContextWindow() {
|
|
1082
1212
|
const st = session.getState();
|
|
1083
1213
|
return getContextWindow(st.model, { provider: st.provider, accountId: st.accountId });
|
|
@@ -1089,6 +1219,7 @@ async function createSession(deps, opts) {
|
|
|
1089
1219
|
contextWindow: currentContextWindow(),
|
|
1090
1220
|
gitBranch,
|
|
1091
1221
|
isGitRepo: gitIsRepo,
|
|
1222
|
+
gitDirtyFileCount,
|
|
1092
1223
|
tasks: session.listBackgroundProcesses(),
|
|
1093
1224
|
};
|
|
1094
1225
|
}
|
|
@@ -1098,8 +1229,94 @@ async function createSession(deps, opts) {
|
|
|
1098
1229
|
// failures (e.g. repeated invalid-argument errors) instead of leaving the
|
|
1099
1230
|
// fatal-abort path with no forensic trail.
|
|
1100
1231
|
const toolCallNames = new Map();
|
|
1232
|
+
// Approved-plan progress belongs beside the plan file, not in the webview.
|
|
1233
|
+
// The implementation can rewrite/expand that file mid-run, so a step count
|
|
1234
|
+
// frozen at approval time becomes dishonest (the exact stale-total bug the
|
|
1235
|
+
// CLI already fixed). Re-read the live file after tools/markers, retain the
|
|
1236
|
+
// last valid step section during transient edits, and send one authoritative
|
|
1237
|
+
// snapshot to the app.
|
|
1238
|
+
let approvedPlanPath = null;
|
|
1239
|
+
let approvedPlanTotal = 0;
|
|
1240
|
+
let approvedPlanMarkers = new Set();
|
|
1241
|
+
let approvedPlanGeneration = 0;
|
|
1242
|
+
let planMarkerTail = "";
|
|
1243
|
+
let planProgressSync = Promise.resolve(false);
|
|
1244
|
+
function planProgressPayload() {
|
|
1245
|
+
const completed = [...approvedPlanMarkers]
|
|
1246
|
+
.filter((step) => step >= 1 && step <= approvedPlanTotal)
|
|
1247
|
+
.sort((a, b) => a - b);
|
|
1248
|
+
return { total: approvedPlanTotal, completed };
|
|
1249
|
+
}
|
|
1250
|
+
async function syncApprovedPlanProgress(generation) {
|
|
1251
|
+
const planPath = approvedPlanPath;
|
|
1252
|
+
if (planPath === null || generation !== approvedPlanGeneration)
|
|
1253
|
+
return false;
|
|
1254
|
+
const content = await fs.readFile(planPath, "utf-8").catch(() => null);
|
|
1255
|
+
if (approvedPlanPath !== planPath || generation !== approvedPlanGeneration)
|
|
1256
|
+
return false;
|
|
1257
|
+
if (content !== null) {
|
|
1258
|
+
const freshTotal = extractPlanSteps(content).length;
|
|
1259
|
+
// During an in-place rewrite the step section can briefly disappear.
|
|
1260
|
+
// Keep the last real total instead of flashing 0 or declaring completion.
|
|
1261
|
+
if (freshTotal > 0 || approvedPlanTotal === 0)
|
|
1262
|
+
approvedPlanTotal = freshTotal;
|
|
1263
|
+
}
|
|
1264
|
+
broadcast("plan_progress", planProgressPayload());
|
|
1265
|
+
return (approvedPlanTotal > 0 &&
|
|
1266
|
+
Array.from({ length: approvedPlanTotal }, (_, index) => index + 1).every((step) => approvedPlanMarkers.has(step)));
|
|
1267
|
+
}
|
|
1268
|
+
function queueApprovedPlanProgressSync() {
|
|
1269
|
+
const generation = approvedPlanGeneration;
|
|
1270
|
+
planProgressSync = planProgressSync
|
|
1271
|
+
.catch(() => false)
|
|
1272
|
+
.then(() => syncApprovedPlanProgress(generation))
|
|
1273
|
+
.catch((error) => {
|
|
1274
|
+
captureSidecarError(error, "app-sidecar.plan.refresh-progress");
|
|
1275
|
+
log("WARN", "app-sidecar", "plan progress refresh failed", {
|
|
1276
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1277
|
+
});
|
|
1278
|
+
return false;
|
|
1279
|
+
});
|
|
1280
|
+
return planProgressSync;
|
|
1281
|
+
}
|
|
1282
|
+
async function activateApprovedPlan(planPath) {
|
|
1283
|
+
deactivateApprovedPlan();
|
|
1284
|
+
await session.setApprovedPlan(planPath);
|
|
1285
|
+
if (!planPath)
|
|
1286
|
+
return 0;
|
|
1287
|
+
approvedPlanPath = planPath;
|
|
1288
|
+
await queueApprovedPlanProgressSync();
|
|
1289
|
+
return approvedPlanTotal;
|
|
1290
|
+
}
|
|
1291
|
+
function deactivateApprovedPlan() {
|
|
1292
|
+
approvedPlanGeneration++;
|
|
1293
|
+
approvedPlanPath = null;
|
|
1294
|
+
approvedPlanTotal = 0;
|
|
1295
|
+
approvedPlanMarkers = new Set();
|
|
1296
|
+
planMarkerTail = "";
|
|
1297
|
+
planProgressSync = Promise.resolve(false);
|
|
1298
|
+
}
|
|
1299
|
+
function recordApprovedPlanMarkers(text) {
|
|
1300
|
+
if (approvedPlanPath === null || !text)
|
|
1301
|
+
return;
|
|
1302
|
+
const candidate = planMarkerTail + text;
|
|
1303
|
+
let changed = false;
|
|
1304
|
+
for (const match of candidate.matchAll(/\[DONE:(\d+)\]/gi)) {
|
|
1305
|
+
const step = Number.parseInt(match[1], 10);
|
|
1306
|
+
if (step >= 1 && !approvedPlanMarkers.has(step)) {
|
|
1307
|
+
approvedPlanMarkers.add(step);
|
|
1308
|
+
changed = true;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
planMarkerTail = candidate.slice(-32);
|
|
1312
|
+
if (changed)
|
|
1313
|
+
void queueApprovedPlanProgressSync();
|
|
1314
|
+
}
|
|
1101
1315
|
// Forward every relevant bus event to the webview.
|
|
1102
|
-
session.eventBus.on("text_delta", (d) =>
|
|
1316
|
+
session.eventBus.on("text_delta", (d) => {
|
|
1317
|
+
broadcast("text_delta", d);
|
|
1318
|
+
recordApprovedPlanMarkers(d.text);
|
|
1319
|
+
});
|
|
1103
1320
|
session.eventBus.on("thinking_delta", (d) => broadcast("thinking_delta", d));
|
|
1104
1321
|
session.eventBus.on("tool_call_start", (d) => {
|
|
1105
1322
|
toolCallNames.set(d.toolCallId, d.name);
|
|
@@ -1109,6 +1326,11 @@ async function createSession(deps, opts) {
|
|
|
1109
1326
|
session.eventBus.on("tool_call_end", (d) => {
|
|
1110
1327
|
const name = toolCallNames.get(d.toolCallId) ?? "unknown";
|
|
1111
1328
|
toolCallNames.delete(d.toolCallId);
|
|
1329
|
+
if (d.isError && shouldCaptureToolFailure(name, d.result)) {
|
|
1330
|
+
// Expected model-correctable validation failures stay in the local log and
|
|
1331
|
+
// conversation. Unexpected failures are reported without private result data.
|
|
1332
|
+
captureSidecarError(new Error(`Tool ${name} failed`), `tool.${name}`, { tool: name });
|
|
1333
|
+
}
|
|
1112
1334
|
log(d.isError ? "ERROR" : "INFO", "tool", `Tool call ended: ${name}`, {
|
|
1113
1335
|
id: d.toolCallId,
|
|
1114
1336
|
durationMs: String(d.durationMs),
|
|
@@ -1122,6 +1344,11 @@ async function createSession(deps, opts) {
|
|
|
1122
1344
|
if (name === "tasks") {
|
|
1123
1345
|
broadcast("tasks_list", { tasks: loadTasksSync(cwd) });
|
|
1124
1346
|
}
|
|
1347
|
+
// Any tool can mutate the approved plan (including bash), so refresh after
|
|
1348
|
+
// every completed call while tracking is active. The file is tiny and this
|
|
1349
|
+
// keeps the displayed total aligned before the next completion marker.
|
|
1350
|
+
if (approvedPlanPath !== null)
|
|
1351
|
+
void queueApprovedPlanProgressSync();
|
|
1125
1352
|
});
|
|
1126
1353
|
// Native server tools (e.g. Anthropic web_search) do NOT end the turn — text
|
|
1127
1354
|
// streams before and after them in the SAME turn. The webview must reset its
|
|
@@ -1130,6 +1357,9 @@ async function createSession(deps, opts) {
|
|
|
1130
1357
|
session.eventBus.on("server_tool_call", (d) => broadcast("server_tool_call", d));
|
|
1131
1358
|
session.eventBus.on("turn_end", (d) => broadcast("turn_end", d));
|
|
1132
1359
|
session.eventBus.on("agent_done", (d) => broadcast("agent_done", d));
|
|
1360
|
+
// Non-clean stop (max_tokens/refusal/provider error) — info-style frame so
|
|
1361
|
+
// the webview can warn instead of presenting truncated output as complete.
|
|
1362
|
+
session.eventBus.on("truncated", (d) => broadcast("truncated", d));
|
|
1133
1363
|
session.eventBus.on("error", (d) => {
|
|
1134
1364
|
broadcastError("error", "agent error", d.error);
|
|
1135
1365
|
});
|
|
@@ -1146,15 +1376,16 @@ async function createSession(deps, opts) {
|
|
|
1146
1376
|
});
|
|
1147
1377
|
const cancelledRunEndGenerations = new Set();
|
|
1148
1378
|
let pendingCancelDrain = null;
|
|
1149
|
-
let titleGenerated = false;
|
|
1150
1379
|
// Bumped by /cancel — a run whose cancel generation changed mid-flight was
|
|
1151
1380
|
// canceled and earns no XP.
|
|
1152
1381
|
let cancelGeneration = 0;
|
|
1153
1382
|
// Autopilot (auto-review) toggle for THIS window's project. Loaded from
|
|
1154
1383
|
// ezcoder-app.json on boot; flipped via POST /autopilot. When on, POST /prompt runs
|
|
1155
1384
|
// runAutopilotCycle after the user's turn settles — Nolan auto-reviews the work
|
|
1156
|
-
// and drives the review→prompt→review loop.
|
|
1385
|
+
// and drives the review→prompt→review loop. Nolan is the sole verification
|
|
1386
|
+
// owner in this mode, so suppress the build session's redundant Ideal hook.
|
|
1157
1387
|
let autopilot = mode === "code" && (await loadAutopilot(cwd));
|
|
1388
|
+
session.setIdealReviewSuppressed(autopilot);
|
|
1158
1389
|
// True while an autopilot review is in flight (used to defer nolanAuto model
|
|
1159
1390
|
// switches, like nolanRunning does for chat Nolan, and to drive the spinner).
|
|
1160
1391
|
let autopilotReviewing = false;
|
|
@@ -1351,6 +1582,9 @@ async function createSession(deps, opts) {
|
|
|
1351
1582
|
// (often >5 min) regardless of the user's global speedProfile pick.
|
|
1352
1583
|
forceLongCacheRetention: true,
|
|
1353
1584
|
});
|
|
1585
|
+
// Nolan is already the independent autopilot reviewer; recursively running
|
|
1586
|
+
// his own Ideal self-review adds latency and can corrupt the verdict shape.
|
|
1587
|
+
ken.setIdealReviewSuppressed(true);
|
|
1354
1588
|
await ken.initialize();
|
|
1355
1589
|
// Deliberately no bus bridge: the review is silent. Errors surface via the
|
|
1356
1590
|
// runAutopilotReview try/catch as autopilot_error frames.
|
|
@@ -1361,20 +1595,6 @@ async function createSession(deps, opts) {
|
|
|
1361
1595
|
});
|
|
1362
1596
|
return ken;
|
|
1363
1597
|
}
|
|
1364
|
-
// Resumed session: if it already has a conversation, generate its title now so
|
|
1365
|
-
// the title bar shows it immediately on load (not just after the next prompt).
|
|
1366
|
-
{
|
|
1367
|
-
const hasHistory = session
|
|
1368
|
-
.getMessages()
|
|
1369
|
-
.some((m) => m.role === "user" || m.role === "assistant");
|
|
1370
|
-
if (hasHistory) {
|
|
1371
|
-
titleGenerated = true;
|
|
1372
|
-
void session.generateTitle().then((title) => {
|
|
1373
|
-
if (title)
|
|
1374
|
-
broadcast("session_title", { title });
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
1598
|
function abortOwnedWork() {
|
|
1379
1599
|
cancelGeneration++;
|
|
1380
1600
|
abort.abort();
|
|
@@ -1437,10 +1657,35 @@ async function createSession(deps, opts) {
|
|
|
1437
1657
|
// Fire-and-forget — XP must never delay or break run teardown.
|
|
1438
1658
|
void progress.awardRun(cwd, runStartedAt, opts.id);
|
|
1439
1659
|
}
|
|
1440
|
-
// A run may have switched branches
|
|
1441
|
-
// background tasks
|
|
1442
|
-
gitBranch = await
|
|
1443
|
-
|
|
1660
|
+
// A run may have switched branches, changed files, or spawned/finished
|
|
1661
|
+
// background tasks. Refresh the workspace extras once it settles.
|
|
1662
|
+
[gitBranch, gitIsRepo, gitDirtyFileCount] = await Promise.all([
|
|
1663
|
+
getGitBranch(cwd).catch(() => gitBranch),
|
|
1664
|
+
isGitRepo(cwd).catch(() => gitIsRepo),
|
|
1665
|
+
getGitDirtyFileCount(cwd).catch(() => gitDirtyFileCount),
|
|
1666
|
+
]);
|
|
1667
|
+
// Serialize behind any marker/tool-triggered refresh so the terminal
|
|
1668
|
+
// progress snapshot uses the live plan file. Once every canonical step
|
|
1669
|
+
// is complete, remove the approved plan from future system prompts and
|
|
1670
|
+
// clear the widget before run_end paints the idle activity bar.
|
|
1671
|
+
if (runSucceeded &&
|
|
1672
|
+
!cancelled &&
|
|
1673
|
+
approvedPlanPath !== null &&
|
|
1674
|
+
(await queueApprovedPlanProgressSync())) {
|
|
1675
|
+
try {
|
|
1676
|
+
await session.setApprovedPlan(undefined);
|
|
1677
|
+
deactivateApprovedPlan();
|
|
1678
|
+
broadcast("plan_progress", { total: 0, completed: [] });
|
|
1679
|
+
}
|
|
1680
|
+
catch (error) {
|
|
1681
|
+
// Keep tracking when prompt cleanup fails; hiding the widget here
|
|
1682
|
+
// would claim completion while the approved-plan contract remained.
|
|
1683
|
+
captureSidecarError(error, "app-sidecar.plan.cleanup");
|
|
1684
|
+
log("WARN", "app-sidecar", "completed plan cleanup failed", {
|
|
1685
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1444
1689
|
if (ownsGeneration)
|
|
1445
1690
|
finishOwnedGeneration(generation, false);
|
|
1446
1691
|
// A cancelled injected run is still owned by the surrounding autopilot
|
|
@@ -1463,13 +1708,6 @@ async function createSession(deps, opts) {
|
|
|
1463
1708
|
broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
|
|
1464
1709
|
broadcast("queued", { count: session.getQueuedCount() });
|
|
1465
1710
|
broadcast("extras", footerExtras());
|
|
1466
|
-
if (!titleGenerated) {
|
|
1467
|
-
titleGenerated = true;
|
|
1468
|
-
void session.generateTitle().then((title) => {
|
|
1469
|
-
if (title)
|
|
1470
|
-
broadcast("session_title", { title });
|
|
1471
|
-
});
|
|
1472
|
-
}
|
|
1473
1711
|
}
|
|
1474
1712
|
}
|
|
1475
1713
|
// ── Autopilot orchestration ─────────────────────────────────
|
|
@@ -1574,6 +1812,7 @@ async function createSession(deps, opts) {
|
|
|
1574
1812
|
const generation = runLifecycle.begin(abortOwnedWork).generation;
|
|
1575
1813
|
pendingCancelDrain = null;
|
|
1576
1814
|
autopilotActive = true;
|
|
1815
|
+
session.setIdealReviewSuppressed(true);
|
|
1577
1816
|
// Generation captured by the last plan review; acceptPlan re-checks it so
|
|
1578
1817
|
// a user Accept/Reject landing mid-review always wins.
|
|
1579
1818
|
let planGenAtReview = -1;
|
|
@@ -1600,22 +1839,22 @@ async function createSession(deps, opts) {
|
|
|
1600
1839
|
if (pendingPlanPath === null || planGeneration !== planGenAtReview)
|
|
1601
1840
|
return false;
|
|
1602
1841
|
const planPath = pendingPlanPath;
|
|
1842
|
+
let planTotal;
|
|
1603
1843
|
try {
|
|
1604
|
-
await session.newSession();
|
|
1844
|
+
await session.newSession(true);
|
|
1605
1845
|
injectedAutopilotPrompts = [];
|
|
1606
|
-
|
|
1607
|
-
await session.setApprovedPlan(planPath);
|
|
1846
|
+
planTotal = await activateApprovedPlan(planPath);
|
|
1608
1847
|
}
|
|
1609
1848
|
catch (err) {
|
|
1610
1849
|
broadcastError("autopilot_error", "autopilot plan accept failed", err);
|
|
1611
1850
|
return false;
|
|
1612
1851
|
}
|
|
1613
1852
|
clearPendingPlan();
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1616
|
-
// session_reset clears it — accepted must land first.
|
|
1853
|
+
// Keep the approval marker ahead of the reset, then seed the reset
|
|
1854
|
+
// with the sidecar's canonical count from the actual plan file.
|
|
1617
1855
|
broadcast("autopilot_plan_accepted", {});
|
|
1618
|
-
broadcast("session_reset", {});
|
|
1856
|
+
broadcast("session_reset", { planTotal });
|
|
1857
|
+
broadcast("plan_progress", planProgressPayload());
|
|
1619
1858
|
// Persisted into the NEW session so a resume shows the marker.
|
|
1620
1859
|
void session.persistAutopilotMarker("plan_approved");
|
|
1621
1860
|
return true;
|
|
@@ -1687,6 +1926,7 @@ async function createSession(deps, opts) {
|
|
|
1687
1926
|
}
|
|
1688
1927
|
finally {
|
|
1689
1928
|
autopilotActive = false;
|
|
1929
|
+
session.setIdealReviewSuppressed(autopilot);
|
|
1690
1930
|
finishOwnedGeneration(generation, true);
|
|
1691
1931
|
queueMicrotask(() => void runStrandedQueue());
|
|
1692
1932
|
}
|
|
@@ -1786,9 +2026,9 @@ async function createSession(deps, opts) {
|
|
|
1786
2026
|
async function runTaskByIdInner(task) {
|
|
1787
2027
|
// Fresh session per task so one task's context never bleeds into the next.
|
|
1788
2028
|
await session.newSession();
|
|
2029
|
+
deactivateApprovedPlan();
|
|
1789
2030
|
injectedAutopilotPrompts = [];
|
|
1790
2031
|
clearPendingPlan();
|
|
1791
|
-
titleGenerated = false;
|
|
1792
2032
|
broadcast("session_reset", {});
|
|
1793
2033
|
markTaskInProgress(cwd, task.id);
|
|
1794
2034
|
broadcast("tasks_list", { tasks: loadTasksSync(cwd) });
|
|
@@ -1872,13 +2112,32 @@ async function createSession(deps, opts) {
|
|
|
1872
2112
|
tasksPoll.unref?.();
|
|
1873
2113
|
};
|
|
1874
2114
|
scheduleTasksPoll(1500);
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
2115
|
+
// Files can change outside the agent (editor saves, terminal commits), so keep
|
|
2116
|
+
// the dirty count current while idle. Branch/repo state already refreshes after
|
|
2117
|
+
// agent runs; polling only the count avoids spawning three git processes per tick.
|
|
2118
|
+
let gitPoll;
|
|
2119
|
+
let gitPollStopped = false;
|
|
2120
|
+
const scheduleGitPoll = (delay) => {
|
|
2121
|
+
if (gitPollStopped)
|
|
2122
|
+
return;
|
|
2123
|
+
gitPoll = setTimeout(() => {
|
|
2124
|
+
void getGitDirtyFileCount(cwd)
|
|
2125
|
+
.catch(() => gitDirtyFileCount)
|
|
2126
|
+
.then((nextDirtyFileCount) => {
|
|
2127
|
+
if (gitPollStopped)
|
|
2128
|
+
return;
|
|
2129
|
+
if (nextDirtyFileCount !== gitDirtyFileCount) {
|
|
2130
|
+
gitDirtyFileCount = nextDirtyFileCount;
|
|
2131
|
+
broadcast("extras", footerExtras());
|
|
2132
|
+
}
|
|
2133
|
+
scheduleGitPoll(5000);
|
|
2134
|
+
});
|
|
2135
|
+
}, delay);
|
|
2136
|
+
gitPoll.unref?.();
|
|
2137
|
+
};
|
|
2138
|
+
scheduleGitPoll(5000);
|
|
2139
|
+
function readBody(req, res) {
|
|
2140
|
+
return readCappedBody(req, res);
|
|
1882
2141
|
}
|
|
1883
2142
|
function json(res, status, body) {
|
|
1884
2143
|
const payload = JSON.stringify(redactValue(body, { secrets: environmentSecrets(process.env) }));
|
|
@@ -1916,7 +2175,10 @@ async function createSession(deps, opts) {
|
|
|
1916
2175
|
void memoryStore
|
|
1917
2176
|
.snapshot()
|
|
1918
2177
|
.then((snapshot) => json(res, 200, snapshot))
|
|
1919
|
-
.catch((error) =>
|
|
2178
|
+
.catch((error) => {
|
|
2179
|
+
captureSidecarError(error, "app-sidecar.memory.snapshot");
|
|
2180
|
+
json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2181
|
+
});
|
|
1920
2182
|
return;
|
|
1921
2183
|
}
|
|
1922
2184
|
if (method === "DELETE" && url.startsWith("/memories/")) {
|
|
@@ -1929,14 +2191,20 @@ async function createSession(deps, opts) {
|
|
|
1929
2191
|
.forget(id)
|
|
1930
2192
|
.then(() => memoryStore.snapshot())
|
|
1931
2193
|
.then((snapshot) => json(res, 200, snapshot))
|
|
1932
|
-
.catch((error) =>
|
|
2194
|
+
.catch((error) => {
|
|
2195
|
+
captureSidecarError(error, "app-sidecar.memory.forget");
|
|
2196
|
+
json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2197
|
+
});
|
|
1933
2198
|
return;
|
|
1934
2199
|
}
|
|
1935
2200
|
if (method === "GET" && url === "/jiwa") {
|
|
1936
2201
|
void jiwaStore
|
|
1937
2202
|
.snapshot()
|
|
1938
2203
|
.then((snapshot) => json(res, 200, snapshot))
|
|
1939
|
-
.catch((error) =>
|
|
2204
|
+
.catch((error) => {
|
|
2205
|
+
captureSidecarError(error, "app-sidecar.jiwa.snapshot");
|
|
2206
|
+
json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2207
|
+
});
|
|
1940
2208
|
return;
|
|
1941
2209
|
}
|
|
1942
2210
|
if (method === "DELETE" && url.startsWith("/jiwa/")) {
|
|
@@ -1949,7 +2217,10 @@ async function createSession(deps, opts) {
|
|
|
1949
2217
|
.forget(id)
|
|
1950
2218
|
.then(() => jiwaStore.snapshot())
|
|
1951
2219
|
.then((snapshot) => json(res, 200, snapshot))
|
|
1952
|
-
.catch((error) =>
|
|
2220
|
+
.catch((error) => {
|
|
2221
|
+
captureSidecarError(error, "app-sidecar.jiwa.forget");
|
|
2222
|
+
json(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2223
|
+
});
|
|
1953
2224
|
return;
|
|
1954
2225
|
}
|
|
1955
2226
|
if (method === "GET" && (url === "/events" || url.startsWith("/events?"))) {
|
|
@@ -2007,7 +2278,9 @@ async function createSession(deps, opts) {
|
|
|
2007
2278
|
return;
|
|
2008
2279
|
}
|
|
2009
2280
|
if (method === "POST" && url === "/settings") {
|
|
2010
|
-
void readBody(req).then(async (raw) => {
|
|
2281
|
+
void readBody(req, res).then(async (raw) => {
|
|
2282
|
+
if (raw === null)
|
|
2283
|
+
return;
|
|
2011
2284
|
let projectsRoot;
|
|
2012
2285
|
try {
|
|
2013
2286
|
projectsRoot = JSON.parse(raw).projectsRoot ?? "";
|
|
@@ -2030,7 +2303,9 @@ async function createSession(deps, opts) {
|
|
|
2030
2303
|
return;
|
|
2031
2304
|
}
|
|
2032
2305
|
if (method === "POST" && url === "/create-project") {
|
|
2033
|
-
void readBody(req).then(async (raw) => {
|
|
2306
|
+
void readBody(req, res).then(async (raw) => {
|
|
2307
|
+
if (raw === null)
|
|
2308
|
+
return;
|
|
2034
2309
|
let name;
|
|
2035
2310
|
try {
|
|
2036
2311
|
name = JSON.parse(raw).name ?? "";
|
|
@@ -2062,6 +2337,7 @@ async function createSession(deps, opts) {
|
|
|
2062
2337
|
json(res, 200, { path: dir });
|
|
2063
2338
|
}
|
|
2064
2339
|
catch (err) {
|
|
2340
|
+
captureSidecarError(err, "app-sidecar.project.create");
|
|
2065
2341
|
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
2066
2342
|
}
|
|
2067
2343
|
});
|
|
@@ -2072,6 +2348,7 @@ async function createSession(deps, opts) {
|
|
|
2072
2348
|
void discoverProjects()
|
|
2073
2349
|
.then((projects) => json(res, 200, { projects }))
|
|
2074
2350
|
.catch((err) => {
|
|
2351
|
+
captureSidecarError(err, "app-sidecar.projects.discover");
|
|
2075
2352
|
log("ERROR", "app-sidecar", "discoverProjects failed", {
|
|
2076
2353
|
message: err instanceof Error ? err.message : String(err),
|
|
2077
2354
|
});
|
|
@@ -2086,37 +2363,14 @@ async function createSession(deps, opts) {
|
|
|
2086
2363
|
return;
|
|
2087
2364
|
}
|
|
2088
2365
|
const requestedAgent = new URL(url, `http://${host}`).searchParams.get("chatAgent");
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
return agentSessions.map((item) => ({ ...item, chatAgent: agentId }));
|
|
2093
|
-
}))
|
|
2094
|
-
.then(async (groups) => {
|
|
2095
|
-
const dated = await Promise.all(groups.flat().map(async (item) => ({
|
|
2096
|
-
item,
|
|
2097
|
-
mtime: await fs
|
|
2098
|
-
.stat(item.path)
|
|
2099
|
-
.then((stat) => stat.mtimeMs)
|
|
2100
|
-
.catch(() => 0),
|
|
2101
|
-
})));
|
|
2102
|
-
const recent = dated
|
|
2103
|
-
.sort((left, right) => right.mtime - left.mtime)
|
|
2104
|
-
.slice(0, 5)
|
|
2105
|
-
.map(({ item }) => item);
|
|
2106
|
-
json(res, 200, { sessions: recent });
|
|
2107
|
-
})
|
|
2108
|
-
.catch(() => json(res, 200, { sessions: [] }));
|
|
2109
|
-
return;
|
|
2110
|
-
}
|
|
2111
|
-
// The picker may be served by a sidecar currently running in Chat mode.
|
|
2112
|
-
// An omitted chatAgent always means coding history; chat callers identify
|
|
2113
|
-
// their namespace explicitly (one agent or "all").
|
|
2114
|
-
const sessionsDir = requestedAgent
|
|
2115
|
-
? sessionsDirForChatAgent(paths.sessionsDir, requestedAgent)
|
|
2116
|
-
: paths.sessionsDir;
|
|
2117
|
-
void listRecentSessions(target, 5, sessionsDir)
|
|
2366
|
+
// An omitted chatAgent means coding history; chat callers identify one
|
|
2367
|
+
// agent or request the combined, recency-sorted "all" listing.
|
|
2368
|
+
void listSidecarSessions(target, requestedAgent, paths.sessionsDir)
|
|
2118
2369
|
.then((sessions) => json(res, 200, { sessions }))
|
|
2119
|
-
.catch(() =>
|
|
2370
|
+
.catch((error) => {
|
|
2371
|
+
captureSidecarError(error, "app-sidecar.sessions.list");
|
|
2372
|
+
json(res, 200, { sessions: [] });
|
|
2373
|
+
});
|
|
2120
2374
|
return;
|
|
2121
2375
|
}
|
|
2122
2376
|
if (method === "GET" && url.startsWith("/files")) {
|
|
@@ -2124,6 +2378,7 @@ async function createSession(deps, opts) {
|
|
|
2124
2378
|
void searchProjectFiles(cwd, q)
|
|
2125
2379
|
.then((files) => json(res, 200, { files }))
|
|
2126
2380
|
.catch((err) => {
|
|
2381
|
+
captureSidecarError(err, "app-sidecar.files.search");
|
|
2127
2382
|
log("ERROR", "app-sidecar", "searchProjectFiles failed", {
|
|
2128
2383
|
message: err instanceof Error ? err.message : String(err),
|
|
2129
2384
|
});
|
|
@@ -2145,6 +2400,20 @@ async function createSession(deps, opts) {
|
|
|
2145
2400
|
void (async () => {
|
|
2146
2401
|
const commandCandidates = [...PROMPT_COMMANDS, ...(await loadCustomCommands(cwd))];
|
|
2147
2402
|
const messages = session.getMessages();
|
|
2403
|
+
// An Ideal hook is injected immediately after the assistant's candidate
|
|
2404
|
+
// no-tool response. That response is review scratch, not a user-visible
|
|
2405
|
+
// final answer. Live SSE drops it when the hook event arrives; mark the
|
|
2406
|
+
// same assistant message here so resumed history stays identical.
|
|
2407
|
+
const hiddenIdealDrafts = new Set();
|
|
2408
|
+
for (let i = 0; i < messages.length - 1; i++) {
|
|
2409
|
+
const draft = messages[i];
|
|
2410
|
+
const hookPrompt = messages[i + 1];
|
|
2411
|
+
if (draft?.role !== "assistant" || hookPrompt?.role !== "user")
|
|
2412
|
+
continue;
|
|
2413
|
+
const restored = restoreUserRow(hookPrompt.content);
|
|
2414
|
+
if (detectHookKind(restored.text) === "ideal")
|
|
2415
|
+
hiddenIdealDrafts.add(draft);
|
|
2416
|
+
}
|
|
2148
2417
|
// Pre-index tool results by toolCallId so we can pair tool calls with
|
|
2149
2418
|
// their results (for sub-agent status + image extraction).
|
|
2150
2419
|
const toolResultMap = new Map();
|
|
@@ -2362,10 +2631,11 @@ async function createSession(deps, opts) {
|
|
|
2362
2631
|
}
|
|
2363
2632
|
}
|
|
2364
2633
|
}
|
|
2365
|
-
else {
|
|
2634
|
+
else if (!hiddenIdealDrafts.has(msg)) {
|
|
2366
2635
|
// Assistant: one wire row per persisted text block — live streaming
|
|
2367
2636
|
// splits bubbles at server_tool_call boundaries, and the persisted
|
|
2368
|
-
// content keeps those blocks separate.
|
|
2637
|
+
// content keeps those blocks separate. Ideal-review candidate drafts
|
|
2638
|
+
// are intentionally omitted to match the live pre-final hook flow.
|
|
2369
2639
|
for (const blockText of restoreAssistantTexts(msg.content)) {
|
|
2370
2640
|
history.push({
|
|
2371
2641
|
role: "assistant",
|
|
@@ -2449,7 +2719,9 @@ async function createSession(deps, opts) {
|
|
|
2449
2719
|
return;
|
|
2450
2720
|
}
|
|
2451
2721
|
if (method === "POST" && url === "/prompt") {
|
|
2452
|
-
void readBody(req).then(async (raw) => {
|
|
2722
|
+
void readBody(req, res).then(async (raw) => {
|
|
2723
|
+
if (raw === null)
|
|
2724
|
+
return;
|
|
2453
2725
|
let text;
|
|
2454
2726
|
let attachments;
|
|
2455
2727
|
let meta;
|
|
@@ -2576,7 +2848,9 @@ async function createSession(deps, opts) {
|
|
|
2576
2848
|
json(res, 404, { error: "Nolan is not available in EZ Chat." });
|
|
2577
2849
|
return;
|
|
2578
2850
|
}
|
|
2579
|
-
void readBody(req).then(async (raw) => {
|
|
2851
|
+
void readBody(req, res).then(async (raw) => {
|
|
2852
|
+
if (raw === null)
|
|
2853
|
+
return;
|
|
2580
2854
|
let text;
|
|
2581
2855
|
try {
|
|
2582
2856
|
text = JSON.parse(raw).text ?? "";
|
|
@@ -2635,7 +2909,9 @@ async function createSession(deps, opts) {
|
|
|
2635
2909
|
json(res, 404, { error: "Autopilot is not available in EZ Chat." });
|
|
2636
2910
|
return;
|
|
2637
2911
|
}
|
|
2638
|
-
void readBody(req).then(async (raw) => {
|
|
2912
|
+
void readBody(req, res).then(async (raw) => {
|
|
2913
|
+
if (raw === null)
|
|
2914
|
+
return;
|
|
2639
2915
|
let enabled;
|
|
2640
2916
|
try {
|
|
2641
2917
|
enabled = Boolean(JSON.parse(raw).enabled);
|
|
@@ -2645,6 +2921,9 @@ async function createSession(deps, opts) {
|
|
|
2645
2921
|
return;
|
|
2646
2922
|
}
|
|
2647
2923
|
autopilot = enabled;
|
|
2924
|
+
// A toggle-off during an active cycle takes effect after Nolan finishes;
|
|
2925
|
+
// until then, injected build runs must not re-enable Ideal self-review.
|
|
2926
|
+
session.setIdealReviewSuppressed(enabled || autopilotActive);
|
|
2648
2927
|
await saveAutopilot(cwd, enabled);
|
|
2649
2928
|
log("INFO", "app-sidecar", "autopilot toggled", { enabled: String(enabled) });
|
|
2650
2929
|
broadcast("autopilot", { autopilot: enabled });
|
|
@@ -2653,7 +2932,9 @@ async function createSession(deps, opts) {
|
|
|
2653
2932
|
return;
|
|
2654
2933
|
}
|
|
2655
2934
|
if (method === "POST" && url === "/enhance") {
|
|
2656
|
-
void readBody(req).then(async (raw) => {
|
|
2935
|
+
void readBody(req, res).then(async (raw) => {
|
|
2936
|
+
if (raw === null)
|
|
2937
|
+
return;
|
|
2657
2938
|
let text;
|
|
2658
2939
|
try {
|
|
2659
2940
|
text = JSON.parse(raw).text ?? "";
|
|
@@ -2674,6 +2955,7 @@ async function createSession(deps, opts) {
|
|
|
2674
2955
|
}
|
|
2675
2956
|
catch (err) {
|
|
2676
2957
|
const message = err instanceof Error ? err.message : String(err);
|
|
2958
|
+
captureSidecarError(err, "app-sidecar.prompt-enhancer");
|
|
2677
2959
|
log("ERROR", "app-sidecar", "enhance failed", { message });
|
|
2678
2960
|
json(res, 500, { error: message });
|
|
2679
2961
|
}
|
|
@@ -2701,7 +2983,9 @@ async function createSession(deps, opts) {
|
|
|
2701
2983
|
return;
|
|
2702
2984
|
}
|
|
2703
2985
|
if (method === "POST" && url === "/radio/volume") {
|
|
2704
|
-
void readBody(req).then((raw) => {
|
|
2986
|
+
void readBody(req, res).then((raw) => {
|
|
2987
|
+
if (raw === null)
|
|
2988
|
+
return;
|
|
2705
2989
|
let volume;
|
|
2706
2990
|
try {
|
|
2707
2991
|
volume = Number(JSON.parse(raw).volume);
|
|
@@ -2724,7 +3008,9 @@ async function createSession(deps, opts) {
|
|
|
2724
3008
|
return;
|
|
2725
3009
|
}
|
|
2726
3010
|
if (method === "POST" && url === "/radio") {
|
|
2727
|
-
void readBody(req).then((raw) => {
|
|
3011
|
+
void readBody(req, res).then((raw) => {
|
|
3012
|
+
if (raw === null)
|
|
3013
|
+
return;
|
|
2728
3014
|
let station;
|
|
2729
3015
|
try {
|
|
2730
3016
|
station = JSON.parse(raw).station ?? "";
|
|
@@ -2748,7 +3034,9 @@ async function createSession(deps, opts) {
|
|
|
2748
3034
|
return;
|
|
2749
3035
|
}
|
|
2750
3036
|
if (method === "POST" && url === "/tasks/run") {
|
|
2751
|
-
void readBody(req).then((raw) => {
|
|
3037
|
+
void readBody(req, res).then((raw) => {
|
|
3038
|
+
if (raw === null)
|
|
3039
|
+
return;
|
|
2752
3040
|
let id;
|
|
2753
3041
|
let all;
|
|
2754
3042
|
try {
|
|
@@ -2770,7 +3058,9 @@ async function createSession(deps, opts) {
|
|
|
2770
3058
|
return;
|
|
2771
3059
|
}
|
|
2772
3060
|
if (method === "POST" && url === "/tasks/add") {
|
|
2773
|
-
void readBody(req).then((raw) => {
|
|
3061
|
+
void readBody(req, res).then((raw) => {
|
|
3062
|
+
if (raw === null)
|
|
3063
|
+
return;
|
|
2774
3064
|
let title;
|
|
2775
3065
|
let prompt;
|
|
2776
3066
|
try {
|
|
@@ -2802,7 +3092,9 @@ async function createSession(deps, opts) {
|
|
|
2802
3092
|
return;
|
|
2803
3093
|
}
|
|
2804
3094
|
if (method === "POST" && url === "/tasks/update") {
|
|
2805
|
-
void readBody(req).then((raw) => {
|
|
3095
|
+
void readBody(req, res).then((raw) => {
|
|
3096
|
+
if (raw === null)
|
|
3097
|
+
return;
|
|
2806
3098
|
let id;
|
|
2807
3099
|
let status;
|
|
2808
3100
|
let title;
|
|
@@ -2847,7 +3139,9 @@ async function createSession(deps, opts) {
|
|
|
2847
3139
|
return;
|
|
2848
3140
|
}
|
|
2849
3141
|
if (method === "POST" && url === "/tasks/delete") {
|
|
2850
|
-
void readBody(req).then((raw) => {
|
|
3142
|
+
void readBody(req, res).then((raw) => {
|
|
3143
|
+
if (raw === null)
|
|
3144
|
+
return;
|
|
2851
3145
|
let id;
|
|
2852
3146
|
try {
|
|
2853
3147
|
id = JSON.parse(raw).id ?? "";
|
|
@@ -2890,7 +3184,9 @@ async function createSession(deps, opts) {
|
|
|
2890
3184
|
return;
|
|
2891
3185
|
}
|
|
2892
3186
|
if (method === "POST" && url === "/model") {
|
|
2893
|
-
void readBody(req).then(async (raw) => {
|
|
3187
|
+
void readBody(req, res).then(async (raw) => {
|
|
3188
|
+
if (raw === null)
|
|
3189
|
+
return;
|
|
2894
3190
|
let modelId;
|
|
2895
3191
|
try {
|
|
2896
3192
|
modelId = JSON.parse(raw).model ?? "";
|
|
@@ -2962,7 +3258,9 @@ async function createSession(deps, opts) {
|
|
|
2962
3258
|
// to BOTH Nolan sessions (chat + autopilot reviewer); a switch landing while
|
|
2963
3259
|
// either is mid-run defers via the pending-model mechanics.
|
|
2964
3260
|
if (method === "POST" && url === "/ken/model") {
|
|
2965
|
-
void readBody(req).then(async (raw) => {
|
|
3261
|
+
void readBody(req, res).then(async (raw) => {
|
|
3262
|
+
if (raw === null)
|
|
3263
|
+
return;
|
|
2966
3264
|
let modelId;
|
|
2967
3265
|
try {
|
|
2968
3266
|
const parsed = JSON.parse(raw).model;
|
|
@@ -3006,7 +3304,9 @@ async function createSession(deps, opts) {
|
|
|
3006
3304
|
return;
|
|
3007
3305
|
}
|
|
3008
3306
|
if (method === "POST" && url === "/kill") {
|
|
3009
|
-
void readBody(req).then(async (raw) => {
|
|
3307
|
+
void readBody(req, res).then(async (raw) => {
|
|
3308
|
+
if (raw === null)
|
|
3309
|
+
return;
|
|
3010
3310
|
let id;
|
|
3011
3311
|
try {
|
|
3012
3312
|
id = JSON.parse(raw).id ?? "";
|
|
@@ -3084,6 +3384,7 @@ async function createSession(deps, opts) {
|
|
|
3084
3384
|
drained,
|
|
3085
3385
|
});
|
|
3086
3386
|
})().catch((error) => {
|
|
3387
|
+
captureSidecarError(error, "app-sidecar.run.cancel");
|
|
3087
3388
|
broadcast("cancel_failed", { error: "cancel_failed", runState: runLifecycle.state });
|
|
3088
3389
|
json(res, 500, {
|
|
3089
3390
|
error: "cancel_failed",
|
|
@@ -3104,12 +3405,14 @@ async function createSession(deps, opts) {
|
|
|
3104
3405
|
if (mode === "chat") {
|
|
3105
3406
|
await session.persistAppMarker("agent_handoff", { chatAgent });
|
|
3106
3407
|
}
|
|
3408
|
+
deactivateApprovedPlan();
|
|
3107
3409
|
injectedAutopilotPrompts = [];
|
|
3108
3410
|
clearPendingPlan();
|
|
3109
3411
|
broadcast("session_reset", {});
|
|
3110
3412
|
json(res, 200, { ok: true });
|
|
3111
3413
|
})
|
|
3112
3414
|
.catch((err) => {
|
|
3415
|
+
captureSidecarError(err, "app-sidecar.session.new");
|
|
3113
3416
|
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
3114
3417
|
});
|
|
3115
3418
|
return;
|
|
@@ -3124,7 +3427,9 @@ async function createSession(deps, opts) {
|
|
|
3124
3427
|
// 3. session_reset tells the webview to clear its transcript; it then runs
|
|
3125
3428
|
// the "implement it now" prompt in the clean session.
|
|
3126
3429
|
if (method === "POST" && url === "/plan/accept") {
|
|
3127
|
-
void readBody(req).then(async (raw) => {
|
|
3430
|
+
void readBody(req, res).then(async (raw) => {
|
|
3431
|
+
if (raw === null)
|
|
3432
|
+
return;
|
|
3128
3433
|
let planPath;
|
|
3129
3434
|
try {
|
|
3130
3435
|
planPath = JSON.parse(raw).planPath || undefined;
|
|
@@ -3156,14 +3461,15 @@ async function createSession(deps, opts) {
|
|
|
3156
3461
|
broadcast("autopilot_ignored", {});
|
|
3157
3462
|
}
|
|
3158
3463
|
try {
|
|
3159
|
-
await session.newSession();
|
|
3464
|
+
await session.newSession(true);
|
|
3160
3465
|
injectedAutopilotPrompts = [];
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
broadcast("
|
|
3164
|
-
json(res, 200, { ok: true });
|
|
3466
|
+
const planTotal = await activateApprovedPlan(planPath);
|
|
3467
|
+
broadcast("session_reset", { planTotal });
|
|
3468
|
+
broadcast("plan_progress", planProgressPayload());
|
|
3469
|
+
json(res, 200, { ok: true, planTotal });
|
|
3165
3470
|
}
|
|
3166
3471
|
catch (err) {
|
|
3472
|
+
captureSidecarError(err, "app-sidecar.plan.accept");
|
|
3167
3473
|
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
3168
3474
|
}
|
|
3169
3475
|
});
|
|
@@ -3175,7 +3481,9 @@ async function createSession(deps, opts) {
|
|
|
3175
3481
|
return;
|
|
3176
3482
|
}
|
|
3177
3483
|
if (method === "POST" && url === "/auth/apikey") {
|
|
3178
|
-
void readBody(req).then(async (raw) => {
|
|
3484
|
+
void readBody(req, res).then(async (raw) => {
|
|
3485
|
+
if (raw === null)
|
|
3486
|
+
return;
|
|
3179
3487
|
let provider = "";
|
|
3180
3488
|
let key;
|
|
3181
3489
|
let variant;
|
|
@@ -3218,7 +3526,9 @@ async function createSession(deps, opts) {
|
|
|
3218
3526
|
return;
|
|
3219
3527
|
}
|
|
3220
3528
|
if (method === "POST" && url === "/auth/oauth/start") {
|
|
3221
|
-
void readBody(req).then((raw) => {
|
|
3529
|
+
void readBody(req, res).then((raw) => {
|
|
3530
|
+
if (raw === null)
|
|
3531
|
+
return;
|
|
3222
3532
|
let provider = "";
|
|
3223
3533
|
try {
|
|
3224
3534
|
provider = JSON.parse(raw).provider ?? "";
|
|
@@ -3260,6 +3570,7 @@ async function createSession(deps, opts) {
|
|
|
3260
3570
|
broadcast("auth_done", { provider });
|
|
3261
3571
|
}
|
|
3262
3572
|
catch (err) {
|
|
3573
|
+
captureSidecarError(err, "app-sidecar.auth.oauth", { provider });
|
|
3263
3574
|
broadcast("auth_error", {
|
|
3264
3575
|
provider,
|
|
3265
3576
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -3274,7 +3585,9 @@ async function createSession(deps, opts) {
|
|
|
3274
3585
|
return;
|
|
3275
3586
|
}
|
|
3276
3587
|
if (method === "POST" && url === "/auth/oauth/code") {
|
|
3277
|
-
void readBody(req).then((raw) => {
|
|
3588
|
+
void readBody(req, res).then((raw) => {
|
|
3589
|
+
if (raw === null)
|
|
3590
|
+
return;
|
|
3278
3591
|
let code;
|
|
3279
3592
|
try {
|
|
3280
3593
|
code = JSON.parse(raw).code ?? "";
|
|
@@ -3294,7 +3607,9 @@ async function createSession(deps, opts) {
|
|
|
3294
3607
|
return;
|
|
3295
3608
|
}
|
|
3296
3609
|
if (method === "POST" && url === "/auth/logout") {
|
|
3297
|
-
void readBody(req).then(async (raw) => {
|
|
3610
|
+
void readBody(req, res).then(async (raw) => {
|
|
3611
|
+
if (raw === null)
|
|
3612
|
+
return;
|
|
3298
3613
|
let provider;
|
|
3299
3614
|
try {
|
|
3300
3615
|
provider = JSON.parse(raw).provider ?? "";
|
|
@@ -3333,7 +3648,9 @@ async function createSession(deps, opts) {
|
|
|
3333
3648
|
return;
|
|
3334
3649
|
}
|
|
3335
3650
|
if (method === "POST" && url === "/telegram") {
|
|
3336
|
-
void readBody(req).then(async (raw) => {
|
|
3651
|
+
void readBody(req, res).then(async (raw) => {
|
|
3652
|
+
if (raw === null)
|
|
3653
|
+
return;
|
|
3337
3654
|
let botTokenInput;
|
|
3338
3655
|
let userIdInput;
|
|
3339
3656
|
try {
|
|
@@ -3401,6 +3718,7 @@ async function createSession(deps, opts) {
|
|
|
3401
3718
|
}
|
|
3402
3719
|
catch (err) {
|
|
3403
3720
|
serveController = null;
|
|
3721
|
+
captureSidecarError(err, "app-sidecar.serve.start", { provider: st.provider });
|
|
3404
3722
|
json(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
3405
3723
|
}
|
|
3406
3724
|
})();
|
|
@@ -3427,6 +3745,7 @@ async function createSession(deps, opts) {
|
|
|
3427
3745
|
void buildMcpRows(targetCwd)
|
|
3428
3746
|
.then((servers) => json(res, 200, { servers }))
|
|
3429
3747
|
.catch((err) => {
|
|
3748
|
+
captureSidecarError(err, "app-sidecar.mcp.list");
|
|
3430
3749
|
log("ERROR", "app-sidecar", "buildMcpRows failed", {
|
|
3431
3750
|
message: err instanceof Error ? err.message : String(err),
|
|
3432
3751
|
});
|
|
@@ -3435,7 +3754,9 @@ async function createSession(deps, opts) {
|
|
|
3435
3754
|
return;
|
|
3436
3755
|
}
|
|
3437
3756
|
if (method === "POST" && url === "/mcp/add") {
|
|
3438
|
-
void readBody(req).then(async (raw) => {
|
|
3757
|
+
void readBody(req, res).then(async (raw) => {
|
|
3758
|
+
if (raw === null)
|
|
3759
|
+
return;
|
|
3439
3760
|
let line;
|
|
3440
3761
|
let scopeValue;
|
|
3441
3762
|
let bodyCwd;
|
|
@@ -3483,6 +3804,7 @@ async function createSession(deps, opts) {
|
|
|
3483
3804
|
});
|
|
3484
3805
|
}
|
|
3485
3806
|
catch (err) {
|
|
3807
|
+
captureSidecarError(err, "app-sidecar.mcp.add", { server: config.name });
|
|
3486
3808
|
json(res, 500, {
|
|
3487
3809
|
error: err instanceof Error ? err.message : String(err),
|
|
3488
3810
|
});
|
|
@@ -3491,7 +3813,9 @@ async function createSession(deps, opts) {
|
|
|
3491
3813
|
return;
|
|
3492
3814
|
}
|
|
3493
3815
|
if (method === "POST" && url === "/mcp/remove") {
|
|
3494
|
-
void readBody(req).then(async (raw) => {
|
|
3816
|
+
void readBody(req, res).then(async (raw) => {
|
|
3817
|
+
if (raw === null)
|
|
3818
|
+
return;
|
|
3495
3819
|
let name;
|
|
3496
3820
|
let scopeValue;
|
|
3497
3821
|
let bodyCwd;
|
|
@@ -3528,7 +3852,9 @@ async function createSession(deps, opts) {
|
|
|
3528
3852
|
// `mcp_auth_error`. Responds 202 immediately and runs the flow in the
|
|
3529
3853
|
// background (the browser round-trip can take a while).
|
|
3530
3854
|
if (method === "POST" && url === "/mcp/login") {
|
|
3531
|
-
void readBody(req).then(async (raw) => {
|
|
3855
|
+
void readBody(req, res).then(async (raw) => {
|
|
3856
|
+
if (raw === null)
|
|
3857
|
+
return;
|
|
3532
3858
|
let name;
|
|
3533
3859
|
let scopeValue;
|
|
3534
3860
|
let bodyCwd;
|
|
@@ -3572,6 +3898,7 @@ async function createSession(deps, opts) {
|
|
|
3572
3898
|
}
|
|
3573
3899
|
}
|
|
3574
3900
|
catch (err) {
|
|
3901
|
+
captureSidecarError(err, "app-sidecar.mcp.login", { server: name });
|
|
3575
3902
|
broadcast("mcp_auth_error", {
|
|
3576
3903
|
name,
|
|
3577
3904
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -3589,6 +3916,9 @@ async function createSession(deps, opts) {
|
|
|
3589
3916
|
tasksPollStopped = true;
|
|
3590
3917
|
if (tasksPoll)
|
|
3591
3918
|
clearTimeout(tasksPoll);
|
|
3919
|
+
gitPollStopped = true;
|
|
3920
|
+
if (gitPoll)
|
|
3921
|
+
clearTimeout(gitPoll);
|
|
3592
3922
|
// Stop the Telegram serve loop + dispose its per-chat sessions.
|
|
3593
3923
|
if (serveController)
|
|
3594
3924
|
await serveController.stop().catch(() => { });
|
|
@@ -3615,7 +3945,9 @@ async function createSession(deps, opts) {
|
|
|
3615
3945
|
dispose,
|
|
3616
3946
|
};
|
|
3617
3947
|
}
|
|
3618
|
-
main().catch((err) => {
|
|
3948
|
+
main().catch(async (err) => {
|
|
3949
|
+
captureSidecarError(err, "app-sidecar.main", { severity: "fatal" });
|
|
3950
|
+
await flushSidecarErrors();
|
|
3619
3951
|
const message = err instanceof Error ? err.message : String(err);
|
|
3620
3952
|
process.stderr.write(`GG_APP_FATAL ${message}\n`);
|
|
3621
3953
|
process.exit(1);
|