@prestyj/cli 5.31.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 +14 -2
- 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.d.ts +14 -0
- package/dist/app-sidecar/run-control-routes.js +104 -25
- package/dist/app-sidecar/run-control-routes.js.map +1 -1
- package/dist/app-sidecar/session-routes.js +15 -1
- package/dist/app-sidecar/session-routes.js.map +1 -1
- package/dist/app-sidecar/task-routes.js +24 -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 +176 -2
- package/dist/core/agent-session.js +515 -30
- 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/goal-run-marker.d.ts +32 -0
- package/dist/core/goal-run-marker.js +178 -0
- package/dist/core/goal-run-marker.js.map +1 -0
- package/dist/core/goal-summary.js +8 -2
- package/dist/core/goal-summary.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/resolve-start.d.ts +2 -0
- package/dist/core/resolve-start.js +34 -5
- package/dist/core/resolve-start.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/subagent-policy.js +4 -1
- package/dist/core/subagent-policy.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 +46 -3
- 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-goals.d.ts +6 -2
- package/dist/modes/acp-goals.js +1 -2
- package/dist/modes/acp-goals.js.map +1 -1
- package/dist/modes/acp-mode.js +14 -3
- 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/Footer.js +4 -0
- package/dist/ui/components/Footer.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-overview.d.ts +41 -0
- package/dist/utils/git-overview.js +146 -0
- package/dist/utils/git-overview.js.map +1 -0
- 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,14 +1,18 @@
|
|
|
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";
|
|
8
12
|
import { loadWorkspaceCustomCommands } from "./custom-commands.js";
|
|
9
13
|
import { expandPromptCommand } from "./prompt-command-expansion.js";
|
|
10
14
|
import { SettingsManager } from "./settings-manager.js";
|
|
11
|
-
import { AuthStorage } from "./auth-storage.js";
|
|
15
|
+
import { AuthStorage, NotLoggedInError } from "./auth-storage.js";
|
|
12
16
|
import { dualAuthProvider } from "@prestyj/core";
|
|
13
17
|
import { getClaudeCliUserAgent, recordRequiredClaudeCodeVersion } from "./claude-code-version.js";
|
|
14
18
|
import { kimiCodingHeaders, isKimiCodingEndpoint } from "./oauth/kimi.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,17 +1700,76 @@ 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 = {}) {
|
|
1512
1750
|
this.prewarmController?.abort();
|
|
1513
1751
|
this.runLoopDepth++;
|
|
1752
|
+
if (this.runLoopDepth === 1)
|
|
1753
|
+
this.notifyRunActive(true);
|
|
1514
1754
|
try {
|
|
1515
1755
|
await this.runLoopInner(options);
|
|
1516
1756
|
}
|
|
1517
1757
|
finally {
|
|
1518
1758
|
this.runLoopDepth--;
|
|
1519
1759
|
this.lastRealRequestAt = Date.now();
|
|
1760
|
+
if (this.runLoopDepth === 0)
|
|
1761
|
+
this.notifyRunActive(false);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
/** `opts.onRunActive`, never allowed to throw into the run. */
|
|
1765
|
+
notifyRunActive(active) {
|
|
1766
|
+
try {
|
|
1767
|
+
this.opts.onRunActive?.(active);
|
|
1768
|
+
}
|
|
1769
|
+
catch (err) {
|
|
1770
|
+
log("WARN", "session", "onRunActive listener threw", {
|
|
1771
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1772
|
+
});
|
|
1520
1773
|
}
|
|
1521
1774
|
}
|
|
1522
1775
|
/** Auto-compact if needed, run agent loop with auth retry, and persist messages. */
|
|
@@ -1536,7 +1789,10 @@ export class AgentSession {
|
|
|
1536
1789
|
}
|
|
1537
1790
|
// Reset self-correction hook state for this run; pin the latest user message
|
|
1538
1791
|
// as the verbatim original request for post-compaction re-grounding.
|
|
1539
|
-
|
|
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");
|
|
1540
1796
|
this.resetHookState(requestTextForRegrounding(lastUser));
|
|
1541
1797
|
// Resolve OAuth credentials and run agent loop.
|
|
1542
1798
|
// On 401, force-refresh the token and retry once — the provider may have
|
|
@@ -1631,8 +1887,8 @@ export class AgentSession {
|
|
|
1631
1887
|
provider: this.provider,
|
|
1632
1888
|
model: this.model,
|
|
1633
1889
|
tools: options.disableTools ? [] : this.tools,
|
|
1634
|
-
webSearch:
|
|
1635
|
-
maxTokens: this.
|
|
1890
|
+
webSearch: options.disableTools ? false : () => this.nativeWebSearchOn(),
|
|
1891
|
+
maxTokens: this.requestMaxTokens(accountId),
|
|
1636
1892
|
maxTurns: this.opts.maxTurns,
|
|
1637
1893
|
maxTurnExtensions: this.opts.maxTurnExtensions,
|
|
1638
1894
|
// Plan mode caps effort at medium (Codex `plan_mode_reasoning_effort`
|
|
@@ -1688,7 +1944,7 @@ export class AgentSession {
|
|
|
1688
1944
|
thinking: this.planModeRef.current || options.capThinking
|
|
1689
1945
|
? clampThinkingForPlanMode(this.thinkingLevel)
|
|
1690
1946
|
: this.thinkingLevel,
|
|
1691
|
-
webSearch: !options.disableTools,
|
|
1947
|
+
webSearch: !options.disableTools && this.nativeWebSearchOn(),
|
|
1692
1948
|
supportsImages: modelInfo?.supportsImages,
|
|
1693
1949
|
promptCacheKey: this.getPromptCacheKey(),
|
|
1694
1950
|
},
|
|
@@ -1738,8 +1994,12 @@ export class AgentSession {
|
|
|
1738
1994
|
if (anchor?.role === "assistant") {
|
|
1739
1995
|
this.providerContext = { usage: { ...transformOpts.usage }, anchor };
|
|
1740
1996
|
// Feed the authoritative usage back into the token estimator so
|
|
1741
|
-
// char-based estimates track this session's real tokenizer.
|
|
1742
|
-
|
|
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());
|
|
1743
2003
|
}
|
|
1744
2004
|
}
|
|
1745
2005
|
const force = transformOpts.force === true;
|
|
@@ -1790,12 +2050,13 @@ export class AgentSession {
|
|
|
1790
2050
|
compactedToolCalls: String(pruneResult.compactedToolCalls),
|
|
1791
2051
|
freedTokens: String(pruneResult.freedTokens),
|
|
1792
2052
|
});
|
|
1793
|
-
// The
|
|
1794
|
-
//
|
|
1795
|
-
//
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
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);
|
|
1799
2060
|
}
|
|
1800
2061
|
}
|
|
1801
2062
|
if (Date.now() < this.compactionRetryAfter)
|
|
@@ -1855,6 +2116,12 @@ export class AgentSession {
|
|
|
1855
2116
|
}
|
|
1856
2117
|
finally {
|
|
1857
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;
|
|
1858
2125
|
}
|
|
1859
2126
|
};
|
|
1860
2127
|
const clearInvalidStaticApiKey = async (error) => {
|
|
@@ -1979,7 +2246,17 @@ export class AgentSession {
|
|
|
1979
2246
|
// prompt. Detached by design; the pre-run path above remains the backstop.
|
|
1980
2247
|
this.maybeCompactPostTurn(creds);
|
|
1981
2248
|
}
|
|
1982
|
-
|
|
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
|
+
}
|
|
1983
2260
|
// The model-switch note is appended to `this.messages`; settle any
|
|
1984
2261
|
// background compaction first so the note cannot be swapped out.
|
|
1985
2262
|
await this.settlePostTurnCompaction();
|
|
@@ -2043,7 +2320,13 @@ export class AgentSession {
|
|
|
2043
2320
|
// Add/remove client-side web_search tool based on provider.
|
|
2044
2321
|
// Anthropic has native server-side web search; all other providers need the client tool.
|
|
2045
2322
|
const hasWebSearch = this.tools.some((t) => t.name === "web_search");
|
|
2046
|
-
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) {
|
|
2047
2330
|
// Switching TO anthropic — remove client-side web_search (server-side handles it)
|
|
2048
2331
|
this.tools = this.tools.filter((t) => t.name !== "web_search");
|
|
2049
2332
|
}
|
|
@@ -2633,6 +2916,117 @@ export class AgentSession {
|
|
|
2633
2916
|
this.setSessionPath(loaded.path);
|
|
2634
2917
|
return this.sessionManager.listBranches(loaded.entries);
|
|
2635
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
|
+
}
|
|
2636
3030
|
getState() {
|
|
2637
3031
|
return {
|
|
2638
3032
|
provider: this.provider,
|
|
@@ -2766,6 +3160,74 @@ export class AgentSession {
|
|
|
2766
3160
|
notify(id, text) {
|
|
2767
3161
|
this.notifications.enqueue("goal", id, text, { terminal: true });
|
|
2768
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
|
+
}
|
|
2769
3231
|
/** This session's sub-agents (workers), newest-state, including ones restored after a restart. */
|
|
2770
3232
|
listSubAgents() {
|
|
2771
3233
|
return this.subAgentManager?.list() ?? [];
|
|
@@ -3295,10 +3757,18 @@ export class AgentSession {
|
|
|
3295
3757
|
return { enhanced: text, segments: [{ kind: "text", text }] };
|
|
3296
3758
|
// Keep credentials and settings on the model selected at click time, even
|
|
3297
3759
|
// if the user switches models while credential resolution is pending.
|
|
3298
|
-
const { provider, model, thinkingLevel,
|
|
3760
|
+
const { provider, model, thinkingLevel, baseUrl } = this;
|
|
3761
|
+
const threshold = this.settingsManager.get("compactThreshold");
|
|
3299
3762
|
const creds = await this.authStorage.resolveCredentials(provider, {
|
|
3300
3763
|
storageKeys: this.currentAuthStorageKeys(),
|
|
3301
3764
|
});
|
|
3765
|
+
const maxTokens = outputCapForRequest({
|
|
3766
|
+
provider,
|
|
3767
|
+
model,
|
|
3768
|
+
maxTokens: this.maxTokens,
|
|
3769
|
+
threshold,
|
|
3770
|
+
accountId: creds.accountId,
|
|
3771
|
+
});
|
|
3302
3772
|
// Cheap, best-effort stack detection from the project root so terminology is
|
|
3303
3773
|
// idiomatic to the user's stack. Never throws (returns "" on any failure).
|
|
3304
3774
|
let stack = "";
|
|
@@ -3415,8 +3885,8 @@ export class AgentSession {
|
|
|
3415
3885
|
model: this.model,
|
|
3416
3886
|
messages,
|
|
3417
3887
|
tools: this.tools,
|
|
3418
|
-
webSearch:
|
|
3419
|
-
maxTokens: this.
|
|
3888
|
+
webSearch: this.nativeWebSearchOn(),
|
|
3889
|
+
maxTokens: this.requestMaxTokens(creds.accountId),
|
|
3420
3890
|
thinking: this.planModeRef.current
|
|
3421
3891
|
? clampThinkingForPlanMode(this.thinkingLevel)
|
|
3422
3892
|
: this.thinkingLevel,
|
|
@@ -3511,6 +3981,7 @@ export class AgentSession {
|
|
|
3511
3981
|
// First and synchronous: nothing below may delay this, or a hung teardown
|
|
3512
3982
|
// step leaves background commands running after the app has quit.
|
|
3513
3983
|
this.stopBackgroundProcesses();
|
|
3984
|
+
this.installedWatchStop?.();
|
|
3514
3985
|
// Quiesce any in-flight post-turn compaction BEFORE tearing down state:
|
|
3515
3986
|
// the background compact() snapshots and replaces `this.messages`, so
|
|
3516
3987
|
// letting it run past this point would checkpoint a near-empty history
|
|
@@ -3620,16 +4091,28 @@ export class AgentSession {
|
|
|
3620
4091
|
}
|
|
3621
4092
|
// Auto-compact on load if the restored session exceeds the context window.
|
|
3622
4093
|
// Without this, huge sessions (1M+ tokens) get loaded into memory and OOM.
|
|
3623
|
-
|
|
3624
|
-
|
|
4094
|
+
// Credentials only refine the context window here (OpenAI OAuth's Codex
|
|
4095
|
+
// window). A missing login must not make the old conversation unopenable —
|
|
4096
|
+
// the user can switch to a model they're signed in to once it's loaded;
|
|
4097
|
+
// the first prompt still fails cleanly through the run's auth handling.
|
|
4098
|
+
const creds = await this.authStorage
|
|
4099
|
+
.resolveCredentials(this.provider, { storageKeys: this.currentAuthStorageKeys() })
|
|
4100
|
+
.catch((err) => {
|
|
4101
|
+
if (!(err instanceof NotLoggedInError))
|
|
4102
|
+
throw err;
|
|
4103
|
+
log("WARN", "session", "Resuming without credentials for the session model", {
|
|
4104
|
+
provider: this.provider,
|
|
4105
|
+
model: this.model,
|
|
4106
|
+
});
|
|
4107
|
+
return undefined;
|
|
3625
4108
|
});
|
|
3626
4109
|
// Cache for sync callers (see field doc) so the app-sidecar's footer shows
|
|
3627
4110
|
// the right context window immediately on resume, before any prompt runs
|
|
3628
4111
|
// runLoop() and would otherwise be the first to set this.
|
|
3629
|
-
this.lastAccountId = creds
|
|
4112
|
+
this.lastAccountId = creds?.accountId;
|
|
3630
4113
|
const contextWindow = getContextWindow(this.model, {
|
|
3631
4114
|
provider: this.provider,
|
|
3632
|
-
accountId: creds
|
|
4115
|
+
accountId: creds?.accountId,
|
|
3633
4116
|
});
|
|
3634
4117
|
this.sessionId = loaded.header.id;
|
|
3635
4118
|
this.setSessionPath(loaded.path);
|
|
@@ -3639,20 +4122,22 @@ export class AgentSession {
|
|
|
3639
4122
|
model: this.model,
|
|
3640
4123
|
contextWindow,
|
|
3641
4124
|
threshold: this.settingsManager.get("compactThreshold"),
|
|
3642
|
-
accountId: creds
|
|
4125
|
+
accountId: creds?.accountId,
|
|
3643
4126
|
approvedPlanPath: this.approvedPlanPath,
|
|
3644
4127
|
});
|
|
3645
4128
|
log("INFO", "compaction", "Restore compaction decision", {
|
|
3646
4129
|
provider: this.provider,
|
|
3647
4130
|
model: this.model,
|
|
3648
|
-
transport: this.provider === "openai" && creds
|
|
4131
|
+
transport: this.provider === "openai" && creds?.accountId ? "codex_oauth" : "public_api",
|
|
3649
4132
|
contextWindow: String(contextWindow),
|
|
3650
4133
|
activeTokens: "estimated",
|
|
3651
4134
|
triggerLimit: String(loadPolicy.targetTokens),
|
|
3652
4135
|
});
|
|
3653
4136
|
const needsLoadCompaction = this.settingsManager.get("autoCompact") &&
|
|
3654
4137
|
shouldCompact(this.messages, contextWindow, loadPolicy.threshold, undefined, loadPolicy.targetTokens);
|
|
3655
|
-
|
|
4138
|
+
// Without credentials the summary call can't run now; defer it to the first
|
|
4139
|
+
// prompt, by which point the user has picked a model they're signed in to.
|
|
4140
|
+
if (needsLoadCompaction && (this.opts.deferLoadCompaction || !creds)) {
|
|
3656
4141
|
// Canonicalize again immediately before the first prompt is persisted:
|
|
3657
4142
|
// another process may create the shared checkpoint after this load.
|
|
3658
4143
|
this.deferredCompactionPending = true;
|
|
@@ -3726,7 +4211,7 @@ export class AgentSession {
|
|
|
3726
4211
|
}
|
|
3727
4212
|
createSlashCommandContext() {
|
|
3728
4213
|
return {
|
|
3729
|
-
switchModel: (provider, model) => this.switchModel(provider, model),
|
|
4214
|
+
switchModel: (provider, model, options) => this.switchModel(provider, model, options),
|
|
3730
4215
|
compact: (focus) => this.compact(undefined, "manual", focus),
|
|
3731
4216
|
newSession: () => this.newSession(),
|
|
3732
4217
|
listSessions: async () => {
|