@prestyj/cli 5.32.0 → 5.32.2
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 +3 -2
- package/assets/skills/task-list/SKILL.md +39 -0
- package/dist/app-sidecar/app-settings.js +11 -5
- package/dist/app-sidecar/app-settings.js.map +1 -1
- package/dist/app-sidecar/auth-routes.js +10 -3
- package/dist/app-sidecar/auth-routes.js.map +1 -1
- package/dist/app-sidecar/git-target.d.ts +15 -2
- package/dist/app-sidecar/git-target.js +24 -10
- package/dist/app-sidecar/git-target.js.map +1 -1
- package/dist/app-sidecar/goal-routes.js +8 -0
- package/dist/app-sidecar/goal-routes.js.map +1 -1
- package/dist/app-sidecar/history-routes.js +7 -0
- package/dist/app-sidecar/history-routes.js.map +1 -1
- package/dist/app-sidecar/history.d.ts +5 -0
- package/dist/app-sidecar/history.js.map +1 -1
- package/dist/app-sidecar/integration-routes.js +4 -0
- package/dist/app-sidecar/integration-routes.js.map +1 -1
- package/dist/app-sidecar/mcp-routes.js +1 -0
- package/dist/app-sidecar/mcp-routes.js.map +1 -1
- package/dist/app-sidecar/mcp-rows.d.ts +2 -0
- package/dist/app-sidecar/mcp-rows.js +4 -1
- package/dist/app-sidecar/mcp-rows.js.map +1 -1
- package/dist/app-sidecar/prompt-routes.js +67 -1
- package/dist/app-sidecar/prompt-routes.js.map +1 -1
- package/dist/app-sidecar/route-context.d.ts +2 -0
- package/dist/app-sidecar/run-control-routes.js +43 -3
- package/dist/app-sidecar/run-control-routes.js.map +1 -1
- package/dist/app-sidecar/session-routes.js +10 -0
- package/dist/app-sidecar/session-routes.js.map +1 -1
- package/dist/app-sidecar/task-routes.js +2 -0
- package/dist/app-sidecar/task-routes.js.map +1 -1
- package/dist/app-sidecar/trophy-manager.d.ts +15 -0
- package/dist/app-sidecar/trophy-manager.js +71 -0
- package/dist/app-sidecar/trophy-manager.js.map +1 -0
- package/dist/app-sidecar/workspace-routes.js +1 -0
- package/dist/app-sidecar/workspace-routes.js.map +1 -1
- package/dist/app-sidecar.js +157 -30
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli/auth.js +22 -1
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/mcp.js +6 -1
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli.js +13 -9
- package/dist/cli.js.map +1 -1
- package/dist/config.js +4 -2
- package/dist/config.js.map +1 -1
- package/dist/core/action-threats.d.ts +68 -0
- package/dist/core/action-threats.js +894 -0
- package/dist/core/action-threats.js.map +1 -0
- package/dist/core/agent-notifications.d.ts +18 -0
- package/dist/core/agent-notifications.js +35 -2
- package/dist/core/agent-notifications.js.map +1 -1
- package/dist/core/agent-session/verification-tracker.d.ts +21 -0
- package/dist/core/agent-session/verification-tracker.js +39 -1
- package/dist/core/agent-session/verification-tracker.js.map +1 -1
- package/dist/core/agent-session.d.ts +168 -2
- package/dist/core/agent-session.js +486 -23
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/blocked-packages-file.d.ts +33 -0
- package/dist/core/blocked-packages-file.js +274 -0
- package/dist/core/blocked-packages-file.js.map +1 -0
- package/dist/core/compaction/policy.d.ts +2 -2
- package/dist/core/compaction/policy.js +86 -43
- package/dist/core/compaction/policy.js.map +1 -1
- package/dist/core/compaction/token-estimator.d.ts +4 -1
- package/dist/core/compaction/token-estimator.js +5 -2
- package/dist/core/compaction/token-estimator.js.map +1 -1
- package/dist/core/event-bus.d.ts +10 -0
- package/dist/core/event-bus.js +2 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/idle-wake.d.ts +62 -0
- package/dist/core/idle-wake.js +103 -0
- package/dist/core/idle-wake.js.map +1 -0
- package/dist/core/injection-detect.d.ts +2 -1
- package/dist/core/injection-detect.js +13 -4
- package/dist/core/injection-detect.js.map +1 -1
- package/dist/core/installed-threats.d.ts +23 -0
- package/dist/core/installed-threats.js +71 -0
- package/dist/core/installed-threats.js.map +1 -0
- package/dist/core/mcp/catalog-cache.d.ts +2 -0
- package/dist/core/mcp/catalog-cache.js +4 -0
- package/dist/core/mcp/catalog-cache.js.map +1 -1
- package/dist/core/mcp/client.d.ts +8 -0
- package/dist/core/mcp/client.js +37 -3
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/deferred-catalog.d.ts +12 -1
- package/dist/core/mcp/deferred-catalog.js +37 -10
- package/dist/core/mcp/deferred-catalog.js.map +1 -1
- package/dist/core/mcp/index.d.ts +2 -0
- package/dist/core/mcp/index.js +1 -0
- package/dist/core/mcp/index.js.map +1 -1
- package/dist/core/mcp/tool-guard.d.ts +58 -0
- package/dist/core/mcp/tool-guard.js +215 -0
- package/dist/core/mcp/tool-guard.js.map +1 -0
- package/dist/core/network-guard.d.ts +0 -20
- package/dist/core/network-guard.js +6 -0
- package/dist/core/network-guard.js.map +1 -1
- package/dist/core/package-cooldown.d.ts +71 -0
- package/dist/core/package-cooldown.js +301 -0
- package/dist/core/package-cooldown.js.map +1 -0
- package/dist/core/package-threats.d.ts +77 -3
- package/dist/core/package-threats.js +1250 -78
- package/dist/core/package-threats.js.map +1 -1
- package/dist/core/process-manager.d.ts +54 -12
- package/dist/core/process-manager.js +233 -56
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/project-files.d.ts +15 -0
- package/dist/core/project-files.js +37 -0
- package/dist/core/project-files.js.map +1 -0
- package/dist/core/prompt-commands.js +1 -0
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/session-manager.d.ts +3 -1
- package/dist/core/session-manager.js +2 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.js +4 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/shell-threats.d.ts +14 -1
- package/dist/core/shell-threats.js +73 -24
- package/dist/core/shell-threats.js.map +1 -1
- package/dist/core/skill-scan.d.ts +112 -0
- package/dist/core/skill-scan.js +781 -0
- package/dist/core/skill-scan.js.map +1 -0
- package/dist/core/skills.d.ts +33 -0
- package/dist/core/skills.js +87 -16
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts +5 -1
- package/dist/core/slash-commands.js +24 -9
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/steering.d.ts +7 -0
- package/dist/core/steering.js +13 -0
- package/dist/core/steering.js.map +1 -1
- package/dist/core/style-packs/index.js +1 -1
- package/dist/core/style-packs/index.js.map +1 -1
- package/dist/core/style-packs/packs.js +2 -2
- package/dist/core/subagent-manager.js +5 -4
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/threat-feed.d.ts +65 -0
- package/dist/core/threat-feed.js +335 -0
- package/dist/core/threat-feed.js.map +1 -0
- package/dist/core/trophies/catalog-more.d.ts +2 -0
- package/dist/core/trophies/catalog-more.js +857 -0
- package/dist/core/trophies/catalog-more.js.map +1 -0
- package/dist/core/trophies/catalog.d.ts +4 -0
- package/dist/core/trophies/catalog.js +649 -0
- package/dist/core/trophies/catalog.js.map +1 -0
- package/dist/core/trophies/engine.d.ts +70 -0
- package/dist/core/trophies/engine.js +351 -0
- package/dist/core/trophies/engine.js.map +1 -0
- package/dist/core/trophies/store.d.ts +17 -0
- package/dist/core/trophies/store.js +113 -0
- package/dist/core/trophies/store.js.map +1 -0
- package/dist/core/trophies/types.d.ts +119 -0
- package/dist/core/trophies/types.js +78 -0
- package/dist/core/trophies/types.js.map +1 -0
- package/dist/core/verification-evidence.d.ts +10 -6
- package/dist/core/verification-evidence.js +190 -11
- package/dist/core/verification-evidence.js.map +1 -1
- package/dist/goals-agent/goals-agent.js +3 -2
- package/dist/goals-agent/goals-agent.js.map +1 -1
- package/dist/interactive.js +16 -5
- package/dist/interactive.js.map +1 -1
- package/dist/modes/acp-mode.js +8 -1
- package/dist/modes/acp-mode.js.map +1 -1
- package/dist/modes/agent-home-mode.js +13 -1
- package/dist/modes/agent-home-mode.js.map +1 -1
- package/dist/modes/rpc-mode.d.ts +2 -1
- package/dist/modes/rpc-mode.js +15 -3
- package/dist/modes/rpc-mode.js.map +1 -1
- package/dist/modes/serve-mode.js +13 -1
- package/dist/modes/serve-mode.js.map +1 -1
- package/dist/system-prompt.d.ts +8 -0
- package/dist/system-prompt.js +34 -13
- package/dist/system-prompt.js.map +1 -1
- package/dist/test-support/slow-git.d.ts +1 -0
- package/dist/test-support/slow-git.js +21 -0
- package/dist/test-support/slow-git.js.map +1 -0
- package/dist/tools/background-watch-lint.d.ts +18 -0
- package/dist/tools/background-watch-lint.js +102 -0
- package/dist/tools/background-watch-lint.js.map +1 -0
- package/dist/tools/bash.d.ts +15 -4
- package/dist/tools/bash.js +75 -47
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/checklist.d.ts +3 -3
- package/dist/tools/compress.js +45 -0
- package/dist/tools/compress.js.map +1 -1
- package/dist/tools/edit.js +5 -0
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/generate-image.d.ts +29 -6
- package/dist/tools/generate-image.js +325 -151
- package/dist/tools/generate-image.js.map +1 -1
- package/dist/tools/grep.d.ts +14 -0
- package/dist/tools/grep.js +36 -2
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/index.d.ts +5 -1
- package/dist/tools/index.js +7 -4
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/prompt-hints.d.ts +8 -1
- package/dist/tools/prompt-hints.js +22 -2
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/routine-output.d.ts +30 -0
- package/dist/tools/routine-output.js +203 -0
- package/dist/tools/routine-output.js.map +1 -0
- package/dist/tools/skill.d.ts +2 -2
- package/dist/tools/skill.js +8 -1
- package/dist/tools/skill.js.map +1 -1
- package/dist/tools/subagent-shared.js +4 -1
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/task-output.js +11 -1
- package/dist/tools/task-output.js.map +1 -1
- package/dist/tools/write.js +8 -0
- package/dist/tools/write.js.map +1 -1
- package/dist/ui/components/ModelSelector.d.ts +10 -0
- package/dist/ui/components/ModelSelector.js +51 -5
- package/dist/ui/components/ModelSelector.js.map +1 -1
- package/dist/ui/components/ToolExecution.js +2 -2
- package/dist/ui/components/ToolExecution.js.map +1 -1
- package/dist/ui/mcp.d.ts +2 -0
- package/dist/ui/mcp.js.map +1 -1
- package/dist/ui/terminal-history.js +2 -1
- package/dist/ui/terminal-history.js.map +1 -1
- package/dist/ui/tool-line-summary.js +3 -1
- package/dist/ui/tool-line-summary.js.map +1 -1
- package/dist/utils/git.d.ts +12 -3
- package/dist/utils/git.js +21 -8
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/image.d.ts +14 -0
- package/dist/utils/image.js +23 -0
- package/dist/utils/image.js.map +1 -1
- package/package.json +5 -5
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { agentLoop, isAbortError, isUsageLimitError, repairToolPairingAdjacent, } from "@prestyj/agent";
|
|
2
|
-
import { ProviderError, stream, } from "@prestyj/ai";
|
|
2
|
+
import { ProviderError, resolveToolSchema, stream, } from "@prestyj/ai";
|
|
3
3
|
import { EventBus } from "./event-bus.js";
|
|
4
4
|
import { flagUntrustedToolResult } from "./injection-detect.js";
|
|
5
|
+
import { sameStamp, scanProjectLockfiles, stampProjectLockfiles, } from "./installed-threats.js";
|
|
6
|
+
import { onThreatFeedUpdated } from "./threat-feed.js";
|
|
7
|
+
import { parseSafetyBlock } from "./action-threats.js";
|
|
8
|
+
import { recordBlockedPackages } from "./blocked-packages-file.js";
|
|
5
9
|
import { COMPLETION_REVIEW_STATE_KIND, } from "./completion-review.js";
|
|
6
10
|
import { SlashCommandRegistry, createBuiltinCommands, } from "./slash-commands.js";
|
|
7
11
|
import { PROMPT_COMMANDS, getPromptCommand } from "./prompt-commands.js";
|
|
@@ -19,7 +23,7 @@ import { shouldCompact, compact, } from "./compaction/compactor.js";
|
|
|
19
23
|
import { getHistoryMessageVisibility } from "./session-history.js";
|
|
20
24
|
import { sourceFingerprint as computeSourceFingerprint } from "./session-compaction.js";
|
|
21
25
|
import { getAuthStorageKeys, getContextWindow, getModel, MODELS } from "./model-registry.js";
|
|
22
|
-
import {
|
|
26
|
+
import { discoverSkillsWithBlocked } from "./skills.js";
|
|
23
27
|
import { ensureAppDirs } from "../config.js";
|
|
24
28
|
import { buildSubAgentSystemPrompt, buildSystemPrompt, } from "../system-prompt.js";
|
|
25
29
|
import { createTools, createWebSearchTool, } from "../tools/index.js";
|
|
@@ -54,7 +58,8 @@ import { buildLoopBreakMessage } from "./loop-breaker.js";
|
|
|
54
58
|
import { buildRegroundingMessage, requestTextForRegrounding } from "./regrounding.js";
|
|
55
59
|
import { buildSemanticLoopJudgePrompt, buildSemanticLoopMessage, parseSemanticLoopVerdict, SEMANTIC_LOOP_JUDGE_TIMEOUT_MS, withJudgeTimeout, } from "./semantic-loop-check.js";
|
|
56
60
|
import { buildEnvDeltaMessage, buildLanguageUpdateMessage } from "./env-delta.js";
|
|
57
|
-
import {
|
|
61
|
+
import { listProjectFiles } from "./project-files.js";
|
|
62
|
+
import { wrapSteeringText, buildNotificationSteeringText, buildIdleWakeNotificationText, STEERING_PREFIX, } from "./steering.js";
|
|
58
63
|
import { AgentNotificationQueue } from "./agent-notifications.js";
|
|
59
64
|
import { VerificationGate, VERIFICATION_STATE_KIND } from "./verification-gate.js";
|
|
60
65
|
import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
|
|
@@ -69,8 +74,101 @@ import { RunLoopMonitor } from "./agent-session/run-loop-monitor.js";
|
|
|
69
74
|
import { VerificationTracker } from "./agent-session/verification-tracker.js";
|
|
70
75
|
import { McpToolRegistry } from "./agent-session/mcp-tools.js";
|
|
71
76
|
import { TranscriptMarkers } from "./agent-session/transcript-markers.js";
|
|
77
|
+
// ── Output cap ─────────────────────────────────────────────
|
|
78
|
+
/**
|
|
79
|
+
* The output cap to send on a request. A model's output ceiling can equal its
|
|
80
|
+
* whole window (Kimi K2.7 Code: 262,144 of 262,144), and providers that count
|
|
81
|
+
* prompt + max_tokens against the window reject every request with a real
|
|
82
|
+
* prompt. Cap it at the room the model's TOTAL window leaves once the context
|
|
83
|
+
* reaches this session's compaction trigger — the largest prompt it lets build
|
|
84
|
+
* up. The total window, not the route's: the ChatGPT/Codex route's 272K
|
|
85
|
+
* window is an input budget, and GPT-6's 128K output sits outside it.
|
|
86
|
+
*/
|
|
87
|
+
export function outputCapForRequest(options) {
|
|
88
|
+
const totalWindow = getContextWindow(options.model, { provider: options.provider });
|
|
89
|
+
const { targetTokens } = resolveCompactionPolicy({
|
|
90
|
+
provider: options.provider,
|
|
91
|
+
model: options.model,
|
|
92
|
+
contextWindow: getContextWindow(options.model, {
|
|
93
|
+
provider: options.provider,
|
|
94
|
+
accountId: options.accountId,
|
|
95
|
+
}),
|
|
96
|
+
threshold: options.threshold,
|
|
97
|
+
accountId: options.accountId,
|
|
98
|
+
});
|
|
99
|
+
const room = totalWindow - targetTokens;
|
|
100
|
+
return room > 0 ? Math.min(options.maxTokens, room) : options.maxTokens;
|
|
101
|
+
}
|
|
102
|
+
// ── Paid models ────────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Reply for chat bridges (Telegram, agent home) that have no confirmation step.
|
|
105
|
+
* Generic on purpose: every paid model (Claude fast mode, MiMo UltraSpeed) is
|
|
106
|
+
* refused here, so the reason must not name one of them.
|
|
107
|
+
*/
|
|
108
|
+
export function paidModelRefusal(modelName) {
|
|
109
|
+
return `${modelName} spends real money, so it can't be switched to from chat. Switch to it in the EZ Coder app or CLI, where you confirm it.`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Thrown by {@link AgentSession.switchModel} when the target model spends real
|
|
113
|
+
* money and the caller didn't pass the user's explicit confirmation.
|
|
114
|
+
*/
|
|
115
|
+
export class PaidModelConfirmationError extends Error {
|
|
116
|
+
modelName;
|
|
117
|
+
detail;
|
|
118
|
+
constructor(modelName, detail) {
|
|
119
|
+
super(`${modelName} spends real money. ${detail} Confirm before switching to it.`);
|
|
120
|
+
this.name = "PaidModelConfirmationError";
|
|
121
|
+
this.modelName = modelName;
|
|
122
|
+
this.detail = detail;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
72
125
|
export { resolveSessionToolResultCharLimit, resolveSessionTurnToolResultCharLimit, } from "./agent-session/helpers.js";
|
|
73
126
|
// ── Agent Session ──────────────────────────────────────────
|
|
127
|
+
/** A prompt that asks for the live web (see AgentSession.attachWebSearchIfAsked). */
|
|
128
|
+
const WEB_INTENT = /\b(?:web|internet|online|google|googling|website|look (?:it |this |that )?up|(?:latest|newest|current) (?:version|release|news|docs?|documentation)|release notes|news)\b/i;
|
|
129
|
+
/** Distinct security rows one run may add to the chat (and session file). */
|
|
130
|
+
const MAX_SECURITY_ROWS_PER_RUN = 20;
|
|
131
|
+
/**
|
|
132
|
+
* A chat row's target: first line, no control or bidi-override characters
|
|
133
|
+
* (they could make a command look like another), at most 120 chars.
|
|
134
|
+
*/
|
|
135
|
+
export function securityRowTarget(raw) {
|
|
136
|
+
const first = (raw.slice(0, 1024).split(/\r?\n/, 1)[0] ?? "")
|
|
137
|
+
// eslint-disable-next-line no-control-regex -- stripping control characters is the point
|
|
138
|
+
.replace(/[\u0000-\u001f\u007f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "")
|
|
139
|
+
.trim();
|
|
140
|
+
return first.length > 120 ? `${first.slice(0, 119)}\u2026` : first;
|
|
141
|
+
}
|
|
142
|
+
/** Not the home folder or a filesystem root (those are never a project). */
|
|
143
|
+
export function isScannableProjectDir(dir, home = os.homedir()) {
|
|
144
|
+
const resolved = path.resolve(dir);
|
|
145
|
+
if (path.parse(resolved).root === resolved)
|
|
146
|
+
return false;
|
|
147
|
+
const norm = (p) => (process.platform === "win32" ? p.toLowerCase() : p);
|
|
148
|
+
return !home || norm(path.resolve(home)) !== norm(resolved);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Windows on the same project in this process share one in-flight scan
|
|
152
|
+
* (session start and a feed update otherwise fire N identical scans).
|
|
153
|
+
*/
|
|
154
|
+
const inflightLockfileScans = new Map();
|
|
155
|
+
function sharedLockfileScan(cwd, options) {
|
|
156
|
+
const run = async () => {
|
|
157
|
+
const stamp = await stampProjectLockfiles(cwd);
|
|
158
|
+
const found = stamp.size === 0 ? [] : await scanProjectLockfiles(cwd, options);
|
|
159
|
+
return { stamp, found };
|
|
160
|
+
};
|
|
161
|
+
// Test-specific check options are never shared.
|
|
162
|
+
if (options)
|
|
163
|
+
return run();
|
|
164
|
+
const key = path.resolve(cwd);
|
|
165
|
+
const existing = inflightLockfileScans.get(key);
|
|
166
|
+
if (existing)
|
|
167
|
+
return existing;
|
|
168
|
+
const promise = run().finally(() => inflightLockfileScans.delete(key));
|
|
169
|
+
inflightLockfileScans.set(key, promise);
|
|
170
|
+
return promise;
|
|
171
|
+
}
|
|
74
172
|
export class AgentSession {
|
|
75
173
|
eventBus = new EventBus();
|
|
76
174
|
slashCommands = new SlashCommandRegistry();
|
|
@@ -97,6 +195,18 @@ export class AgentSession {
|
|
|
97
195
|
clearReadTracker;
|
|
98
196
|
recordBashReads;
|
|
99
197
|
skills = [];
|
|
198
|
+
/** Skills discovery refused to load (safety scan); shown to the user, named by the skill tool. */
|
|
199
|
+
blockedSkills = [];
|
|
200
|
+
/** Lockfile entries now known to be malware (background check), for the app/CLI to show. */
|
|
201
|
+
installedThreats = [];
|
|
202
|
+
installedWatchStop;
|
|
203
|
+
installedStamp;
|
|
204
|
+
installedScanRunning = false;
|
|
205
|
+
installedWatchStopped = false;
|
|
206
|
+
/** rule\0target of the security rows already shown this run. */
|
|
207
|
+
securityRowsThisRun = new Set();
|
|
208
|
+
/** Project session (code mode): the only kind that writes the "don't install" list. */
|
|
209
|
+
projectSession = false;
|
|
100
210
|
cacheKeyLogged = false;
|
|
101
211
|
// ── Self-correction hook state (mirrors the TUI's useAgentLoop refs) ──
|
|
102
212
|
// Reset at the start of every run; observed from the event stream; read by
|
|
@@ -109,6 +219,8 @@ export class AgentSession {
|
|
|
109
219
|
* delta; anything that changes WITHOUT one is caught by the hook below.
|
|
110
220
|
*/
|
|
111
221
|
renderedEnvironment = {};
|
|
222
|
+
/** Project files as of the first prompt build (undefined until then). */
|
|
223
|
+
projectFiles;
|
|
112
224
|
/** Wall-clock start of the current run; scopes the background-process gate. */
|
|
113
225
|
runStartedAt = 0;
|
|
114
226
|
/** Gate injections spent this run, capped by MAX_PROCESS_GATE_INJECTIONS. */
|
|
@@ -158,6 +270,8 @@ export class AgentSession {
|
|
|
158
270
|
deferredCompactionPending = false;
|
|
159
271
|
/** Latest provider count, anchored to the assistant response it measured. */
|
|
160
272
|
providerContext = null;
|
|
273
|
+
/** See {@link toolDefinitionChars}. */
|
|
274
|
+
toolCharsCache = null;
|
|
161
275
|
originalRequest = "";
|
|
162
276
|
// Messages queued by the user while a run is in flight. Drained at the
|
|
163
277
|
// mid-loop steering boundary (user steering wins over the hooks), mirroring
|
|
@@ -169,6 +283,8 @@ export class AgentSession {
|
|
|
169
283
|
// different message (or past the end) by the time the cancel arrives.
|
|
170
284
|
userQueue = [];
|
|
171
285
|
queueSeq = 0;
|
|
286
|
+
/** Monotonic count of human-originated prompts; idle-wake budgets reset on it. */
|
|
287
|
+
humanPromptSeq = 0;
|
|
172
288
|
/** Instant interrupt: the running loop's preempt listeners, fired on queueMessage. */
|
|
173
289
|
steeringListeners = new Set();
|
|
174
290
|
processManager;
|
|
@@ -299,9 +415,25 @@ export class AgentSession {
|
|
|
299
415
|
}
|
|
300
416
|
return modelInfo?.maxOutputTokens ?? 16384;
|
|
301
417
|
}
|
|
418
|
+
/** {@link outputCapForRequest} for this session's live model and settings. */
|
|
419
|
+
requestMaxTokens(accountId) {
|
|
420
|
+
return outputCapForRequest({
|
|
421
|
+
provider: this.provider,
|
|
422
|
+
model: this.model,
|
|
423
|
+
maxTokens: this.maxTokens,
|
|
424
|
+
threshold: this.settingsManager.get("compactThreshold"),
|
|
425
|
+
accountId,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
302
428
|
async initialize() {
|
|
303
429
|
// Set model for accurate token estimation
|
|
304
430
|
setEstimatorModel(this.model);
|
|
431
|
+
// The Anthropic transport needs the current Claude Code version for its
|
|
432
|
+
// User-Agent before the first request. Without a fresh on-disk cache that
|
|
433
|
+
// is an npm round-trip (~0.3–3s) on the user's first prompt; start it now
|
|
434
|
+
// so it overlaps session setup and the user's typing instead.
|
|
435
|
+
if (this.provider === "anthropic")
|
|
436
|
+
void getClaudeCliUserAgent().catch(() => { });
|
|
305
437
|
const paths = await ensureAppDirs();
|
|
306
438
|
// Load settings & auth
|
|
307
439
|
this.settingsManager = new SettingsManager(paths.settingsFile);
|
|
@@ -312,6 +444,12 @@ export class AgentSession {
|
|
|
312
444
|
// Session manager. Agent-specific roots keep chat and coder histories isolated.
|
|
313
445
|
this.sessionManager = new SessionManager(this.opts.sessionRootDir ?? paths.sessionsDir);
|
|
314
446
|
const projectCustomization = this.opts.projectCustomization !== false;
|
|
447
|
+
// A real project folder: not Motion (own skills), not GG Goals (goal notes
|
|
448
|
+
// folder, agentContext "none"), not chat (no project customization).
|
|
449
|
+
this.projectSession =
|
|
450
|
+
projectCustomization &&
|
|
451
|
+
!this.opts.skills &&
|
|
452
|
+
(this.opts.agentContext ?? "project") === "project";
|
|
315
453
|
// A sub-agent worker never creates them: the session that owns the
|
|
316
454
|
// project does. EZ Goals' workers work in the user's attached folders,
|
|
317
455
|
// which mustn't gain empty .ezcoder folders as a side effect.
|
|
@@ -326,10 +464,13 @@ export class AgentSession {
|
|
|
326
464
|
this.skills = [...this.opts.skills];
|
|
327
465
|
}
|
|
328
466
|
else if (projectCustomization) {
|
|
329
|
-
|
|
467
|
+
const discovery = await discoverSkillsWithBlocked({
|
|
330
468
|
globalSkillsDir: paths.skillsDir,
|
|
331
469
|
projectDir: this.cwd,
|
|
332
470
|
});
|
|
471
|
+
this.skills = discovery.skills;
|
|
472
|
+
this.blockedSkills = discovery.blocked;
|
|
473
|
+
this.startInstalledPackageWatch();
|
|
333
474
|
}
|
|
334
475
|
else {
|
|
335
476
|
this.skills = [];
|
|
@@ -351,6 +492,7 @@ export class AgentSession {
|
|
|
351
492
|
shareWorkspaceRoots: this.opts.shareRootsWithSubAgents,
|
|
352
493
|
defaultSubAgent: this.opts.defaultSubAgent,
|
|
353
494
|
skills: this.skills,
|
|
495
|
+
blockedSkills: this.blockedSkills,
|
|
354
496
|
contextLimits: this.contextLimits,
|
|
355
497
|
provider: this.provider,
|
|
356
498
|
model: this.model,
|
|
@@ -360,6 +502,7 @@ export class AgentSession {
|
|
|
360
502
|
allowOutsideWorkspaceWrites: this.settingsManager.get("allowOutsideWorkspaceWrites"),
|
|
361
503
|
additionalRoots: this.additionalRoots,
|
|
362
504
|
}),
|
|
505
|
+
getPackageCooldownDays: () => this.settingsManager.get("packageCooldownDays"),
|
|
363
506
|
getNetworkPolicy: () => ({
|
|
364
507
|
mode: this.settingsManager.get("networkMode"),
|
|
365
508
|
allow: this.settingsManager.get("networkAllow"),
|
|
@@ -718,6 +861,45 @@ export class AgentSession {
|
|
|
718
861
|
this.wrapToPromote("spawn_agent", ["wait_agent"]);
|
|
719
862
|
this.wrapToPromote("bash", ["task_output", "task_send", "task_stop"], (args) => args.run_in_background === true);
|
|
720
863
|
}
|
|
864
|
+
/**
|
|
865
|
+
* Whether provider-native web search rides along with this request.
|
|
866
|
+
*
|
|
867
|
+
* Anthropic's `web_search_20250305` server tool costs ~2.5k prefix tokens on
|
|
868
|
+
* every request (count_tokens: 6,628 vs 4,098 for the GG prefix without it),
|
|
869
|
+
* yet web search is reached in ~6% of sessions — the reason the client tool
|
|
870
|
+
* is deferred everywhere else. So on a tiered Anthropic session it is only
|
|
871
|
+
* attached once the model has promoted `web_search` (tool_search or a direct
|
|
872
|
+
* call). The server tool is appended last, so the tool block stays
|
|
873
|
+
* append-only. Untiered sessions and other providers keep it always on.
|
|
874
|
+
*/
|
|
875
|
+
nativeWebSearchOn() {
|
|
876
|
+
if (this.provider !== "anthropic")
|
|
877
|
+
return true;
|
|
878
|
+
if (!this.deferredBuiltinToolNames.includes("web_search"))
|
|
879
|
+
return true;
|
|
880
|
+
return this.tools.some((t) => t.name === "web_search");
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* A human prompt that plainly asks for the web gets native web search from
|
|
884
|
+
* its first request. Without this, an on-demand web_search costs an extra
|
|
885
|
+
* round trip (tool_search or a client search) before the native tool is
|
|
886
|
+
* attached — slower than the always-on tool it replaced for exactly the
|
|
887
|
+
* sessions that need it. Conservative on purpose: a miss only costs that
|
|
888
|
+
* one discovery turn, a false hit only the ~2.5k-token schema.
|
|
889
|
+
*/
|
|
890
|
+
attachWebSearchIfAsked(content) {
|
|
891
|
+
if (this.provider !== "anthropic" || this.nativeWebSearchOn())
|
|
892
|
+
return;
|
|
893
|
+
const text = typeof content === "string"
|
|
894
|
+
? content
|
|
895
|
+
: Array.isArray(content)
|
|
896
|
+
? content
|
|
897
|
+
.map((part) => part && typeof part === "object" && "text" in part ? String(part.text) : "")
|
|
898
|
+
.join(" ")
|
|
899
|
+
: "";
|
|
900
|
+
if (WEB_INTENT.test(text))
|
|
901
|
+
this.promoteDeferredBuiltin("web_search");
|
|
902
|
+
}
|
|
721
903
|
/** Append a deferred built-in to the live tool list (append-only). */
|
|
722
904
|
promoteDeferredBuiltin(name) {
|
|
723
905
|
if (!this.deferredBuiltinToolNames.includes(name))
|
|
@@ -810,6 +992,8 @@ export class AgentSession {
|
|
|
810
992
|
visibility: "transcript",
|
|
811
993
|
}, options = {}) {
|
|
812
994
|
this.prewarmController?.abort();
|
|
995
|
+
if (provenance.source === "human")
|
|
996
|
+
this.humanPromptSeq += 1;
|
|
813
997
|
await this.settlePostTurnCompaction();
|
|
814
998
|
await this.adoptDeferredCheckpointBeforePrompt();
|
|
815
999
|
const slash = await this.resolveSlashInput(content);
|
|
@@ -837,6 +1021,8 @@ export class AgentSession {
|
|
|
837
1021
|
this.messages.push(userMessage);
|
|
838
1022
|
await this.persistMessage(userMessage);
|
|
839
1023
|
this.lastPersistedIndex = this.messages.length;
|
|
1024
|
+
if (provenance.source === "human")
|
|
1025
|
+
this.attachWebSearchIfAsked(content);
|
|
840
1026
|
await this.runLoop(options);
|
|
841
1027
|
}
|
|
842
1028
|
/**
|
|
@@ -848,11 +1034,13 @@ export class AgentSession {
|
|
|
848
1034
|
*/
|
|
849
1035
|
async promptWithAttachments(text, attachments) {
|
|
850
1036
|
this.prewarmController?.abort();
|
|
1037
|
+
this.humanPromptSeq += 1;
|
|
851
1038
|
await this.settlePostTurnCompaction();
|
|
852
1039
|
await this.adoptDeferredCheckpointBeforePrompt();
|
|
853
1040
|
const parts = this.buildAttachmentParts(text, attachments);
|
|
854
1041
|
if (parts.length === 0)
|
|
855
1042
|
return;
|
|
1043
|
+
this.attachWebSearchIfAsked(text);
|
|
856
1044
|
const userMessage = {
|
|
857
1045
|
role: "user",
|
|
858
1046
|
content: parts,
|
|
@@ -948,6 +1136,7 @@ export class AgentSession {
|
|
|
948
1136
|
this.opts.completionReview?.begin(originalRequest);
|
|
949
1137
|
this.lspManager?.clearPendingDiagnostics();
|
|
950
1138
|
this.loopMonitor.reset();
|
|
1139
|
+
this.securityRowsThisRun.clear();
|
|
951
1140
|
this.verificationTracker.resetRun();
|
|
952
1141
|
this.regroundingInjected = false;
|
|
953
1142
|
this.runStartedAt = Date.now();
|
|
@@ -991,6 +1180,9 @@ export class AgentSession {
|
|
|
991
1180
|
const call = this.verificationTracker.getToolCall(event.toolCallId);
|
|
992
1181
|
const name = call?.name ?? "";
|
|
993
1182
|
const args = call?.args;
|
|
1183
|
+
const safetyRule = parseSafetyBlock(event.result);
|
|
1184
|
+
if (safetyRule)
|
|
1185
|
+
this.recordSafetyBlock(safetyRule, name, args, event.details);
|
|
994
1186
|
this.loopMonitor.recordToolEnd(name, args, event.result, event.isError);
|
|
995
1187
|
const verificationChanged = await this.verificationTracker.recordToolCallEnd(event, call, this.planModeRef.current);
|
|
996
1188
|
if (verificationChanged)
|
|
@@ -1432,6 +1624,11 @@ export class AgentSession {
|
|
|
1432
1624
|
});
|
|
1433
1625
|
return [...diagnosticMessages, ...processFollowUp];
|
|
1434
1626
|
}
|
|
1627
|
+
// A check whose exit status could not count (`npm test | tail; node -e …`)
|
|
1628
|
+
// is re-run on its own before the gate decides, so a passing run is not
|
|
1629
|
+
// demanded again. Diagnostics errors take precedence over the re-run.
|
|
1630
|
+
if (diagnosticMessages.length === 0)
|
|
1631
|
+
await this.rerunRefusedCheck();
|
|
1435
1632
|
// Verification gate: code was edited but nothing verified since the last
|
|
1436
1633
|
// edit. Off via the `verificationGateEnabled` setting.
|
|
1437
1634
|
if (this.verificationGateActive()) {
|
|
@@ -1506,6 +1703,50 @@ export class AgentSession {
|
|
|
1506
1703
|
}
|
|
1507
1704
|
return null;
|
|
1508
1705
|
}
|
|
1706
|
+
/**
|
|
1707
|
+
* The model often runs its tests inside a longer command (`npm test 2>&1 |
|
|
1708
|
+
* tail; node -e '…'`) whose exit status belongs to a later part, so the
|
|
1709
|
+
* tests can pass without counting. When the turn would end unverified that
|
|
1710
|
+
* way, run that check once more on its own, through the same bash tool, and
|
|
1711
|
+
* record its real exit status. No model turn and nothing in the transcript:
|
|
1712
|
+
* only the run's verification status changes.
|
|
1713
|
+
*/
|
|
1714
|
+
async rerunRefusedCheck() {
|
|
1715
|
+
if (this.planModeRef.current || this.opts.signal?.aborted)
|
|
1716
|
+
return;
|
|
1717
|
+
const check = this.verificationTracker.takeRefusedCheck();
|
|
1718
|
+
if (!check)
|
|
1719
|
+
return;
|
|
1720
|
+
const bash = this.tools.find((tool) => tool.name === "bash");
|
|
1721
|
+
// An explicit timeout stops a stuck run instead of moving it to the
|
|
1722
|
+
// background, which would leave a process for the next turn to collect.
|
|
1723
|
+
const params = bash?.parameters.safeParse({ command: check.command, timeout: 120_000 });
|
|
1724
|
+
if (!bash || !params?.success)
|
|
1725
|
+
return;
|
|
1726
|
+
const startedAt = Date.now();
|
|
1727
|
+
let passed = false;
|
|
1728
|
+
try {
|
|
1729
|
+
const result = await bash.execute(params.data, {
|
|
1730
|
+
signal: this.opts.signal ?? new AbortController().signal,
|
|
1731
|
+
toolCallId: `ez-recheck-${startedAt}`,
|
|
1732
|
+
});
|
|
1733
|
+
const text = typeof result === "string" ? result : result.content;
|
|
1734
|
+
passed = typeof text === "string" && /^Exit code:\s*0(?:\s|$)/.test(text.trim());
|
|
1735
|
+
}
|
|
1736
|
+
catch (error) {
|
|
1737
|
+
if (isAbortError(error) || this.opts.signal?.aborted)
|
|
1738
|
+
return;
|
|
1739
|
+
log("WARN", "verification", `Re-running a refused check failed: ${String(error)}`);
|
|
1740
|
+
}
|
|
1741
|
+
if (this.opts.signal?.aborted)
|
|
1742
|
+
return;
|
|
1743
|
+
this.verificationTracker.recordRefusedCheckRerun(check, passed);
|
|
1744
|
+
log("INFO", "verification", "Re-ran a check whose original exit status could not count", {
|
|
1745
|
+
passed: String(passed),
|
|
1746
|
+
durationMs: String(Date.now() - startedAt),
|
|
1747
|
+
});
|
|
1748
|
+
await this.persistVerificationState();
|
|
1749
|
+
}
|
|
1509
1750
|
/** Wraps the real run: cancels an in-flight cache prewarm and tracks run
|
|
1510
1751
|
* activity / last real request time for {@link prewarm}. */
|
|
1511
1752
|
async runLoop(options = {}) {
|
|
@@ -1551,7 +1792,10 @@ export class AgentSession {
|
|
|
1551
1792
|
}
|
|
1552
1793
|
// Reset self-correction hook state for this run; pin the latest user message
|
|
1553
1794
|
// as the verbatim original request for post-compaction re-grounding.
|
|
1554
|
-
|
|
1795
|
+
// A status update that started an idle-wake run is not the user's request.
|
|
1796
|
+
const lastUser = [...this.messages]
|
|
1797
|
+
.reverse()
|
|
1798
|
+
.find((m) => m.role === "user" && m.provenance?.kind !== "notification");
|
|
1555
1799
|
this.resetHookState(requestTextForRegrounding(lastUser));
|
|
1556
1800
|
// Resolve OAuth credentials and run agent loop.
|
|
1557
1801
|
// On 401, force-refresh the token and retry once — the provider may have
|
|
@@ -1646,8 +1890,8 @@ export class AgentSession {
|
|
|
1646
1890
|
provider: this.provider,
|
|
1647
1891
|
model: this.model,
|
|
1648
1892
|
tools: options.disableTools ? [] : this.tools,
|
|
1649
|
-
webSearch:
|
|
1650
|
-
maxTokens: this.
|
|
1893
|
+
webSearch: options.disableTools ? false : () => this.nativeWebSearchOn(),
|
|
1894
|
+
maxTokens: this.requestMaxTokens(accountId),
|
|
1651
1895
|
maxTurns: this.opts.maxTurns,
|
|
1652
1896
|
maxTurnExtensions: this.opts.maxTurnExtensions,
|
|
1653
1897
|
// Plan mode caps effort at medium (Codex `plan_mode_reasoning_effort`
|
|
@@ -1703,7 +1947,7 @@ export class AgentSession {
|
|
|
1703
1947
|
thinking: this.planModeRef.current || options.capThinking
|
|
1704
1948
|
? clampThinkingForPlanMode(this.thinkingLevel)
|
|
1705
1949
|
: this.thinkingLevel,
|
|
1706
|
-
webSearch: !options.disableTools,
|
|
1950
|
+
webSearch: !options.disableTools && this.nativeWebSearchOn(),
|
|
1707
1951
|
supportsImages: modelInfo?.supportsImages,
|
|
1708
1952
|
promptCacheKey: this.getPromptCacheKey(),
|
|
1709
1953
|
},
|
|
@@ -1753,8 +1997,12 @@ export class AgentSession {
|
|
|
1753
1997
|
if (anchor?.role === "assistant") {
|
|
1754
1998
|
this.providerContext = { usage: { ...transformOpts.usage }, anchor };
|
|
1755
1999
|
// Feed the authoritative usage back into the token estimator so
|
|
1756
|
-
// char-based estimates track this session's real tokenizer.
|
|
1757
|
-
|
|
2000
|
+
// char-based estimates track this session's real tokenizer. The
|
|
2001
|
+
// provider's count includes the tool definitions sent with the
|
|
2002
|
+
// request, so their characters must be counted too — without
|
|
2003
|
+
// them a short history reads as ~2 chars/token and drags the
|
|
2004
|
+
// shared ratio down for every session in the process.
|
|
2005
|
+
calibrateEstimatorFromUsage(messages.slice(0, anchorIndex), transformOpts.usage, options.disableTools ? 0 : this.toolDefinitionChars());
|
|
1758
2006
|
}
|
|
1759
2007
|
}
|
|
1760
2008
|
const force = transformOpts.force === true;
|
|
@@ -1805,12 +2053,13 @@ export class AgentSession {
|
|
|
1805
2053
|
compactedToolCalls: String(pruneResult.compactedToolCalls),
|
|
1806
2054
|
freedTokens: String(pruneResult.freedTokens),
|
|
1807
2055
|
});
|
|
1808
|
-
// The
|
|
1809
|
-
//
|
|
1810
|
-
//
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
2056
|
+
// The measured size counted the pruned content, so take the
|
|
2057
|
+
// freed tokens off it. Never swap the provider's count for a
|
|
2058
|
+
// whole-history estimate here: that estimate rides a shared
|
|
2059
|
+
// chars-per-token ratio, and it once read a 234K-token MiMo
|
|
2060
|
+
// context (23% of 1M) as ~400K and compacted it against a 340K
|
|
2061
|
+
// trigger.
|
|
2062
|
+
activeTokens = Math.max(0, activeTokens - pruneResult.freedTokens);
|
|
1814
2063
|
}
|
|
1815
2064
|
}
|
|
1816
2065
|
if (Date.now() < this.compactionRetryAfter)
|
|
@@ -1870,6 +2119,12 @@ export class AgentSession {
|
|
|
1870
2119
|
}
|
|
1871
2120
|
finally {
|
|
1872
2121
|
this.activeLoopMessages = null;
|
|
2122
|
+
// An in-flight compaction rebinds `this.messages` to the compacted array
|
|
2123
|
+
// while the loop keeps appending to `loopMessages`. Resync on every exit
|
|
2124
|
+
// — a failed or aborted run included — or the session silently drops
|
|
2125
|
+
// this run's post-compaction messages (they are still on disk) and the
|
|
2126
|
+
// next prompt replays a history the provider may reject.
|
|
2127
|
+
this.messages = loopMessages;
|
|
1873
2128
|
}
|
|
1874
2129
|
};
|
|
1875
2130
|
const clearInvalidStaticApiKey = async (error) => {
|
|
@@ -1994,7 +2249,17 @@ export class AgentSession {
|
|
|
1994
2249
|
// prompt. Detached by design; the pre-run path above remains the backstop.
|
|
1995
2250
|
this.maybeCompactPostTurn(creds);
|
|
1996
2251
|
}
|
|
1997
|
-
|
|
2252
|
+
/**
|
|
2253
|
+
* Switch the session's model. A model that spends real money (Claude fast
|
|
2254
|
+
* mode bills usage credits) is refused unless `confirmPaid` carries the
|
|
2255
|
+
* user's explicit yes, so no caller can switch onto one by accident: every
|
|
2256
|
+
* path (app, CLI, editors, chat bridges, RPC) fails closed by default.
|
|
2257
|
+
*/
|
|
2258
|
+
async switchModel(provider, model, options = {}) {
|
|
2259
|
+
const target = getModel(model);
|
|
2260
|
+
if (target?.billing?.paid && options.confirmPaid !== true && model !== this.model) {
|
|
2261
|
+
throw new PaidModelConfirmationError(target.name, target.billing.detail);
|
|
2262
|
+
}
|
|
1998
2263
|
// The model-switch note is appended to `this.messages`; settle any
|
|
1999
2264
|
// background compaction first so the note cannot be swapped out.
|
|
2000
2265
|
await this.settlePostTurnCompaction();
|
|
@@ -2058,7 +2323,13 @@ export class AgentSession {
|
|
|
2058
2323
|
// Add/remove client-side web_search tool based on provider.
|
|
2059
2324
|
// Anthropic has native server-side web search; all other providers need the client tool.
|
|
2060
2325
|
const hasWebSearch = this.tools.some((t) => t.name === "web_search");
|
|
2061
|
-
if (this.
|
|
2326
|
+
if (this.deferredBuiltinToolNames.includes("web_search")) {
|
|
2327
|
+
// Tiered: the client tool lives in the catalog for every provider and,
|
|
2328
|
+
// once promoted, is swapped for the native server tool by the Anthropic
|
|
2329
|
+
// transport. Nothing to add or remove — doing so would either load the
|
|
2330
|
+
// schema eagerly or take away a capability the model already has.
|
|
2331
|
+
}
|
|
2332
|
+
else if (this.provider === "anthropic" && hasWebSearch) {
|
|
2062
2333
|
// Switching TO anthropic — remove client-side web_search (server-side handles it)
|
|
2063
2334
|
this.tools = this.tools.filter((t) => t.name !== "web_search");
|
|
2064
2335
|
}
|
|
@@ -2648,6 +2919,117 @@ export class AgentSession {
|
|
|
2648
2919
|
this.setSessionPath(loaded.path);
|
|
2649
2920
|
return this.sessionManager.listBranches(loaded.entries);
|
|
2650
2921
|
}
|
|
2922
|
+
/** Skills the safety scan kept out of this session, for the app/CLI to show. */
|
|
2923
|
+
getBlockedSkills() {
|
|
2924
|
+
return this.blockedSkills;
|
|
2925
|
+
}
|
|
2926
|
+
/** Installed packages (project lockfiles) now known to be malware. */
|
|
2927
|
+
getInstalledThreats() {
|
|
2928
|
+
return this.installedThreats;
|
|
2929
|
+
}
|
|
2930
|
+
/**
|
|
2931
|
+
* Background check of the project's lockfiles for packages flagged as
|
|
2932
|
+
* malware after they were installed: shortly after start, after each daily
|
|
2933
|
+
* threat-feed update, and after a run that changed a lockfile. Never awaited
|
|
2934
|
+
* by anything; a new hit is shown to the user once (security_notice) and told
|
|
2935
|
+
* to the agent at its next turn. Off under vitest unless a test opts in.
|
|
2936
|
+
*/
|
|
2937
|
+
startInstalledPackageWatch() {
|
|
2938
|
+
const enabled = this.opts.installedPackageWatch ?? !process.env.VITEST;
|
|
2939
|
+
// Workers share their parent's project: the parent session watches it.
|
|
2940
|
+
if (!enabled || !this.projectSession || this.opts.subagentWorker || this.installedWatchStop) {
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
// The home folder or a filesystem root is not a project: never scan it.
|
|
2944
|
+
if (!isScannableProjectDir(this.cwd))
|
|
2945
|
+
return;
|
|
2946
|
+
const delay = this.opts.installedPackageWatchDelayMs ?? 5_000;
|
|
2947
|
+
const timer = setTimeout(() => void this.scanInstalledPackages(), delay);
|
|
2948
|
+
timer.unref?.();
|
|
2949
|
+
const offFeed = onThreatFeedUpdated(() => void this.scanInstalledPackages());
|
|
2950
|
+
const offDone = this.eventBus.on("agent_done", () => void this.rescanIfLockfilesChanged());
|
|
2951
|
+
this.installedWatchStop = () => {
|
|
2952
|
+
this.installedWatchStopped = true;
|
|
2953
|
+
clearTimeout(timer);
|
|
2954
|
+
offFeed();
|
|
2955
|
+
offDone();
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
async rescanIfLockfilesChanged() {
|
|
2959
|
+
if (!this.installedStamp || this.installedWatchStopped)
|
|
2960
|
+
return;
|
|
2961
|
+
const stamp = await stampProjectLockfiles(this.cwd);
|
|
2962
|
+
if (!sameStamp(stamp, this.installedStamp))
|
|
2963
|
+
await this.scanInstalledPackages();
|
|
2964
|
+
}
|
|
2965
|
+
/**
|
|
2966
|
+
* A guard refused a tool call: a lasting chat row (live event + persisted
|
|
2967
|
+
* marker, so a reopened session shows it too) and, for packages, an entry in
|
|
2968
|
+
* the project's "don't install" list. All fire-and-forget; never blocks.
|
|
2969
|
+
*/
|
|
2970
|
+
recordSafetyBlock(rule, toolName, args, details) {
|
|
2971
|
+
const a = (args ?? {});
|
|
2972
|
+
const raw = String(a.command ?? a.input ?? a.file_path ?? a.path ?? a.url ?? toolName);
|
|
2973
|
+
this.emitSecurityRow(rule, securityRowTarget(raw));
|
|
2974
|
+
const packages = details
|
|
2975
|
+
?.safetyBlock?.packages;
|
|
2976
|
+
if (packages?.length)
|
|
2977
|
+
this.recordBlockedList(packages);
|
|
2978
|
+
}
|
|
2979
|
+
/**
|
|
2980
|
+
* One row per distinct block per run: an agent retrying the same blocked
|
|
2981
|
+
* install 30 times leaves one row, and a run hitting 100 blocks leaves at
|
|
2982
|
+
* most MAX_SECURITY_ROWS_PER_RUN (the session file stays small).
|
|
2983
|
+
*/
|
|
2984
|
+
emitSecurityRow(rule, target) {
|
|
2985
|
+
const key = `${rule}\0${target}`;
|
|
2986
|
+
if (this.securityRowsThisRun.has(key))
|
|
2987
|
+
return;
|
|
2988
|
+
if (this.securityRowsThisRun.size >= MAX_SECURITY_ROWS_PER_RUN)
|
|
2989
|
+
return;
|
|
2990
|
+
this.securityRowsThisRun.add(key);
|
|
2991
|
+
this.eventBus.emit("security_block", { rule, target });
|
|
2992
|
+
void this.persistAppMarker("security", { rule, target }).catch(() => { });
|
|
2993
|
+
}
|
|
2994
|
+
recordBlockedList(packages) {
|
|
2995
|
+
const enabled = this.opts.blockedPackagesFile ?? !process.env.VITEST;
|
|
2996
|
+
if (!enabled || !this.projectSession)
|
|
2997
|
+
return;
|
|
2998
|
+
void recordBlockedPackages(this.cwd, packages).then((file) => {
|
|
2999
|
+
if (file)
|
|
3000
|
+
log("INFO", "blocked-packages", `recorded blocked packages in ${file}`);
|
|
3001
|
+
}, (err) => log("WARN", "blocked-packages", `could not record blocked packages: ${String(err)}`));
|
|
3002
|
+
}
|
|
3003
|
+
/** Exposed for tests; safe to call any time (one scan at a time, never throws). */
|
|
3004
|
+
async scanInstalledPackages() {
|
|
3005
|
+
if (this.installedScanRunning || this.installedWatchStopped)
|
|
3006
|
+
return;
|
|
3007
|
+
this.installedScanRunning = true;
|
|
3008
|
+
try {
|
|
3009
|
+
const { stamp, found } = await sharedLockfileScan(this.cwd, this.opts.installedPackageCheck);
|
|
3010
|
+
// Disposed mid-scan: no late events.
|
|
3011
|
+
if (this.installedWatchStopped)
|
|
3012
|
+
return;
|
|
3013
|
+
this.installedStamp = stamp;
|
|
3014
|
+
const known = new Set(this.installedThreats.map((t) => t.key));
|
|
3015
|
+
this.installedThreats = found;
|
|
3016
|
+
for (const threat of found) {
|
|
3017
|
+
if (known.has(threat.key))
|
|
3018
|
+
continue;
|
|
3019
|
+
log("WARN", "installed-threats", threat.message);
|
|
3020
|
+
this.notify(`installed-threat:${threat.file}`, threat.agentText);
|
|
3021
|
+
this.eventBus.emit("security_notice", { key: threat.key, message: threat.message });
|
|
3022
|
+
this.emitSecurityRow("installed-malware", securityRowTarget(`${threat.packages.join(", ")} in ${path.relative(this.cwd, threat.file) || threat.file}`));
|
|
3023
|
+
this.recordBlockedList(threat.blocked);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
catch (err) {
|
|
3027
|
+
log("WARN", "installed-threats", `installed-package check failed: ${String(err)}`);
|
|
3028
|
+
}
|
|
3029
|
+
finally {
|
|
3030
|
+
this.installedScanRunning = false;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
2651
3033
|
getState() {
|
|
2652
3034
|
return {
|
|
2653
3035
|
provider: this.provider,
|
|
@@ -2781,6 +3163,74 @@ export class AgentSession {
|
|
|
2781
3163
|
notify(id, text) {
|
|
2782
3164
|
this.notifications.enqueue("goal", id, text, { terminal: true });
|
|
2783
3165
|
}
|
|
3166
|
+
/**
|
|
3167
|
+
* Observe urgent background alerts — a background task failed or exited
|
|
3168
|
+
* non-zero, or a child agent finished — as they are queued. Hosts use this to
|
|
3169
|
+
* wake an idle session (see {@link continueFromNotifications}); a live run
|
|
3170
|
+
* already receives them at its next steering boundary. Returns unsubscribe.
|
|
3171
|
+
*/
|
|
3172
|
+
onBackgroundAlert(listener) {
|
|
3173
|
+
return this.notifications.onUrgent(() => listener());
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* Characters of the tool definitions sent with every request (name,
|
|
3177
|
+
* description, JSON schema), for estimator calibration. Cached by tool count:
|
|
3178
|
+
* the tool array is append-only.
|
|
3179
|
+
*/
|
|
3180
|
+
toolDefinitionChars() {
|
|
3181
|
+
if (this.toolCharsCache?.count === this.tools.length)
|
|
3182
|
+
return this.toolCharsCache.chars;
|
|
3183
|
+
let chars = 0;
|
|
3184
|
+
for (const tool of this.tools) {
|
|
3185
|
+
chars += tool.name.length + tool.description.length;
|
|
3186
|
+
try {
|
|
3187
|
+
chars += JSON.stringify(resolveToolSchema(tool)).length;
|
|
3188
|
+
}
|
|
3189
|
+
catch {
|
|
3190
|
+
// A schema that cannot be serialized is not sent either.
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
this.toolCharsCache = { count: this.tools.length, chars };
|
|
3194
|
+
return chars;
|
|
3195
|
+
}
|
|
3196
|
+
/** True while an urgent background alert is waiting to be delivered. */
|
|
3197
|
+
hasPendingBackgroundAlert() {
|
|
3198
|
+
return this.notifications.hasUrgent();
|
|
3199
|
+
}
|
|
3200
|
+
/** Monotonic count of human-originated prompts this session has run. */
|
|
3201
|
+
getHumanPromptSeq() {
|
|
3202
|
+
return this.humanPromptSeq;
|
|
3203
|
+
}
|
|
3204
|
+
/**
|
|
3205
|
+
* Start a run from pending background notifications, for a session that had
|
|
3206
|
+
* already finished its turn. The notifications become one hidden runtime
|
|
3207
|
+
* message (never shown as something the user typed), and the agent decides
|
|
3208
|
+
* whether they need action. Returns false, starting nothing, when no urgent
|
|
3209
|
+
* alert is pending — a live run may have delivered it in the meantime.
|
|
3210
|
+
*/
|
|
3211
|
+
async continueFromNotifications() {
|
|
3212
|
+
if (!this.notifications.hasUrgent())
|
|
3213
|
+
return false;
|
|
3214
|
+
this.prewarmController?.abort();
|
|
3215
|
+
await this.settlePostTurnCompaction();
|
|
3216
|
+
await this.adoptDeferredCheckpointBeforePrompt();
|
|
3217
|
+
const notified = this.notifications.drain();
|
|
3218
|
+
if (notified.length === 0)
|
|
3219
|
+
return false;
|
|
3220
|
+
const message = {
|
|
3221
|
+
role: "user",
|
|
3222
|
+
content: buildIdleWakeNotificationText(notified.map((entry) => entry.text)),
|
|
3223
|
+
provenance: { source: "runtime", kind: "notification", visibility: "hidden" },
|
|
3224
|
+
};
|
|
3225
|
+
log("INFO", "notifications", "Starting a run from background alerts", {
|
|
3226
|
+
count: String(notified.length),
|
|
3227
|
+
});
|
|
3228
|
+
this.messages.push(message);
|
|
3229
|
+
await this.persistMessage(message);
|
|
3230
|
+
this.lastPersistedIndex = this.messages.length;
|
|
3231
|
+
await this.runLoop();
|
|
3232
|
+
return true;
|
|
3233
|
+
}
|
|
2784
3234
|
/** This session's sub-agents (workers), newest-state, including ones restored after a restart. */
|
|
2785
3235
|
listSubAgents() {
|
|
2786
3236
|
return this.subAgentManager?.list() ?? [];
|
|
@@ -2942,7 +3392,7 @@ export class AgentSession {
|
|
|
2942
3392
|
const networkAllow = this.settingsManager.get("networkMode") === "allowlist"
|
|
2943
3393
|
? this.settingsManager.get("networkAllow")
|
|
2944
3394
|
: [];
|
|
2945
|
-
return { additionalRoots: this.additionalRoots, networkAllow };
|
|
3395
|
+
return { additionalRoots: this.additionalRoots, networkAllow, projectFiles: this.projectFiles };
|
|
2946
3396
|
}
|
|
2947
3397
|
/**
|
|
2948
3398
|
* Build the stable system-prompt prefix for the current tool set and state.
|
|
@@ -2954,6 +3404,10 @@ export class AgentSession {
|
|
|
2954
3404
|
async buildBasePrompt(planMode, approvedPlanPath) {
|
|
2955
3405
|
if (this.customSystemPrompt)
|
|
2956
3406
|
return this.customSystemPrompt;
|
|
3407
|
+
// Listed once: a rebuild (plan mode, /add-dir, compaction) must not churn the
|
|
3408
|
+
// prompt as the agent adds files.
|
|
3409
|
+
if (this.projectFiles === undefined)
|
|
3410
|
+
this.projectFiles = await listProjectFiles(this.cwd);
|
|
2957
3411
|
const toolNames = this.tools.map((tool) => tool.name);
|
|
2958
3412
|
const deferredToolNames = this.deferredToolNamesForPrompt(toolNames);
|
|
2959
3413
|
// Whatever is rendered now is what the model has been told.
|
|
@@ -3310,10 +3764,18 @@ export class AgentSession {
|
|
|
3310
3764
|
return { enhanced: text, segments: [{ kind: "text", text }] };
|
|
3311
3765
|
// Keep credentials and settings on the model selected at click time, even
|
|
3312
3766
|
// if the user switches models while credential resolution is pending.
|
|
3313
|
-
const { provider, model, thinkingLevel,
|
|
3767
|
+
const { provider, model, thinkingLevel, baseUrl } = this;
|
|
3768
|
+
const threshold = this.settingsManager.get("compactThreshold");
|
|
3314
3769
|
const creds = await this.authStorage.resolveCredentials(provider, {
|
|
3315
3770
|
storageKeys: this.currentAuthStorageKeys(),
|
|
3316
3771
|
});
|
|
3772
|
+
const maxTokens = outputCapForRequest({
|
|
3773
|
+
provider,
|
|
3774
|
+
model,
|
|
3775
|
+
maxTokens: this.maxTokens,
|
|
3776
|
+
threshold,
|
|
3777
|
+
accountId: creds.accountId,
|
|
3778
|
+
});
|
|
3317
3779
|
// Cheap, best-effort stack detection from the project root so terminology is
|
|
3318
3780
|
// idiomatic to the user's stack. Never throws (returns "" on any failure).
|
|
3319
3781
|
let stack = "";
|
|
@@ -3430,8 +3892,8 @@ export class AgentSession {
|
|
|
3430
3892
|
model: this.model,
|
|
3431
3893
|
messages,
|
|
3432
3894
|
tools: this.tools,
|
|
3433
|
-
webSearch:
|
|
3434
|
-
maxTokens: this.
|
|
3895
|
+
webSearch: this.nativeWebSearchOn(),
|
|
3896
|
+
maxTokens: this.requestMaxTokens(creds.accountId),
|
|
3435
3897
|
thinking: this.planModeRef.current
|
|
3436
3898
|
? clampThinkingForPlanMode(this.thinkingLevel)
|
|
3437
3899
|
: this.thinkingLevel,
|
|
@@ -3526,6 +3988,7 @@ export class AgentSession {
|
|
|
3526
3988
|
// First and synchronous: nothing below may delay this, or a hung teardown
|
|
3527
3989
|
// step leaves background commands running after the app has quit.
|
|
3528
3990
|
this.stopBackgroundProcesses();
|
|
3991
|
+
this.installedWatchStop?.();
|
|
3529
3992
|
// Quiesce any in-flight post-turn compaction BEFORE tearing down state:
|
|
3530
3993
|
// the background compact() snapshots and replaces `this.messages`, so
|
|
3531
3994
|
// letting it run past this point would checkpoint a near-empty history
|
|
@@ -3755,7 +4218,7 @@ export class AgentSession {
|
|
|
3755
4218
|
}
|
|
3756
4219
|
createSlashCommandContext() {
|
|
3757
4220
|
return {
|
|
3758
|
-
switchModel: (provider, model) => this.switchModel(provider, model),
|
|
4221
|
+
switchModel: (provider, model, options) => this.switchModel(provider, model, options),
|
|
3759
4222
|
compact: (focus) => this.compact(undefined, "manual", focus),
|
|
3760
4223
|
newSession: () => this.newSession(),
|
|
3761
4224
|
listSessions: async () => {
|