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