@epoch-agent/runtime 0.1.0 → 0.3.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 +152 -78
- package/dist/index.d.ts +2143 -78
- package/dist/index.js +1754 -1240
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { loadCommandDefinitions, createToolScope, expandCommand, RecentWorkspaces, clearModelCache, discoverModels, ScheduleStore, isWithinWindow, acquireScheduleLock, triggerPeriodMs, ScheduleRecorder, pruneRecordings, suggestRuleFromApproval, needsAllowlist, isNonInteractive, loadPlugins, assertLevelAllowed, discoverProject, ProviderRouter, ProviderError, PROVIDER_NO_CREDENTIAL, SessionManager, GoalService, MemoryManager, MemoryReviewer, resolveSkillDirs, SkillSystem, SkillLearner, HookManager, loadHookSources, ApprovalCache, PermissionManager, resolvePolicyDirs, resolvePoliciesDir, loadPolicyFiles, checkShadowing, compileRuleLists, ContextCompressor, compressionOptions, TrackerDB, getConfigOrigins, mapOperationType, affectedPaths, workspaceDisplayPath, persistParts, persistToolResults, ToolRegistry, createTodoTool, createGoalTool, createMemoryTool, createCodeExecTool, createAskQuestionTool, resolveWorkspace, unloadedInstructionFiles, resolveProjectRoot, loadConfig, CodeSandbox, CheckpointManager, createDelegateTool, loadRoleDefinitions, mergeRoles, builtinRoles, unknownRoleTools, BLOCKED_TOOLS, blockedRoleTools,
|
|
2
|
-
export { SANDBOX_COVERS, SANDBOX_EXCLUDES, SCHEDULE_DEFAULTS, SCHEDULE_DEFAULT_ALLOWLIST, nextRunAt, readRecording, skillIndexDescription } from '@epoch-agent/core';
|
|
3
|
-
import { projectCommandsDir, commandsDir,
|
|
1
|
+
import { loadCommandDefinitions, SLASH_COMMANDS_DIAG_MODULE, createToolScope, expandCommand, RecentWorkspaces, clearModelCache, discoverModels, ScheduleStore, isWithinWindow, acquireScheduleLock, triggerPeriodMs, ScheduleRecorder, pruneRecordings, suggestRuleFromApproval, needsAllowlist, isNonInteractive, loadPlugins, assertLevelAllowed, discoverProject, ProviderRouter, ProviderError, PROVIDER_NO_CREDENTIAL, SessionManager, GoalService, MemoryManager, MemoryReviewer, resolveSkillDirs, SkillSystem, SkillLearner, HookManager, loadHookSources, ApprovalCache, PermissionManager, resolvePolicyDirs, resolvePoliciesDir, loadPolicyFiles, checkShadowing, compileRuleLists, ContextCompressor, compressionOptions, TrackerDB, getConfigOrigins, resolveComplianceDirs, resolveComplianceDir, loadLexiconFiles, ComplianceEngine, DEFAULT_COMPLIANCE_SETTINGS, mapOperationType, affectedPaths, workspaceDisplayPath, persistParts, persistToolResults, ToolRegistry, createTodoTool, createGoalTool, createMemoryTool, createCodeExecTool, createRunCodeTool, createSkillViewTool, createSkillWriteTools, createToolSearchTool, createToolRevealState, createAskQuestionTool, resolveWorkspace, unloadedInstructionFiles, resolveProjectRoot, loadConfig, CodeSandbox, CheckpointManager, createDelegateTool, findRole, systemOpenArgv, resolveInWorkspace, listWorkspaceFiles, loadRoleDefinitions, mergeRoles, AGENT_ROLE_DIAG_MODULE, builtinRoles, unknownRoleTools, BLOCKED_TOOLS, blockedRoleTools, unknownRoleSkills, BudgetStore, mergeHeadlessPolicy, describeHeadlessPolicy, importSkills, previewSkillImport, removeSessionArtifacts, gateForTool, managedSwitchNotes, originOf, EpochConfigSchema, defaultConfig, SettingsFileSchema, isWritableSettingKey, writeSettingValue, SETTING_WRITE_APPLY, resolveSettings, SETTING_WRITE_LAYERS, readProviderKeyEnv, validateSchedule, createAllowAllGate, setTrustGate, TrustManager, createTrustGate, createDenyAllGate, getImportGate, setImportGate, createAllowAllImportGate, ImportTrustStore, createDenyAllImportGate, createImportGate, getCapability, restoreParts, createSessionSearchTools, PlanModeState, createPlanModeTools, toolTableTokens, explainSetting, settingWriteTargets, settingValueKind, settingValueChoices, uninstallPlugin, readMarketplaces, searchMarketplaces, resolveEntrySource, resolveSource, readPluginRecords, createRoleFile, AgentLoop, resolveArtifactRetention, roleScopedProvider, createRoleScope, revealScopedProvider, withGoalPrompt, withGoalCompression, ScheduleRegistrar, resolveScheduleBackend, updateMarketplace, addMarketplace, SETTINGS_LAYER_ORDER, resolveMarketplaceRef, BudgetGuard, installPlugin, currentEpochVersion } from '@epoch-agent/core';
|
|
2
|
+
export { DEFAULT_GOAL_MAX_ROUNDS, MAX_GOAL_MAX_ROUNDS, MAX_OBJECTIVE_CHARS, MAX_REASON_CHARS, SANDBOX_COVERS, SANDBOX_EXCLUDES, SCHEDULE_DEFAULTS, SCHEDULE_DEFAULT_ALLOWLIST, nextRunAt, readRecording, skillIndexDescription } from '@epoch-agent/core';
|
|
3
|
+
import { projectCommandsDir, commandsDir, t, issueDetails, resolveHomeDir, configPath, readKey, setScalar, setSectionField, mcpConfigPath, workspacesPath, writeProviderSecret, envPath, maskApiKey, dbPath, automationLogsDir, pluginsStatePath, safeInit, memoriesDir, skillsDir, claudeUserSettingsPath, hooksPath, artifactsDir, resolveRipgrep, withTimeout, createProcessTable, marketplacesPath, pluginsDir, agentsDir, budgetStatePath, createSecretStore, setSecretStore, prefetchProviderSecrets, setSecretValues, migrateEnvSecrets, setDataKey, parseDataKey, generateDataKey, i18nDiagnostics, automationWorkDir, trustPath, trustedImportsPath, setShell, probeShell } from '@epoch-agent/infra';
|
|
4
4
|
export { t } from '@epoch-agent/infra';
|
|
5
|
-
import { isAbsolute, dirname, delimiter, resolve, join } from 'path';
|
|
6
|
-
import { AGENT_ROLE_NAME_PATTERN, isValidAgentRoleName, apiKeyEnvVar, getProviderInfo, PROVIDER_INFOS, parseModelRef, contentToText, systemNote, DiagnosticSink, METRIC, API_KEY_ENV_VARS, MCP_DATA_KEY_NAME, isPermissionLevel, NOOP_TELEMETRY, DEFAULT_AGENT_ROLE } from '@epoch-agent/protocol';
|
|
7
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync, renameSync, rmSync, appendFileSync } from 'fs';
|
|
8
|
-
import { parseMcpConfig, McpRegistry, McpAuthStore, mcpAuthStatus, loginToMcpServer, logoutFromMcpServer } from '@epoch-agent/plugin-mcp';
|
|
5
|
+
import { isAbsolute, dirname, delimiter, resolve, join, relative, sep } from 'path';
|
|
6
|
+
import { AGENT_ROLE_NAME_PATTERN, isValidAgentRoleName, apiKeyEnvVar, getProviderInfo, PROVIDER_INFOS, parseModelRef, contentToText, assistantTurnText, systemNote, ToolExposure, isMentionableFilePath, DiagnosticSink, METRIC, API_KEY_ENV_VARS, MCP_DATA_KEY_NAME, isPermissionLevel, NOOP_TELEMETRY, DEFAULT_AGENT_ROLE } from '@epoch-agent/protocol';
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync, renameSync, rmSync, realpathSync, openSync, readSync, closeSync, appendFileSync } from 'fs';
|
|
9
8
|
import { randomUUID, createHash } from 'crypto';
|
|
9
|
+
import { parseMcpConfig, McpRegistry, McpAuthStore, mcpAuthStatus, loginToMcpServer, logoutFromMcpServer } from '@epoch-agent/plugin-mcp';
|
|
10
10
|
import { filePlugin } from '@epoch-agent/plugin-file';
|
|
11
11
|
import { lspPlugin } from '@epoch-agent/plugin-lsp';
|
|
12
|
-
import { terminalPlugin,
|
|
12
|
+
import { terminalPlugin, rekeyTasks, listTasks } from '@epoch-agent/plugin-terminal';
|
|
13
13
|
export { taskOutput as backgroundTaskOutput, listTasks as listBackgroundTasks } from '@epoch-agent/plugin-terminal';
|
|
14
14
|
import { webPlugin, selectSearchProvider, createWebSearchTool } from '@epoch-agent/plugin-web';
|
|
15
15
|
|
|
@@ -53,18 +53,22 @@ function wireCommands(opts) {
|
|
|
53
53
|
});
|
|
54
54
|
if (summary.commands.length === 0 && summary.issues.length > 0) {
|
|
55
55
|
diags.warn(
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
SLASH_COMMANDS_DIAG_MODULE,
|
|
57
|
+
t("boot.commands_all_failed", { count: issueDetails(summary.issues).length }),
|
|
58
58
|
"no-command-loaded"
|
|
59
59
|
);
|
|
60
60
|
}
|
|
61
|
-
const available = new Set(toolProvider.list().map((
|
|
61
|
+
const available = new Set(toolProvider.list().map((t25) => t25.name));
|
|
62
62
|
for (const cmd of summary.commands) {
|
|
63
63
|
const unknown = (cmd.allowedTools ?? []).filter((name) => !available.has(name));
|
|
64
64
|
if (unknown.length > 0) {
|
|
65
65
|
diags.warn(
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
SLASH_COMMANDS_DIAG_MODULE,
|
|
67
|
+
(lang) => t(
|
|
68
|
+
"boot.command_allowed_tools",
|
|
69
|
+
{ name: `/${cmd.name}`, tools: unknown.join(", ") },
|
|
70
|
+
lang
|
|
71
|
+
),
|
|
68
72
|
"unknown-tool"
|
|
69
73
|
);
|
|
70
74
|
}
|
|
@@ -87,6 +91,65 @@ var init_commands = __esm({
|
|
|
87
91
|
"src/commands.ts"() {
|
|
88
92
|
}
|
|
89
93
|
});
|
|
94
|
+
// src/events.ts
|
|
95
|
+
function publishAgentEvent(sessionId, event) {
|
|
96
|
+
bus.publish(sessionId, event);
|
|
97
|
+
}
|
|
98
|
+
var ProcessEventBus, bus, processEventBus;
|
|
99
|
+
var init_events = __esm({
|
|
100
|
+
"src/events.ts"() {
|
|
101
|
+
ProcessEventBus = class {
|
|
102
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
103
|
+
failures = 0;
|
|
104
|
+
subscribe(fn) {
|
|
105
|
+
this.subscribers.add(fn);
|
|
106
|
+
let released = false;
|
|
107
|
+
return () => {
|
|
108
|
+
if (released) return;
|
|
109
|
+
released = true;
|
|
110
|
+
this.subscribers.delete(fn);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
get subscriberCount() {
|
|
114
|
+
return this.subscribers.size;
|
|
115
|
+
}
|
|
116
|
+
get deliveryFailures() {
|
|
117
|
+
return this.failures;
|
|
118
|
+
}
|
|
119
|
+
publish(sessionId, event) {
|
|
120
|
+
if (this.subscribers.size === 0) return;
|
|
121
|
+
for (const fn of this.subscribers) {
|
|
122
|
+
try {
|
|
123
|
+
fn(event, sessionId);
|
|
124
|
+
} catch {
|
|
125
|
+
this.failures += 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
bus = new ProcessEventBus();
|
|
131
|
+
processEventBus = bus;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
function dropOverlongToolNames(tools, module, code, diags) {
|
|
135
|
+
const kept = [];
|
|
136
|
+
for (const tool of tools) {
|
|
137
|
+
if (tool.name.length > MAX_TOOL_NAME_LENGTH) {
|
|
138
|
+
diags.warn(
|
|
139
|
+
module,
|
|
140
|
+
(lang) => t(
|
|
141
|
+
"host_capabilities.tool_name_too_long",
|
|
142
|
+
{ name: tool.name, limit: MAX_TOOL_NAME_LENGTH },
|
|
143
|
+
lang
|
|
144
|
+
),
|
|
145
|
+
code
|
|
146
|
+
);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
kept.push(tool);
|
|
150
|
+
}
|
|
151
|
+
return kept;
|
|
152
|
+
}
|
|
90
153
|
function wireHostCapabilities(caps, diags) {
|
|
91
154
|
if (!caps) return EMPTY;
|
|
92
155
|
const namespace = caps.namespace;
|
|
@@ -109,7 +172,8 @@ function wireHostCapabilities(caps, diags) {
|
|
|
109
172
|
const roles = wireRoles(caps.roles ?? [], namespace, diags);
|
|
110
173
|
const skillDirs = wireSkillDirs(caps.skillDirs ?? [], namespace, diags);
|
|
111
174
|
const mcpServers = wireMcpServers(caps.mcpServers ?? [], namespace, diags);
|
|
112
|
-
|
|
175
|
+
const tools = wireTools(caps.tools ?? [], namespace, diags);
|
|
176
|
+
if (roles.length > 0 || skillDirs.length > 0 || mcpServers.length > 0 || tools.length > 0) {
|
|
113
177
|
diags.ok(
|
|
114
178
|
HOST_CAPABILITIES_MODULE,
|
|
115
179
|
(lang) => t(
|
|
@@ -118,13 +182,14 @@ function wireHostCapabilities(caps, diags) {
|
|
|
118
182
|
namespace,
|
|
119
183
|
roles: roles.length,
|
|
120
184
|
skillDirs: skillDirs.length,
|
|
121
|
-
mcpServers: mcpServers.length
|
|
185
|
+
mcpServers: mcpServers.length,
|
|
186
|
+
tools: tools.length
|
|
122
187
|
},
|
|
123
188
|
lang
|
|
124
189
|
)
|
|
125
190
|
);
|
|
126
191
|
}
|
|
127
|
-
return { roles, skillDirs, mcpServers };
|
|
192
|
+
return { roles, skillDirs, mcpServers, tools };
|
|
128
193
|
}
|
|
129
194
|
function wireRoles(input, namespace, diags) {
|
|
130
195
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -153,7 +218,6 @@ function wireRoles(input, namespace, diags) {
|
|
|
153
218
|
...role.prompt ? { prompt: role.prompt } : {},
|
|
154
219
|
...role.tools ? { tools: [...role.tools] } : {},
|
|
155
220
|
...role.maxTurns ? { maxTurns: role.maxTurns } : {},
|
|
156
|
-
// 宿主说不出这一格(`HostAgentRole` 上没有它),这一行是它唯一的来源
|
|
157
221
|
source: "host"
|
|
158
222
|
});
|
|
159
223
|
}
|
|
@@ -199,11 +263,95 @@ function wireMcpServers(input, namespace, diags) {
|
|
|
199
263
|
}
|
|
200
264
|
return servers;
|
|
201
265
|
}
|
|
202
|
-
|
|
266
|
+
function wireTools(input, namespace, diags) {
|
|
267
|
+
const seen = /* @__PURE__ */ new Set();
|
|
268
|
+
const tools = [];
|
|
269
|
+
for (const tool of input) {
|
|
270
|
+
if (!HOST_TOOL_NAME_PATTERN.test(tool.name)) {
|
|
271
|
+
diags.warn(
|
|
272
|
+
HOST_CAPABILITIES_MODULE,
|
|
273
|
+
(lang) => t("host_capabilities.bad_tool_name", { name: tool.name }, lang),
|
|
274
|
+
"host-tool-invalid-name"
|
|
275
|
+
);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (seen.has(tool.name)) {
|
|
279
|
+
diags.warn(
|
|
280
|
+
HOST_CAPABILITIES_MODULE,
|
|
281
|
+
(lang) => t("host_capabilities.duplicate_tool", { name: tool.name }, lang),
|
|
282
|
+
"host-tool-duplicate"
|
|
283
|
+
);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
seen.add(tool.name);
|
|
287
|
+
const describeTarget = tool.describeTarget;
|
|
288
|
+
tools.push({
|
|
289
|
+
...tool,
|
|
290
|
+
name: `${namespace}__${tool.name}`,
|
|
291
|
+
execute: (args, ctx) => tool.execute(args, ctx),
|
|
292
|
+
...describeTarget ? { describeTarget: (args) => describeTarget.call(tool, args) } : {}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return tools;
|
|
296
|
+
}
|
|
297
|
+
var HOST_CAPABILITIES_MODULE, HOST_TOOL_NAME_PATTERN, MAX_TOOL_NAME_LENGTH, EMPTY;
|
|
203
298
|
var init_host_capabilities = __esm({
|
|
204
299
|
"src/host-capabilities.ts"() {
|
|
205
300
|
HOST_CAPABILITIES_MODULE = "HostCapabilities";
|
|
206
|
-
|
|
301
|
+
HOST_TOOL_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
302
|
+
MAX_TOOL_NAME_LENGTH = 128;
|
|
303
|
+
EMPTY = { roles: [], skillDirs: [], mcpServers: [], tools: [] };
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
async function wireHostMarketplaces(input) {
|
|
307
|
+
const { statePath, diagnostics: diags } = input;
|
|
308
|
+
const allowRemote = input.hostMarketplaces.allowRemote === true;
|
|
309
|
+
for (const raw of input.hostMarketplaces.sources) {
|
|
310
|
+
const source = raw.trim();
|
|
311
|
+
if (!source) {
|
|
312
|
+
diags.warn(HOST_MARKETPLACE_MODULE, (l) => t("plugin_control.market_empty", void 0, l));
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (!allowRemote && !isLocalSource(source)) {
|
|
316
|
+
diags.warn(
|
|
317
|
+
HOST_MARKETPLACE_MODULE,
|
|
318
|
+
(l) => t("plugin_control.market_remote_off", { source }, l),
|
|
319
|
+
"remote-disabled"
|
|
320
|
+
);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const outcome = await addOrRefresh(source, statePath);
|
|
324
|
+
if (!outcome.ok) {
|
|
325
|
+
diags.warn(
|
|
326
|
+
HOST_MARKETPLACE_MODULE,
|
|
327
|
+
(l) => t("plugin_control.market_failed", { source, reason: outcome.reason }, l),
|
|
328
|
+
"marketplace-unavailable"
|
|
329
|
+
);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
diags.ok(
|
|
333
|
+
HOST_MARKETPLACE_MODULE,
|
|
334
|
+
(l) => t("plugin_control.market_ready", { name: outcome.name, count: outcome.count }, l)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function isLocalSource(source) {
|
|
339
|
+
return resolveSource(source)?.type === "local";
|
|
340
|
+
}
|
|
341
|
+
async function addOrRefresh(source, statePath) {
|
|
342
|
+
try {
|
|
343
|
+
const existing = readMarketplaces(statePath).records.find((r) => r.source === source);
|
|
344
|
+
const outcome = existing ? await updateMarketplace(existing.name, { statePath }) : await addMarketplace(source, { statePath });
|
|
345
|
+
return outcome.ok ? { ok: true, name: outcome.record.name, count: outcome.record.catalog.plugins.length } : outcome;
|
|
346
|
+
} catch (err) {
|
|
347
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
348
|
+
return { ok: false, reason: message.split("\n")[0] ?? message };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
var HOST_MARKETPLACE_MODULE;
|
|
352
|
+
var init_host_marketplaces = __esm({
|
|
353
|
+
"src/host-marketplaces.ts"() {
|
|
354
|
+
HOST_MARKETPLACE_MODULE = "HostMarketplaces";
|
|
207
355
|
}
|
|
208
356
|
});
|
|
209
357
|
async function applyHostPreset(opts) {
|
|
@@ -441,6 +589,233 @@ var init_host_preset = __esm({
|
|
|
441
589
|
HOST_PRESET_MODULE = "HostPreset";
|
|
442
590
|
}
|
|
443
591
|
});
|
|
592
|
+
function fail(message, start, code, suggestion) {
|
|
593
|
+
return {
|
|
594
|
+
success: false,
|
|
595
|
+
output: "",
|
|
596
|
+
error: { code, message, ...suggestion ? { suggestion } : {} },
|
|
597
|
+
duration: Date.now() - start
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function done(output, start) {
|
|
601
|
+
return { success: true, output, duration: Date.now() - start };
|
|
602
|
+
}
|
|
603
|
+
function str(args, key) {
|
|
604
|
+
const raw = args[key];
|
|
605
|
+
return typeof raw === "string" ? raw : "";
|
|
606
|
+
}
|
|
607
|
+
function toAddInput(args) {
|
|
608
|
+
const name = str(args, "name").trim();
|
|
609
|
+
if (!name) return "name must not be empty";
|
|
610
|
+
const transport = str(args, "transport").trim();
|
|
611
|
+
if (transport !== "stdio" && transport !== "sse" && transport !== "http") {
|
|
612
|
+
return `transport must be one of "stdio" / "sse" / "http", got ${JSON.stringify(transport)}`;
|
|
613
|
+
}
|
|
614
|
+
const rawArgs = args["args"];
|
|
615
|
+
if (rawArgs !== void 0 && !isStringArray(rawArgs)) {
|
|
616
|
+
return "args must be an array of strings";
|
|
617
|
+
}
|
|
618
|
+
const rawEnv = args["env"];
|
|
619
|
+
if (rawEnv !== void 0 && !isStringRecord(rawEnv)) {
|
|
620
|
+
return "env must be an object whose values are all strings";
|
|
621
|
+
}
|
|
622
|
+
const command = str(args, "command").trim();
|
|
623
|
+
const url = str(args, "url").trim();
|
|
624
|
+
return {
|
|
625
|
+
name,
|
|
626
|
+
transport,
|
|
627
|
+
...command ? { command } : {},
|
|
628
|
+
...rawArgs === void 0 ? {} : { args: rawArgs },
|
|
629
|
+
...url ? { url } : {},
|
|
630
|
+
...rawEnv === void 0 ? {} : { env: rawEnv }
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function isStringArray(value) {
|
|
634
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
635
|
+
}
|
|
636
|
+
function isStringRecord(value) {
|
|
637
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
638
|
+
return Object.values(value).every((v) => typeof v === "string");
|
|
639
|
+
}
|
|
640
|
+
function listTool(deps) {
|
|
641
|
+
return {
|
|
642
|
+
name: "mcp_list",
|
|
643
|
+
description: "\u5217\u51FA\u8FD9\u4E2A\u8FDB\u7A0B\u91CC\u6240\u6709 MCP server \u7684\u72B6\u6001\uFF1A\u540D\u5B57\u3001\u6765\u6E90\u3001\u8FDE\u4E0A\u6CA1\u6709\u3001\u51E0\u4E2A\u5DE5\u5177\u3001\u4E0A\u6B21\u9519\u5728\u54EA\u3002\u6539\u4E00\u53F0\u4E4B\u524D\u5148\u770B\u4E00\u773C\u3002\nsource \u51B3\u5B9A\u6539\u4E0D\u6539\u5F97\u52A8\uFF1A\u53EA\u6709 user \u90A3\u6279\u662F\u7528\u6237\u914D\u7684\uFF1Bhost / plugin \u4E24\u6279\u4E0D\u5728 mcp.json \u91CC\uFF0C\u6539\u4E0D\u4E86\u3002\n\u26A0\uFE0F \u4E0D\u56DE command / url / env \u2014\u2014 \u90A3\u51E0\u683C\u5E38\u85CF\u4EE4\u724C\uFF0C\u8981\u770B\u8BA9\u7528\u6237\u81EA\u5DF1\u6253\u5F00 ~/.epoch/mcp.json\u3002",
|
|
644
|
+
parameters: { type: "object", properties: {} },
|
|
645
|
+
exposure: ToolExposure.Direct,
|
|
646
|
+
supportsParallel: true,
|
|
647
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
648
|
+
operation: "file_read",
|
|
649
|
+
describeTarget: () => MCP_LIST_TOOL,
|
|
650
|
+
execute() {
|
|
651
|
+
const start = Date.now();
|
|
652
|
+
const rows = deps.servers();
|
|
653
|
+
if (rows.length === 0) {
|
|
654
|
+
return Promise.resolve(
|
|
655
|
+
done("No MCP servers are configured in this process. Use mcp_add to add one.", start)
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const lines = rows.map((s) => {
|
|
659
|
+
const state = s.connected ? "connected" : "disconnected";
|
|
660
|
+
const login = s.needsLogin === true ? ", needs login" : "";
|
|
661
|
+
const err = s.lastError === void 0 ? "" : ` \u2014 ${s.lastError}`;
|
|
662
|
+
return `${s.name} [${s.source}] ${state}, ${s.toolCount} tools${login}${err}`;
|
|
663
|
+
});
|
|
664
|
+
return Promise.resolve(done(lines.join("\n"), start));
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function runWrite(deps, args, kind) {
|
|
669
|
+
const start = Date.now();
|
|
670
|
+
const input = toAddInput(args);
|
|
671
|
+
if (typeof input === "string") return Promise.resolve(fail(input, start, 3201));
|
|
672
|
+
const call = kind === "add" ? deps.control.add(input) : deps.control.update(input);
|
|
673
|
+
return call.then((outcome) => {
|
|
674
|
+
if (!outcome.ok) {
|
|
675
|
+
return fail(
|
|
676
|
+
`${outcome.reason}: ${outcome.detail}`,
|
|
677
|
+
start,
|
|
678
|
+
3202,
|
|
679
|
+
HINT_BY_REASON[outcome.reason]
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
return done(describeWrite(kind, outcome.configPath, outcome.status), start);
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
function describeWrite(kind, configPath2, status) {
|
|
686
|
+
const verb = kind === "add" ? "Added" : "Updated";
|
|
687
|
+
const why = status.lastError === void 0 ? "" : ` \u2014 ${status.lastError}`;
|
|
688
|
+
const login = status.needsLogin === true ? " It needs a login: run `epoch mcp login`." : "";
|
|
689
|
+
const live = status.connected ? `It is connected and exposes ${status.toolCount} tools now.` : `It is NOT connected right now${why}. The config is saved either way \u2014 the server may just not be up yet.`;
|
|
690
|
+
return `${verb} MCP server "${status.name}" in ${configPath2}. ${live}${login}`;
|
|
691
|
+
}
|
|
692
|
+
function addTool(deps) {
|
|
693
|
+
return {
|
|
694
|
+
name: "mcp_add",
|
|
695
|
+
description: "\u52A0\u4E00\u53F0 MCP server\uFF1A\u5199\u8FDB ~/.epoch/mcp.json\uFF0C\u7136\u540E\u5F53\u573A\u8FDE\u4E00\u6B21\u3002\n\u26A0\uFE0F \u5F71\u54CD\u7684\u4E0D\u53EA\u662F\u8FD9\u6BB5\u4F1A\u8BDD \u2014\u2014 \u90A3\u53F0\u4EE5\u540E\u6BCF\u4E2A epoch \u8FDB\u7A0B\u90FD\u4F1A\u8FDE\u5B83\uFF08stdio \u90A3\u6863\u8981\u8D77\u5B50\u8FDB\u7A0B\uFF09\u3002\u52A0\u4E4B\u524D\u5148\u8DDF\u7528\u6237\u8BF4\u6E05\u4F60\u8981\u52A0\u4EC0\u4E48\u3002\n\u8FDE\u4E0D\u4E0A\u4E0D\u7B97\u5931\u8D25\uFF08\u914D\u7F6E\u5BF9\u3001\u5BF9\u9762\u6CA1\u8D77\u6765\u7167\u6837\u52A0\u5F97\u8FDB\u53BB\uFF0C\u56DE\u6267\u4F1A\u8BF4\u6E05\uFF09\u3002\u540D\u5B57\u88AB\u5360\u4F1A\u88AB\u62D2 \u2014\u2014 \u90A3\u65F6\u6539\u7528 mcp_update\u3002",
|
|
696
|
+
parameters: {
|
|
697
|
+
type: "object",
|
|
698
|
+
properties: SERVER_PROPERTIES,
|
|
699
|
+
required: ["name", "transport"]
|
|
700
|
+
},
|
|
701
|
+
exposure: ToolExposure.Direct,
|
|
702
|
+
supportsParallel: false,
|
|
703
|
+
annotations: ADMIN_ANNOTATIONS,
|
|
704
|
+
operation: "command",
|
|
705
|
+
describeTarget: (args) => `${MCP_ADD_TOOL}:${String(args["name"] ?? "")} ${String(args["command"] ?? args["url"] ?? "")}`.trim(),
|
|
706
|
+
execute: (args) => runWrite(deps, args, "add")
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function updateTool(deps) {
|
|
710
|
+
return {
|
|
711
|
+
name: "mcp_update",
|
|
712
|
+
description: "\u6539\u4E00\u53F0\u5DF2\u6709 MCP server\uFF1A\u5C31\u5730\u6362\u6389 ~/.epoch/mcp.json \u91CC\u90A3\u4E00\u6BB5\uFF0C\u518D\u6309\u65B0\u914D\u7F6E\u91CD\u8FDE\u5B83\u3002\u522B\u7684 server \u548C\u6587\u4EF6\u91CC\u7684\u6CE8\u91CA\u6392\u7248\u4E00\u4E2A\u5B57\u8282\u4E0D\u52A8\u3002\n\u26A0\uFE0F \u6536\u7684\u662F**\u6574\u5957**\u914D\u7F6E\u4E0D\u662F\u8865\u4E01 \u2014\u2014 \u6CA1\u7ED9\u7684\u683C\u5B50\uFF08args / url / env\uFF09\u4F1A\u88AB\u5F53\u6210\u300C\u4E0D\u8981\u4E86\u300D\uFF0C\u8981\u4FDD\u4F4F\u7684\u6BCF\u6B21\u90FD\u5F97\u539F\u6837\u518D\u7ED9\u3002\n\u540D\u5B57\u6539\u4E0D\u4E86\uFF08\u90A3\u7B49\u4E8E\u6362\u4E00\u6279\u5DE5\u5177\u540D\uFF0C\u8981\u6539\u540D\u5C31 mcp_remove \u518D mcp_add\uFF09\u3002\u6587\u4EF6\u91CC\u6CA1\u8FD9\u4E2A\u540D\u5B57\u4F1A\u88AB\u62D2\u3002",
|
|
713
|
+
parameters: {
|
|
714
|
+
type: "object",
|
|
715
|
+
properties: SERVER_PROPERTIES,
|
|
716
|
+
required: ["name", "transport"]
|
|
717
|
+
},
|
|
718
|
+
exposure: ToolExposure.Direct,
|
|
719
|
+
supportsParallel: false,
|
|
720
|
+
annotations: ADMIN_ANNOTATIONS,
|
|
721
|
+
operation: "command",
|
|
722
|
+
describeTarget: (args) => `${MCP_UPDATE_TOOL}:${String(args["name"] ?? "")} ${String(args["command"] ?? args["url"] ?? "")}`.trim(),
|
|
723
|
+
execute: (args) => runWrite(deps, args, "update")
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function removeTool(deps) {
|
|
727
|
+
return {
|
|
728
|
+
name: "mcp_remove",
|
|
729
|
+
description: "\u5220\u6389\u4E00\u53F0 MCP server\uFF1A\u4ECE ~/.epoch/mcp.json \u91CC\u53BB\u6389\u90A3\u4E00\u6BB5\uFF0C\u5E76\u65AD\u5F00\u5B83\u3001\u628A\u5B83\u90A3\u6279\u5DE5\u5177\u6458\u6389\u3002\u522B\u7684 server \u548C\u6CE8\u91CA\u6392\u7248\u4E00\u4E2A\u5B57\u8282\u4E0D\u52A8\u3002\n\u26A0\uFE0F \u4E0D\u53EF\u64A4\u9500 \u2014\u2014 \u90A3\u4E00\u6BB5\u914D\u7F6E\uFF08\u542B env \u91CC\u7684\u4EE4\u724C\uFF09\u4ECE\u6587\u4EF6\u91CC\u6D88\u5931\uFF0C\u8981\u56DE\u6765\u53EA\u80FD\u91CD\u586B\u3002\u5220\u4E4B\u524D\u5148\u8DDF\u7528\u6237\u786E\u8BA4\u3002\n\u6587\u4EF6\u91CC\u6CA1\u8FD9\u4E2A\u540D\u5B57\u4F1A\u88AB\u62D2\uFF08host / plugin \u90A3\u4E24\u6279\u5220\u4E0D\u6389\uFF09\u3002",
|
|
730
|
+
parameters: {
|
|
731
|
+
type: "object",
|
|
732
|
+
properties: { name: SERVER_PROPERTIES.name },
|
|
733
|
+
required: ["name"]
|
|
734
|
+
},
|
|
735
|
+
exposure: ToolExposure.Direct,
|
|
736
|
+
supportsParallel: false,
|
|
737
|
+
annotations: ADMIN_ANNOTATIONS,
|
|
738
|
+
operation: "command",
|
|
739
|
+
describeTarget: (args) => `${MCP_REMOVE_TOOL}:${String(args["name"] ?? "")}`,
|
|
740
|
+
async execute(args) {
|
|
741
|
+
const start = Date.now();
|
|
742
|
+
const name = str(args, "name").trim();
|
|
743
|
+
if (!name) return fail("name must not be empty", start, 3201);
|
|
744
|
+
const outcome = await deps.control.remove(name);
|
|
745
|
+
if (!outcome.ok) {
|
|
746
|
+
return fail(
|
|
747
|
+
`${outcome.reason}: ${outcome.detail}`,
|
|
748
|
+
start,
|
|
749
|
+
3202,
|
|
750
|
+
HINT_BY_REASON[outcome.reason]
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
return done(
|
|
754
|
+
`removed MCP server "${name}" from ${outcome.configPath}. Its tools are gone from this process.`,
|
|
755
|
+
start
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function createMcpTools(deps) {
|
|
761
|
+
return [listTool(deps), addTool(deps), updateTool(deps), removeTool(deps)];
|
|
762
|
+
}
|
|
763
|
+
var MCP_LIST_TOOL, MCP_ADD_TOOL, MCP_UPDATE_TOOL, MCP_REMOVE_TOOL, SERVER_PROPERTIES, ADMIN_ANNOTATIONS, HINT_BY_REASON;
|
|
764
|
+
var init_mcp_tools = __esm({
|
|
765
|
+
"src/mcp-tools.ts"() {
|
|
766
|
+
MCP_LIST_TOOL = "mcp_list";
|
|
767
|
+
MCP_ADD_TOOL = "mcp_add";
|
|
768
|
+
MCP_UPDATE_TOOL = "mcp_update";
|
|
769
|
+
MCP_REMOVE_TOOL = "mcp_remove";
|
|
770
|
+
SERVER_PROPERTIES = {
|
|
771
|
+
name: {
|
|
772
|
+
type: "string",
|
|
773
|
+
description: "server \u540D\u3002\u4F1A\u539F\u6837\u53D8\u6210\u5DE5\u5177\u540D\u7684\u4E00\u90E8\u5206\uFF08mcp__<name>__<\u5DE5\u5177>\uFF09"
|
|
774
|
+
},
|
|
775
|
+
transport: {
|
|
776
|
+
type: "string",
|
|
777
|
+
enum: ["stdio", "sse", "http"],
|
|
778
|
+
description: "\u4F20\u8F93\u7C7B\u578B\uFF1Astdio \u8D77\u4E00\u4E2A\u672C\u5730\u5B50\u8FDB\u7A0B\uFF0Csse / http \u8FDE\u4E00\u4E2A\u5730\u5740"
|
|
779
|
+
},
|
|
780
|
+
command: { type: "string", description: "stdio \u90A3\u4E00\u6863\u7684\u53EF\u6267\u884C\u6587\u4EF6\uFF0C\u5982 npx" },
|
|
781
|
+
args: {
|
|
782
|
+
type: "array",
|
|
783
|
+
items: { type: "string" },
|
|
784
|
+
description: "stdio \u90A3\u4E00\u6863\u7684\u547D\u4EE4\u884C\u53C2\u6570\uFF0C\u5143\u7D20\u5FC5\u987B\u5168\u662F\u5B57\u7B26\u4E32"
|
|
785
|
+
},
|
|
786
|
+
url: { type: "string", description: "sse / http \u90A3\u4E24\u6863\u7684\u5730\u5740" },
|
|
787
|
+
env: {
|
|
788
|
+
type: "object",
|
|
789
|
+
additionalProperties: { type: "string" },
|
|
790
|
+
description: "\u5B50\u8FDB\u7A0B\u73AF\u5883\u53D8\u91CF\uFF0C\u503C\u5FC5\u987B\u5168\u662F\u5B57\u7B26\u4E32"
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
ADMIN_ANNOTATIONS = {
|
|
794
|
+
readOnlyHint: false,
|
|
795
|
+
destructiveHint: true,
|
|
796
|
+
idempotentHint: false,
|
|
797
|
+
openWorldHint: true
|
|
798
|
+
};
|
|
799
|
+
HINT_BY_REASON = {
|
|
800
|
+
"invalid-name": 'Use only letters, digits, "_" and "-", starting with a letter or digit (max 48 chars) \u2014 the name becomes part of every tool name this server exposes.',
|
|
801
|
+
"invalid-config": 'stdio needs "command"; sse and http need a valid "url". Fix that one field and retry.',
|
|
802
|
+
duplicate: "That name is already taken \u2014 call mcp_list, then use mcp_update instead of mcp_add.",
|
|
803
|
+
"not-found": "That name is not in ~/.epoch/mcp.json \u2014 call mcp_list first, or use mcp_add.",
|
|
804
|
+
unwritable: "The config file could not be read, written, or parsed. Do not retry \u2014 tell the user to look at ~/.epoch/mcp.json themselves.",
|
|
805
|
+
"apply-failed": "The config file was written but this process could not be brought in line. The change takes effect on the next epoch start."
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
function providerLabel(info, lang) {
|
|
810
|
+
switch (info.type) {
|
|
811
|
+
case "ollama":
|
|
812
|
+
return t("provider.label_ollama", void 0, lang);
|
|
813
|
+
case "openai-compatible":
|
|
814
|
+
return t("provider.label_openai_compatible", void 0, lang);
|
|
815
|
+
default:
|
|
816
|
+
return info.label;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
444
819
|
function buildModelCatalogControl(opts) {
|
|
445
820
|
const { homeDir, configured } = opts;
|
|
446
821
|
const keyOf = (envVar) => {
|
|
@@ -449,13 +824,16 @@ function buildModelCatalogControl(opts) {
|
|
|
449
824
|
};
|
|
450
825
|
return {
|
|
451
826
|
providers() {
|
|
452
|
-
return PROVIDER_INFOS.filter((p) => p.interactive).map((p) =>
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
827
|
+
return PROVIDER_INFOS.filter((p) => p.interactive).map((p) => {
|
|
828
|
+
const key = p.apiKeyEnv ? keyOf(p.apiKeyEnv) : "";
|
|
829
|
+
return {
|
|
830
|
+
type: p.type,
|
|
831
|
+
label: p.label,
|
|
832
|
+
envVar: p.apiKeyEnv,
|
|
833
|
+
hasKey: p.apiKeyEnv === null || key !== "",
|
|
834
|
+
keyHint: key === "" ? null : maskApiKey(key)
|
|
835
|
+
};
|
|
836
|
+
});
|
|
459
837
|
},
|
|
460
838
|
async discover(provider, discoverOpts) {
|
|
461
839
|
const envVar = getProviderInfo(provider)?.apiKeyEnv ?? null;
|
|
@@ -475,6 +853,19 @@ function buildModelCatalogControl(opts) {
|
|
|
475
853
|
source: result.source,
|
|
476
854
|
status: probeStatus(result, { needsKey: envVar !== null, hasKey: apiKey !== "" })
|
|
477
855
|
};
|
|
856
|
+
},
|
|
857
|
+
async setKey(provider, apiKey) {
|
|
858
|
+
const envVar = getProviderInfo(provider)?.apiKeyEnv ?? null;
|
|
859
|
+
if (!envVar) throw new Error(t("provider.key_not_applicable", { provider }));
|
|
860
|
+
const written = await writeProviderSecret(envVar, apiKey, envPath(homeDir));
|
|
861
|
+
clearModelCache(homeDir, provider);
|
|
862
|
+
return {
|
|
863
|
+
envVar,
|
|
864
|
+
backend: written.backend,
|
|
865
|
+
encrypted: written.encrypted,
|
|
866
|
+
envPath: written.envPath,
|
|
867
|
+
keyHint: maskApiKey(apiKey)
|
|
868
|
+
};
|
|
478
869
|
}
|
|
479
870
|
};
|
|
480
871
|
}
|
|
@@ -488,6 +879,209 @@ var init_model_catalog = __esm({
|
|
|
488
879
|
"src/model-catalog.ts"() {
|
|
489
880
|
}
|
|
490
881
|
});
|
|
882
|
+
function createInstallFlow(deps) {
|
|
883
|
+
const locations = () => ({
|
|
884
|
+
pluginsDir: deps.pluginsDir,
|
|
885
|
+
statePath: deps.statePath,
|
|
886
|
+
epochVersion: currentEpochVersion()
|
|
887
|
+
});
|
|
888
|
+
return {
|
|
889
|
+
preview: async (source, extra, lang) => {
|
|
890
|
+
let seen;
|
|
891
|
+
const outcome = await installPlugin(source, {
|
|
892
|
+
...locations(),
|
|
893
|
+
...extra,
|
|
894
|
+
confirm: async (preview) => {
|
|
895
|
+
seen = preview;
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
if (seen) return { ok: true, preview: seen, token: previewToken(seen) };
|
|
900
|
+
return refuse(outcome, lang);
|
|
901
|
+
},
|
|
902
|
+
install: async (source, token, extra, lang) => {
|
|
903
|
+
let matched;
|
|
904
|
+
const outcome = await installPlugin(source, {
|
|
905
|
+
...locations(),
|
|
906
|
+
...extra,
|
|
907
|
+
confirm: async (preview) => {
|
|
908
|
+
matched = previewToken(preview) === token;
|
|
909
|
+
return matched;
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
if (outcome.ok) return { ok: true, record: outcome.record, preview: outcome.preview };
|
|
913
|
+
if (matched === false) {
|
|
914
|
+
return {
|
|
915
|
+
ok: false,
|
|
916
|
+
reason: "stale-token",
|
|
917
|
+
detail: t("plugin_control.stale_token", void 0, lang),
|
|
918
|
+
...outcome.preview ? { preview: outcome.preview } : {}
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
return refuse(outcome, lang);
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function replaceOpts(record) {
|
|
926
|
+
return {
|
|
927
|
+
replace: record.name,
|
|
928
|
+
...record.marketplace ? { marketplace: record.marketplace } : {}
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function refuse(outcome, lang) {
|
|
932
|
+
if (outcome.ok) {
|
|
933
|
+
return {
|
|
934
|
+
ok: false,
|
|
935
|
+
reason: "failed",
|
|
936
|
+
detail: t("plugin_control.unexpected_commit", { name: outcome.record.name }, lang)
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
ok: false,
|
|
941
|
+
reason: outcome.preview?.conflict ? "conflict" : "failed",
|
|
942
|
+
detail: outcome.reason,
|
|
943
|
+
...outcome.preview ? { preview: outcome.preview } : {}
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function previewToken(preview) {
|
|
947
|
+
const inv = preview.inventory;
|
|
948
|
+
const hash = createHash("sha256");
|
|
949
|
+
hash.update(`${preview.manifest.name}@${preview.manifest.version}
|
|
950
|
+
`);
|
|
951
|
+
hash.update(`${preview.source.type} ${preview.source.raw}
|
|
952
|
+
`);
|
|
953
|
+
for (const list of [inv.commands, inv.roles, inv.skills, inv.mcpServers, inv.ignoredBuckets]) {
|
|
954
|
+
hash.update(`${[...list].sort().join(",")}
|
|
955
|
+
`);
|
|
956
|
+
}
|
|
957
|
+
for (const tally of [...inv.hooks].sort((a, b) => a.type.localeCompare(b.type))) {
|
|
958
|
+
hash.update(`${tally.type}=${tally.count};`);
|
|
959
|
+
}
|
|
960
|
+
hash.update(`
|
|
961
|
+
${inv.denyRules}|${inv.jsTools ? 1 : 0}
|
|
962
|
+
`);
|
|
963
|
+
hash.update(`${preview.versionUnmet?.required ?? "-"}
|
|
964
|
+
`);
|
|
965
|
+
return hash.digest("hex").slice(0, 32);
|
|
966
|
+
}
|
|
967
|
+
var init_plugin_install = __esm({
|
|
968
|
+
"src/plugin-install.ts"() {
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
function createPluginControl(deps) {
|
|
972
|
+
let pendingRestart = false;
|
|
973
|
+
const live = new Map(deps.loaded.map((p) => [p.name, p.counts]));
|
|
974
|
+
const flow = createInstallFlow({ pluginsDir: deps.pluginsDir, statePath: deps.statePath });
|
|
975
|
+
const mark = (outcome) => {
|
|
976
|
+
if (outcome.ok) pendingRestart = true;
|
|
977
|
+
return outcome;
|
|
978
|
+
};
|
|
979
|
+
const resolveRef = (ref, lang) => {
|
|
980
|
+
const hit = resolveMarketplaceRef(ref, deps.marketplacesPath);
|
|
981
|
+
if (!hit) {
|
|
982
|
+
return {
|
|
983
|
+
ok: false,
|
|
984
|
+
reason: "unknown-ref",
|
|
985
|
+
detail: t("plugin_control.not_a_ref", { ref }, lang)
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
if (!hit.ok) return { ok: false, reason: "unknown-ref", detail: hit.reason };
|
|
989
|
+
if (!deps.allowRemote && !isLocalSource(hit.ref.source)) {
|
|
990
|
+
return {
|
|
991
|
+
ok: false,
|
|
992
|
+
reason: "remote-disabled",
|
|
993
|
+
detail: t("plugin_control.entry_remote_off", { ref, source: hit.ref.source }, lang)
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
return { ok: true, source: hit.ref.source, marketplace: hit.ref.marketplace };
|
|
997
|
+
};
|
|
998
|
+
const recordOf = (name, lang) => {
|
|
999
|
+
const record = readPluginRecords(deps.statePath).records.find((r) => r.name === name);
|
|
1000
|
+
if (!record) {
|
|
1001
|
+
return {
|
|
1002
|
+
ok: false,
|
|
1003
|
+
reason: "not-installed",
|
|
1004
|
+
detail: t("plugin_control.not_installed", { name }, lang)
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
if (record.linked) {
|
|
1008
|
+
return {
|
|
1009
|
+
ok: false,
|
|
1010
|
+
reason: "nothing-to-do",
|
|
1011
|
+
detail: t("plugin_control.linked_no_update", { name, source: record.source }, lang)
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
if (!deps.allowRemote && !isLocalSource(record.source)) {
|
|
1015
|
+
return {
|
|
1016
|
+
ok: false,
|
|
1017
|
+
reason: "remote-disabled",
|
|
1018
|
+
detail: t("plugin_control.update_remote_off", { name, source: record.source }, lang)
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
return { ok: true, record };
|
|
1022
|
+
};
|
|
1023
|
+
return {
|
|
1024
|
+
list: () => readPluginRecords(deps.statePath).records.map((r) => ({
|
|
1025
|
+
...r,
|
|
1026
|
+
active: live.has(r.name),
|
|
1027
|
+
...live.get(r.name) ? { counts: live.get(r.name) } : {}
|
|
1028
|
+
})),
|
|
1029
|
+
search: (keyword = "") => {
|
|
1030
|
+
const markets = new Map(
|
|
1031
|
+
readMarketplaces(deps.marketplacesPath).records.map((r) => [r.name, r.source])
|
|
1032
|
+
);
|
|
1033
|
+
return searchMarketplaces(keyword, deps.marketplacesPath).map((hit) => {
|
|
1034
|
+
const market = markets.get(hit.marketplace);
|
|
1035
|
+
const absolute = market === void 0 ? void 0 : resolveEntrySource(hit.entry.source, market);
|
|
1036
|
+
const kind = absolute === void 0 ? void 0 : resolveSource(absolute)?.type;
|
|
1037
|
+
return {
|
|
1038
|
+
marketplace: hit.marketplace,
|
|
1039
|
+
entry: hit.entry,
|
|
1040
|
+
ref: hit.ref,
|
|
1041
|
+
...kind ? { sourceType: kind } : {},
|
|
1042
|
+
installable: deps.allowRemote ? kind !== void 0 : kind === "local"
|
|
1043
|
+
};
|
|
1044
|
+
});
|
|
1045
|
+
},
|
|
1046
|
+
preview: async (ref, lang) => {
|
|
1047
|
+
const target = resolveRef(ref, lang);
|
|
1048
|
+
return target.ok ? flow.preview(target.source, { marketplace: target.marketplace }, lang) : target;
|
|
1049
|
+
},
|
|
1050
|
+
install: async (ref, token, lang) => {
|
|
1051
|
+
const target = resolveRef(ref, lang);
|
|
1052
|
+
return target.ok ? mark(await flow.install(target.source, token, { marketplace: target.marketplace }, lang)) : target;
|
|
1053
|
+
},
|
|
1054
|
+
previewUpdate: async (name, lang) => {
|
|
1055
|
+
const found = recordOf(name, lang);
|
|
1056
|
+
return found.ok ? flow.preview(found.record.source, replaceOpts(found.record), lang) : found;
|
|
1057
|
+
},
|
|
1058
|
+
update: async (name, token, lang) => {
|
|
1059
|
+
const found = recordOf(name, lang);
|
|
1060
|
+
return found.ok ? mark(await flow.install(found.record.source, token, replaceOpts(found.record), lang)) : found;
|
|
1061
|
+
},
|
|
1062
|
+
uninstall: async (name, lang) => {
|
|
1063
|
+
const outcome = await uninstallPlugin(name, { statePath: deps.statePath });
|
|
1064
|
+
if (!outcome.ok) {
|
|
1065
|
+
return {
|
|
1066
|
+
ok: false,
|
|
1067
|
+
reason: "not-installed",
|
|
1068
|
+
detail: t("plugin_control.not_installed", { name }, lang)
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
pendingRestart = true;
|
|
1072
|
+
return { ok: true, detail: outcome.message };
|
|
1073
|
+
},
|
|
1074
|
+
get pendingRestart() {
|
|
1075
|
+
return pendingRestart;
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
var init_plugin_control = __esm({
|
|
1080
|
+
"src/plugin-control.ts"() {
|
|
1081
|
+
init_host_marketplaces();
|
|
1082
|
+
init_plugin_install();
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
491
1085
|
function wirePluginMcpServers(mcpPaths, diags) {
|
|
492
1086
|
const out = [];
|
|
493
1087
|
for (const ref of mcpPaths) {
|
|
@@ -550,9 +1144,6 @@ function createRoleWriteControl(deps) {
|
|
|
550
1144
|
return {
|
|
551
1145
|
ok: false,
|
|
552
1146
|
reason: "duplicate",
|
|
553
|
-
// 把**哪一层**占着这个名字说出来:内置和项目级的出路不同(前者只能换名字,
|
|
554
|
-
// 后者他还可以去改自己仓库里那一份)。只说「重名了」的话,一个撞上
|
|
555
|
-
// `general` 的人会以为自己的 `~/.epoch/agents/` 里躺着一份看不见的文件
|
|
556
1147
|
detail: t(
|
|
557
1148
|
"role_write.dup_in_table",
|
|
558
1149
|
{ name, layer: t(LAYER_KEY[clash.source], void 0, lang) },
|
|
@@ -588,6 +1179,7 @@ function createRoleWriteControl(deps) {
|
|
|
588
1179
|
description: input.description,
|
|
589
1180
|
...input.prompt === void 0 ? {} : { prompt: input.prompt },
|
|
590
1181
|
...input.tools === void 0 ? {} : { tools: input.tools },
|
|
1182
|
+
...input.skills === void 0 ? {} : { skills: input.skills },
|
|
591
1183
|
...input.maxTurns === void 0 ? {} : { maxTurns: input.maxTurns }
|
|
592
1184
|
},
|
|
593
1185
|
{ dir: deps.userDir, source: "user" }
|
|
@@ -597,10 +1189,6 @@ function createRoleWriteControl(deps) {
|
|
|
597
1189
|
return {
|
|
598
1190
|
ok: true,
|
|
599
1191
|
path: written.path,
|
|
600
|
-
// 回**重载之后那张表**里的那一份,不是 core 刚从文件里读出来的那个:
|
|
601
|
-
// 两者在 `tools` 上真会不同(`mergeRoles` 的交集语义)。撞名已经在上面
|
|
602
|
-
// 拦掉了,所以今天这两份恒等 —— `??` 那一支是给「没有 provider、
|
|
603
|
-
// 角色表恒空」的那种进程用的,那时表里找不到它,而文件确实写了
|
|
604
1192
|
role: findRole(deps.roles(), name) ?? written.role
|
|
605
1193
|
};
|
|
606
1194
|
}
|
|
@@ -645,32 +1233,11 @@ var ApprovalRelay, QuestionRelay;
|
|
|
645
1233
|
var init_serialize = __esm({
|
|
646
1234
|
"src/serialize.ts"() {
|
|
647
1235
|
ApprovalRelay = class {
|
|
648
|
-
/**
|
|
649
|
-
* `requestId` → 请求内容 + 答复闭包 + 登记那一刻。
|
|
650
|
-
*
|
|
651
|
-
* 存了 `request` 而不只是闭包,是为了 `listPending()` —— 见它的注释。
|
|
652
|
-
* Map 的插入序 = 请求发生的时间序,`listPending()` 靠它给出稳定顺序。
|
|
653
|
-
*
|
|
654
|
-
* ⚠️ **`since` 是 2026-08-16 补的,在那之前这张表「顺序有了、时刻没有」**:
|
|
655
|
-
* 插入序答得出「谁先谁后」,答不出「第一条是几点挂上的」,而界面上那一格要说
|
|
656
|
-
* 「已等多久」只能问后者。判据在 {@link ApprovalRelay.pendingSince} 上。
|
|
657
|
-
*/
|
|
658
1236
|
pendingMap = /* @__PURE__ */ new Map();
|
|
659
|
-
/** 登记时刻从哪儿读。见 {@link RelayClock} */
|
|
660
1237
|
clock;
|
|
661
1238
|
constructor(clock = Date.now) {
|
|
662
1239
|
this.clock = clock;
|
|
663
1240
|
}
|
|
664
|
-
/**
|
|
665
|
-
* 登记一个刚发给 renderer 的审批。
|
|
666
|
-
*
|
|
667
|
-
* 直接吃 `toSerializable()` 的产出:非审批事件(没有 `respond`)传进来是空操作,
|
|
668
|
-
* 于是宿主的消费循环里不用先判类型。
|
|
669
|
-
*
|
|
670
|
-
* ⚠️ 时钟**只在真的要插一条时才读**(在 `return` 后面)。写在函数头上的话,
|
|
671
|
-
* 每一条流过的事件都会读一次挂钟 —— 而这张表是被 `serializeStream()` 就着
|
|
672
|
-
* **每一个**事件调的,注入过时钟的宿主那边会当场看见自己的脚本被走空。
|
|
673
|
-
*/
|
|
674
1241
|
track(serialized) {
|
|
675
1242
|
const { event, respond } = serialized;
|
|
676
1243
|
if (event.type !== "approval-request" || !respond) return;
|
|
@@ -680,13 +1247,6 @@ var init_serialize = __esm({
|
|
|
680
1247
|
since: this.clock()
|
|
681
1248
|
});
|
|
682
1249
|
}
|
|
683
|
-
/**
|
|
684
|
-
* 答复。
|
|
685
|
-
*
|
|
686
|
-
* @returns 是否真的接回了引擎。`false` 意味着这个 `requestId` 不认识 ——
|
|
687
|
-
* 重复答复、或者 renderer 拿着一个上一轮的 ID 回来了。宿主该把它
|
|
688
|
-
* 当成日志而不是错误:用户在窗口上双击一下审批按钮就会走到这里。
|
|
689
|
-
*/
|
|
690
1250
|
respond(requestId, outcome, note) {
|
|
691
1251
|
const pending = this.pendingMap.get(requestId);
|
|
692
1252
|
if (!pending) return false;
|
|
@@ -694,52 +1254,13 @@ var init_serialize = __esm({
|
|
|
694
1254
|
pending.respond(outcome, note);
|
|
695
1255
|
return true;
|
|
696
1256
|
}
|
|
697
|
-
/** 还有多少个审批挂在 renderer 那边没回来 */
|
|
698
1257
|
get pendingCount() {
|
|
699
1258
|
return this.pendingMap.size;
|
|
700
1259
|
}
|
|
701
|
-
/**
|
|
702
|
-
* **最早**那一条还挂着的审批是什么时候登记的(epoch 毫秒);一条都没挂着时 null。
|
|
703
|
-
*
|
|
704
|
-
* 存在的理由只有一个:界面上要说得出「**已等**多久」。`pendingCount` 答的是
|
|
705
|
-
* 「有几条」,而那一格问的是「等了多久」—— 两者之间差的正是这个时刻,
|
|
706
|
-
* 而在它之前网线上**没有任何一个字段答得出来**(全文在
|
|
707
|
-
* `WireSessionSummary.pendingSince` 上,包括「为什么 `updatedAt` 顶不了它」)。
|
|
708
|
-
*
|
|
709
|
-
* ## 为什么是**最早**那一条,不是最近那一条
|
|
710
|
-
*
|
|
711
|
-
* 消费它的那一格挑的是「晾得最久的那个会话」。报最近那一条的话,一个已经卡了
|
|
712
|
-
* 两小时、刚刚又多挂上一条的会话会说「已等 3 秒」—— 屏幕上那个数会和它被挑中
|
|
713
|
-
* 的理由互相打脸。「最早」还有一条硬性质:它**只在这个会话彻底不卡了之后才动**
|
|
714
|
-
* (表清空 → null),而「最近」每多挂一条就往前跳一次,读起来像是等待被重置了。
|
|
715
|
-
*
|
|
716
|
-
* ## 取的是表里第一条,靠的正是上面那条插入序不变量
|
|
717
|
-
*
|
|
718
|
-
* `pendingMap` 的插入序 = 请求发生的时间序(见它自己的注释),所以第一条就是
|
|
719
|
-
* 最早那一条,答完出账之后自然轮到下一条。**刻意不遍历取 `Math.min`**:
|
|
720
|
-
* 那等于给「最早」立第二个定义,而两个定义哪天对不上时,症状是屏幕上一个
|
|
721
|
-
* 说不出来历的数字 —— 没有任何一处会红。
|
|
722
|
-
*
|
|
723
|
-
* **只读**:这张表的时刻由 `track()` 一处写死,宿主不许改。一个能被外面拨的
|
|
724
|
-
* 「已等多久」等于让每个宿主自己发明一套等待口径。
|
|
725
|
-
*/
|
|
726
1260
|
get pendingSince() {
|
|
727
1261
|
const first = this.pendingMap.values().next();
|
|
728
1262
|
return first.done ? null : first.value.since;
|
|
729
1263
|
}
|
|
730
|
-
/**
|
|
731
|
-
* 列出还挂着的审批,按发生的先后顺序。
|
|
732
|
-
*
|
|
733
|
-
* 给「**新连上来的宿主要补拉**」用:浏览器刷新一次页面,SSE 是新连接,
|
|
734
|
-
* 之前推过去的 `approval-request` 就没人接了 —— 而引擎那侧还阻塞在等答复。
|
|
735
|
-
* 没有这个方法,新页面只能干等到超时(对标 OpenCode 的
|
|
736
|
-
* `session.permission.list`,`packages/server/src/handlers/permission.ts`)。
|
|
737
|
-
*
|
|
738
|
-
* 返回的形状**与 SSE 上推的那一条完全一致**(`SerializableApprovalRequest`),
|
|
739
|
-
* 于是客户端的「收到一条审批」只有一条代码路径,不用为补拉再写一份解析。
|
|
740
|
-
*
|
|
741
|
-
* `pendingCount` 不能替代它:数字告诉你有几条挂着,但补拉需要的是内容。
|
|
742
|
-
*/
|
|
743
1264
|
listPending() {
|
|
744
1265
|
return [...this.pendingMap].map(([requestId, { request }]) => ({
|
|
745
1266
|
type: "approval-request",
|
|
@@ -747,16 +1268,6 @@ var init_serialize = __esm({
|
|
|
747
1268
|
request
|
|
748
1269
|
}));
|
|
749
1270
|
}
|
|
750
|
-
/**
|
|
751
|
-
* 界面没了:把挂起的审批全部**放弃**,返回放弃的个数。
|
|
752
|
-
*
|
|
753
|
-
* 刻意**不** resolve 任何 promise —— 那会变成一串「用户拒绝」。调用方拿到非 0
|
|
754
|
-
* 返回值后该做的是 `abort()` 掉这一轮(`session.run(msg, { signal })` 的那个
|
|
755
|
-
* signal):流一结束,`AgentSession` 的 finally 会把剩下的请求兜底成 deny,
|
|
756
|
-
* 引擎不会永久挂住,而模型看到的是「这一轮被中止了」而不是「用户否了我五次」。
|
|
757
|
-
*
|
|
758
|
-
* 接法见 [docs/EMBEDDING.md](../../../docs/EMBEDDING.md) 的「窗口关掉了怎么办」。
|
|
759
|
-
*/
|
|
760
1271
|
abandon() {
|
|
761
1272
|
const count = this.pendingMap.size;
|
|
762
1273
|
this.pendingMap.clear();
|
|
@@ -764,24 +1275,11 @@ var init_serialize = __esm({
|
|
|
764
1275
|
}
|
|
765
1276
|
};
|
|
766
1277
|
QuestionRelay = class {
|
|
767
|
-
/**
|
|
768
|
-
* `requestId` → 请求内容 + 答复闭包 + 登记那一刻。
|
|
769
|
-
* Map 的插入序 = 提问发生的时间序。`since` 见 {@link QuestionRelay.pendingSince}
|
|
770
|
-
*/
|
|
771
1278
|
pendingMap = /* @__PURE__ */ new Map();
|
|
772
|
-
/** 登记时刻从哪儿读。见 {@link RelayClock} */
|
|
773
1279
|
clock;
|
|
774
1280
|
constructor(clock = Date.now) {
|
|
775
1281
|
this.clock = clock;
|
|
776
1282
|
}
|
|
777
|
-
/**
|
|
778
|
-
* 登记一个刚发给宿主的提问。
|
|
779
|
-
*
|
|
780
|
-
* 直接吃 `toSerializable()` 的产出:非提问事件传进来是空操作,
|
|
781
|
-
* 于是宿主的消费循环里不用先判类型(同 `ApprovalRelay.track`)。
|
|
782
|
-
*
|
|
783
|
-
* 时钟同样只在真要插一条时才读,理由逐字同 `ApprovalRelay.track`。
|
|
784
|
-
*/
|
|
785
1283
|
track(serialized) {
|
|
786
1284
|
const { event, respondQuestion } = serialized;
|
|
787
1285
|
if (event.type !== "question" || !respondQuestion) return;
|
|
@@ -791,13 +1289,6 @@ var init_serialize = __esm({
|
|
|
791
1289
|
since: this.clock()
|
|
792
1290
|
});
|
|
793
1291
|
}
|
|
794
|
-
/**
|
|
795
|
-
* 答复。
|
|
796
|
-
*
|
|
797
|
-
* @returns 是否真的接回了引擎。`false` = 这个 `requestId` 不认识
|
|
798
|
-
* (重复答复、上一轮的 ID、或者已经被 `abandon()` 掉了)。
|
|
799
|
-
* 调用方该把它当日志而不是错误。
|
|
800
|
-
*/
|
|
801
1292
|
respond(requestId, answer) {
|
|
802
1293
|
const pending = this.pendingMap.get(requestId);
|
|
803
1294
|
if (!pending) return false;
|
|
@@ -805,25 +1296,13 @@ var init_serialize = __esm({
|
|
|
805
1296
|
pending.respond(answer);
|
|
806
1297
|
return true;
|
|
807
1298
|
}
|
|
808
|
-
/** 还有多少个提问挂在宿主那边没回来 */
|
|
809
1299
|
get pendingCount() {
|
|
810
1300
|
return this.pendingMap.size;
|
|
811
1301
|
}
|
|
812
|
-
/**
|
|
813
|
-
* **最早**那一条还挂着的提问是什么时候登记的;一条都没挂着时 null。
|
|
814
|
-
*
|
|
815
|
-
* 与 {@link ApprovalRelay.pendingSince} 逐字同构、同三条判据(最早不是最近、
|
|
816
|
-
* 取表里第一条、只读)。写在那一个上,别在这儿再判一遍。
|
|
817
|
-
*
|
|
818
|
-
* ⚠️ **两张表各给一个,网线上却只有一格**:合并成「这个会话从什么时候起被拦住」
|
|
819
|
-
* 是消费方(Hub)的事,不是这一层的 —— 这一层只知道自己这张表。
|
|
820
|
-
* 判据在 `WireSessionSummary.pendingSince` 的第一节。
|
|
821
|
-
*/
|
|
822
1302
|
get pendingSince() {
|
|
823
1303
|
const first = this.pendingMap.values().next();
|
|
824
1304
|
return first.done ? null : first.value.since;
|
|
825
1305
|
}
|
|
826
|
-
/** 列出还挂着的提问,按发生的先后顺序。给「新连上来的宿主要补拉」用(验收 9) */
|
|
827
1306
|
listPending() {
|
|
828
1307
|
return [...this.pendingMap].map(([requestId, { request }]) => ({
|
|
829
1308
|
type: "question",
|
|
@@ -831,13 +1310,6 @@ var init_serialize = __esm({
|
|
|
831
1310
|
request
|
|
832
1311
|
}));
|
|
833
1312
|
}
|
|
834
|
-
/**
|
|
835
|
-
* 界面没了:把挂起的提问全部**放弃**,返回放弃的个数(验收 8)。
|
|
836
|
-
*
|
|
837
|
-
* 刻意**不** resolve 任何 promise —— 那会变成一串编出来的答案。调用方拿到非 0
|
|
838
|
-
* 返回值后该 `abort()` 掉这一轮:流一结束,`AgentSession` 的 finally 会把剩下的
|
|
839
|
-
* 请求兜底成 `skipped`(那是实话:确实没人答),引擎不会永久挂住。
|
|
840
|
-
*/
|
|
841
1313
|
abandon() {
|
|
842
1314
|
const count = this.pendingMap.size;
|
|
843
1315
|
this.pendingMap.clear();
|
|
@@ -947,12 +1419,7 @@ async function runOnce(store, def, opts, startedAt, now) {
|
|
|
947
1419
|
const runtime = await build({
|
|
948
1420
|
homeDir: opts.homeDir,
|
|
949
1421
|
workDir: def.workDir,
|
|
950
|
-
// **显式 false**:不靠 TTY 探测。OS 调度器起的进程天然没有 TTY,
|
|
951
|
-
// 但嵌入宿主那条路(`headless.js` 由宿主起)可能有,而那时按交互处理
|
|
952
|
-
// 会让整个「没人可问」的语义塌掉 —— 预授权清单不生效、审批挂着等一个不在的人
|
|
953
1422
|
interactive: false,
|
|
954
|
-
// 任务级清单是**取代**不是叠加:一个任务的授权面必须只由任务自己说清,
|
|
955
|
-
// 否则用户改一次 `~/.epoch/config.yaml` 就静默放宽了全部十个定时任务
|
|
956
1423
|
headless: { allowTools: [...def.allowTools], allowOperations: [...def.allowOperations] },
|
|
957
1424
|
sessionSource: "schedule",
|
|
958
1425
|
installSignalHandlers: true
|
|
@@ -971,7 +1438,7 @@ async function runOnce(store, def, opts, startedAt, now) {
|
|
|
971
1438
|
});
|
|
972
1439
|
return { status: "failed", exitCode: SCHEDULE_EXIT_CODES.FAILURE, run, detail: reason2 };
|
|
973
1440
|
}
|
|
974
|
-
const level = runtime.permissions.setLevel(def.permission);
|
|
1441
|
+
const level = runtime.permissions.setLevel(def.permission, { remember: false });
|
|
975
1442
|
if (!level.ok) {
|
|
976
1443
|
const reason2 = `managed-policy: ${def.permission}`;
|
|
977
1444
|
const run = store.recordRun({
|
|
@@ -1137,37 +1604,50 @@ var init_executor = __esm({
|
|
|
1137
1604
|
}
|
|
1138
1605
|
});
|
|
1139
1606
|
function createScheduleControl(opts) {
|
|
1140
|
-
|
|
1607
|
+
let opened = null;
|
|
1608
|
+
let openError = null;
|
|
1609
|
+
try {
|
|
1610
|
+
opened = new ScheduleStore(dbPath(opts.homeDir));
|
|
1611
|
+
} catch (err) {
|
|
1612
|
+
openError = err instanceof Error ? err : new Error(String(err));
|
|
1613
|
+
}
|
|
1614
|
+
const store = () => {
|
|
1615
|
+
if (!opened) {
|
|
1616
|
+
throw new Error(
|
|
1617
|
+
`schedule store unavailable: ${openError?.message ?? "unknown"} (see ScheduleControl.storeAvailable)`
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
return opened;
|
|
1621
|
+
};
|
|
1141
1622
|
let backendPromise = null;
|
|
1142
1623
|
const backendOf = () => {
|
|
1143
1624
|
backendPromise ??= resolveScheduleBackend(opts.platform ?? process.platform);
|
|
1144
1625
|
return backendPromise;
|
|
1145
1626
|
};
|
|
1146
1627
|
const registrarOf = async () => new ScheduleRegistrar({
|
|
1147
|
-
store,
|
|
1628
|
+
store: store(),
|
|
1148
1629
|
backend: await backendOf(),
|
|
1149
1630
|
homeDir: opts.homeDir,
|
|
1150
1631
|
...opts.runner ? { runner: opts.runner } : {}
|
|
1151
|
-
// ⚠️ **刻意不给 `cli`**:这一层不知道宿主怎么被唤起,猜的代价见文件头
|
|
1152
1632
|
});
|
|
1153
1633
|
const save = async (def, register) => {
|
|
1154
1634
|
if (!register) return { schedule: def, issues: [] };
|
|
1155
1635
|
const registrar = await registrarOf();
|
|
1156
1636
|
const registration = await registrar.register(def);
|
|
1157
|
-
return { schedule: store.get(def.id) ?? def, issues: [], registration };
|
|
1637
|
+
return { schedule: store().get(def.id) ?? def, issues: [], registration };
|
|
1158
1638
|
};
|
|
1159
1639
|
const control = {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1640
|
+
storeAvailable: opened !== null,
|
|
1641
|
+
list: () => store().list(),
|
|
1642
|
+
get: (id) => store().get(id),
|
|
1643
|
+
runs: (id, limit) => store().listRuns(id, limit),
|
|
1644
|
+
recentRuns: (limit) => store().listRecentRuns(limit),
|
|
1164
1645
|
capability: async () => {
|
|
1165
1646
|
const backend = await backendOf();
|
|
1166
1647
|
return {
|
|
1167
1648
|
backend: backend?.kind ?? null,
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
canRegister: backend !== null && opts.runner !== void 0
|
|
1649
|
+
canRegister: backend !== null && opts.runner !== void 0,
|
|
1650
|
+
storeAvailable: opened !== null
|
|
1171
1651
|
};
|
|
1172
1652
|
},
|
|
1173
1653
|
create: async (input, saveOpts) => {
|
|
@@ -1188,12 +1668,12 @@ function createScheduleControl(opts) {
|
|
|
1188
1668
|
homeDir: opts.homeDir
|
|
1189
1669
|
}).issues;
|
|
1190
1670
|
if (issues.length > 0) return { issues };
|
|
1191
|
-
const created = store.create({ ...input, workDir: workDir || opts.homeDir });
|
|
1192
|
-
const def = workDir ? created : store.update(created.id, { workDir: automationWorkDir(created.id, opts.homeDir) }) ?? created;
|
|
1671
|
+
const created = store().create({ ...input, workDir: workDir || opts.homeDir });
|
|
1672
|
+
const def = workDir ? created : store().update(created.id, { workDir: automationWorkDir(created.id, opts.homeDir) }) ?? created;
|
|
1193
1673
|
return save(def, saveOpts?.register !== false);
|
|
1194
1674
|
},
|
|
1195
1675
|
update: async (id, patch, saveOpts) => {
|
|
1196
|
-
const current = store.get(id);
|
|
1676
|
+
const current = store().get(id);
|
|
1197
1677
|
if (!current) return { issues: [] };
|
|
1198
1678
|
const merged = { ...current, ...patch };
|
|
1199
1679
|
const issues = validateSchedule({
|
|
@@ -1212,18 +1692,18 @@ function createScheduleControl(opts) {
|
|
|
1212
1692
|
homeDir: opts.homeDir
|
|
1213
1693
|
}).issues;
|
|
1214
1694
|
if (issues.length > 0) return { issues };
|
|
1215
|
-
const next = store.update(id, patch);
|
|
1695
|
+
const next = store().update(id, patch);
|
|
1216
1696
|
if (!next) return { issues: [] };
|
|
1217
1697
|
return save(next, saveOpts?.register !== false);
|
|
1218
1698
|
},
|
|
1219
1699
|
remove: async (id) => {
|
|
1220
|
-
const def = store.get(id);
|
|
1700
|
+
const def = store().get(id);
|
|
1221
1701
|
if (!def) return false;
|
|
1222
1702
|
if (def.osTaskId) await (await registrarOf()).unregister(def);
|
|
1223
|
-
return store.remove(id);
|
|
1703
|
+
return store().remove(id);
|
|
1224
1704
|
},
|
|
1225
1705
|
setEnabled: async (id, enabled) => {
|
|
1226
|
-
const def = store.get(id);
|
|
1706
|
+
const def = store().get(id);
|
|
1227
1707
|
if (!def) return { ok: false, reason: "backend-error", detail: "schedule-not-found" };
|
|
1228
1708
|
return (await registrarOf()).setEnabled(def, enabled);
|
|
1229
1709
|
},
|
|
@@ -1235,7 +1715,7 @@ function createScheduleControl(opts) {
|
|
|
1235
1715
|
}),
|
|
1236
1716
|
needsAllowlist
|
|
1237
1717
|
};
|
|
1238
|
-
return { control, dispose: () =>
|
|
1718
|
+
return { control, dispose: () => opened?.close(), openError };
|
|
1239
1719
|
}
|
|
1240
1720
|
var init_control = __esm({
|
|
1241
1721
|
"src/schedule/control.ts"() {
|
|
@@ -1244,7 +1724,7 @@ var init_control = __esm({
|
|
|
1244
1724
|
});
|
|
1245
1725
|
function resolvePermissionLevel(raw, diags) {
|
|
1246
1726
|
if (isPermissionLevel(raw)) return raw;
|
|
1247
|
-
diags.warn("Perm",
|
|
1727
|
+
diags.warn("Perm", t("boot.bad_permission_level", { raw }), "bad-permission-level");
|
|
1248
1728
|
return "default";
|
|
1249
1729
|
}
|
|
1250
1730
|
function resolveModelCapability(config, diags) {
|
|
@@ -1252,45 +1732,49 @@ function resolveModelCapability(config, diags) {
|
|
|
1252
1732
|
const cap = getCapability(config.model, config.homeDir);
|
|
1253
1733
|
return {
|
|
1254
1734
|
contextLength: cap.contextLength,
|
|
1255
|
-
supportsImages: cap.supportsImages,
|
|
1256
1735
|
...cap.pricing ? { pricing: cap.pricing } : {}
|
|
1257
1736
|
};
|
|
1258
1737
|
} catch {
|
|
1259
1738
|
diags.warn(
|
|
1260
1739
|
"Capability",
|
|
1261
|
-
|
|
1740
|
+
t("boot.no_model_metadata", { model: config.model }),
|
|
1262
1741
|
"no-model-metadata"
|
|
1263
1742
|
);
|
|
1264
|
-
return { contextLength: config.contextLength
|
|
1743
|
+
return { contextLength: config.contextLength };
|
|
1265
1744
|
}
|
|
1266
1745
|
}
|
|
1267
1746
|
function reportUtilityModel(info, diags) {
|
|
1268
1747
|
if (!info.configured) {
|
|
1269
|
-
diags.skipped("
|
|
1748
|
+
diags.skipped("UtilityModel", (lang) => t("boot.utility_model_none", void 0, lang));
|
|
1270
1749
|
return;
|
|
1271
1750
|
}
|
|
1272
1751
|
if (info.issue) {
|
|
1273
|
-
diags.warn("
|
|
1752
|
+
diags.warn("UtilityModel", info.issue, "bad-model-ref");
|
|
1274
1753
|
return;
|
|
1275
1754
|
}
|
|
1276
|
-
diags.ok(
|
|
1755
|
+
diags.ok(
|
|
1756
|
+
"UtilityModel",
|
|
1757
|
+
(lang) => t(
|
|
1758
|
+
"boot.utility_model_selected",
|
|
1759
|
+
{ provider: info.selection.provider, model: info.selection.model },
|
|
1760
|
+
lang
|
|
1761
|
+
)
|
|
1762
|
+
);
|
|
1277
1763
|
}
|
|
1278
1764
|
function buildTrust(config, workDir, diags, interactive) {
|
|
1279
1765
|
const root = resolveProjectRoot(workDir);
|
|
1280
1766
|
if (config.trust?.enabled === false) {
|
|
1281
|
-
const gate2 = createAllowAllGate(
|
|
1282
|
-
"\u4FE1\u4EFB\u95F8\u95E8\u5DF2\u5173\u95ED\uFF08config.trust.enabled=false\uFF09\uFF1A\u4EFB\u4F55\u76EE\u5F55\u7684\u9879\u76EE\u6307\u4EE4\u90FD\u4F1A\u52A0\u8F7D"
|
|
1283
|
-
);
|
|
1767
|
+
const gate2 = createAllowAllGate(t("boot.trust_disabled"));
|
|
1284
1768
|
setTrustGate(gate2);
|
|
1285
1769
|
const trust2 = gate2.decide(workDir);
|
|
1286
|
-
diags.
|
|
1770
|
+
diags.skipped("Trust", trust2.reason);
|
|
1287
1771
|
const disabled = safeInit("Trust", () => new TrustManager(trustPath(config.homeDir)), diags);
|
|
1288
1772
|
for (const err of disabled?.loadErrors ?? []) diags.warn("Trust", err, "trust-load-error");
|
|
1289
1773
|
return { manager: disabled, trust: trust2, root, gate: gate2 };
|
|
1290
1774
|
}
|
|
1291
1775
|
const manager = safeInit("Trust", () => new TrustManager(trustPath(config.homeDir)), diags);
|
|
1292
1776
|
for (const err of manager?.loadErrors ?? []) diags.warn("Trust", err, "trust-load-error");
|
|
1293
|
-
const gate = manager ? createTrustGate({ manager, interactive }) : createDenyAllGate("
|
|
1777
|
+
const gate = manager ? createTrustGate({ manager, interactive }) : createDenyAllGate(t("boot.trust_unavailable"));
|
|
1294
1778
|
setTrustGate(gate);
|
|
1295
1779
|
const trust = gate.decide(root);
|
|
1296
1780
|
if (trust.trusted) diags.ok("Trust", trust.reason);
|
|
@@ -1299,11 +1783,11 @@ function buildTrust(config, workDir, diags, interactive) {
|
|
|
1299
1783
|
}
|
|
1300
1784
|
function buildImportGate(config, diags) {
|
|
1301
1785
|
if (getImportGate()) {
|
|
1302
|
-
diags.ok("Instructions", "
|
|
1786
|
+
diags.ok("Instructions", t("boot.import_host_gate"));
|
|
1303
1787
|
return;
|
|
1304
1788
|
}
|
|
1305
1789
|
if (config.trust?.enabled === false) {
|
|
1306
|
-
setImportGate(createAllowAllImportGate("
|
|
1790
|
+
setImportGate(createAllowAllImportGate(t("boot.import_trust_disabled")));
|
|
1307
1791
|
return;
|
|
1308
1792
|
}
|
|
1309
1793
|
const store = safeInit(
|
|
@@ -1312,7 +1796,7 @@ function buildImportGate(config, diags) {
|
|
|
1312
1796
|
diags
|
|
1313
1797
|
);
|
|
1314
1798
|
if (!store) {
|
|
1315
|
-
setImportGate(createDenyAllImportGate("
|
|
1799
|
+
setImportGate(createDenyAllImportGate(t("boot.import_store_unavailable")));
|
|
1316
1800
|
return;
|
|
1317
1801
|
}
|
|
1318
1802
|
for (const err of store.loadErrors) diags.warn("Instructions", err, "import-trust-load-error");
|
|
@@ -1359,7 +1843,7 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1359
1843
|
for (const p of projectContext.skippedInstructions) {
|
|
1360
1844
|
diags.warn(
|
|
1361
1845
|
"Instructions",
|
|
1362
|
-
|
|
1846
|
+
t("boot.instructions_untrusted", { path: p }),
|
|
1363
1847
|
"workspace-untrusted"
|
|
1364
1848
|
);
|
|
1365
1849
|
}
|
|
@@ -1369,7 +1853,7 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1369
1853
|
diags.ok("Provider");
|
|
1370
1854
|
} catch (err) {
|
|
1371
1855
|
if (err instanceof ProviderError && err.code === PROVIDER_NO_CREDENTIAL) {
|
|
1372
|
-
diags.skipped("Provider",
|
|
1856
|
+
diags.skipped("Provider", t("boot.provider_no_credential", { message: err.message }));
|
|
1373
1857
|
} else {
|
|
1374
1858
|
diags.failed(
|
|
1375
1859
|
"Provider",
|
|
@@ -1379,11 +1863,19 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1379
1863
|
}
|
|
1380
1864
|
}
|
|
1381
1865
|
if (provider) provider.refreshMetadata();
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1384
|
-
|
|
1866
|
+
const router = provider;
|
|
1867
|
+
const providerBridge = router ? {
|
|
1868
|
+
generate: router.generate.bind(router),
|
|
1869
|
+
stream: router.stream.bind(router),
|
|
1385
1870
|
get capabilities() {
|
|
1386
|
-
|
|
1871
|
+
try {
|
|
1872
|
+
return { supportsImages: router.getCapability().supportsImages };
|
|
1873
|
+
} catch {
|
|
1874
|
+
return { supportsImages: false };
|
|
1875
|
+
}
|
|
1876
|
+
},
|
|
1877
|
+
get modelId() {
|
|
1878
|
+
return router.getSelection().model;
|
|
1387
1879
|
}
|
|
1388
1880
|
} : null;
|
|
1389
1881
|
const session = safeInit("Session", () => new SessionManager(config.dbPath), diags);
|
|
@@ -1406,19 +1898,20 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1406
1898
|
}
|
|
1407
1899
|
const skillDirs = resolveSkillDirs({
|
|
1408
1900
|
userDir: skillsDir(config.homeDir),
|
|
1409
|
-
// 插件技能排在最前(优先级最低)且全部带前缀,见 core 的 skill/sources.ts
|
|
1410
1901
|
pluginDirs: plugins.skillDirs,
|
|
1411
|
-
// 宿主注入的紧跟在插件之后(方案 44 §2.1),同样全部带前缀、同样不过工作区
|
|
1412
|
-
// 闸门 —— 它的信任来自「宿主就是跑我们这个库的那个进程」,和 secretStore 同一档
|
|
1413
1902
|
...opts.hostSkillDirs ? { hostDirs: opts.hostSkillDirs } : {},
|
|
1414
1903
|
projectRoot: projectContext.rootDir,
|
|
1415
1904
|
trusted: trust.trusted,
|
|
1416
1905
|
diags
|
|
1417
1906
|
});
|
|
1418
|
-
const skill = safeInit(
|
|
1907
|
+
const skill = safeInit(
|
|
1908
|
+
"Skill",
|
|
1909
|
+
() => new SkillSystem(skillDirs.dirs, { maxIndexed: config.skills?.maxIndexed }),
|
|
1910
|
+
diags
|
|
1911
|
+
);
|
|
1419
1912
|
if (skill) {
|
|
1420
1913
|
for (const detail of issueDetails(skill.getIssues())) {
|
|
1421
|
-
diags.warn("Skill",
|
|
1914
|
+
diags.warn("Skill", t("boot.skill_issue", { detail }), "invalid-skill-file");
|
|
1422
1915
|
}
|
|
1423
1916
|
}
|
|
1424
1917
|
let skillLearner;
|
|
@@ -1442,15 +1935,17 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1442
1935
|
() => new HookManager(opts.logger ? { log: opts.logger } : {}),
|
|
1443
1936
|
diags
|
|
1444
1937
|
);
|
|
1938
|
+
const hookStatus = { sources: [], projectSkipped: false };
|
|
1445
1939
|
if (hookManager && managedPolicy.allowManagedHooksOnly) {
|
|
1446
1940
|
diags.warn(
|
|
1447
1941
|
"Hook",
|
|
1448
|
-
|
|
1942
|
+
t("boot.managed_hooks_only", { path: managedPolicy.path }),
|
|
1449
1943
|
"managed-hooks-only"
|
|
1450
1944
|
);
|
|
1451
1945
|
} else if (hookManager) {
|
|
1452
1946
|
const hooks = loadHookSources({
|
|
1453
1947
|
userPath: hooksPath(config.homeDir),
|
|
1948
|
+
claudeUserPath: claudeUserSettingsPath(),
|
|
1454
1949
|
pluginPaths: plugins.hookPaths,
|
|
1455
1950
|
...projectContext.rootDir ? { projectRoot: projectContext.rootDir } : {},
|
|
1456
1951
|
trusted: trust.trusted,
|
|
@@ -1458,6 +1953,8 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1458
1953
|
diags
|
|
1459
1954
|
});
|
|
1460
1955
|
hookManager.loadEntries(hooks.entries);
|
|
1956
|
+
hookStatus.sources = [...hooks.sources];
|
|
1957
|
+
hookStatus.projectSkipped = hooks.projectSkipped;
|
|
1461
1958
|
}
|
|
1462
1959
|
const cache = new ApprovalCache(config.homeDir);
|
|
1463
1960
|
const permission = safeInit("Perm", () => new PermissionManager(permLevel, cache), diags);
|
|
@@ -1484,7 +1981,7 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1484
1981
|
policyStatus.projectSkipped = policyDirs.projectSkipped;
|
|
1485
1982
|
policyStatus.skippedDir = policyDirs.skippedDir ?? null;
|
|
1486
1983
|
} catch {
|
|
1487
|
-
diags.warn("Policy", "
|
|
1984
|
+
diags.warn("Policy", t("boot.policy_load_failed"), "policy-load-failed");
|
|
1488
1985
|
}
|
|
1489
1986
|
const lists = config.permissions;
|
|
1490
1987
|
const counts = {
|
|
@@ -1497,23 +1994,25 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1497
1994
|
permission.setPermissionRules(lists);
|
|
1498
1995
|
diags.ok(
|
|
1499
1996
|
"Perm",
|
|
1500
|
-
|
|
1997
|
+
t("boot.perm_rules", {
|
|
1998
|
+
total: ruleCount,
|
|
1999
|
+
allow: counts.allow,
|
|
2000
|
+
ask: counts.ask,
|
|
2001
|
+
deny: counts.deny
|
|
2002
|
+
})
|
|
1501
2003
|
);
|
|
1502
2004
|
for (const finding of checkShadowing(compileRuleLists(lists))) {
|
|
1503
2005
|
diags.warn("Perm", finding.message, `rule-${finding.kind}`);
|
|
1504
2006
|
}
|
|
1505
2007
|
}
|
|
1506
2008
|
}
|
|
2009
|
+
const compliance = buildCompliance(config, projectContext.rootDir, trust.trusted, diags);
|
|
1507
2010
|
let contextEngine = null;
|
|
1508
2011
|
if (provider) {
|
|
1509
2012
|
reportUtilityModel(provider.getUtilityInfo(), diags);
|
|
1510
2013
|
contextEngine = safeInit(
|
|
1511
2014
|
"Context",
|
|
1512
2015
|
() => new ContextCompressor({
|
|
1513
|
-
// 2026-08-16 合并时收的一笔(compressor.ts 文件头那张 📮 的第一条):
|
|
1514
|
-
// `compression.enabled` / `compression.threshold` 两个键的装配点在这儿,
|
|
1515
|
-
// 而写它们的那一轮禁区正是 `runtime/**`。不接这一行的后果不是少一个功能,
|
|
1516
|
-
// 是**一句谎话** —— 配得进、设置页印得出来、还标着「② 用户级」,而压缩照压。
|
|
1517
2016
|
...compressionOptions(config),
|
|
1518
2017
|
summarize: async (prompt) => {
|
|
1519
2018
|
const utility = provider.getUtilityInfo().selection;
|
|
@@ -1534,8 +2033,6 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1534
2033
|
const capability = resolveModelCapability(config, diags);
|
|
1535
2034
|
return {
|
|
1536
2035
|
config,
|
|
1537
|
-
// 合并**之前**那一份,连同「哪个键是谁写的」那本账 —— 两样都只给设置页的
|
|
1538
|
-
// 来源标注用(判据在 `Services.baseConfig` 上)
|
|
1539
2036
|
baseConfig,
|
|
1540
2037
|
configOrigins: opts.configOrigins ?? getConfigOrigins(),
|
|
1541
2038
|
provider,
|
|
@@ -1559,15 +2056,59 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1559
2056
|
settingsLayers: merged.layers,
|
|
1560
2057
|
managedPolicy,
|
|
1561
2058
|
policy: policyStatus,
|
|
2059
|
+
hook: hookStatus,
|
|
1562
2060
|
plugins,
|
|
2061
|
+
compliance,
|
|
1563
2062
|
cleanups
|
|
1564
2063
|
};
|
|
1565
2064
|
}
|
|
2065
|
+
function buildCompliance(config, projectRoot, trusted, diags) {
|
|
2066
|
+
const settings = resolveComplianceSettings(config.compliance);
|
|
2067
|
+
if (!settings.enabled) {
|
|
2068
|
+
diags.skipped("Compliance", t("boot.compliance_off"));
|
|
2069
|
+
return null;
|
|
2070
|
+
}
|
|
2071
|
+
const rules = [];
|
|
2072
|
+
const exempt = [];
|
|
2073
|
+
try {
|
|
2074
|
+
const dirs = resolveComplianceDirs({
|
|
2075
|
+
userDir: resolveComplianceDir(config.homeDir),
|
|
2076
|
+
...projectRoot ? { projectRoot } : {},
|
|
2077
|
+
trusted,
|
|
2078
|
+
diags
|
|
2079
|
+
});
|
|
2080
|
+
for (const spec of dirs.dirs) {
|
|
2081
|
+
const loaded = loadLexiconFiles(spec.dir, spec.source, diags);
|
|
2082
|
+
rules.push(...loaded.rules);
|
|
2083
|
+
exempt.push(...loaded.exempt);
|
|
2084
|
+
}
|
|
2085
|
+
} catch {
|
|
2086
|
+
diags.warn("Compliance", t("boot.compliance_load_failed"), "compliance-load-failed");
|
|
2087
|
+
}
|
|
2088
|
+
const engine = new ComplianceEngine({ settings, rules, exempt });
|
|
2089
|
+
diags.ok(
|
|
2090
|
+
"Compliance",
|
|
2091
|
+
t("boot.compliance_ready", {
|
|
2092
|
+
active: engine.activeRuleCount,
|
|
2093
|
+
builtin: engine.counts.builtin,
|
|
2094
|
+
external: engine.counts.user + engine.counts.project
|
|
2095
|
+
})
|
|
2096
|
+
);
|
|
2097
|
+
return engine;
|
|
2098
|
+
}
|
|
2099
|
+
function resolveComplianceSettings(raw) {
|
|
2100
|
+
return {
|
|
2101
|
+
enabled: raw?.enabled ?? DEFAULT_COMPLIANCE_SETTINGS.enabled,
|
|
2102
|
+
scope: { ...DEFAULT_COMPLIANCE_SETTINGS.scope, ...raw?.scope },
|
|
2103
|
+
skipCodeBlocks: raw?.skipCodeBlocks ?? DEFAULT_COMPLIANCE_SETTINGS.skipCodeBlocks,
|
|
2104
|
+
maxHoldChars: raw?.maxHoldChars ?? DEFAULT_COMPLIANCE_SETTINGS.maxHoldChars,
|
|
2105
|
+
actions: { ...DEFAULT_COMPLIANCE_SETTINGS.actions, ...raw?.actions }
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
1566
2108
|
var init_services = __esm({
|
|
1567
2109
|
"src/services.ts"() {
|
|
1568
2110
|
}
|
|
1569
2111
|
});
|
|
1570
|
-
|
|
1571
2112
|
// src/turn-steps.ts
|
|
1572
2113
|
function draft() {
|
|
1573
2114
|
return { text: "", reasoning: "", toolCalls: [], toolResults: [] };
|
|
@@ -1586,12 +2127,6 @@ var TurnStepCollector;
|
|
|
1586
2127
|
var init_turn_steps = __esm({
|
|
1587
2128
|
"src/turn-steps.ts"() {
|
|
1588
2129
|
TurnStepCollector = class {
|
|
1589
|
-
/**
|
|
1590
|
-
* @param startedAt 这一轮开跑的毫秒时间戳,用来算 `durationMs`。
|
|
1591
|
-
* @param now 取当下时间。**注进来只为让用例能钉一个确定的耗时** ——
|
|
1592
|
-
* 生产路径上就是 `Date.now()`,而这一层是服务端,用本机钟是对的
|
|
1593
|
-
* (web 那边不许用 `Date.now()`,因为它得用服务端信封上的 `ts`)。
|
|
1594
|
-
*/
|
|
1595
2130
|
constructor(startedAt, now = Date.now) {
|
|
1596
2131
|
this.startedAt = startedAt;
|
|
1597
2132
|
this.now = now;
|
|
@@ -1600,11 +2135,8 @@ var init_turn_steps = __esm({
|
|
|
1600
2135
|
now;
|
|
1601
2136
|
steps = [];
|
|
1602
2137
|
current = null;
|
|
1603
|
-
/** 当前这一步已经收口,下一笔内容另开一步。见文件头那条 ⚠️ */
|
|
1604
2138
|
sealed = false;
|
|
1605
|
-
/** 这一轮发过几次工具调用。**跨步累计**,`toolCalls` 那一格要的是整轮的数 */
|
|
1606
2139
|
calls = 0;
|
|
1607
|
-
/** `finish` 那一帧带回来的账。没收到那一帧就没有 —— 中止 / 报错时就是这样 */
|
|
1608
2140
|
finish = null;
|
|
1609
2141
|
observe(event) {
|
|
1610
2142
|
switch (event.type) {
|
|
@@ -1627,8 +2159,6 @@ var init_turn_steps = __esm({
|
|
|
1627
2159
|
toolCallId: event.callId,
|
|
1628
2160
|
toolName: event.toolName,
|
|
1629
2161
|
output: event.output,
|
|
1630
|
-
// artifact 原样带走(含 `data`)。剥 base64 是落盘那一层的事
|
|
1631
|
-
// (`persistToolResults()`)—— 内存里的宿主要那份数据渲染缩略图
|
|
1632
2162
|
...event.artifacts ? { artifacts: event.artifacts } : {}
|
|
1633
2163
|
});
|
|
1634
2164
|
return;
|
|
@@ -1646,27 +2176,10 @@ var init_turn_steps = __esm({
|
|
|
1646
2176
|
return;
|
|
1647
2177
|
}
|
|
1648
2178
|
}
|
|
1649
|
-
/** 冲掉最后一步,交出整轮。空步已经在 {@link seal} 里滤掉 */
|
|
1650
2179
|
take() {
|
|
1651
2180
|
this.flush();
|
|
1652
2181
|
return this.steps;
|
|
1653
2182
|
}
|
|
1654
|
-
/**
|
|
1655
|
-
* 这一轮的账(决定 8 第三个时刻),落在最后一条 assistant 行上。
|
|
1656
|
-
*
|
|
1657
|
-
* ## 三格各自可以没有,而且**都不补 0**
|
|
1658
|
-
*
|
|
1659
|
-
* - `usage` 没有 = provider 这一轮没上报用量。写 0 会读成「这一轮没花钱」,
|
|
1660
|
-
* 而真相是「不知道」(同 `SessionDB.recordUsage` 那条「宁可少记」)。
|
|
1661
|
-
* - `model` 没有 = 引擎没告诉我们这一轮跑的是谁。**不许拿配置里那个顶上** ——
|
|
1662
|
-
* 降级和 `switch_model` 之后那是个从没跑过的模型名,比不显示更糟。
|
|
1663
|
-
* - `durationMs` 没有 = 起点未知(没给 `startedAt`)。写 0 是「0s」,那是假事实。
|
|
1664
|
-
*
|
|
1665
|
-
* `toolCalls` 反过来 —— 它**必有**,因为 `0` 在这一格是真的 0(「这一轮没动手」)。
|
|
1666
|
-
*
|
|
1667
|
-
* 整帧 `finish` 都没收到(中止 / 报错)时返回 `undefined`:那一轮**没有**账,
|
|
1668
|
-
* 而不是一笔全是空格的账。
|
|
1669
|
-
*/
|
|
1670
2183
|
facts() {
|
|
1671
2184
|
if (!this.finish) return void 0;
|
|
1672
2185
|
const durationMs = this.startedAt === void 0 ? void 0 : Math.max(0, this.finish.at - this.startedAt);
|
|
@@ -1700,46 +2213,20 @@ function settleAbandoned(pending) {
|
|
|
1700
2213
|
var AgentSession;
|
|
1701
2214
|
var init_agent_session = __esm({
|
|
1702
2215
|
"src/agent-session.ts"() {
|
|
2216
|
+
init_events();
|
|
1703
2217
|
init_turn_steps();
|
|
1704
2218
|
AgentSession = class {
|
|
1705
|
-
/**
|
|
1706
|
-
* 当前会话 id。
|
|
1707
|
-
*
|
|
1708
|
-
* **不再是 `readonly`**(方案 25 PR-4 的 `/resume`):换会话时它必须跟着换,
|
|
1709
|
-
* 否则接下来 `store.record()` 会把新对话记到**旧会话**名下 ——
|
|
1710
|
-
* 症状是「我明明 resume 了另一段,怎么两段混在一起」。
|
|
1711
|
-
* 换的入口只有 `resume()` 一个,不要在别处赋值。
|
|
1712
|
-
*/
|
|
1713
2219
|
sessionId;
|
|
1714
2220
|
agent;
|
|
1715
2221
|
store;
|
|
1716
2222
|
checkpoints;
|
|
1717
2223
|
goals;
|
|
1718
2224
|
currentRole;
|
|
1719
|
-
/**
|
|
1720
|
-
* 上一轮是谁在跑(方案 57 §3.5)。`null` = 这段会话此刻没有角色,**那也是一个值**
|
|
1721
|
-
* —— 从「有」变成「没有」(用户点掉 chip 退回一段本来就没有角色的会话)同样
|
|
1722
|
-
* 要留痕。
|
|
1723
|
-
*
|
|
1724
|
-
* ⚠️ **它只在进程内有效,从历史里恢复不出来。** 构造时拿的是这个会话**默认**
|
|
1725
|
-
* 那个专家,于是重启之后第一条不带 chip 的消息**不插标记** —— 那是对的:
|
|
1726
|
-
* 上一条标记还在历史里,语义没断。
|
|
1727
|
-
*
|
|
1728
|
-
* 别去做一次「从历史里反推上一轮是谁」的解析:那要 parse 我们自己印的那句
|
|
1729
|
-
* 中文,而它是可翻译的(catalog 真源 `locales/*.yaml`)—— 换一次界面语言,
|
|
1730
|
-
* 那个解析器就静默失灵。
|
|
1731
|
-
*/
|
|
1732
2225
|
lastRole;
|
|
1733
|
-
|
|
2226
|
+
currentWorkDir;
|
|
2227
|
+
lastWorkDir = null;
|
|
1734
2228
|
history;
|
|
1735
|
-
/** 待交给宿主的请求队列(审批 + 提问)+ 「有新请求」的唤醒信号 */
|
|
1736
2229
|
queue = [];
|
|
1737
|
-
/**
|
|
1738
|
-
* 已经交给宿主、但还没答复的请求。
|
|
1739
|
-
*
|
|
1740
|
-
* 必须单独记:请求一旦 yield 给宿主就已经出了 queue,如果宿主提前 break
|
|
1741
|
-
* 走了,只看 queue 是空的,那个挂在 await 上的引擎就永久等下去。
|
|
1742
|
-
*/
|
|
1743
2230
|
inFlight = /* @__PURE__ */ new Set();
|
|
1744
2231
|
notify = null;
|
|
1745
2232
|
constructor(opts) {
|
|
@@ -1749,15 +2236,10 @@ var init_agent_session = __esm({
|
|
|
1749
2236
|
this.checkpoints = opts.checkpoints;
|
|
1750
2237
|
this.goals = opts.goals;
|
|
1751
2238
|
this.currentRole = opts.currentRole;
|
|
2239
|
+
this.currentWorkDir = opts.currentWorkDir;
|
|
1752
2240
|
this.lastRole = opts.currentRole?.() ?? null;
|
|
1753
2241
|
this.history = [...opts.history ?? []];
|
|
1754
2242
|
}
|
|
1755
|
-
/**
|
|
1756
|
-
* 传给 AgentLoop 的审批回调。
|
|
1757
|
-
*
|
|
1758
|
-
* 必须在构造 AgentLoop **之前**拿到(它是构造参数),所以这里做成静态工厂:
|
|
1759
|
-
* 先建 pending 队列,再建 AgentLoop,最后建 AgentSession。
|
|
1760
|
-
*/
|
|
1761
2243
|
static createApprovalBridge() {
|
|
1762
2244
|
let target = null;
|
|
1763
2245
|
const buffered = [];
|
|
@@ -1770,11 +2252,11 @@ var init_agent_session = __esm({
|
|
|
1770
2252
|
}
|
|
1771
2253
|
};
|
|
1772
2254
|
return {
|
|
1773
|
-
onApprovalRequest: (req) => new Promise((
|
|
1774
|
-
enqueue({ kind: "approval", req, resolve:
|
|
2255
|
+
onApprovalRequest: (req) => new Promise((resolve4) => {
|
|
2256
|
+
enqueue({ kind: "approval", req, resolve: resolve4 });
|
|
1775
2257
|
}),
|
|
1776
|
-
onQuestionRequest: (req) => new Promise((
|
|
1777
|
-
enqueue({ kind: "question", req, resolve:
|
|
2258
|
+
onQuestionRequest: (req) => new Promise((resolve4) => {
|
|
2259
|
+
enqueue({ kind: "question", req, resolve: resolve4 });
|
|
1778
2260
|
}),
|
|
1779
2261
|
attach: (session) => {
|
|
1780
2262
|
target = session;
|
|
@@ -1783,49 +2265,24 @@ var init_agent_session = __esm({
|
|
|
1783
2265
|
}
|
|
1784
2266
|
};
|
|
1785
2267
|
}
|
|
1786
|
-
/** 当前累积的历史(只读快照) */
|
|
1787
2268
|
getHistory() {
|
|
1788
2269
|
return this.history;
|
|
1789
2270
|
}
|
|
1790
|
-
/**
|
|
1791
|
-
* 往上下文里记一句话,**不跑模型**(方案 25 §2.3 的 `!` 走这条)。
|
|
1792
|
-
*
|
|
1793
|
-
* 用户在 TUI 里敲 `!git status` 时,输出得让模型看得见 —— 「你看一下这个命令的
|
|
1794
|
-
* 输出」正是 `!` 存在的意义。但它不该触发一轮对话:用户只是想让它知道,
|
|
1795
|
-
* 不是想让它现在就说点什么。
|
|
1796
|
-
*
|
|
1797
|
-
* 角色是 `user` 而不是 `system`:这**确实是用户做的事**,把它标成系统消息会让
|
|
1798
|
-
* 模型以为是我们的指令。也**不落盘** —— `run()` 那条路才管持久化,
|
|
1799
|
-
* 而这里没有一轮可以归属;resume 之后丢掉一条「我刚跑了 ls」是可以接受的,
|
|
1800
|
-
* 把它塞进 DB 反而会让会话回放里出现一条没有回答的用户消息。
|
|
1801
|
-
*/
|
|
1802
2271
|
note(text) {
|
|
1803
2272
|
if (!text.trim()) return;
|
|
1804
2273
|
this.history.push({ role: "user", content: text });
|
|
1805
2274
|
}
|
|
1806
|
-
/**
|
|
1807
|
-
* 手动压缩一次上下文(`/compact`,方案 25 §2.6)。
|
|
1808
|
-
*
|
|
1809
|
-
* 和自动压缩共用引擎里同一段实现,区别只有两个:**不看阈值**(用户敲这条命令
|
|
1810
|
-
* 就是要它现在压),以及可以带一句「这次帮我留住什么」。
|
|
1811
|
-
*
|
|
1812
|
-
* 压缩本身是一次真实的 LLM 请求,那笔钱进的是同一个会话预算 —— 账单由
|
|
1813
|
-
* `AgentLoop` 抛出来,这里原样转给调用方,别在这一层吞掉。
|
|
1814
|
-
*
|
|
1815
|
-
* 历史只有一条(或空)时直接不压:压一条消息拿不到任何收益,
|
|
1816
|
-
* 却要花一次 utility 模型的请求。
|
|
1817
|
-
*/
|
|
1818
2275
|
async compact(instruction) {
|
|
1819
2276
|
const before = this.history.length;
|
|
1820
2277
|
if (before <= 1) {
|
|
1821
|
-
return { ran: false, before, after: before, reason: "
|
|
2278
|
+
return { ran: false, before, after: before, reason: t("boot.compact_too_short") };
|
|
1822
2279
|
}
|
|
1823
2280
|
const result = await this.agent.compactNow(
|
|
1824
2281
|
this.history.map((m) => ({ role: m.role, content: m.content })),
|
|
1825
2282
|
instruction
|
|
1826
2283
|
);
|
|
1827
2284
|
if (!result.ran) {
|
|
1828
|
-
return { ran: false, before, after: before, reason: "
|
|
2285
|
+
return { ran: false, before, after: before, reason: t("boot.compact_unavailable") };
|
|
1829
2286
|
}
|
|
1830
2287
|
this.history = result.messages.map((m) => ({
|
|
1831
2288
|
role: m.role === "assistant" ? "assistant" : "user",
|
|
@@ -1838,23 +2295,6 @@ var init_agent_session = __esm({
|
|
|
1838
2295
|
...result.usage ? { usage: result.usage } : {}
|
|
1839
2296
|
};
|
|
1840
2297
|
}
|
|
1841
|
-
/**
|
|
1842
|
-
* 把某个会话落盘的那段历史装进内存 —— **「模型看到的」那一份只有这一个装法**。
|
|
1843
|
-
*
|
|
1844
|
-
* 三处共用:{@link resume}(换到另一段对话)、{@link rewindConversation}
|
|
1845
|
-
* (回退之后重装),以及 web 的复活(`SessionRuntimeFactory.revive()`:
|
|
1846
|
-
* 引擎装好之后把落盘的那段接上)。抄成三份的代价不是「重复」,
|
|
1847
|
-
* 而是**它们一旦分叉,症状就是「屏幕上和模型看到的对不上」** ——
|
|
1848
|
-
* 下面 {@link resume} 那段注释骂的正是这个。
|
|
1849
|
-
*
|
|
1850
|
-
* 没有 store(纯内存模式)就装成空的:没有 DB 就没有历史可言。
|
|
1851
|
-
*
|
|
1852
|
-
* ⚠️ **不改 `this.sessionId`。** 换会话是 {@link resume} 的事 ——
|
|
1853
|
-
* 复活装的就是自己这一段,那儿本来就不该动。
|
|
1854
|
-
*
|
|
1855
|
-
* @param sessionId 从哪个会话读
|
|
1856
|
-
* @returns 装进去的那一份,供宿主回放进屏幕
|
|
1857
|
-
*/
|
|
1858
2298
|
reloadHistory(sessionId) {
|
|
1859
2299
|
const restored = this.store?.loadHistory(sessionId) ?? [];
|
|
1860
2300
|
this.history = restored.map((m) => ({
|
|
@@ -1864,38 +2304,11 @@ var init_agent_session = __esm({
|
|
|
1864
2304
|
}));
|
|
1865
2305
|
return this.history;
|
|
1866
2306
|
}
|
|
1867
|
-
/**
|
|
1868
|
-
* 换到另一个会话(`/resume`,方案 25 §2.6)。
|
|
1869
|
-
*
|
|
1870
|
-
* **模型真的接上那段上下文**,不只是把历史打印到屏幕上 —— 后者在终端里
|
|
1871
|
-
* 和真恢复长得一模一样,而用户会照着屏幕上那段去问「你刚才说的那个方案」,
|
|
1872
|
-
* 然后收到一句完全对不上的回答。
|
|
1873
|
-
*
|
|
1874
|
-
* 两样都要换:历史(模型看的)和 `sessionId`(往哪儿记)。只换前者的话,
|
|
1875
|
-
* 接下来这一轮会被记进**上一个**会话里 —— 所以这儿在
|
|
1876
|
-
* {@link reloadHistory} 之外还多一步。
|
|
1877
|
-
*
|
|
1878
|
-
* 内存模式下装出来是空的,但 `sessionId` 仍然换:调用方已经决定
|
|
1879
|
-
* 「这是另一段对话」了。
|
|
1880
|
-
*
|
|
1881
|
-
* @returns 恢复出来的历史,供宿主回放进屏幕
|
|
1882
|
-
*/
|
|
1883
2307
|
resume(sessionId) {
|
|
1884
2308
|
const restored = this.reloadHistory(sessionId);
|
|
1885
2309
|
this.sessionId = sessionId;
|
|
1886
2310
|
return restored;
|
|
1887
2311
|
}
|
|
1888
|
-
/**
|
|
1889
|
-
* 对话回退(方案 27):删掉水位线之后的消息,然后把内存历史重新装一遍。
|
|
1890
|
-
*
|
|
1891
|
-
* **重装而不是自己在内存里 splice**:DB 是唯一真源,重装出来的历史天然和它一致。
|
|
1892
|
-
* 自己截内存的话就有两套截断逻辑,而它们一旦分叉,症状是「屏幕上和模型看到的
|
|
1893
|
-
* 对不上」—— `resume()` 那段注释骂过的正是这个。
|
|
1894
|
-
*
|
|
1895
|
-
* 没有 store(纯内存模式)就什么都不做并返回 `null`:没有 DB 就没有水位线可言。
|
|
1896
|
-
*
|
|
1897
|
-
* @returns 删掉几条消息;失败或不支持时 `null`
|
|
1898
|
-
*/
|
|
1899
2312
|
rewindConversation(lastKeptId) {
|
|
1900
2313
|
if (!this.store || lastKeptId < 0) return null;
|
|
1901
2314
|
const removed = this.store.rewindTo(this.sessionId, lastKeptId);
|
|
@@ -1903,13 +2316,6 @@ var init_agent_session = __esm({
|
|
|
1903
2316
|
this.reloadHistory(this.sessionId);
|
|
1904
2317
|
return removed;
|
|
1905
2318
|
}
|
|
1906
|
-
/**
|
|
1907
|
-
* 一条排队的请求 → 一个带答复闭包的事件。
|
|
1908
|
-
*
|
|
1909
|
-
* **答复一次就出账**:先从 `inFlight` 里删掉,再 resolve。重复调用 respond
|
|
1910
|
-
* 是无害的(promise 已 settle),但漏了这一步 `finally` 会再对它补一个兜底答复,
|
|
1911
|
-
* 于是「用户明明点了允许」和「引擎收到拒绝」同时成立。
|
|
1912
|
-
*/
|
|
1913
2319
|
toEvent(pending) {
|
|
1914
2320
|
if (pending.kind === "question") {
|
|
1915
2321
|
return {
|
|
@@ -1924,26 +2330,12 @@ var init_agent_session = __esm({
|
|
|
1924
2330
|
return {
|
|
1925
2331
|
type: "approval-request",
|
|
1926
2332
|
request: pending.req,
|
|
1927
|
-
// 没带 `note` 时原样传裸出口而不是包成 `{outcome}`:两者语义相同,
|
|
1928
|
-
// 但裸串是既有形态,包一层会让所有断言 `resolve` 参数的用例白红一次
|
|
1929
2333
|
respond: (outcome, note) => {
|
|
1930
2334
|
this.inFlight.delete(pending);
|
|
1931
2335
|
pending.resolve(note === void 0 ? outcome : { outcome, note });
|
|
1932
2336
|
}
|
|
1933
2337
|
};
|
|
1934
2338
|
}
|
|
1935
|
-
/**
|
|
1936
|
-
* 跑一轮对话,产出 AgentEvent 流(含 approval-request / question)。
|
|
1937
|
-
*
|
|
1938
|
-
* 宿主只要 for-await 这个流,把 approval-request 交给用户点选即可 ——
|
|
1939
|
-
* 不用关心阻塞回调、不用自己拼历史、不用自己写持久化。
|
|
1940
|
-
*
|
|
1941
|
-
* @param opts.signal 中止信号。`AgentLoop` 一直支持 `input.signal`(轮次之间
|
|
1942
|
-
* 和工具执行处都会检查),但这个签名以前不透传,于是宿主唯一的中止手段
|
|
1943
|
-
* 是硬杀进程 —— `dispose()` 跑不到,SQLite 连接和子进程都留着。
|
|
1944
|
-
* TUI 的 Esc 就靠它:只靠 `break` 退出 for-await 要等下一个事件到达
|
|
1945
|
-
* 才生效,模型正在长考时按 Esc 完全没反应。
|
|
1946
|
-
*/
|
|
1947
2339
|
async *run(userMessage, opts = {}) {
|
|
1948
2340
|
let assistantText = "";
|
|
1949
2341
|
const userText = contentToText(userMessage);
|
|
@@ -1955,13 +2347,11 @@ var init_agent_session = __esm({
|
|
|
1955
2347
|
});
|
|
1956
2348
|
if (userText.trim()) this.goals?.beginRound(this.sessionId);
|
|
1957
2349
|
this.markRoleHandoff();
|
|
2350
|
+
this.markWorkspaceHandoff();
|
|
1958
2351
|
const iter = this.agent.stream({
|
|
1959
2352
|
userMessage,
|
|
1960
2353
|
sessionId: this.sessionId,
|
|
1961
2354
|
history: this.history,
|
|
1962
|
-
// 逐轮把真实用量累进 `sessions` 表(方案 42 PR-0)。
|
|
1963
|
-
// 收的是**本轮增量** —— 事件流里的 `usage.cumulative` 会被 BudgetStore
|
|
1964
|
-
// 从上次会话恢复,减不出干净的增量(见 `RunInput.onTurnUsage`)
|
|
1965
2355
|
...this.store ? {
|
|
1966
2356
|
onTurnUsage: (usage) => void this.store?.recordUsage(this.sessionId, usage)
|
|
1967
2357
|
} : {},
|
|
@@ -1980,15 +2370,18 @@ var init_agent_session = __esm({
|
|
|
1980
2370
|
while (this.queue.length > 0) {
|
|
1981
2371
|
const pending = this.queue.shift();
|
|
1982
2372
|
this.inFlight.add(pending);
|
|
1983
|
-
|
|
2373
|
+
const request = this.toEvent(pending);
|
|
2374
|
+
publishAgentEvent(this.sessionId, request);
|
|
2375
|
+
yield request;
|
|
1984
2376
|
}
|
|
1985
2377
|
continue;
|
|
1986
2378
|
}
|
|
1987
|
-
const { value: event, done } = await pendingNext;
|
|
1988
|
-
if (
|
|
2379
|
+
const { value: event, done: done2 } = await pendingNext;
|
|
2380
|
+
if (done2) break;
|
|
1989
2381
|
pendingNext = iter.next();
|
|
1990
2382
|
if (event.type === "text-delta") assistantText += event.text;
|
|
1991
2383
|
turn.observe(event);
|
|
2384
|
+
publishAgentEvent(this.sessionId, event);
|
|
1992
2385
|
yield event;
|
|
1993
2386
|
}
|
|
1994
2387
|
} finally {
|
|
@@ -1997,47 +2390,20 @@ var init_agent_session = __esm({
|
|
|
1997
2390
|
this.inFlight.clear();
|
|
1998
2391
|
this.notify = null;
|
|
1999
2392
|
}
|
|
2000
|
-
this.history.push({ role: "user", content: userText, ...parts ? { parts } : {} });
|
|
2001
|
-
if (assistantText) this.history.push({ role: "assistant", content: assistantText });
|
|
2002
2393
|
const steps = turn.take();
|
|
2003
2394
|
const facts = turn.facts();
|
|
2395
|
+
const assistantHistory = assistantTurnText(
|
|
2396
|
+
assistantText,
|
|
2397
|
+
steps.flatMap((s) => s.toolResults ?? [])
|
|
2398
|
+
);
|
|
2399
|
+
this.history.push({ role: "user", content: userText, ...parts ? { parts } : {} });
|
|
2400
|
+
if (assistantHistory) this.history.push({ role: "assistant", content: assistantHistory });
|
|
2004
2401
|
this.store?.record(this.sessionId, userText, assistantText, {
|
|
2005
2402
|
...parts ? { parts } : {},
|
|
2006
2403
|
...steps.length > 0 ? { steps } : {},
|
|
2007
|
-
// 这一轮的账(决定 8 第三个时刻)。不落它的话刷新之后每一轮的
|
|
2008
|
-
// 「共 N token · 哪个模型 · 花了几秒」全部消失 —— 那是网线补不回来的
|
|
2009
2404
|
...facts ? { facts } : {}
|
|
2010
2405
|
});
|
|
2011
2406
|
}
|
|
2012
|
-
/**
|
|
2013
|
-
* 换专家的那一刻,往历史里插一条 `[系统]` 消息(方案 57 §2.3 / §3.5)。
|
|
2014
|
-
*
|
|
2015
|
-
* ## 为什么在这一层,不在 `loop.ts` 里
|
|
2016
|
-
*
|
|
2017
|
-
* 循环里那个 `messages` 是 **per-run** 的数组,往那儿 push 的东西不进 DB 历史
|
|
2018
|
-
* (后台任务通知就是这么设计的 —— 它不该被回放)。而这一条要的**恰恰是落盘**:
|
|
2019
|
-
* 身份行活在那一次请求里、chip 活在浏览器里,历史是唯一会落盘的那一份,
|
|
2020
|
-
* 而「这段对话里谁干的」正是重开会话回放时才会被问的问题。
|
|
2021
|
-
*
|
|
2022
|
-
* ## 三条形状判据
|
|
2023
|
-
*
|
|
2024
|
-
* 1. **`role: 'user'` + `[系统]` 前缀,不是 `role: 'system'`** —— AI SDK v7 不许
|
|
2025
|
-
* `messages` 里出现 system,会抛 `AI_InvalidPromptError`。这条坑 `loop.ts`
|
|
2026
|
-
* 已经踩过一次并逐字记下来了,别重踩。⚠️ 前缀**由这里拼**({@link systemNote}),
|
|
2027
|
-
* catalog 里 `agent_role` 那两条是光正文:前缀是全仓共用的一条契约,真源在
|
|
2028
|
-
* `protocol/src/system-note.ts`,烤进句子里就会跟着译者走;
|
|
2029
|
-
* 2. **两条连续的 `user` 消息是安全的** —— 后台任务通知今天就是这么挂的,
|
|
2030
|
-
* 而它在真 provider 上跑通了。⚠️ `message-utils.ts` 里那个
|
|
2031
|
-
* `validateAlternation()` 看起来禁止这件事,但它**全仓只有自己的用例调用,
|
|
2032
|
-
* 生产路径上一个调用点都没有**(2026-08-17 复核)。别为了让它自洽去「修」
|
|
2033
|
-
* 这条路,那会连后台通知一起打挂;
|
|
2034
|
-
* 3. **只在真的换人那一条上插**(含「从有到无」)。每条都插等于往历史里灌噪音,
|
|
2035
|
-
* 还会让压缩器把预算花在这上面。
|
|
2036
|
-
*
|
|
2037
|
-
* ⚠️ 落盘走 {@link SessionStore.recordNote} 而不是 `record()`:后者写的是**一轮
|
|
2038
|
-
* 问答**(user + assistant + tool×N),拿它写一条孤零零的 user 行是在赌
|
|
2039
|
-
* 「assistantText 为空时它恰好不写第二行」——那是一个实现细节,不是一条承诺。
|
|
2040
|
-
*/
|
|
2041
2407
|
markRoleHandoff() {
|
|
2042
2408
|
if (!this.currentRole) return;
|
|
2043
2409
|
const now = this.currentRole() ?? null;
|
|
@@ -2048,14 +2414,24 @@ var init_agent_session = __esm({
|
|
|
2048
2414
|
this.history.push({ role: "user", content: text });
|
|
2049
2415
|
this.store?.recordNote(this.sessionId, text);
|
|
2050
2416
|
}
|
|
2051
|
-
|
|
2417
|
+
markWorkspaceHandoff() {
|
|
2418
|
+
if (!this.currentWorkDir) return;
|
|
2419
|
+
const now = this.currentWorkDir();
|
|
2420
|
+
const previous = this.lastWorkDir;
|
|
2421
|
+
this.lastWorkDir = now;
|
|
2422
|
+
if (previous === null || previous === now) return;
|
|
2423
|
+
if (this.history.length === 0) return;
|
|
2424
|
+
const text = systemNote(t("workspace.handoff", { root: now }));
|
|
2425
|
+
this.history.push({ role: "user", content: text });
|
|
2426
|
+
this.store?.recordNote(this.sessionId, text);
|
|
2427
|
+
}
|
|
2052
2428
|
async once(userMessage, opts = {}) {
|
|
2053
2429
|
let text = "";
|
|
2054
2430
|
let reasoning = "";
|
|
2055
2431
|
for await (const ev of this.run(userMessage, opts)) {
|
|
2056
2432
|
if (ev.type === "text-delta") text += ev.text;
|
|
2057
2433
|
else if (ev.type === "reasoning-delta") reasoning += ev.text;
|
|
2058
|
-
else if (ev.type === "error") text =
|
|
2434
|
+
else if (ev.type === "error") text = t("boot.error", { message: ev.message });
|
|
2059
2435
|
}
|
|
2060
2436
|
return { text, ...reasoning ? { reasoning } : {} };
|
|
2061
2437
|
}
|
|
@@ -2065,8 +2441,6 @@ var init_agent_session = __esm({
|
|
|
2065
2441
|
function createRoleControl(scope, roles) {
|
|
2066
2442
|
return {
|
|
2067
2443
|
has: (name) => findRole(roles(), name) !== void 0,
|
|
2068
|
-
// `findRole` 找不到时回 `undefined`,而 `enter(undefined)` 的语义正好是
|
|
2069
|
-
// 「退回会话默认角色」—— 上面那段 ⚠️ 说的就是这一格,不用再判一次
|
|
2070
2444
|
enterTurn: (name) => scope.enter(findRole(roles(), name))
|
|
2071
2445
|
};
|
|
2072
2446
|
}
|
|
@@ -2086,7 +2460,6 @@ var init_role_scope = __esm({
|
|
|
2086
2460
|
"src/role-scope.ts"() {
|
|
2087
2461
|
}
|
|
2088
2462
|
});
|
|
2089
|
-
|
|
2090
2463
|
// src/session-goal.ts
|
|
2091
2464
|
function buildGoalControl(service, sessionId) {
|
|
2092
2465
|
return {
|
|
@@ -2100,41 +2473,70 @@ function buildGoalControl(service, sessionId) {
|
|
|
2100
2473
|
clear: () => service.clear(sessionId())
|
|
2101
2474
|
};
|
|
2102
2475
|
}
|
|
2476
|
+
function buildGoalCatalog(service) {
|
|
2477
|
+
return {
|
|
2478
|
+
current: (sessionId) => service.current(sessionId),
|
|
2479
|
+
create: (sessionId, objective, maxRounds) => service.create(sessionId, objective, maxRounds),
|
|
2480
|
+
edit: (sessionId, objective) => service.edit(sessionId, objective),
|
|
2481
|
+
setBudget: (sessionId, maxRounds) => service.setBudget(sessionId, maxRounds),
|
|
2482
|
+
pause: (sessionId) => service.pause(sessionId),
|
|
2483
|
+
resume: (sessionId) => service.resume(sessionId),
|
|
2484
|
+
complete: (sessionId, evidence) => service.complete(sessionId, evidence),
|
|
2485
|
+
clear: (sessionId) => service.clear(sessionId)
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2103
2488
|
var init_session_goal = __esm({
|
|
2104
2489
|
"src/session-goal.ts"() {
|
|
2105
2490
|
}
|
|
2106
2491
|
});
|
|
2107
2492
|
function buildSessionGuards(input) {
|
|
2108
|
-
const {
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
}
|
|
2493
|
+
const {
|
|
2494
|
+
permission,
|
|
2495
|
+
planState,
|
|
2496
|
+
managed,
|
|
2497
|
+
defaultLevel,
|
|
2498
|
+
requestApproval,
|
|
2499
|
+
interactive,
|
|
2500
|
+
deferredTools
|
|
2501
|
+
} = input;
|
|
2502
|
+
const bootstrapReveal = input.bootstrapReveal;
|
|
2503
|
+
const scopeOf = (perm, state, own) => {
|
|
2504
|
+
const reveal = own ? createToolRevealState() : bootstrapReveal;
|
|
2505
|
+
return {
|
|
2506
|
+
permission: perm,
|
|
2507
|
+
permissions: perm ? buildSessionPermissions(perm, managed, state, defaultLevel) : null,
|
|
2508
|
+
plan: state ? buildPlanControl(state) : null,
|
|
2509
|
+
planState: state,
|
|
2510
|
+
tools: own ? [
|
|
2511
|
+
...state ? createPlanModeTools({ state, ...interactive ? { requestApproval } : {} }) : [],
|
|
2512
|
+
...deferredTools().length > 0 ? [createToolSearchTool({ deferred: deferredTools, reveal })] : []
|
|
2513
|
+
] : [],
|
|
2514
|
+
reveal
|
|
2515
|
+
};
|
|
2516
|
+
};
|
|
2117
2517
|
const bootstrapScope = scopeOf(permission, planState, false);
|
|
2118
2518
|
return {
|
|
2119
2519
|
bootstrap: () => bootstrapScope,
|
|
2120
2520
|
open: () => {
|
|
2121
2521
|
if (!permission) return scopeOf(null, null, true);
|
|
2122
|
-
const own = permission.scoped(
|
|
2522
|
+
const own = permission.scoped(defaultLevel.get());
|
|
2123
2523
|
return scopeOf(own, planState ? new PlanModeState(own) : null, true);
|
|
2124
2524
|
}
|
|
2125
2525
|
};
|
|
2126
2526
|
}
|
|
2127
|
-
function buildSessionPermissions(permission, managed, planState) {
|
|
2527
|
+
function buildSessionPermissions(permission, managed, planState, defaultLevel) {
|
|
2128
2528
|
return {
|
|
2129
2529
|
level: () => permission.getLevel(),
|
|
2130
2530
|
managed: () => ({ bypassDisabled: managed.disableBypassPermissionsMode }),
|
|
2131
|
-
setLevel: (level) => {
|
|
2531
|
+
setLevel: (level, opts) => {
|
|
2132
2532
|
if (level === "bypass" && managed.disableBypassPermissionsMode) {
|
|
2133
2533
|
return { ok: false, reason: "managed-bypass-disabled" };
|
|
2134
2534
|
}
|
|
2135
2535
|
permission.setLevel(level);
|
|
2136
2536
|
planState?.forget();
|
|
2137
|
-
return { ok: true };
|
|
2537
|
+
if (!opts.remember) return { ok: true };
|
|
2538
|
+
const failure = defaultLevel.remember(level);
|
|
2539
|
+
return failure ? { ok: true, notRemembered: failure } : { ok: true };
|
|
2138
2540
|
}
|
|
2139
2541
|
};
|
|
2140
2542
|
}
|
|
@@ -2216,19 +2618,6 @@ var init_session_store = __esm({
|
|
|
2216
2618
|
"src/session-store.ts"() {
|
|
2217
2619
|
init_session_file_changes();
|
|
2218
2620
|
SessionStore = class {
|
|
2219
|
-
/**
|
|
2220
|
-
* @param artifactsRoot artifact 目录的**家目录**(即 `~/.epoch` 那一层),
|
|
2221
|
-
* 缺省按 `EPOCH_HOME` / profile 解析。只有测试和多 profile 场景要显式给。
|
|
2222
|
-
* @param cwd 这段会话归属的**项目根**,落进 `sessions.cwd` 列(方案 29 §2.1)。
|
|
2223
|
-
*
|
|
2224
|
-
* `cwd` 在方案 29 之前从来没人写过:列是有的(v1 基线里就在,还带着
|
|
2225
|
-
* `idx_sessions_cwd` 索引),但 `ensure()` 只传了 id,于是全库的 cwd 都是 NULL、
|
|
2226
|
-
* `epoch -c`「接上本目录最近一个会话」压根无从判起。
|
|
2227
|
-
*
|
|
2228
|
-
* 传项目根而不是 `process.cwd()`:在子目录里敲 `epoch -c` 要接上同一个仓库的
|
|
2229
|
-
* 会话。读取侧(`SessionManager.getLatestByCwd`)用的是同一个口径。
|
|
2230
|
-
* 不给就仍然写 NULL —— 那段会话只是不参与 `-c` 匹配,别的一切照旧。
|
|
2231
|
-
*/
|
|
2232
2621
|
constructor(sessions, artifactsRoot, cwd, source, decision) {
|
|
2233
2622
|
this.sessions = sessions;
|
|
2234
2623
|
this.artifactsRoot = artifactsRoot;
|
|
@@ -2241,40 +2630,10 @@ var init_session_store = __esm({
|
|
|
2241
2630
|
cwd;
|
|
2242
2631
|
source;
|
|
2243
2632
|
decision;
|
|
2244
|
-
/** 已经确认存在的会话 id,避免每轮都试着 create */
|
|
2245
2633
|
known = /* @__PURE__ */ new Set();
|
|
2246
|
-
/** 这个会话的 artifact 目录 */
|
|
2247
2634
|
artifactDir(sessionId) {
|
|
2248
2635
|
return this.artifactsRoot ? artifactsDir(sessionId, this.artifactsRoot) : artifactsDir(sessionId);
|
|
2249
2636
|
}
|
|
2250
|
-
/**
|
|
2251
|
-
* 记录一轮问答。持久化失败不该影响对话本身,所以吞异常但返回是否成功。
|
|
2252
|
-
*
|
|
2253
|
-
* `userMessage` / `assistantText` 收的是**文本投影**:`Message.content` 是
|
|
2254
|
-
* SQLite 里的一个 TEXT 列,把 base64 灌进去会让 sessions.db 以每张截图几百 KB
|
|
2255
|
-
* 的速度膨胀,而且 `epoch sessions list` 的预览、标题截取、全文检索全会被那串
|
|
2256
|
-
* 字符污染。图片本身走 `detail.parts` —— 落进 artifact 目录,列里只留路径。
|
|
2257
|
-
*
|
|
2258
|
-
* 落盘的行顺序是 **user →(assistant → tool×k)× 步数**,不是时间序:这是
|
|
2259
|
-
* `EpochMessage` 的规范顺序(assistant 声明 `toolCalls`,tool 行携带
|
|
2260
|
-
* `toolResults`)。宿主按 `toolCallId` 配对分组渲染,**不要依赖行序**去猜
|
|
2261
|
-
* 哪个结果属于哪次调用 —— 那正是 `AgentEvent` 加 `callId` 时骂过的病
|
|
2262
|
-
* (靠「工具名 + 调用顺序 FIFO」猜,并行工具必然串台)。
|
|
2263
|
-
*
|
|
2264
|
-
* ## ⚠️ 一轮不再只有**一条** assistant 行(2026-08-18)
|
|
2265
|
-
*
|
|
2266
|
-
* 以前是「整轮正文拼成一条 + 全部 toolCalls 挂上去 + 所有 tool 行垫在最后」。
|
|
2267
|
-
* 那个形状丢掉了步序,直接后果是**刷新页面之后屏幕上的顺序和刚才不一样**:
|
|
2268
|
-
* 回放只能照行序重建,于是「先调工具、后回答」被画成「先回答、后工具卡」。
|
|
2269
|
-
*
|
|
2270
|
-
* 现在按 {@link TurnDetail.steps} 一步一行。这不是新发明的形状 —— `loop.ts`
|
|
2271
|
-
* 自己往上下文里 push 的就是它,只是那一份是 per-run 的、从不落盘。
|
|
2272
|
-
*
|
|
2273
|
-
* 两个连带的地方,改这里时别忘了:
|
|
2274
|
-
* - {@link loadHistory} 要把连续的 assistant 行**合并回一条**再喂模型;
|
|
2275
|
-
* - `web/src/state/replay.ts` 和假服务端的 `toReplayMessages()` 是读侧的
|
|
2276
|
-
* 两份实现,形状变了两边都要跟。
|
|
2277
|
-
*/
|
|
2278
2637
|
record(sessionId, userMessage, assistantText, detail = {}) {
|
|
2279
2638
|
try {
|
|
2280
2639
|
this.ensure(sessionId);
|
|
@@ -2298,8 +2657,6 @@ var init_session_store = __esm({
|
|
|
2298
2657
|
for (const result of persistToolResults(step.toolResults ?? [])) {
|
|
2299
2658
|
this.sessions.appendMessage(sessionId, {
|
|
2300
2659
|
role: "tool",
|
|
2301
|
-
// content 列存输出的文本投影:FTS 索引和 `epoch sessions search`
|
|
2302
|
-
// 靠它才搜得到工具输出。结构化的那份在 toolResults 里
|
|
2303
2660
|
content: result.output,
|
|
2304
2661
|
toolCallId: result.toolCallId,
|
|
2305
2662
|
toolName: result.toolName,
|
|
@@ -2312,18 +2669,6 @@ var init_session_store = __esm({
|
|
|
2312
2669
|
return false;
|
|
2313
2670
|
}
|
|
2314
2671
|
}
|
|
2315
|
-
/**
|
|
2316
|
-
* 记一条**孤立的 `user` 行**,没有与之配对的回答(方案 57 §3.5 那条换人留痕)。
|
|
2317
|
-
*
|
|
2318
|
-
* 和 {@link record} 分开而不是拿 `record(id, text, '')` 顶:那个方法写的是**一轮
|
|
2319
|
-
* 问答**,「assistantText 为空时它恰好只写一行」是一个实现细节而不是一条承诺 ——
|
|
2320
|
-
* 哪天它改成「assistant 行无条件写」(那是一个合理的改动),这条留痕后面就会
|
|
2321
|
-
* 凭空多出一条空回答,而没有任何东西会红。
|
|
2322
|
-
*
|
|
2323
|
-
* **要 `ensure()`**,判据同 `recordUsage()`:这一行写在轮次开始那一刻,
|
|
2324
|
-
* 比 `record()`(整轮结束才写)早。不建会话行的话 `appendMessage` 会失败,
|
|
2325
|
-
* 而这条留痕正是重开会话回放时唯一说得出「从这里起换人了」的东西。
|
|
2326
|
-
*/
|
|
2327
2672
|
recordNote(sessionId, text) {
|
|
2328
2673
|
try {
|
|
2329
2674
|
this.ensure(sessionId);
|
|
@@ -2333,15 +2678,6 @@ var init_session_store = __esm({
|
|
|
2333
2678
|
return false;
|
|
2334
2679
|
}
|
|
2335
2680
|
}
|
|
2336
|
-
/**
|
|
2337
|
-
* 累加一轮模型请求的真实用量(方案 42 PR-0)。持久化失败照旧不影响对话。
|
|
2338
|
-
*
|
|
2339
|
-
* **这里要 `ensure()`**,和 `lastMessageId()` 的克制正相反:用量是逐轮来的,
|
|
2340
|
-
* 比 `record()`(整轮对话结束才写)早,新会话的第一条消息还没落盘。
|
|
2341
|
-
* 不建会话行的话那条 UPDATE 影响 0 行 —— 一段全新会话的用量会**整段丢掉**。
|
|
2342
|
-
* 而且这里的触发条件比那边硬:能走到这儿说明模型请求已经发出去、钱已经花了,
|
|
2343
|
-
* 那就是一段值得记录的会话,不是「点开又退出」。
|
|
2344
|
-
*/
|
|
2345
2681
|
recordUsage(sessionId, usage) {
|
|
2346
2682
|
try {
|
|
2347
2683
|
this.ensure(sessionId);
|
|
@@ -2351,17 +2687,6 @@ var init_session_store = __esm({
|
|
|
2351
2687
|
return false;
|
|
2352
2688
|
}
|
|
2353
2689
|
}
|
|
2354
|
-
/**
|
|
2355
|
-
* 记下(或清掉)这段会话当前生效的那份已批准计划(方案 35 PR-2 验收 9)。
|
|
2356
|
-
*
|
|
2357
|
-
* **要 `ensure()`**,和 `recordUsage()` 同一个理由、比它更硬:计划被批准
|
|
2358
|
-
* 通常发生在第一轮对话**结束之前**(模型 `enter_plan_mode` → 调研 →
|
|
2359
|
-
* `exit_plan_mode` → 用户批),那时 `record()` 还一条都没写。不建会话行的话
|
|
2360
|
-
* 那条 UPDATE 影响 0 行 —— 用户刚点完「批准并执行」,计划就已经没落盘。
|
|
2361
|
-
*
|
|
2362
|
-
* 传 `null` 是**清掉**(用户拒绝了计划)。这里不做「空串当没给」的归一:
|
|
2363
|
-
* 调用方是 `PlanModeState`,它给的只会是一份正文或者 `null`。
|
|
2364
|
-
*/
|
|
2365
2690
|
savePlan(sessionId, markdown) {
|
|
2366
2691
|
try {
|
|
2367
2692
|
this.ensure(sessionId);
|
|
@@ -2371,12 +2696,6 @@ var init_session_store = __esm({
|
|
|
2371
2696
|
return false;
|
|
2372
2697
|
}
|
|
2373
2698
|
}
|
|
2374
|
-
/**
|
|
2375
|
-
* 读回这段会话的已批准计划;没有(或会话不存在)时 `null`。
|
|
2376
|
-
*
|
|
2377
|
-
* **不 `ensure()`**:这是查询。`--resume` 指着一个不存在的 id 时该回「没有」,
|
|
2378
|
-
* 而不是顺手建一段空会话。
|
|
2379
|
-
*/
|
|
2380
2699
|
loadPlan(sessionId) {
|
|
2381
2700
|
try {
|
|
2382
2701
|
return this.sessions.get(sessionId)?.meta.plan ?? null;
|
|
@@ -2384,13 +2703,6 @@ var init_session_store = __esm({
|
|
|
2384
2703
|
return null;
|
|
2385
2704
|
}
|
|
2386
2705
|
}
|
|
2387
|
-
/**
|
|
2388
|
-
* 这个会话最后一条消息的 DB id(方案 27 的检查点水位线)。
|
|
2389
|
-
*
|
|
2390
|
-
* 会话还没建出来时是 0 —— **不 `ensure()`**:开轮是每条用户消息都要做的事,
|
|
2391
|
-
* 为它去建一个可能马上就没有任何内容的会话行,等于让「只是聊了两句就退出」
|
|
2392
|
-
* 也在 DB 里留一条空会话。
|
|
2393
|
-
*/
|
|
2394
2706
|
lastMessageId(sessionId) {
|
|
2395
2707
|
try {
|
|
2396
2708
|
return this.sessions.lastMessageId(sessionId);
|
|
@@ -2398,13 +2710,6 @@ var init_session_store = __esm({
|
|
|
2398
2710
|
return 0;
|
|
2399
2711
|
}
|
|
2400
2712
|
}
|
|
2401
|
-
/**
|
|
2402
|
-
* 对话回退:删掉水位线之后的消息(方案 27)。返回删了几条,失败时 `null`。
|
|
2403
|
-
*
|
|
2404
|
-
* 吞异常的口径和 `record()` 一致 —— 但这里返回 `null` 而不是 `false`:
|
|
2405
|
-
* 调用方要据此决定还要不要重装内存历史,而「删了 0 条」和「没删成」
|
|
2406
|
-
* 对那个决定是两回事。
|
|
2407
|
-
*/
|
|
2408
2713
|
rewindTo(sessionId, lastKeptId) {
|
|
2409
2714
|
try {
|
|
2410
2715
|
return this.sessions.rewindTo(sessionId, lastKeptId);
|
|
@@ -2412,23 +2717,14 @@ var init_session_store = __esm({
|
|
|
2412
2717
|
return null;
|
|
2413
2718
|
}
|
|
2414
2719
|
}
|
|
2415
|
-
/**
|
|
2416
|
-
* 会话不存在就建。create 对已存在的 id 会抛错,这里按「已存在」处理。
|
|
2417
|
-
*
|
|
2418
|
-
* `cwd` 只在**新建**时写,不去 update 已存在的那一行:一段会话属于哪个项目
|
|
2419
|
-
* 是它开始那一刻的事实,`epoch -c` 之后在别的目录里接着聊也不该把它改籍。
|
|
2420
|
-
*
|
|
2421
|
-
* ⚠️ 工作区决定(v7 那两格)走的是同一条「只在新建时写」,但**理由不同**:
|
|
2422
|
-
* 不是「不该改」,而是「改它的路在别处」—— 那一格的 update 归
|
|
2423
|
-
* `SessionWorkspaces` 的 `decisions.record()`(判据在构造函数的 `decision` 上)。
|
|
2424
|
-
*/
|
|
2425
2720
|
ensure(sessionId) {
|
|
2426
2721
|
if (this.known.has(sessionId)) return;
|
|
2427
2722
|
const decision = this.decision?.(sessionId) ?? null;
|
|
2723
|
+
const cwd = this.cwd?.();
|
|
2428
2724
|
try {
|
|
2429
2725
|
this.sessions.create({
|
|
2430
2726
|
id: sessionId,
|
|
2431
|
-
...
|
|
2727
|
+
...cwd ? { cwd } : {},
|
|
2432
2728
|
...this.source ? { source: this.source } : {},
|
|
2433
2729
|
...decision ? {
|
|
2434
2730
|
workspaceState: decision.state,
|
|
@@ -2439,61 +2735,42 @@ var init_session_store = __esm({
|
|
|
2439
2735
|
}
|
|
2440
2736
|
this.known.add(sessionId);
|
|
2441
2737
|
}
|
|
2442
|
-
/**
|
|
2443
|
-
* 恢复一个会话的历史消息,**喂给模型用**。
|
|
2444
|
-
*
|
|
2445
|
-
* 只取 user / assistant:tool 行是给宿主回放的,单独一条没有配对 tool-call 的
|
|
2446
|
-
* tool 消息会被 provider 拒掉。图片部件在这里被**读回 base64**,所以恢复出来的
|
|
2447
|
-
* 会话里模型仍然看得见上一轮那张图 —— 这就是方案 12 留下的「resume 恢复图片」。
|
|
2448
|
-
*
|
|
2449
|
-
* 图片文件找不到(artifact 清理跑过、用户手删)时退化成一句占位文本,不抛。
|
|
2450
|
-
*
|
|
2451
|
-
* ## ⚠️ 连续的 assistant 行在这里被**合并回一条**(2026-08-18)
|
|
2452
|
-
*
|
|
2453
|
-
* `record()` 改成一步一行之后,一轮在 DB 里会留下多条 assistant,中间垫着
|
|
2454
|
-
* tool 行。上面那道 user/assistant 过滤把 tool 行滤掉,剩下的就是**连着的几条
|
|
2455
|
-
* assistant**,其中还有「只调工具没说话」那种正文为空的。
|
|
2456
|
-
*
|
|
2457
|
-
* 两样都是没人验过的形状:`sdk-adapter` 会把 `content: ''` 原样发出去,而连着
|
|
2458
|
-
* 两条同角色消息在各家 provider 上的行为各不相同(仓里那个
|
|
2459
|
-
* `validateAlternation()` 干脆直接抛 —— 它今天没有生产调用方,但它写下的
|
|
2460
|
-
* 顾虑是真的)。
|
|
2461
|
-
*
|
|
2462
|
-
* 合并口径**按直播那一份来**:文本直接相接、不插分隔符(`run()` 里就是
|
|
2463
|
-
* `assistantText += event.text`),合完是空就整条不发(那边也是
|
|
2464
|
-
* `if (assistantText)`)。判据是 resume 回来的上下文必须和刚才在内存里的那份
|
|
2465
|
-
* **逐字相同** —— 差一个换行,模型看到的就是另一段历史。
|
|
2466
|
-
*/
|
|
2467
2738
|
loadHistory(sessionId) {
|
|
2468
2739
|
const prev = this.sessions.get(sessionId);
|
|
2469
2740
|
if (!prev) return [];
|
|
2470
2741
|
this.known.add(sessionId);
|
|
2471
2742
|
const out = [];
|
|
2743
|
+
let text = "";
|
|
2744
|
+
let results = [];
|
|
2745
|
+
let open = false;
|
|
2746
|
+
const flush = () => {
|
|
2747
|
+
if (!open) return;
|
|
2748
|
+
const content = assistantTurnText(text, results);
|
|
2749
|
+
if (content) out.push({ role: "assistant", content });
|
|
2750
|
+
text = "";
|
|
2751
|
+
results = [];
|
|
2752
|
+
open = false;
|
|
2753
|
+
};
|
|
2472
2754
|
for (const m of prev.messages) {
|
|
2473
2755
|
if (m.role === "user") {
|
|
2756
|
+
flush();
|
|
2474
2757
|
const parts = toParts(m.parts, true);
|
|
2475
2758
|
out.push({ role: "user", content: m.content, ...parts ? { parts } : {} });
|
|
2476
2759
|
continue;
|
|
2477
2760
|
}
|
|
2478
|
-
if (m.role
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2761
|
+
if (m.role === "assistant") {
|
|
2762
|
+
open = true;
|
|
2763
|
+
text += m.content;
|
|
2764
|
+
continue;
|
|
2765
|
+
}
|
|
2766
|
+
if (m.role === "tool") {
|
|
2767
|
+
if (open) results.push(...m.toolResults ?? []);
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2482
2770
|
}
|
|
2771
|
+
flush();
|
|
2483
2772
|
return out;
|
|
2484
2773
|
}
|
|
2485
|
-
/**
|
|
2486
|
-
* 恢复一个会话的**全部**消息,用于宿主回放(web 刷新页面、会话详情页)。
|
|
2487
|
-
*
|
|
2488
|
-
* 与 `loadHistory()` 的三点区别,都是「给人看」和「给模型看」的差别:
|
|
2489
|
-
* 1. 带 tool 行 —— 工具调用和 artifact 就挂在那上面
|
|
2490
|
-
* 2. 带 `id` / `timestamp`
|
|
2491
|
-
* 3. **图片不内联 base64**:`EpochImagePart.image` 里是绝对文件路径。
|
|
2492
|
-
* 宿主拿它拼自己的 artifact 端点,把几百 KB 的 base64 塞进一次 REST 响应
|
|
2493
|
-
* 或 SSE 是纯粹的浪费。工具产物同理:`ToolArtifact` 有 `path`、**没有** `data`。
|
|
2494
|
-
*
|
|
2495
|
-
* ⚠️ 因此这个方法的返回值**不能**直接喂给 provider。
|
|
2496
|
-
*/
|
|
2497
2774
|
loadMessages(sessionId) {
|
|
2498
2775
|
const prev = this.sessions.get(sessionId);
|
|
2499
2776
|
if (!prev) return [];
|
|
@@ -2504,13 +2781,8 @@ var init_session_store = __esm({
|
|
|
2504
2781
|
id: m.id,
|
|
2505
2782
|
timestamp: m.timestamp,
|
|
2506
2783
|
role: m.role,
|
|
2507
|
-
// protocol 的约定:tool 消息 content 为空串,内容在 toolResults 里,
|
|
2508
|
-
// 「取这条消息的文本」才永远不用判类型。DB 里那份是给 FTS 用的
|
|
2509
2784
|
content: m.role === "tool" ? "" : m.content,
|
|
2510
2785
|
...parts ? { parts } : {},
|
|
2511
|
-
// 思考原文和这一轮的账(2026-08-18)。**两样都只有回放这一支带** ——
|
|
2512
|
-
// `loadHistory()` 那份是喂模型的,模型不需要(也不该)看自己上一轮
|
|
2513
|
-
// 想过什么、花了多少钱
|
|
2514
2786
|
...m.reasoning ? { reasoning: m.reasoning } : {},
|
|
2515
2787
|
...m.turnFacts ? { turnFacts: m.turnFacts } : {},
|
|
2516
2788
|
...m.toolCalls ? { toolCalls: m.toolCalls } : {},
|
|
@@ -2518,15 +2790,6 @@ var init_session_store = __esm({
|
|
|
2518
2790
|
};
|
|
2519
2791
|
});
|
|
2520
2792
|
}
|
|
2521
|
-
/**
|
|
2522
|
-
* 这个会话在工作区里改过哪些文件(方案 42 PR-4)。
|
|
2523
|
-
*
|
|
2524
|
-
* @param workDir 相对路径按哪个根算。**这个会话绑定的工作区主根**(决定 18),
|
|
2525
|
-
* 不是服务进程的工作目录 —— 后者在多会话下只会是其中一个的,而它不说是哪个。
|
|
2526
|
-
*
|
|
2527
|
-
* 读不出来(SQLite 起不来 / 会话不存在)时是空数组,不抛:这是一个展示用的
|
|
2528
|
-
* 汇总,它自己坏了不该让整屏 500。
|
|
2529
|
-
*/
|
|
2530
2793
|
fileChanges(sessionId, workDir) {
|
|
2531
2794
|
try {
|
|
2532
2795
|
return collectFileChanges(this.loadMessages(sessionId), workDir);
|
|
@@ -2534,6 +2797,12 @@ var init_session_store = __esm({
|
|
|
2534
2797
|
return [];
|
|
2535
2798
|
}
|
|
2536
2799
|
}
|
|
2800
|
+
ftsHealth() {
|
|
2801
|
+
return this.sessions.ftsHealth();
|
|
2802
|
+
}
|
|
2803
|
+
reclaimFts(vacuum = true) {
|
|
2804
|
+
return this.sessions.reclaimFts(vacuum);
|
|
2805
|
+
}
|
|
2537
2806
|
};
|
|
2538
2807
|
}
|
|
2539
2808
|
});
|
|
@@ -2655,8 +2924,6 @@ async function applyMcpConfig(mcp, sync, opts) {
|
|
|
2655
2924
|
return {
|
|
2656
2925
|
ok: false,
|
|
2657
2926
|
reason: "no-servers",
|
|
2658
|
-
// 只带**第一条**诊断:屏幕上那句话后面已经跟着完整的诊断列表了,
|
|
2659
|
-
// 把十条拼成一行只会把它挤出去
|
|
2660
2927
|
detail: issues[0]?.message ?? ""
|
|
2661
2928
|
};
|
|
2662
2929
|
}
|
|
@@ -2720,9 +2987,6 @@ var init_mcp_config_apply = __esm({
|
|
|
2720
2987
|
init_mcp_config_file();
|
|
2721
2988
|
}
|
|
2722
2989
|
});
|
|
2723
|
-
function isWritableName(name) {
|
|
2724
|
-
return /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(name);
|
|
2725
|
-
}
|
|
2726
2990
|
function skipTrivia(raw, from) {
|
|
2727
2991
|
let i = from;
|
|
2728
2992
|
for (; ; ) {
|
|
@@ -2786,13 +3050,15 @@ function walkObject(raw, braceAt) {
|
|
|
2786
3050
|
const entries = [];
|
|
2787
3051
|
let i = skipTrivia(raw, braceAt + 1);
|
|
2788
3052
|
while (i < raw.length && raw[i] !== "}") {
|
|
3053
|
+
const keyAt = i;
|
|
2789
3054
|
const key = readString(raw, i);
|
|
2790
3055
|
if (!key) return { entries, clean: false };
|
|
2791
3056
|
const afterKey = skipTrivia(raw, key[1]);
|
|
2792
3057
|
if (raw[afterKey] !== ":") return { entries, clean: false };
|
|
2793
3058
|
const valueAt2 = skipTrivia(raw, afterKey + 1);
|
|
2794
|
-
|
|
2795
|
-
|
|
3059
|
+
const valueEnd = skipValue(raw, valueAt2);
|
|
3060
|
+
entries.push({ key: key[0], keyAt, valueAt: valueAt2, valueEnd });
|
|
3061
|
+
i = skipTrivia(raw, valueEnd);
|
|
2796
3062
|
if (raw[i] === ",") i = skipTrivia(raw, i + 1);
|
|
2797
3063
|
else if (raw[i] !== "}") return { entries, clean: false };
|
|
2798
3064
|
}
|
|
@@ -2810,10 +3076,14 @@ function locate(raw) {
|
|
|
2810
3076
|
}
|
|
2811
3077
|
const inner = walkObject(raw, servers.valueAt);
|
|
2812
3078
|
if (!inner.clean) return { ok: false, detail: t("mcp_write.servers_unreadable") };
|
|
2813
|
-
const keys = inner.entries.map((e) => e.key);
|
|
2814
3079
|
return {
|
|
2815
3080
|
ok: true,
|
|
2816
|
-
landing: {
|
|
3081
|
+
landing: {
|
|
3082
|
+
braceAt: servers.valueAt,
|
|
3083
|
+
hasMembers: inner.entries.length > 0,
|
|
3084
|
+
entries: inner.entries,
|
|
3085
|
+
kind: "servers"
|
|
3086
|
+
}
|
|
2817
3087
|
};
|
|
2818
3088
|
}
|
|
2819
3089
|
if (top.entries.some((e) => e.key === "mcpServers")) {
|
|
@@ -2821,15 +3091,29 @@ function locate(raw) {
|
|
|
2821
3091
|
}
|
|
2822
3092
|
return {
|
|
2823
3093
|
ok: true,
|
|
2824
|
-
landing: { braceAt: start, hasMembers: top.entries.length > 0,
|
|
3094
|
+
landing: { braceAt: start, hasMembers: top.entries.length > 0, entries: [], kind: "top" }
|
|
2825
3095
|
};
|
|
2826
3096
|
}
|
|
2827
3097
|
function indentOf(raw) {
|
|
2828
3098
|
return /\n([ \t]+)\S/.exec(raw)?.[1] ?? " ";
|
|
2829
3099
|
}
|
|
2830
|
-
function
|
|
2831
|
-
const
|
|
2832
|
-
|
|
3100
|
+
function eolOf(raw) {
|
|
3101
|
+
const at = raw.indexOf("\n");
|
|
3102
|
+
if (at < 0) return "\n";
|
|
3103
|
+
return raw[at - 1] === "\r" ? "\r\n" : "\n";
|
|
3104
|
+
}
|
|
3105
|
+
function withEol(block, eol) {
|
|
3106
|
+
return eol === "\n" ? block : block.replaceAll("\n", eol);
|
|
3107
|
+
}
|
|
3108
|
+
var init_mcp_json_scan = __esm({
|
|
3109
|
+
"src/mcp-json-scan.ts"() {
|
|
3110
|
+
}
|
|
3111
|
+
});
|
|
3112
|
+
function isWritableName(name) {
|
|
3113
|
+
return /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(name);
|
|
3114
|
+
}
|
|
3115
|
+
function renderEntry(input, indent, pad = indent.repeat(2)) {
|
|
3116
|
+
const inner = pad + indent;
|
|
2833
3117
|
const fields = [`${inner}"transport": ${JSON.stringify(input.transport)}`];
|
|
2834
3118
|
if (input.command) fields.push(`${inner}"command": ${JSON.stringify(input.command)}`);
|
|
2835
3119
|
if (input.args && input.args.length > 0) {
|
|
@@ -2853,7 +3137,7 @@ function insertServerText(raw, input) {
|
|
|
2853
3137
|
const scan = locate(raw);
|
|
2854
3138
|
if (!scan.ok) return { ok: false, reason: "unwritable", detail: scan.detail };
|
|
2855
3139
|
const { landing } = scan;
|
|
2856
|
-
if (landing.
|
|
3140
|
+
if (landing.entries.some((e) => e.key === input.name)) {
|
|
2857
3141
|
return { ok: false, reason: "duplicate", detail: input.name };
|
|
2858
3142
|
}
|
|
2859
3143
|
const indent = indentOf(raw);
|
|
@@ -2865,7 +3149,8 @@ ${indent}"servers": {
|
|
|
2865
3149
|
${entry}
|
|
2866
3150
|
${indent}}${landing.hasMembers ? "," : "\n"}`;
|
|
2867
3151
|
const at = landing.braceAt + 1;
|
|
2868
|
-
|
|
3152
|
+
const eol = eolOf(raw);
|
|
3153
|
+
return { ok: true, text: raw.slice(0, at) + withEol(block, eol) + raw.slice(at) };
|
|
2869
3154
|
}
|
|
2870
3155
|
function writeMcpServer(input, opts = {}) {
|
|
2871
3156
|
if (!isWritableName(input.name)) {
|
|
@@ -2874,19 +3159,8 @@ function writeMcpServer(input, opts = {}) {
|
|
|
2874
3159
|
for (const name of opts.taken ?? []) {
|
|
2875
3160
|
if (name === input.name) return { ok: false, reason: "duplicate", detail: input.name };
|
|
2876
3161
|
}
|
|
2877
|
-
const
|
|
2878
|
-
|
|
2879
|
-
"(add-form)"
|
|
2880
|
-
);
|
|
2881
|
-
if (probe.servers.length === 0) {
|
|
2882
|
-
return {
|
|
2883
|
-
ok: false,
|
|
2884
|
-
reason: "invalid-config",
|
|
2885
|
-
// 分隔符用 ASCII 的 `; `:这几条 message 本身是 plugin-mcp 那边的原文,
|
|
2886
|
-
// 我们只是把它们串起来,不该在中间插一个由**我们**的语言决定的标点
|
|
2887
|
-
detail: probe.issues.map((i) => i.message).join("; ") || t("mcp_write.invalid")
|
|
2888
|
-
};
|
|
2889
|
-
}
|
|
3162
|
+
const invalid = validateAddInput(input);
|
|
3163
|
+
if (invalid !== null) return { ok: false, reason: "invalid-config", detail: invalid };
|
|
2890
3164
|
const configPath2 = mcpConfigPath(opts.homeDir);
|
|
2891
3165
|
let raw;
|
|
2892
3166
|
try {
|
|
@@ -2904,6 +3178,14 @@ function writeMcpServer(input, opts = {}) {
|
|
|
2904
3178
|
}
|
|
2905
3179
|
return { ok: true, configPath: configPath2 };
|
|
2906
3180
|
}
|
|
3181
|
+
function validateAddInput(input, lang) {
|
|
3182
|
+
const probe = parseMcpConfig(
|
|
3183
|
+
JSON.stringify({ servers: { [input.name]: rawFields(input) } }),
|
|
3184
|
+
"(add-form)"
|
|
3185
|
+
);
|
|
3186
|
+
if (probe.servers.length > 0) return null;
|
|
3187
|
+
return probe.issues.map((i) => i.message).join("; ") || t("mcp_write.invalid", {}, lang);
|
|
3188
|
+
}
|
|
2907
3189
|
function rawFields(input) {
|
|
2908
3190
|
return {
|
|
2909
3191
|
transport: input.transport,
|
|
@@ -2930,9 +3212,177 @@ function toServerConfig(input) {
|
|
|
2930
3212
|
var EMPTY_CONFIG;
|
|
2931
3213
|
var init_mcp_config_write = __esm({
|
|
2932
3214
|
"src/mcp-config-write.ts"() {
|
|
3215
|
+
init_mcp_json_scan();
|
|
2933
3216
|
EMPTY_CONFIG = '{\n "servers": {}\n}\n';
|
|
2934
3217
|
}
|
|
2935
3218
|
});
|
|
3219
|
+
function skipSpaces(raw, from) {
|
|
3220
|
+
let i = from;
|
|
3221
|
+
while (i < raw.length && /[ \t\r\n]/.test(raw[i])) i++;
|
|
3222
|
+
return i;
|
|
3223
|
+
}
|
|
3224
|
+
function lineStart(raw, at) {
|
|
3225
|
+
return raw.lastIndexOf("\n", at - 1) + 1;
|
|
3226
|
+
}
|
|
3227
|
+
function lineEnd(raw, at) {
|
|
3228
|
+
const nl = raw.indexOf("\n", at);
|
|
3229
|
+
return nl === -1 ? raw.length : nl;
|
|
3230
|
+
}
|
|
3231
|
+
function padBefore(raw, at) {
|
|
3232
|
+
const ls = lineStart(raw, at);
|
|
3233
|
+
const head = raw.slice(ls, at);
|
|
3234
|
+
return /^[ \t]*$/.test(head) ? head : "";
|
|
3235
|
+
}
|
|
3236
|
+
function findEntry(raw, name) {
|
|
3237
|
+
const scan = locate(raw);
|
|
3238
|
+
if (!scan.ok) return { detail: scan.detail };
|
|
3239
|
+
const entries = scan.landing.entries;
|
|
3240
|
+
const index = entries.findIndex((e) => e.key === name);
|
|
3241
|
+
return index === -1 ? null : { entries, index };
|
|
3242
|
+
}
|
|
3243
|
+
function replaceServerText(raw, input) {
|
|
3244
|
+
const found = findEntry(raw, input.name);
|
|
3245
|
+
if (found !== null && "detail" in found) {
|
|
3246
|
+
return { ok: false, reason: "unwritable", detail: found.detail };
|
|
3247
|
+
}
|
|
3248
|
+
if (found === null) return { ok: false, reason: "invalid-name", detail: input.name };
|
|
3249
|
+
const target = found.entries[found.index];
|
|
3250
|
+
const pad = padBefore(raw, target.keyAt);
|
|
3251
|
+
const entry = renderEntry(input, indentOf(raw), pad);
|
|
3252
|
+
const block = withEol(entry.slice(pad.length), eolOf(raw));
|
|
3253
|
+
const text = raw.slice(0, target.keyAt) + block + raw.slice(target.valueEnd);
|
|
3254
|
+
return { ok: true, text };
|
|
3255
|
+
}
|
|
3256
|
+
function removeCuts(raw, entries, index) {
|
|
3257
|
+
const self = entries[index];
|
|
3258
|
+
const afterValue = skipTrivia(raw, self.valueEnd);
|
|
3259
|
+
if (raw[afterValue] === ",") {
|
|
3260
|
+
return skipSpaces(raw, self.valueEnd) === afterValue ? [tidyLine(raw, self.keyAt, afterValue + 1)] : [
|
|
3261
|
+
[self.keyAt, self.valueEnd],
|
|
3262
|
+
[afterValue, afterValue + 1]
|
|
3263
|
+
];
|
|
3264
|
+
}
|
|
3265
|
+
const prev = index > 0 ? entries[index - 1] : void 0;
|
|
3266
|
+
const prevComma = prev === void 0 ? -1 : skipTrivia(raw, prev.valueEnd);
|
|
3267
|
+
if (prevComma < 0 || raw[prevComma] !== ",") {
|
|
3268
|
+
return [tidyLine(raw, self.keyAt, self.valueEnd)];
|
|
3269
|
+
}
|
|
3270
|
+
return [
|
|
3271
|
+
[prevComma, prevComma + 1],
|
|
3272
|
+
[lineStartIfBlank(raw, self.keyAt), self.valueEnd]
|
|
3273
|
+
];
|
|
3274
|
+
}
|
|
3275
|
+
function tidyLine(raw, keyAt, to) {
|
|
3276
|
+
const ls = lineStart(raw, keyAt);
|
|
3277
|
+
const le = lineEnd(raw, to);
|
|
3278
|
+
const headBlank = /^[ \t]*$/.test(raw.slice(ls, keyAt));
|
|
3279
|
+
const tailBlank = /^[ \t\r]*$/.test(raw.slice(to, le));
|
|
3280
|
+
if (!headBlank || !tailBlank) return [keyAt, to];
|
|
3281
|
+
return [ls, Math.min(le + 1, raw.length)];
|
|
3282
|
+
}
|
|
3283
|
+
function lineStartIfBlank(raw, at) {
|
|
3284
|
+
const ls = lineStart(raw, at);
|
|
3285
|
+
return /^[ \t]*$/.test(raw.slice(ls, at)) ? ls : at;
|
|
3286
|
+
}
|
|
3287
|
+
function removeServerText(raw, name) {
|
|
3288
|
+
const found = findEntry(raw, name);
|
|
3289
|
+
if (found !== null && "detail" in found) {
|
|
3290
|
+
return { ok: false, reason: "unwritable", detail: found.detail };
|
|
3291
|
+
}
|
|
3292
|
+
if (found === null) return { ok: false, reason: "invalid-name", detail: name };
|
|
3293
|
+
const cuts = [...removeCuts(raw, found.entries, found.index)].sort((a, b) => b[0] - a[0]);
|
|
3294
|
+
let text = raw;
|
|
3295
|
+
for (const [from, to] of cuts) text = text.slice(0, from) + text.slice(to);
|
|
3296
|
+
return { ok: true, text };
|
|
3297
|
+
}
|
|
3298
|
+
function commit(splice, expectKeys, opts) {
|
|
3299
|
+
const configPath2 = mcpConfigPath(opts.homeDir);
|
|
3300
|
+
if (!existsSync(configPath2)) return { ok: false, reason: "not-found", detail: configPath2 };
|
|
3301
|
+
let raw;
|
|
3302
|
+
try {
|
|
3303
|
+
raw = readFileSync(configPath2, "utf-8");
|
|
3304
|
+
} catch (err) {
|
|
3305
|
+
return { ok: false, reason: "unwritable", detail: describe5(err) };
|
|
3306
|
+
}
|
|
3307
|
+
const before = locate(raw);
|
|
3308
|
+
if (!before.ok) return { ok: false, reason: "unwritable", detail: before.detail };
|
|
3309
|
+
const spliced = splice(raw);
|
|
3310
|
+
if (!spliced.ok) {
|
|
3311
|
+
return spliced.reason === "invalid-name" ? { ok: false, reason: "not-found", detail: spliced.detail } : spliced;
|
|
3312
|
+
}
|
|
3313
|
+
const bad = checkSpliced(
|
|
3314
|
+
spliced.text,
|
|
3315
|
+
expectKeys(before.landing.entries.map((e) => e.key)),
|
|
3316
|
+
opts.lang
|
|
3317
|
+
);
|
|
3318
|
+
if (bad !== null) return { ok: false, reason: "unwritable", detail: bad };
|
|
3319
|
+
return write(configPath2, raw, spliced.text);
|
|
3320
|
+
}
|
|
3321
|
+
function checkSpliced(text, expect, lang) {
|
|
3322
|
+
const after = locate(text);
|
|
3323
|
+
if (!after.ok) return after.detail;
|
|
3324
|
+
const got = after.landing.entries.map((e) => e.key);
|
|
3325
|
+
if (got.length !== expect.length || got.some((k, i) => k !== expect[i])) {
|
|
3326
|
+
return `${t("mcp_write.invalid", {}, lang)} (servers: [${got.join(", ")}] != [${expect.join(", ")}])`;
|
|
3327
|
+
}
|
|
3328
|
+
return null;
|
|
3329
|
+
}
|
|
3330
|
+
function write(configPath2, expectDisk, body) {
|
|
3331
|
+
let disk;
|
|
3332
|
+
try {
|
|
3333
|
+
disk = readFileSync(configPath2, "utf-8");
|
|
3334
|
+
} catch (err) {
|
|
3335
|
+
return { ok: false, reason: "unwritable", detail: describe5(err) };
|
|
3336
|
+
}
|
|
3337
|
+
if (disk !== expectDisk) {
|
|
3338
|
+
return { ok: false, reason: "unwritable", detail: t("mcp_config.stale_disk") };
|
|
3339
|
+
}
|
|
3340
|
+
const tmp = `${configPath2}.${process.pid}.tmp`;
|
|
3341
|
+
try {
|
|
3342
|
+
mkdirSync(dirname(configPath2), { recursive: true });
|
|
3343
|
+
const mode = statSync(configPath2).mode & 511;
|
|
3344
|
+
try {
|
|
3345
|
+
writeFileSync(tmp, body, { encoding: "utf-8", mode });
|
|
3346
|
+
renameSync(tmp, configPath2);
|
|
3347
|
+
} catch (err) {
|
|
3348
|
+
rmSync(tmp, { force: true });
|
|
3349
|
+
throw err;
|
|
3350
|
+
}
|
|
3351
|
+
} catch (err) {
|
|
3352
|
+
return { ok: false, reason: "unwritable", detail: describe5(err) };
|
|
3353
|
+
}
|
|
3354
|
+
return { ok: true, configPath: configPath2 };
|
|
3355
|
+
}
|
|
3356
|
+
function updateMcpServer(input, opts = {}) {
|
|
3357
|
+
const invalid = validateAddInput(input, opts.lang);
|
|
3358
|
+
if (invalid !== null) return { ok: false, reason: "invalid-config", detail: invalid };
|
|
3359
|
+
return commit(
|
|
3360
|
+
(raw) => replaceServerText(raw, input),
|
|
3361
|
+
(before) => before,
|
|
3362
|
+
opts
|
|
3363
|
+
);
|
|
3364
|
+
}
|
|
3365
|
+
function removeMcpServer(name, opts = {}) {
|
|
3366
|
+
return commit(
|
|
3367
|
+
(raw) => removeServerText(raw, name),
|
|
3368
|
+
(before) => before.filter((k) => k !== name),
|
|
3369
|
+
opts
|
|
3370
|
+
);
|
|
3371
|
+
}
|
|
3372
|
+
function describe5(err) {
|
|
3373
|
+
return err instanceof Error ? err.message : String(err);
|
|
3374
|
+
}
|
|
3375
|
+
var init_mcp_config_edit = __esm({
|
|
3376
|
+
"src/mcp-config-edit.ts"() {
|
|
3377
|
+
init_mcp_config_write();
|
|
3378
|
+
init_mcp_json_scan();
|
|
3379
|
+
}
|
|
3380
|
+
});
|
|
3381
|
+
function foldToolMode(mode) {
|
|
3382
|
+
if (mode === "both") return "both";
|
|
3383
|
+
if (mode === "code") return "code";
|
|
3384
|
+
return "tools";
|
|
3385
|
+
}
|
|
2936
3386
|
function applyShell(kind, diags) {
|
|
2937
3387
|
if (!kind || kind === "cmd") {
|
|
2938
3388
|
setShell(kind ?? null);
|
|
@@ -2945,29 +3395,40 @@ function applyShell(kind, diags) {
|
|
|
2945
3395
|
return;
|
|
2946
3396
|
}
|
|
2947
3397
|
setShell(kind);
|
|
2948
|
-
diags.ok("Shell",
|
|
3398
|
+
diags.ok("Shell", t("boot.shell_kind", { kind }));
|
|
2949
3399
|
}
|
|
2950
3400
|
function registerLocalTools(input) {
|
|
2951
|
-
const { trackerDb, goals, memory, sandbox, diags } = input;
|
|
2952
|
-
const registry = new ToolRegistry();
|
|
3401
|
+
const { trackerDb, goals, memory, sandbox, skills, diags } = input;
|
|
3402
|
+
const registry = new ToolRegistry({ codeMode: input.toolMode === "code" });
|
|
2953
3403
|
for (const plugin of [filePlugin, terminalPlugin, webPlugin, lspPlugin]) {
|
|
2954
3404
|
try {
|
|
2955
3405
|
registry.registerPlugin(plugin);
|
|
2956
3406
|
} catch (err) {
|
|
2957
3407
|
diags.failed(
|
|
2958
3408
|
`Plugin ${plugin.name}`,
|
|
2959
|
-
|
|
3409
|
+
t("boot.plugin_register_failed", {
|
|
3410
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
3411
|
+
}),
|
|
2960
3412
|
"plugin-register-failed"
|
|
2961
3413
|
);
|
|
2962
3414
|
}
|
|
2963
3415
|
}
|
|
2964
3416
|
const rg = resolveRipgrep();
|
|
2965
3417
|
if (rg.mode === "missing") {
|
|
2966
|
-
diags.warn("Ripgrep",
|
|
3418
|
+
diags.warn("Ripgrep", t("boot.ripgrep_missing", { hint: rg.hint ?? "" }), "ripgrep-missing");
|
|
3419
|
+
} else if (rg.mode === "not-executable") {
|
|
3420
|
+
diags.warn(
|
|
3421
|
+
"Ripgrep",
|
|
3422
|
+
t("boot.ripgrep_not_executable", { hint: rg.hint ?? "" }),
|
|
3423
|
+
"ripgrep-not-executable"
|
|
3424
|
+
);
|
|
2967
3425
|
} else if (rg.mode === "system") {
|
|
2968
|
-
diags.ok("Ripgrep", "
|
|
3426
|
+
diags.ok("Ripgrep", t("boot.ripgrep_system"));
|
|
2969
3427
|
} else {
|
|
2970
|
-
diags.ok(
|
|
3428
|
+
diags.ok(
|
|
3429
|
+
"Ripgrep",
|
|
3430
|
+
t("boot.ripgrep_builtin", { version: rg.version === void 0 ? "" : ` ${rg.version}` })
|
|
3431
|
+
);
|
|
2971
3432
|
}
|
|
2972
3433
|
const search = selectSearchProvider(input.search);
|
|
2973
3434
|
if (search.provider) {
|
|
@@ -2977,16 +3438,47 @@ function registerLocalTools(input) {
|
|
|
2977
3438
|
input.search?.maxResults ? { defaultMaxResults: input.search.maxResults } : {}
|
|
2978
3439
|
)
|
|
2979
3440
|
);
|
|
2980
|
-
diags.ok("
|
|
3441
|
+
diags.ok("WebSearch", search.detail);
|
|
2981
3442
|
} else {
|
|
2982
|
-
diags.skipped("
|
|
3443
|
+
diags.skipped("WebSearch", search.detail);
|
|
2983
3444
|
}
|
|
2984
3445
|
if (trackerDb) registry.registerTool(createTodoTool(trackerDb));
|
|
2985
3446
|
if (goals) registry.registerTool(createGoalTool({ service: goals }));
|
|
2986
3447
|
if (memory) registry.registerTool(createMemoryTool(memory));
|
|
2987
3448
|
if (sandbox) registry.registerTool(createCodeExecTool(sandbox));
|
|
3449
|
+
if (input.toolMode === "both" || input.toolMode === "code") {
|
|
3450
|
+
registry.registerTool(createRunCodeTool());
|
|
3451
|
+
}
|
|
3452
|
+
if (skills) registry.registerTool(createSkillViewTool({ skills }));
|
|
3453
|
+
if (skills) registry.registerTools(createSkillWriteTools({ skills }));
|
|
3454
|
+
if (input.deferMcp === true) {
|
|
3455
|
+
registry.registerTool(
|
|
3456
|
+
createToolSearchTool({
|
|
3457
|
+
deferred: () => registry.byExposure(ToolExposure.Deferred),
|
|
3458
|
+
reveal: input.bootstrapReveal ?? createToolRevealState()
|
|
3459
|
+
})
|
|
3460
|
+
);
|
|
3461
|
+
}
|
|
2988
3462
|
return registry;
|
|
2989
3463
|
}
|
|
3464
|
+
function reportToolCost(registry, diags) {
|
|
3465
|
+
const exposed = registry.exposed();
|
|
3466
|
+
const mcp = exposed.filter(
|
|
3467
|
+
(tool) => (registry.sourceOf(tool.name) ?? "").startsWith("mcp:")
|
|
3468
|
+
).length;
|
|
3469
|
+
const local = exposed.length - mcp;
|
|
3470
|
+
const tokens = Math.round(toolTableTokens(exposed).total / 100) / 10;
|
|
3471
|
+
const split = (lang) => mcp > 0 ? t("boot.tools_split_mixed", { local, mcp }, lang) : t("boot.tools_split_local", { local }, lang);
|
|
3472
|
+
const deferred = registry.byExposure(ToolExposure.Deferred).length;
|
|
3473
|
+
diags.ok(
|
|
3474
|
+
TOOLS_MODULE,
|
|
3475
|
+
(lang) => deferred > 0 ? t(
|
|
3476
|
+
"boot.tools_cost_deferred",
|
|
3477
|
+
{ count: exposed.length, split: split(lang), tokens, deferred },
|
|
3478
|
+
lang
|
|
3479
|
+
) : t("boot.tools_cost", { count: exposed.length, split: split(lang), tokens }, lang)
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
2990
3482
|
function registerSessionSearch(registry, input) {
|
|
2991
3483
|
if (!input.session) {
|
|
2992
3484
|
input.diags.skipped(
|
|
@@ -3030,7 +3522,13 @@ function registerAskQuestion(registry, onQuestionRequest, interactive) {
|
|
|
3030
3522
|
createAskQuestionTool(interactive ? { requestQuestion: onQuestionRequest } : {})
|
|
3031
3523
|
);
|
|
3032
3524
|
}
|
|
3033
|
-
function
|
|
3525
|
+
function registerMcpBatch(registry, tools, server, defer) {
|
|
3526
|
+
registry.registerTools(
|
|
3527
|
+
defer ? tools.map((tool) => ({ ...tool, exposure: ToolExposure.Deferred })) : [...tools],
|
|
3528
|
+
`mcp:${server}`
|
|
3529
|
+
);
|
|
3530
|
+
}
|
|
3531
|
+
function createToolSync(mcp, registry, defer) {
|
|
3034
3532
|
let queue = Promise.resolve();
|
|
3035
3533
|
const replace = (server) => {
|
|
3036
3534
|
queue = queue.then(async () => {
|
|
@@ -3038,9 +3536,11 @@ function createToolSync(mcp, registry) {
|
|
|
3038
3536
|
const fresh = await withTimeout(mcp.refresh(server), MCP_LIST_BUDGET_MS, null) ?? [];
|
|
3039
3537
|
const prefix = `mcp__${server}__`;
|
|
3040
3538
|
registry.unregisterBySource(`mcp:${server}`);
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3539
|
+
registerMcpBatch(
|
|
3540
|
+
registry,
|
|
3541
|
+
fresh.filter((t25) => t25.name.startsWith(prefix)),
|
|
3542
|
+
server,
|
|
3543
|
+
defer
|
|
3044
3544
|
);
|
|
3045
3545
|
} catch {
|
|
3046
3546
|
}
|
|
@@ -3049,7 +3549,7 @@ function createToolSync(mcp, registry) {
|
|
|
3049
3549
|
};
|
|
3050
3550
|
return { settled: () => queue, replace };
|
|
3051
3551
|
}
|
|
3052
|
-
function wireToolRefresh(mcp, registry, sync = createToolSync(mcp, registry)) {
|
|
3552
|
+
function wireToolRefresh(mcp, registry, sync = createToolSync(mcp, registry, false)) {
|
|
3053
3553
|
mcp.onToolsChanged = (server) => void sync.replace(server);
|
|
3054
3554
|
return sync;
|
|
3055
3555
|
}
|
|
@@ -3098,8 +3598,6 @@ async function addServer(mcp, sync, input, homeDir) {
|
|
|
3098
3598
|
return {
|
|
3099
3599
|
ok: true,
|
|
3100
3600
|
configPath: written.configPath,
|
|
3101
|
-
// `connectAll` 恒回一台一条,兜底那一支是给类型系统看的 —— 真走到了说明
|
|
3102
|
-
// registry 那边变了形状,那时报一台「连不上、没说为什么」比崩掉强
|
|
3103
3601
|
status: status ?? mcp.getStatus(input.name) ?? {
|
|
3104
3602
|
name: input.name,
|
|
3105
3603
|
connected: false,
|
|
@@ -3109,6 +3607,32 @@ async function addServer(mcp, sync, input, homeDir) {
|
|
|
3109
3607
|
}
|
|
3110
3608
|
};
|
|
3111
3609
|
}
|
|
3610
|
+
async function updateServer(mcp, sync, input, homeDir) {
|
|
3611
|
+
const written = updateMcpServer(input, { homeDir });
|
|
3612
|
+
if (!written.ok) return written;
|
|
3613
|
+
await mcp.disconnect(input.name);
|
|
3614
|
+
const config = toServerConfig(input);
|
|
3615
|
+
const [status] = await mcp.connectAll([config], "user");
|
|
3616
|
+
await sync.replace(input.name);
|
|
3617
|
+
return {
|
|
3618
|
+
ok: true,
|
|
3619
|
+
configPath: written.configPath,
|
|
3620
|
+
status: status ?? mcp.getStatus(input.name) ?? {
|
|
3621
|
+
name: input.name,
|
|
3622
|
+
connected: false,
|
|
3623
|
+
toolCount: 0,
|
|
3624
|
+
reconnectAttempts: 0,
|
|
3625
|
+
source: "user"
|
|
3626
|
+
}
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
async function removeServer(mcp, sync, name, homeDir) {
|
|
3630
|
+
const written = removeMcpServer(name, { homeDir });
|
|
3631
|
+
if (!written.ok) return written;
|
|
3632
|
+
await mcp.disconnect(name);
|
|
3633
|
+
await sync.replace(name);
|
|
3634
|
+
return { ok: true, configPath: written.configPath };
|
|
3635
|
+
}
|
|
3112
3636
|
async function connectAllServers(mcp, batches, budgetMs = MCP_CONNECT_BUDGET_MS) {
|
|
3113
3637
|
const all = await withTimeout(
|
|
3114
3638
|
Promise.all(batches.map((b) => mcp.connectAll([...b.servers], b.source))),
|
|
@@ -3142,9 +3666,9 @@ function reportTaken(source, name, diags) {
|
|
|
3142
3666
|
"host-mcp-name-taken"
|
|
3143
3667
|
);
|
|
3144
3668
|
}
|
|
3145
|
-
async function registerMcpTools(registry, homeDir, diags, injected = []) {
|
|
3669
|
+
async function registerMcpTools(registry, homeDir, diags, injected = [], defer = false) {
|
|
3146
3670
|
const mcp = new McpRegistry({ homeDir });
|
|
3147
|
-
const sync = createToolSync(mcp, registry);
|
|
3671
|
+
const sync = createToolSync(mcp, registry, defer);
|
|
3148
3672
|
const live = () => ({
|
|
3149
3673
|
dispose: () => void mcp.disconnectAll().catch(() => {
|
|
3150
3674
|
}),
|
|
@@ -3156,6 +3680,12 @@ async function registerMcpTools(registry, homeDir, diags, injected = []) {
|
|
|
3156
3680
|
if (outcome.ok) wireToolRefresh(mcp, registry, sync);
|
|
3157
3681
|
return outcome;
|
|
3158
3682
|
},
|
|
3683
|
+
update: async (input) => {
|
|
3684
|
+
const outcome = await updateServer(mcp, sync, input, homeDir);
|
|
3685
|
+
if (outcome.ok) wireToolRefresh(mcp, registry, sync);
|
|
3686
|
+
return outcome;
|
|
3687
|
+
},
|
|
3688
|
+
remove: (name) => removeServer(mcp, sync, name, homeDir),
|
|
3159
3689
|
readConfig: () => readMcpConfigFile({ homeDir }),
|
|
3160
3690
|
writeConfig: (text, revision, lang) => writeMcpConfigFile(text, {
|
|
3161
3691
|
revision,
|
|
@@ -3176,19 +3706,19 @@ async function registerMcpTools(registry, homeDir, diags, injected = []) {
|
|
|
3176
3706
|
try {
|
|
3177
3707
|
const { servers, issues } = mcp.loadFromConfig(mcpConfigPath(homeDir));
|
|
3178
3708
|
for (const issue of issues) {
|
|
3179
|
-
diags.warn(`
|
|
3709
|
+
diags.warn(`MCPConfig ${issue.path}`, issue.message, "mcp-config-invalid");
|
|
3180
3710
|
}
|
|
3181
3711
|
const extra = dropTakenNames(injected, new Set(servers.map((s) => s.name)), diags);
|
|
3182
3712
|
const injectedCount = extra.reduce((sum, b) => sum + b.servers.length, 0);
|
|
3183
3713
|
if (servers.length === 0 && injectedCount === 0) {
|
|
3184
|
-
diags.skipped("MCP", "
|
|
3714
|
+
diags.skipped("MCP", t("boot.mcp_none"));
|
|
3185
3715
|
return live();
|
|
3186
3716
|
}
|
|
3187
3717
|
const statuses = await connectAllServers(mcp, [{ source: "user", servers }, ...extra]);
|
|
3188
3718
|
if (statuses === null) {
|
|
3189
3719
|
diags.failed(
|
|
3190
3720
|
"MCP",
|
|
3191
|
-
|
|
3721
|
+
t("boot.mcp_connect_timeout", { seconds: MCP_CONNECT_BUDGET_MS / 1e3 }),
|
|
3192
3722
|
"mcp-connect-timeout"
|
|
3193
3723
|
);
|
|
3194
3724
|
return live();
|
|
@@ -3198,14 +3728,22 @@ async function registerMcpTools(registry, homeDir, diags, injected = []) {
|
|
|
3198
3728
|
if (!st.connected) {
|
|
3199
3729
|
diags.failed(
|
|
3200
3730
|
`MCP ${st.name}`,
|
|
3201
|
-
|
|
3731
|
+
t("boot.mcp_connect_failed", {
|
|
3732
|
+
detail: st.lastError ? t("boot.mcp_connect_failed_detail", { detail: st.lastError }) : ""
|
|
3733
|
+
}),
|
|
3202
3734
|
"mcp-connect-failed"
|
|
3203
3735
|
);
|
|
3204
3736
|
continue;
|
|
3205
3737
|
}
|
|
3206
|
-
const own = mcpTools.filter((
|
|
3207
|
-
|
|
3208
|
-
|
|
3738
|
+
const own = mcpTools.filter((t25) => t25.name.startsWith(`mcp__${st.name}__`));
|
|
3739
|
+
const registered = dropOverlongToolNames(
|
|
3740
|
+
own,
|
|
3741
|
+
`MCP ${st.name}`,
|
|
3742
|
+
"mcp-tool-name-too-long",
|
|
3743
|
+
diags
|
|
3744
|
+
);
|
|
3745
|
+
registerMcpBatch(registry, registered, st.name, defer);
|
|
3746
|
+
diags.ok(`MCP ${st.name}`, t("boot.mcp_tools", { count: registered.length }));
|
|
3209
3747
|
if (st.lastError) diags.warn(`MCP ${st.name}`, st.lastError, "mcp-degraded");
|
|
3210
3748
|
}
|
|
3211
3749
|
wireToolRefresh(mcp, registry, sync);
|
|
@@ -3213,21 +3751,25 @@ async function registerMcpTools(registry, homeDir, diags, injected = []) {
|
|
|
3213
3751
|
} catch (err) {
|
|
3214
3752
|
diags.failed(
|
|
3215
3753
|
"MCP",
|
|
3216
|
-
|
|
3754
|
+
t("boot.mcp_init_failed", {
|
|
3755
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
3756
|
+
}),
|
|
3217
3757
|
"mcp-init-failed"
|
|
3218
3758
|
);
|
|
3219
3759
|
return live();
|
|
3220
3760
|
}
|
|
3221
3761
|
}
|
|
3222
|
-
var MCP_CONNECT_BUDGET_MS, MCP_LIST_BUDGET_MS, SESSION_SEARCH_MODULE;
|
|
3762
|
+
var MCP_CONNECT_BUDGET_MS, MCP_LIST_BUDGET_MS, TOOLS_MODULE, SESSION_SEARCH_MODULE;
|
|
3223
3763
|
var init_tools = __esm({
|
|
3224
3764
|
"src/tools.ts"() {
|
|
3225
3765
|
init_host_capabilities();
|
|
3226
3766
|
init_mcp_config_apply();
|
|
3767
|
+
init_mcp_config_edit();
|
|
3227
3768
|
init_mcp_config_file();
|
|
3228
3769
|
init_mcp_config_write();
|
|
3229
3770
|
MCP_CONNECT_BUDGET_MS = 15e3;
|
|
3230
3771
|
MCP_LIST_BUDGET_MS = 1e4;
|
|
3772
|
+
TOOLS_MODULE = "Tools";
|
|
3231
3773
|
SESSION_SEARCH_MODULE = "SessionSearch";
|
|
3232
3774
|
}
|
|
3233
3775
|
});
|
|
@@ -3271,11 +3813,6 @@ var init_session_factory = __esm({
|
|
|
3271
3813
|
SessionBridges = class {
|
|
3272
3814
|
byId = /* @__PURE__ */ new Map();
|
|
3273
3815
|
override;
|
|
3274
|
-
/**
|
|
3275
|
-
* @param override 宿主自己实现的阻塞式确认(`BuildRuntimeOptions.onApprovalRequest`)。
|
|
3276
|
-
* 给了就**所有会话都走它**:那种宿主(CLI 的 inquirer)本来就是单会话的,
|
|
3277
|
-
* 它的回调签名里也没有 sessionId 的位置。
|
|
3278
|
-
*/
|
|
3279
3816
|
constructor(override) {
|
|
3280
3817
|
this.override = override;
|
|
3281
3818
|
}
|
|
@@ -3285,32 +3822,11 @@ var init_session_factory = __esm({
|
|
|
3285
3822
|
release(sessionId) {
|
|
3286
3823
|
this.byId.delete(sessionId);
|
|
3287
3824
|
}
|
|
3288
|
-
/*
|
|
3289
|
-
* ⚠️ 这儿原来有一个 `planOwner()`(记「最后一次交计划的是谁」),
|
|
3290
|
-
* 2026-08-16 **删掉**了。它存在的唯一理由是 `PlanModeState.onChange` 上没有
|
|
3291
|
-
* 会话 id —— 因为那时候 plan 模式是**进程级**的一个状态对象。现在一个会话一个
|
|
3292
|
-
* 状态,`onChange` 的宿主自己就知道自己是谁。
|
|
3293
|
-
*
|
|
3294
|
-
* **不是留着不调,是必须删**:两个会话各自挂着落盘 hook、而引导会话那一份还
|
|
3295
|
-
* 按 `planOwner()` 决定写到谁名下的话,会话 B 交过一次计划之后,引导会话再批
|
|
3296
|
-
* 一份就会把 A 的正文写到 B 名下。判据全文在 `session-guard.ts` 的文件头末节。
|
|
3297
|
-
*/
|
|
3298
|
-
/**
|
|
3299
|
-
* 转一条审批给它该去的那个会话。
|
|
3300
|
-
*
|
|
3301
|
-
* 认不出这个 sessionId(会话刚被删 / 被冷却,而它那一轮还在收尾)时回 `deny` ——
|
|
3302
|
-
* 方向逐字同 `AgentSession` 的 `settleAbandoned`:审批的安全侧就是拒绝。
|
|
3303
|
-
* 挂着不答的话,那个孤儿 `AgentLoop` 会永远停在 await 上,进程退不掉。
|
|
3304
|
-
*/
|
|
3305
3825
|
approve(sessionId, req) {
|
|
3306
3826
|
if (this.override) return this.override(req);
|
|
3307
3827
|
const bridge = this.byId.get(sessionId);
|
|
3308
3828
|
return bridge ? bridge.approve(req) : Promise.resolve("deny");
|
|
3309
3829
|
}
|
|
3310
|
-
/**
|
|
3311
|
-
* 转一条提问。认不出时回 `skipped` —— 那是**实话**(确实没人答),
|
|
3312
|
-
* 而替用户挑一个 label 才是伪造(方案 34 §2.4)。
|
|
3313
|
-
*/
|
|
3314
3830
|
ask(sessionId, req) {
|
|
3315
3831
|
const bridge = this.byId.get(sessionId);
|
|
3316
3832
|
return bridge ? bridge.ask(req) : Promise.resolve({ answers: {}, skipped: true });
|
|
@@ -3334,82 +3850,27 @@ var init_session_factory = __esm({
|
|
|
3334
3850
|
get(sessionId) {
|
|
3335
3851
|
return this.live.get(sessionId) ?? null;
|
|
3336
3852
|
}
|
|
3337
|
-
/**
|
|
3338
|
-
* 把**引导会话**装进表里。装配层专用,`bind()` 那两道门在这条路上不走 ——
|
|
3339
|
-
* 它的工作区已经 `seed()` 进去了(判据见 workspace.ts 的 `seed`)。
|
|
3340
|
-
*
|
|
3341
|
-
* @returns 建不出 `AgentLoop`(provider 没起来)时 null。那不是错误:
|
|
3342
|
-
* 一台还没配 API key 的机器上跑 `epoch doctor` 走的就是这条路
|
|
3343
|
-
*/
|
|
3344
3853
|
adopt(sessionId, history) {
|
|
3345
3854
|
this.bootstrap = sessionId;
|
|
3346
|
-
const live = this.assemble(sessionId,
|
|
3855
|
+
const live = this.assemble(sessionId, history, false);
|
|
3347
3856
|
if (live) this.live.set(sessionId, live);
|
|
3348
3857
|
return live;
|
|
3349
3858
|
}
|
|
3350
|
-
/**
|
|
3351
|
-
* `/resume` 换了 id 之后把表跟着改一下(`SessionControl.resume`)。
|
|
3352
|
-
*
|
|
3353
|
-
* `AgentSession.resume()` 自己会把 `sessionId` 换掉,但**这张表的键不会跟着变** ——
|
|
3354
|
-
* 不补这一步的话 `get(新 id)` 是 null,于是浏览器刚 resume 过去的那个会话
|
|
3355
|
-
* 在这个进程眼里不存在。
|
|
3356
|
-
*
|
|
3357
|
-
* ## ⚠️ 搬的是**两格**,桥那一格 2026-08-18 才补上
|
|
3358
|
-
*
|
|
3359
|
-
* 原来这儿只有 `bridges.release(from)`,没有按新键 `register` 一次
|
|
3360
|
-
* (`register` 只出现在建会话那一条路上,见 {@link assemble})。于是 resume
|
|
3361
|
-
* 之后新 id 在路由表里是空的,而**后果是静默答错不是报错**:
|
|
3362
|
-
* `ask_user_question` 回 `skipped`、plan 审批回 `deny` —— 模型问不出问题、
|
|
3363
|
-
* 批不了计划,屏幕上只是「它没问」。
|
|
3364
|
-
*
|
|
3365
|
-
* **这不是 2026-08-17 那轮 `from` 取早一步的修复改出来的**:那之前
|
|
3366
|
-
* `ToolContext.sessionId` 已经是新 id 而桥登记在旧 id 上,一样查不到。
|
|
3367
|
-
* 两种情形下新 id 都无桥,那一轮既没让它变坏也没变好。
|
|
3368
|
-
*
|
|
3369
|
-
* ⚠️ **不是「只有 web 才踩」**(2026-08-18 实测修正的一处口径)。`/resume`
|
|
3370
|
-
* 今天确实只有 TUI 走得到,而 TUI 上三条路**分成两档**:
|
|
3371
|
-
*
|
|
3372
|
-
* - 普通工具的权限审批 **不走**这一层(`onApprovalRequest` 直接给这个会话
|
|
3373
|
-
* 自己那座桥,见 {@link assemble} 那条注释)—— 那一档确实没事;
|
|
3374
|
-
* - `ask_user_question` 和 `exit_plan_mode` 交计划 **走**(`build.ts` 里
|
|
3375
|
-
* 那两句 `(req, sid) => bridges.ask/approve(sid, req)`)—— 这两样 TUI
|
|
3376
|
-
* `/resume` 之后一样是坏的。判据:`ToolContext.sessionId` 来自
|
|
3377
|
-
* `AgentLoop` 的 `input.sessionId`,也就是换过之后那个新 id。
|
|
3378
|
-
*
|
|
3379
|
-
* ## 判了哪条治法:**把建会话那一刻那份桥原样 register 到新键上**
|
|
3380
|
-
*
|
|
3381
|
-
* 判据只有一条,而它是**读出来的不是推的**:{@link
|
|
3382
|
-
* AgentSession.createApprovalBridge} 那两个回调闭的是 `target`(那个
|
|
3383
|
-
* `AgentSession` **实例**)和一个 `buffered` 数组,**一个 id 都没闭** ——
|
|
3384
|
-
* 没有 sessionId、没有工作区、没有 workDir。而 `AgentSession.resume()` 是
|
|
3385
|
-
* **就地**改 `this.sessionId`(实例不换、`attach` 过的 `target` 还是它)。
|
|
3386
|
-
* 所以「同一份桥」指的就是「用户此刻在看的那一段会话」,不是一个过期快照。
|
|
3387
|
-
*
|
|
3388
|
-
* 这也顺带答了「resume 过去之后这些回调该指向谁」——**照旧指向这一头**。
|
|
3389
|
-
* 判据同 `build.ts` 那个回调里 `workspaces.seed` / `rekeyBackgroundTasks`
|
|
3390
|
-
* 两行(2026-08-17 写的):`/resume` 只是把另一段对话接上**这个进程的这个
|
|
3391
|
-
* 引擎**,问答通道的另一头照旧是这个进程的宿主(TUI 的屏幕 / 浏览器那条 SSE)。
|
|
3392
|
-
* 「那段会话当初那个宿主」根本不存在 —— 那个进程早退了。
|
|
3393
|
-
*
|
|
3394
|
-
* **被否掉的另一条:在这里 `createApprovalBridge()` 新建一座登记上去。**
|
|
3395
|
-
* 它比今天更坏,两处:① `attach()` 绑的是新闭包里那个 `target`,而正在跑的
|
|
3396
|
-
* 那个 `AgentSession` 已经和旧桥绑好了 —— 新桥的请求会落进一条**没有消费者**
|
|
3397
|
-
* 的 `buffered`,于是 `ask` 从「回 `skipped`」变成「永远不返回」,那个
|
|
3398
|
-
* `AgentLoop` 挂在 await 上,进程退不掉;② 旧桥上**已经在飞**的请求
|
|
3399
|
-
* (`inFlight` 里那些)连带失去归属。一句话:桥不是按 id 建的,
|
|
3400
|
-
* 所以换 id 这件事上它压根不该重建。
|
|
3401
|
-
*/
|
|
3402
3859
|
rekey(from, to) {
|
|
3403
3860
|
const live = this.live.get(from);
|
|
3404
3861
|
if (!live || from === to) return;
|
|
3405
3862
|
this.live.delete(from);
|
|
3406
3863
|
this.opts.bridges.release(from);
|
|
3864
|
+
const workspaces = this.opts.workspaces;
|
|
3407
3865
|
const moved = {
|
|
3408
3866
|
...live,
|
|
3409
3867
|
sessionId: to,
|
|
3410
|
-
workspace
|
|
3868
|
+
get workspace() {
|
|
3869
|
+
return workspaces.of(to);
|
|
3870
|
+
}
|
|
3411
3871
|
};
|
|
3412
3872
|
this.live.set(to, moved);
|
|
3873
|
+
this.opts.processes.claim(to);
|
|
3413
3874
|
this.opts.bridges.register(to, moved.bridge);
|
|
3414
3875
|
if (this.bootstrap === from) this.bootstrap = to;
|
|
3415
3876
|
}
|
|
@@ -3425,15 +3886,13 @@ var init_session_factory = __esm({
|
|
|
3425
3886
|
if (hit) return { ok: true, live: hit, created: false };
|
|
3426
3887
|
}
|
|
3427
3888
|
const sessionId = this.newId();
|
|
3428
|
-
let bound = null;
|
|
3429
3889
|
if (input.workspace !== null) {
|
|
3430
3890
|
const outcome = this.opts.workspaces.bind(sessionId, input.workspace, input.lang);
|
|
3431
3891
|
if (!outcome.ok) return { ok: false, reason: outcome.reason, detail: outcome.detail };
|
|
3432
|
-
bound = outcome.bound;
|
|
3433
3892
|
} else {
|
|
3434
3893
|
this.opts.workspaces.decideNone(sessionId);
|
|
3435
3894
|
}
|
|
3436
|
-
const live = this.assemble(sessionId,
|
|
3895
|
+
const live = this.assemble(sessionId, [], true, role);
|
|
3437
3896
|
if (!live) {
|
|
3438
3897
|
this.opts.workspaces.release(sessionId);
|
|
3439
3898
|
return { ok: false, reason: "unavailable" };
|
|
@@ -3441,7 +3900,6 @@ var init_session_factory = __esm({
|
|
|
3441
3900
|
this.live.set(sessionId, live);
|
|
3442
3901
|
return { ok: true, live, created: true };
|
|
3443
3902
|
}
|
|
3444
|
-
/** 判据全文在 {@link SessionFactory.revive} 上 */
|
|
3445
3903
|
revive(input) {
|
|
3446
3904
|
const { sessionId } = input;
|
|
3447
3905
|
const already = this.live.get(sessionId);
|
|
@@ -3451,15 +3909,13 @@ var init_session_factory = __esm({
|
|
|
3451
3909
|
const decision = remembered ?? input.decision;
|
|
3452
3910
|
const awaitingPick = this.opts.workspaces.stateOf(sessionId) === "unbound";
|
|
3453
3911
|
if (!decision && !awaitingPick) return { ok: false, reason: "no-decision" };
|
|
3454
|
-
let bound = null;
|
|
3455
3912
|
if (decision?.state === "bound") {
|
|
3456
3913
|
const outcome = this.opts.workspaces.bind(sessionId, decision.root, input.lang);
|
|
3457
3914
|
if (!outcome.ok) return { ok: false, reason: outcome.reason, detail: outcome.detail };
|
|
3458
|
-
bound = outcome.bound;
|
|
3459
3915
|
} else if (decision) {
|
|
3460
3916
|
this.opts.workspaces.decideNone(sessionId);
|
|
3461
3917
|
}
|
|
3462
|
-
const live = this.assemble(sessionId,
|
|
3918
|
+
const live = this.assemble(sessionId, [], true);
|
|
3463
3919
|
if (!live) {
|
|
3464
3920
|
if (!remembered && !awaitingPick) this.opts.workspaces.release(sessionId);
|
|
3465
3921
|
return { ok: false, reason: "unavailable" };
|
|
@@ -3472,67 +3928,41 @@ var init_session_factory = __esm({
|
|
|
3472
3928
|
this.live.delete(sessionId);
|
|
3473
3929
|
this.opts.bridges.release(sessionId);
|
|
3474
3930
|
}
|
|
3475
|
-
/**
|
|
3476
|
-
* 一个 `delegate_task` 子跑手。**全进程一个**,按 `parentSessionId` 现查现配。
|
|
3477
|
-
*
|
|
3478
|
-
* 三处 id 必须都是**派活那个会话**的,缺一处就是 §6.3 第 3 条那个洞:
|
|
3479
|
-
* `budgetSessionId`(钱记谁头上)、两个 `checkpoints` 回调(快照落谁的目录)、
|
|
3480
|
-
* 以及审批弹给谁看。在这里现查而不是装配时闭包一个,正是因为那个闭包
|
|
3481
|
-
* 捕获的是引导会话。
|
|
3482
|
-
*
|
|
3483
|
-
* @param provider **收参数而不是读 `this.opts.provider`**:`delegate_task`
|
|
3484
|
-
* 只在 provider 起得来时才注册,收进来之后这条路上就没有「provider 是 null」
|
|
3485
|
-
* 这个状态了 —— 少一个只能靠一句话解释的不可达分支。
|
|
3486
|
-
*
|
|
3487
|
-
* ⚠️ 它今天只是**兜底**(派活的会话已经被冷却掉了):2026-08-16 起子 agent
|
|
3488
|
-
* 跑在**派活那个会话**的 provider 门面上,理由见下面那两行注释
|
|
3489
|
-
*/
|
|
3490
3931
|
subRunner(provider) {
|
|
3491
3932
|
return async (goal, context, subSessionId, roleName, parentSessionId) => {
|
|
3492
3933
|
const { services, config } = this.opts;
|
|
3493
3934
|
const parent = this.live.get(parentSessionId);
|
|
3494
3935
|
const workDir = parent?.workspace?.workspace.root ?? this.opts.defaultWorkDir;
|
|
3495
3936
|
const projectContext = parent?.workspace?.projectContext ?? services.projectContext;
|
|
3496
|
-
const checkpoints = new CheckpointManager({
|
|
3937
|
+
const checkpoints = new CheckpointManager({
|
|
3938
|
+
workDir: () => workDir,
|
|
3939
|
+
homeDir: config.homeDir
|
|
3940
|
+
});
|
|
3497
3941
|
const role = findRole(this.opts.roles(), roleName ?? DEFAULT_AGENT_ROLE);
|
|
3498
3942
|
const permission = parent?.permission ?? services.permission;
|
|
3499
3943
|
const sub = new AgentLoop({
|
|
3500
|
-
// 跑在**派活那个会话**的模型上(2026-08-16)。和下面 `budgetSessionId` /
|
|
3501
|
-
// `checkpoints` / `onApprovalRequest` 是同一条判据:子任务是那个会话的
|
|
3502
|
-
// 一部分。读进程那一份的话,会话 B 明明换到了别的模型,它派出去的活
|
|
3503
|
-
// 还在打引导会话那个 —— 而账是记在 B 头上的
|
|
3504
3944
|
provider: parent?.provider ?? provider,
|
|
3505
3945
|
toolProvider: roleScopedProvider(this.opts.rawToolProvider, role, BLOCKED_TOOLS),
|
|
3946
|
+
codeModeTools: this.opts.codeModeTools,
|
|
3506
3947
|
telemetry: this.opts.telemetry,
|
|
3507
3948
|
workDir,
|
|
3508
|
-
// 复用父会话那份:信任判定和指令文件已经算过、也已经进过诊断,
|
|
3509
|
-
// 每个子 agent 再算一遍只是重复读盘和重复跑 git
|
|
3510
3949
|
projectContext,
|
|
3511
|
-
// 角色说了算,没说才回落到「父的一半」——避免单个子任务吃掉全部预算
|
|
3512
3950
|
maxTurns: role?.maxTurns ?? Math.max(4, Math.floor(config.maxTurns / 2)),
|
|
3513
3951
|
contextLength: services.contextLength,
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
//
|
|
3518
|
-
// 这条路上**没有槽**(方案 57 §3.2 只换了 `assemble()` 那一行):一个子任务
|
|
3519
|
-
// 从派出去到跑完就是一次 run,中途没有「下一条消息」这个概念。写成函数
|
|
3520
|
-
// 只是跟着 `AgentConfig.role` 的形状走,它每次都回同一份对象
|
|
3952
|
+
artifactRetention: resolveArtifactRetention(config.artifacts),
|
|
3953
|
+
sandboxTerminal: config.sandbox?.terminal !== false,
|
|
3954
|
+
compliance: this.opts.compliance ?? void 0,
|
|
3521
3955
|
...role ? { role: () => ({ name: role.name, ...role.prompt ? { prompt: role.prompt } : {} }) } : {},
|
|
3522
3956
|
...permission ? { permissionManager: permission } : {},
|
|
3523
3957
|
...services.hookManager ? { hookManager: services.hookManager } : {},
|
|
3958
|
+
skillSystem: services.skill ?? void 0,
|
|
3959
|
+
roleSkills: () => role?.skills,
|
|
3524
3960
|
...this.opts.budget,
|
|
3525
|
-
// 子任务的花费记到**派活的那个会话**头上。不这么做的话 `delegate_task`
|
|
3526
|
-
// 就是一条绕过成本上限的通道;而记到引导会话头上(工厂之前那样)更糟 ——
|
|
3527
|
-
// 那是**另一个人**的预算
|
|
3528
3961
|
budgetSessionId: parentSessionId,
|
|
3529
|
-
// 同上:子 agent 改的文件要能被派活那个会话的 `/rewind` 看见
|
|
3530
3962
|
checkpoints: {
|
|
3531
3963
|
capture: (_sub, paths) => checkpoints.capture(parentSessionId, paths),
|
|
3532
3964
|
record: (_sub, paths) => checkpoints.record(parentSessionId, paths)
|
|
3533
3965
|
},
|
|
3534
|
-
// 审批弹给派活那个会话的界面。`BLOCKED_TOOLS` 挡掉了 plan 和提问那几个,
|
|
3535
|
-
// 剩下的写类工具照样要问人
|
|
3536
3966
|
onApprovalRequest: (req) => this.opts.bridges.approve(parentSessionId, req)
|
|
3537
3967
|
});
|
|
3538
3968
|
let r;
|
|
@@ -3554,23 +3984,9 @@ ${context}` : goal,
|
|
|
3554
3984
|
};
|
|
3555
3985
|
};
|
|
3556
3986
|
}
|
|
3557
|
-
|
|
3558
|
-
// 私有:全仓唯一一处「主会话的 `new AgentLoop` + `new AgentSession`」
|
|
3559
|
-
// -------------------------------------------------------------------------
|
|
3560
|
-
/**
|
|
3561
|
-
* @param own 这个会话要不要**自己开一份**模型 / 权限档 / plan 模式
|
|
3562
|
-
* (2026-08-16)。引导会话是 `false` —— 它拿的就是进程那两份本体。
|
|
3563
|
-
*
|
|
3564
|
-
* **收一个布尔而不是在这里判 `sessionId === this.bootstrap`**:`adopt()`
|
|
3565
|
-
* 恰好在给 `this.bootstrap` 赋值的那一行之后调进来,照那个字段判的话,
|
|
3566
|
-
* 这段代码的正确性靠的是两行之间的时序 —— 而下一个人挪一行就会把它挪坏
|
|
3567
|
-
* @param role **这个会话**的专家(决定 20 ②)。不给就回落到 `--agent` 那一份
|
|
3568
|
-
* (`opts.mainRole`,进程级)—— 两者形状完全一样,走的也是同一条收窄路,
|
|
3569
|
-
* 区别只在作用域。回落而不是二选一:`epoch web --agent explore` 之后新建的
|
|
3570
|
-
* 会话仍然该是 explore,那是用户在命令行上说过的话。
|
|
3571
|
-
*/
|
|
3572
|
-
assemble(sessionId, bound, history, own, role) {
|
|
3987
|
+
assemble(sessionId, history, own, role) {
|
|
3573
3988
|
const opts = this.opts;
|
|
3989
|
+
opts.processes.claim(sessionId);
|
|
3574
3990
|
const { services, config, provider, models } = opts;
|
|
3575
3991
|
if (!provider || !models) return null;
|
|
3576
3992
|
const scope = {
|
|
@@ -3579,14 +3995,26 @@ ${context}` : goal,
|
|
|
3579
3995
|
};
|
|
3580
3996
|
const active = role ?? opts.mainRole ?? void 0;
|
|
3581
3997
|
const roleScope = createRoleScope(
|
|
3582
|
-
withSessionTools(
|
|
3998
|
+
withSessionTools(
|
|
3999
|
+
revealScopedProvider(opts.toolProvider, opts.deferredTools, scope.guard.reveal),
|
|
4000
|
+
scope.guard.tools
|
|
4001
|
+
),
|
|
3583
4002
|
active,
|
|
3584
4003
|
[]
|
|
3585
4004
|
);
|
|
3586
4005
|
const planState = scope.guard.planState;
|
|
3587
|
-
const
|
|
3588
|
-
const
|
|
3589
|
-
|
|
4006
|
+
const live = {};
|
|
4007
|
+
const site = () => {
|
|
4008
|
+
const now = opts.workspaces.of(live.session?.sessionId ?? sessionId);
|
|
4009
|
+
return {
|
|
4010
|
+
workDir: now?.workspace.root ?? opts.defaultWorkDir,
|
|
4011
|
+
projectContext: now?.projectContext ?? services.projectContext
|
|
4012
|
+
};
|
|
4013
|
+
};
|
|
4014
|
+
const checkpoints = new CheckpointManager({
|
|
4015
|
+
workDir: () => site().workDir,
|
|
4016
|
+
homeDir: config.homeDir
|
|
4017
|
+
});
|
|
3590
4018
|
const loopProvider = opts.goals ? withGoalPrompt(
|
|
3591
4019
|
scope.model.provider,
|
|
3592
4020
|
() => opts.goals.promptView(live.session?.sessionId ?? sessionId)
|
|
@@ -3594,59 +4022,19 @@ ${context}` : goal,
|
|
|
3594
4022
|
const bridge = AgentSession.createApprovalBridge();
|
|
3595
4023
|
const routed = { approve: bridge.onApprovalRequest, ask: bridge.onQuestionRequest };
|
|
3596
4024
|
opts.bridges.register(sessionId, routed);
|
|
3597
|
-
const live = {};
|
|
3598
4025
|
const agent = safeInit(
|
|
3599
4026
|
"AgentLoop",
|
|
3600
4027
|
() => new AgentLoop({
|
|
3601
|
-
// **这个会话自己那份门面**,不是 `opts.provider`(那是引导会话的)。
|
|
3602
|
-
// 拿错的表现是:第二个会话换完模型,界面上写着新的、引擎下一轮打的
|
|
3603
|
-
// 还是旧的 —— 判据在 `SessionModelScope.provider` 上
|
|
3604
4028
|
provider: loopProvider,
|
|
3605
|
-
// **给 provider 而不是 `exposed()` 的快照**:MCP server 发
|
|
3606
|
-
// list_changed / 断线重连 / login 解锁新工具时,正在跑的这个 loop
|
|
3607
|
-
// 下一轮就能看到新表(方案 15)。这里给的是**包了两层收窄的那个** ——
|
|
3608
|
-
// 命令那一层(方案 23)在 `opts.toolProvider` 里,角色那一层是槽
|
|
3609
|
-
// (方案 57 §3.2)。两层都是可变的,上面那条性质仍然成立。
|
|
3610
|
-
//
|
|
3611
|
-
// 收窄**不再按 `active` 分支**:槽在 `active` 缺席时是一层透明包装
|
|
3612
|
-
// (`roleScopedProvider(base, undefined, [])` 一个工具都不减),
|
|
3613
|
-
// 所以这一行的字节行为和改造前逐字相同。分支留着的话,身份那一半会在
|
|
3614
|
-
// 建 loop 那一刻定死「有没有角色」,而工具那一半是活的 —— 那正是
|
|
3615
|
-
// 方案 57 §1.3 那个劈叉,只是换了个更不容易看出来的位置
|
|
3616
4029
|
toolProvider: roleScope.provider,
|
|
4030
|
+
codeModeTools: opts.codeModeTools,
|
|
3617
4031
|
telemetry: opts.telemetry,
|
|
3618
|
-
|
|
3619
|
-
projectContext,
|
|
3620
|
-
// artifact 落在**这次实际生效的数据目录**下,而不是全局 `~/.epoch`
|
|
3621
|
-
// (方案 54 §二)。给 `config.homeDir` 而不是 `opts.homeDir`:
|
|
3622
|
-
// 不传时它是引擎探测出来的结果,只有它是真源
|
|
4032
|
+
site,
|
|
3623
4033
|
multimodal: { homeDir: config.homeDir },
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
// 不换轮次预算**,这是一个答案不是一笔欠账(方案 57 §3.2.1,2026-08-17)。
|
|
3628
|
-
// 判据一句话:身份和工具必须同作用域是因为它们**同时被模型读到**、
|
|
3629
|
-
// 劈叉了看不出来;而轮次预算是宿主侧的一个上界,它劈叉的表现是
|
|
3630
|
-
// `finish` 上那个 `max-turns`,屏幕上说得清。真要换只能走
|
|
3631
|
-
// `RunInput.maxTurns`,而那一格已经有调用方了(`--max-turns`),
|
|
3632
|
-
// 「谁压过谁」是另一次判断。全文在 `agent-role/scope.ts` 的 `current` 上
|
|
4034
|
+
artifactRetention: resolveArtifactRetention(config.artifacts),
|
|
4035
|
+
sandboxTerminal: config.sandbox?.terminal !== false,
|
|
4036
|
+
compliance: opts.compliance ?? void 0,
|
|
3633
4037
|
maxTurns: active?.maxTurns ?? config.maxTurns,
|
|
3634
|
-
// 工具白名单**不传**:它已经被上面的 `roleScopedProvider` 消化掉了,
|
|
3635
|
-
// loop 里不该存在「工具表和角色不一致」这种状态。
|
|
3636
|
-
//
|
|
3637
|
-
// `scope: 'session'` 是这一轮加的那一格,而且**必须显式给** ——
|
|
3638
|
-
// 它缺省是 `'delegate'`(老调用方的 prompt 要逐字节不变),不给的话
|
|
3639
|
-
// 顶层会话会收到一句「由主智能体派来完成一个独立子任务」的假话,
|
|
3640
|
-
// 而这一条从 `--agent` 落地那天起就错着(判据见 `AgentRoleScope`)。
|
|
3641
|
-
//
|
|
3642
|
-
// `source` 纯展示,进不了 prompt —— 它是 `run-start` 事件上那枚来源
|
|
3643
|
-
// 徽标的唯一来路(决定 8 第一个时刻)。这一层是全仓唯一同时握着
|
|
3644
|
-
// 「完整 AgentRole」和「这个会话的 loop」的地方,不在这儿给就没人给得出
|
|
3645
|
-
//
|
|
3646
|
-
// **从槽里读,而且这一格不再按 `active` 分支**(方案 57 §3.2):
|
|
3647
|
-
// 工具那一半已经是活的了,身份这一半再钉死在建 loop 那一刻,就是
|
|
3648
|
-
// §1.3 那个劈叉。槽空着时函数回 `undefined`,loop 那边整个键缺席 ——
|
|
3649
|
-
// 和改造前 `active` 缺席时**逐字节等价**
|
|
3650
4038
|
role: () => {
|
|
3651
4039
|
const now = roleScope.current;
|
|
3652
4040
|
if (!now) return void 0;
|
|
@@ -3658,28 +4046,11 @@ ${context}` : goal,
|
|
|
3658
4046
|
};
|
|
3659
4047
|
},
|
|
3660
4048
|
contextLength: services.contextLength,
|
|
3661
|
-
// 已批准的计划(方案 35 PR-2 验收 8)。给函数不给快照 —— 计划是这一轮
|
|
3662
|
-
// 跑到一半才被批准的,快照恒为 null。
|
|
3663
|
-
//
|
|
3664
|
-
// ⚠️ 读的是**这个会话自己那份** plan 状态(2026-08-16):原来这儿是一个
|
|
3665
|
-
// 进程级闭包,于是会话 B 的 system prompt 附加段里挂的是会话 A 批准的
|
|
3666
|
-
// 那份纲 —— 模型照着一份不属于这段对话的计划干活,而屏幕上什么都看不出来
|
|
3667
4049
|
...planState ? { approvedPlan: () => planState.approvedPlan() } : {},
|
|
3668
|
-
// 宿主给了阻塞式确认就用它的(单会话宿主),否则走这个会话自己那座桥。
|
|
3669
|
-
// **不绕 `bridges.approve`**:那一层是给共享工具路由用的,而 `AgentLoop`
|
|
3670
|
-
// 本来就知道自己是谁 —— 多绕一圈只多一处能对错的地方
|
|
3671
4050
|
onApprovalRequest: opts.hostApproval ?? bridge.onApprovalRequest,
|
|
3672
|
-
// 只给**这个会话**那一批(2026-08-15)。会话 id 走 `live` 而不是入参,
|
|
3673
|
-
// 理由见它的声明
|
|
3674
4051
|
backgroundTasks: () => opts.backgroundTasks(live.session?.sessionId ?? sessionId),
|
|
3675
|
-
// 写类工具动手前留一份旧内容(方案 27)。开轮在 `AgentSession.run()`,
|
|
3676
|
-
// 循环自己不认识这个东西 —— 所以上面那条「同一个实例」是硬约束
|
|
3677
4052
|
checkpoints,
|
|
3678
4053
|
...services.memory ? { memory: services.memory } : {},
|
|
3679
|
-
// 压缩时把当前目标递给摘要模型(方案 52 PR-3 / 验收 3 后半句)。
|
|
3680
|
-
// **包的是同一个进程级实例**,不是新建一个 —— 冷却期、熔断计数、
|
|
3681
|
-
// 上一版摘要都在那份状态里,复制一份等于每个会话各有一套闸门。
|
|
3682
|
-
// 判据全文在 `goal/engine.ts`
|
|
3683
4054
|
...services.contextEngine ? {
|
|
3684
4055
|
contextEngine: opts.goals ? withGoalCompression(
|
|
3685
4056
|
services.contextEngine,
|
|
@@ -3687,11 +4058,9 @@ ${context}` : goal,
|
|
|
3687
4058
|
) : services.contextEngine
|
|
3688
4059
|
} : {},
|
|
3689
4060
|
...services.hookManager ? { hookManager: services.hookManager } : {},
|
|
3690
|
-
|
|
4061
|
+
skillSystem: services.skill ?? void 0,
|
|
4062
|
+
roleSkills: () => roleScope.current?.skills,
|
|
3691
4063
|
...services.skillLearner ? { skillLearner: services.skillLearner } : {},
|
|
3692
|
-
// **这个会话自己那一档**(2026-08-16)。规则表 / 审批缓存 / 审计流水
|
|
3693
|
-
// 照旧是同一份(core 的 `PermissionManager.scoped()` 共享它们),
|
|
3694
|
-
// 分家的只有「此刻是哪一档」
|
|
3695
4064
|
...scope.guard.permission ? { permissionManager: scope.guard.permission } : {},
|
|
3696
4065
|
...opts.budget
|
|
3697
4066
|
}),
|
|
@@ -3704,7 +4073,7 @@ ${context}` : goal,
|
|
|
3704
4073
|
const store = opts.sessions ? new SessionStore(
|
|
3705
4074
|
opts.sessions,
|
|
3706
4075
|
config.homeDir,
|
|
3707
|
-
projectContext.rootDir,
|
|
4076
|
+
() => site().projectContext.rootDir,
|
|
3708
4077
|
opts.sessionSource,
|
|
3709
4078
|
(id) => opts.workspaces.decisionOf(id)
|
|
3710
4079
|
) : void 0;
|
|
@@ -3714,15 +4083,9 @@ ${context}` : goal,
|
|
|
3714
4083
|
...store ? { store } : {},
|
|
3715
4084
|
history: [...history],
|
|
3716
4085
|
checkpoints,
|
|
3717
|
-
// 一轮开始时给目标 +1 并检查预算(方案 52 PR-2)。**接在轮次边界上,
|
|
3718
|
-
// 而 `loop.ts` 一行没改** —— 判据在方案 52 §四:一次 `run()` 恰好是一条
|
|
3719
|
-
// 被接纳的用户消息,而循环内部那个 `for (turn = 1..maxTurns)` 数的是模型
|
|
3720
|
-
// 请求次数。挂在这儿和 `checkpoints` 是同一个位置、同一条理由
|
|
3721
4086
|
...opts.goals ? { goals: opts.goals } : {},
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
// 「没换人」—— 而那正是这条留痕唯一会坏的方式(不报错、只是永远不出现)
|
|
3725
|
-
currentRole: () => roleScope.current?.name
|
|
4087
|
+
currentRole: () => roleScope.current?.name,
|
|
4088
|
+
currentWorkDir: () => site().workDir
|
|
3726
4089
|
});
|
|
3727
4090
|
bridge.attach(session);
|
|
3728
4091
|
live.session = session;
|
|
@@ -3734,38 +4097,17 @@ ${context}` : goal,
|
|
|
3734
4097
|
session,
|
|
3735
4098
|
agent,
|
|
3736
4099
|
model: scope.model.control,
|
|
3737
|
-
// 这个会话那个角色槽的宿主面(方案 57 §3.4)。**在这儿包而不是把 `roleScope`
|
|
3738
|
-
// 整个交出去**:`enter()` 收的是 `AgentRole` 对象,而服务端不许 import core
|
|
3739
|
-
// ——它手里只有网线上那个字符串。判据全文在 `role-scope.ts` 文件头
|
|
3740
4100
|
roles: createRoleControl(roleScope, opts.roles),
|
|
3741
|
-
/*
|
|
3742
|
-
* 会话级那一份身份(2026-08-18)。**取 `active` 而不是入参 `role`**:
|
|
3743
|
-
* 那个变量已经把「没带就退到 `--agent`」算过一次,而上面那句「全函数只算
|
|
3744
|
-
* 这一次」正是为了不让两处给出不同答案。判据全文在 {@link LiveSession.role}。
|
|
3745
|
-
*
|
|
3746
|
-
* 只带 `name` + `source` 两格(`ActiveAgentRole`),不把整个 `AgentRole`
|
|
3747
|
-
* 交出去:服务端不许 import core,而它要的就是这两个字段
|
|
3748
|
-
* —— 同 `roles` 那一格「包成两个动词」的判据。
|
|
3749
|
-
*/
|
|
3750
4101
|
role: toActiveRole(active),
|
|
3751
4102
|
provider: scope.model.provider,
|
|
3752
4103
|
permission: scope.guard.permission,
|
|
3753
4104
|
permissions: scope.guard.permissions,
|
|
3754
4105
|
plan: scope.guard.plan,
|
|
3755
|
-
// 会话 id 走 `session.sessionId` 而不是入参,判据同下面那个
|
|
3756
|
-
// `buildCheckpointControl`:`/resume` 会把它换掉
|
|
3757
4106
|
goals: opts.goals ? buildGoalControl(opts.goals, () => session.sessionId) : null,
|
|
3758
|
-
// **同一个 `checkpoints` 实例**再包一层交出去,不是新建一个管理器:
|
|
3759
|
-
// 上面那条「`AgentSession` 和它的 `AgentLoop` 必须共用同一个实例」
|
|
3760
|
-
// 对宿主这一侧同样成立 —— 另建一个的表现是回退列表读得出、
|
|
3761
|
-
// 而「这一轮开过没有」是另一本账。
|
|
3762
|
-
//
|
|
3763
|
-
// 会话 id 走 `session.sessionId` 而不是上面那个入参:`/resume` 会把它换掉
|
|
3764
|
-
// (`rekey` 只换工厂表里的键,换不了一个拍过快照的闭包)
|
|
3765
4107
|
checkpoints: buildCheckpointControl(checkpoints, session, () => session.sessionId),
|
|
3766
|
-
workspace
|
|
3767
|
-
|
|
3768
|
-
|
|
4108
|
+
get workspace() {
|
|
4109
|
+
return opts.workspaces.of(live.session?.sessionId ?? sessionId);
|
|
4110
|
+
},
|
|
3769
4111
|
bridge: routed
|
|
3770
4112
|
};
|
|
3771
4113
|
}
|
|
@@ -3804,6 +4146,9 @@ function toProvider(router) {
|
|
|
3804
4146
|
} catch {
|
|
3805
4147
|
return { supportsImages: false };
|
|
3806
4148
|
}
|
|
4149
|
+
},
|
|
4150
|
+
get modelId() {
|
|
4151
|
+
return router.getSelection().model;
|
|
3807
4152
|
}
|
|
3808
4153
|
};
|
|
3809
4154
|
}
|
|
@@ -3823,7 +4168,6 @@ var init_file = __esm({
|
|
|
3823
4168
|
opts;
|
|
3824
4169
|
now;
|
|
3825
4170
|
ready = false;
|
|
3826
|
-
/** 自增的 span id。同一个进程内唯一就够 —— 这文件本来就是单机看的 */
|
|
3827
4171
|
seq = 0;
|
|
3828
4172
|
startSpan(name, attrs, parent) {
|
|
3829
4173
|
const startedAt = this.now();
|
|
@@ -3957,8 +4301,6 @@ async function tryCreateOtelTelemetry(endpoint) {
|
|
|
3957
4301
|
};
|
|
3958
4302
|
return {
|
|
3959
4303
|
telemetry,
|
|
3960
|
-
// 退出时必须 flush(批量导出器手上压着最后一轮的 span),但**必须有上限** ——
|
|
3961
|
-
// 见 SHUTDOWN_BUDGET_MS
|
|
3962
4304
|
shutdown: async () => {
|
|
3963
4305
|
await withTimeout(
|
|
3964
4306
|
Promise.allSettled([tracerProvider.shutdown(), meterProvider.shutdown()]).then(() => {
|
|
@@ -3982,7 +4324,7 @@ async function buildTelemetry(input) {
|
|
|
3982
4324
|
const noop = { telemetry: NOOP_TELEMETRY, dispose: () => {
|
|
3983
4325
|
} };
|
|
3984
4326
|
if (!config?.enabled) {
|
|
3985
|
-
diags.skipped("Telemetry", "
|
|
4327
|
+
diags.skipped("Telemetry", t("boot.telemetry_off"));
|
|
3986
4328
|
return noop;
|
|
3987
4329
|
}
|
|
3988
4330
|
const target = config.target ?? "file";
|
|
@@ -3995,7 +4337,7 @@ async function buildTelemetry(input) {
|
|
|
3995
4337
|
}
|
|
3996
4338
|
diags.warn(
|
|
3997
4339
|
"Telemetry",
|
|
3998
|
-
|
|
4340
|
+
(lang) => t("boot.telemetry_otlp_missing", { command: OTLP_INSTALL_COMMAND }, lang),
|
|
3999
4341
|
"otlp-deps-missing"
|
|
4000
4342
|
);
|
|
4001
4343
|
}
|
|
@@ -4009,7 +4351,6 @@ async function buildTelemetry(input) {
|
|
|
4009
4351
|
return {
|
|
4010
4352
|
telemetry: new FileTelemetry({
|
|
4011
4353
|
path,
|
|
4012
|
-
// 写不进去要说出来,但只说一次 —— 每条记录报一次会把诊断刷爆
|
|
4013
4354
|
onError: onceWarn(diags, path)
|
|
4014
4355
|
}),
|
|
4015
4356
|
dispose: () => {
|
|
@@ -4023,7 +4364,11 @@ function onceWarn(diags, path) {
|
|
|
4023
4364
|
reported = true;
|
|
4024
4365
|
diags.warn(
|
|
4025
4366
|
"Telemetry",
|
|
4026
|
-
|
|
4367
|
+
(lang) => t(
|
|
4368
|
+
"boot.telemetry_write_failed",
|
|
4369
|
+
{ reason: err instanceof Error ? err.message : String(err), path },
|
|
4370
|
+
lang
|
|
4371
|
+
),
|
|
4027
4372
|
"telemetry-write-failed"
|
|
4028
4373
|
);
|
|
4029
4374
|
};
|
|
@@ -4050,16 +4395,18 @@ function consoleTelemetry() {
|
|
|
4050
4395
|
histogram: (name, value, attrs) => line({ t: "histogram", name, v: value, attrs })
|
|
4051
4396
|
};
|
|
4052
4397
|
}
|
|
4398
|
+
var OTLP_INSTALL_COMMAND;
|
|
4053
4399
|
var init_telemetry = __esm({
|
|
4054
4400
|
"src/telemetry/index.ts"() {
|
|
4055
4401
|
init_file();
|
|
4056
4402
|
init_otel();
|
|
4057
4403
|
init_file();
|
|
4058
4404
|
init_otel();
|
|
4405
|
+
OTLP_INSTALL_COMMAND = "pnpm add @opentelemetry/api @opentelemetry/sdk-trace-base @opentelemetry/sdk-metrics @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http @opentelemetry/resources";
|
|
4059
4406
|
}
|
|
4060
4407
|
});
|
|
4061
4408
|
function missing(tool) {
|
|
4062
|
-
return { output: `${tool}
|
|
4409
|
+
return { output: t("boot.tool_unavailable", { tool: `${tool} ` }), ok: false };
|
|
4063
4410
|
}
|
|
4064
4411
|
function toResult(result) {
|
|
4065
4412
|
const output = typeof result.output === "string" ? result.output : JSON.stringify(result.output, null, 2);
|
|
@@ -4078,7 +4425,11 @@ function createUserActions(deps) {
|
|
|
4078
4425
|
const type = mapOperationType(toolName);
|
|
4079
4426
|
const decision = pm.check({ type, detail, workDir: deps.workDir, target, toolName });
|
|
4080
4427
|
if (decision.allowed) return null;
|
|
4081
|
-
if (!decision.requireApproval)
|
|
4428
|
+
if (!decision.requireApproval) {
|
|
4429
|
+
return t("boot.blocked_by_permission", {
|
|
4430
|
+
reason: decision.reason || t("boot.needs_authorization")
|
|
4431
|
+
});
|
|
4432
|
+
}
|
|
4082
4433
|
const outcome = await approve({
|
|
4083
4434
|
toolName,
|
|
4084
4435
|
type,
|
|
@@ -4087,7 +4438,7 @@ function createUserActions(deps) {
|
|
|
4087
4438
|
...decision.reason ? { reason: decision.reason } : {}
|
|
4088
4439
|
});
|
|
4089
4440
|
pm.auditUserDecision?.({ type, target, toolName }, outcome);
|
|
4090
|
-
if (outcome === "deny") return "
|
|
4441
|
+
if (outcome === "deny") return t("boot.cancelled");
|
|
4091
4442
|
pm.recordApproval(toolName, target, outcome);
|
|
4092
4443
|
return null;
|
|
4093
4444
|
};
|
|
@@ -4128,6 +4479,258 @@ var init_user_shell = __esm({
|
|
|
4128
4479
|
"src/user-shell.ts"() {
|
|
4129
4480
|
}
|
|
4130
4481
|
});
|
|
4482
|
+
function utilityBudgetKey(input) {
|
|
4483
|
+
const owner = input.sessionId?.trim();
|
|
4484
|
+
if (owner) return owner;
|
|
4485
|
+
const label = input.label !== void 0 && LABEL_SHAPE.test(input.label) ? input.label : ANONYMOUS;
|
|
4486
|
+
return `${BUCKET_PREFIX}${label}`;
|
|
4487
|
+
}
|
|
4488
|
+
function buildUtilityControl(opts) {
|
|
4489
|
+
const { provider, store, pricingOf } = opts;
|
|
4490
|
+
const record = (input, out) => {
|
|
4491
|
+
if (!store || !out.usage) return;
|
|
4492
|
+
const pricing = pricingOf?.(out.model);
|
|
4493
|
+
new BudgetGuard({ store, sessionId: utilityBudgetKey(input) }).add(out.usage, {
|
|
4494
|
+
model: out.model,
|
|
4495
|
+
...pricing ? { pricing } : {}
|
|
4496
|
+
});
|
|
4497
|
+
};
|
|
4498
|
+
return {
|
|
4499
|
+
async generate(input) {
|
|
4500
|
+
if (input.signal?.aborted === true) throw new Error(t("utility.aborted"));
|
|
4501
|
+
const wanted = input.model ?? refOf(provider.getUtilityInfo());
|
|
4502
|
+
const out = await provider.generate({
|
|
4503
|
+
messages: input.messages,
|
|
4504
|
+
...input.system !== void 0 ? { system: input.system } : {},
|
|
4505
|
+
...input.maxTokens !== void 0 ? { maxTokens: input.maxTokens } : {},
|
|
4506
|
+
...wanted.provider ? { provider: wanted.provider } : {},
|
|
4507
|
+
model: wanted.model,
|
|
4508
|
+
...input.responseFormat ? { responseFormat: input.responseFormat } : {},
|
|
4509
|
+
...input.providerOptions ? { providerOptions: input.providerOptions } : {}
|
|
4510
|
+
});
|
|
4511
|
+
record(input, out);
|
|
4512
|
+
return {
|
|
4513
|
+
text: out.text,
|
|
4514
|
+
...out.usage ? { usage: out.usage } : {},
|
|
4515
|
+
model: out.model,
|
|
4516
|
+
provider: out.provider,
|
|
4517
|
+
...out.responseFormatApplied === void 0 ? {} : { responseFormatApplied: out.responseFormatApplied },
|
|
4518
|
+
...out.warnings ? { warnings: out.warnings } : {}
|
|
4519
|
+
};
|
|
4520
|
+
}
|
|
4521
|
+
};
|
|
4522
|
+
}
|
|
4523
|
+
function refOf(info) {
|
|
4524
|
+
return { provider: info.selection.provider, model: info.selection.model };
|
|
4525
|
+
}
|
|
4526
|
+
var BUCKET_PREFIX, ANONYMOUS, LABEL_SHAPE;
|
|
4527
|
+
var init_utility = __esm({
|
|
4528
|
+
"src/utility.ts"() {
|
|
4529
|
+
BUCKET_PREFIX = "utility:";
|
|
4530
|
+
ANONYMOUS = "anonymous";
|
|
4531
|
+
LABEL_SHAPE = /^[a-z0-9_-]{1,32}$/;
|
|
4532
|
+
}
|
|
4533
|
+
});
|
|
4534
|
+
function isSystemOpenable(path) {
|
|
4535
|
+
const dot = path.lastIndexOf(".");
|
|
4536
|
+
if (dot < 0) return false;
|
|
4537
|
+
return SYSTEM_OPENABLE_EXTENSIONS.has(path.slice(dot).toLowerCase());
|
|
4538
|
+
}
|
|
4539
|
+
function toWorkspaceRelative(root, userPath) {
|
|
4540
|
+
const trimmed = userPath.trim();
|
|
4541
|
+
if (trimmed === "") return null;
|
|
4542
|
+
if (!isAbsolute(trimmed)) return trimmed.replace(/^\.\/+/, "") || null;
|
|
4543
|
+
const rel = relative(resolve(root), resolve(trimmed));
|
|
4544
|
+
if (rel === "" || rel.startsWith("..")) return null;
|
|
4545
|
+
return rel.split(sep).join("/");
|
|
4546
|
+
}
|
|
4547
|
+
function insideRoot(root, abs) {
|
|
4548
|
+
let realRoot;
|
|
4549
|
+
try {
|
|
4550
|
+
realRoot = realpathSync(root);
|
|
4551
|
+
} catch {
|
|
4552
|
+
return false;
|
|
4553
|
+
}
|
|
4554
|
+
const rel = relative(realRoot, abs);
|
|
4555
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
4556
|
+
}
|
|
4557
|
+
function sniffKind(absPath) {
|
|
4558
|
+
let fd;
|
|
4559
|
+
try {
|
|
4560
|
+
fd = openSync(absPath, "r");
|
|
4561
|
+
const buf = Buffer.alloc(8192);
|
|
4562
|
+
const read = readSync(fd, buf, 0, buf.length, 0);
|
|
4563
|
+
return buf.subarray(0, read).includes(0) ? "binary" : "text";
|
|
4564
|
+
} catch {
|
|
4565
|
+
return null;
|
|
4566
|
+
} finally {
|
|
4567
|
+
if (fd !== void 0) {
|
|
4568
|
+
try {
|
|
4569
|
+
closeSync(fd);
|
|
4570
|
+
} catch {
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
function withinWorkspace(resolveInWorkspace2, root, rel) {
|
|
4576
|
+
return resolveInWorkspace2(root, rel) !== null;
|
|
4577
|
+
}
|
|
4578
|
+
function buildWorkspaceFilesControl(deps, permission) {
|
|
4579
|
+
let cache = null;
|
|
4580
|
+
const now = () => Date.now();
|
|
4581
|
+
function statFileImpl(path, root) {
|
|
4582
|
+
const rel = toWorkspaceRelative(root, path);
|
|
4583
|
+
if (rel === null) return { ok: false, reason: "denied" };
|
|
4584
|
+
const full = deps.resolveInWorkspace(root, rel);
|
|
4585
|
+
if (!full) return { ok: false, reason: "denied" };
|
|
4586
|
+
const decision = permission?.check({
|
|
4587
|
+
type: "file_read",
|
|
4588
|
+
detail: rel,
|
|
4589
|
+
workDir: root,
|
|
4590
|
+
target: full,
|
|
4591
|
+
toolName: "file_read"
|
|
4592
|
+
});
|
|
4593
|
+
if (permission && !decision?.allowed) return { ok: false, reason: "denied" };
|
|
4594
|
+
let stat;
|
|
4595
|
+
try {
|
|
4596
|
+
stat = statSync(full);
|
|
4597
|
+
} catch (err) {
|
|
4598
|
+
const code = err.code;
|
|
4599
|
+
return {
|
|
4600
|
+
ok: false,
|
|
4601
|
+
reason: code === "ENOENT" || code === "ENOTDIR" ? "missing" : "unreadable"
|
|
4602
|
+
};
|
|
4603
|
+
}
|
|
4604
|
+
if (!stat.isFile()) return { ok: false, reason: "not-a-file" };
|
|
4605
|
+
let absPath;
|
|
4606
|
+
try {
|
|
4607
|
+
absPath = realpathSync(full);
|
|
4608
|
+
} catch {
|
|
4609
|
+
return { ok: false, reason: "unreadable" };
|
|
4610
|
+
}
|
|
4611
|
+
if (!insideRoot(root, absPath)) return { ok: false, reason: "denied" };
|
|
4612
|
+
const kind = sniffKind(absPath);
|
|
4613
|
+
if (kind === null) return { ok: false, reason: "unreadable" };
|
|
4614
|
+
return { ok: true, rel, absPath, bytes: stat.size, mtimeMs: stat.mtimeMs, kind };
|
|
4615
|
+
}
|
|
4616
|
+
function computeList(root) {
|
|
4617
|
+
const { files, truncated } = deps.listFiles(root);
|
|
4618
|
+
return { files, truncated, at: now(), root };
|
|
4619
|
+
}
|
|
4620
|
+
function listFor(root) {
|
|
4621
|
+
const t25 = now();
|
|
4622
|
+
if (!cache || cache.root !== root) cache = computeList(root);
|
|
4623
|
+
else if (t25 - cache.at > LIST_TTL_MS) cache = computeList(root);
|
|
4624
|
+
return cache;
|
|
4625
|
+
}
|
|
4626
|
+
return {
|
|
4627
|
+
candidates(query, limit, root) {
|
|
4628
|
+
const q = query.trim().toLowerCase();
|
|
4629
|
+
let base;
|
|
4630
|
+
try {
|
|
4631
|
+
base = listFor(root);
|
|
4632
|
+
} catch {
|
|
4633
|
+
return null;
|
|
4634
|
+
}
|
|
4635
|
+
const mentionable = [];
|
|
4636
|
+
let unmentionable = 0;
|
|
4637
|
+
for (const rel of base.files) {
|
|
4638
|
+
if (!withinWorkspace(deps.resolveInWorkspace, root, rel)) continue;
|
|
4639
|
+
const hit = q.length === 0 || rel.toLowerCase().includes(q);
|
|
4640
|
+
if (!hit) continue;
|
|
4641
|
+
if (isMentionableFilePath(rel)) {
|
|
4642
|
+
mentionable.push({ rel, depth: rel.split("/").length });
|
|
4643
|
+
} else {
|
|
4644
|
+
unmentionable += 1;
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
mentionable.sort((a, b) => a.depth - b.depth || (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
4648
|
+
return {
|
|
4649
|
+
candidates: mentionable.slice(0, limit).map((row) => row.rel),
|
|
4650
|
+
unmentionable,
|
|
4651
|
+
truncated: base.truncated
|
|
4652
|
+
};
|
|
4653
|
+
},
|
|
4654
|
+
readFile(path, root) {
|
|
4655
|
+
const full = deps.resolveInWorkspace(root, path);
|
|
4656
|
+
if (!full) return { ok: false, reason: "denied" };
|
|
4657
|
+
const decision = permission?.check({
|
|
4658
|
+
type: "file_read",
|
|
4659
|
+
detail: path,
|
|
4660
|
+
workDir: root,
|
|
4661
|
+
target: full,
|
|
4662
|
+
toolName: "file_read"
|
|
4663
|
+
});
|
|
4664
|
+
if (permission && !decision?.allowed) return { ok: false, reason: "denied" };
|
|
4665
|
+
let stat;
|
|
4666
|
+
try {
|
|
4667
|
+
stat = statSync(full);
|
|
4668
|
+
if (!stat.isFile()) return { ok: false, reason: "unreadable" };
|
|
4669
|
+
} catch {
|
|
4670
|
+
return { ok: false, reason: "unreadable" };
|
|
4671
|
+
}
|
|
4672
|
+
let raw;
|
|
4673
|
+
try {
|
|
4674
|
+
raw = readFileSync(full);
|
|
4675
|
+
} catch {
|
|
4676
|
+
return { ok: false, reason: "unreadable" };
|
|
4677
|
+
}
|
|
4678
|
+
if (raw.subarray(0, 8192).includes(0)) {
|
|
4679
|
+
return { ok: false, reason: "binary", bytes: stat.size };
|
|
4680
|
+
}
|
|
4681
|
+
return { ok: true, text: raw.toString("utf8"), bytes: stat.size };
|
|
4682
|
+
},
|
|
4683
|
+
statFile: statFileImpl,
|
|
4684
|
+
planSystemOpen(path, root) {
|
|
4685
|
+
const stat = statFileImpl(path, root);
|
|
4686
|
+
if (!stat.ok) return { ok: false, reason: stat.reason };
|
|
4687
|
+
if (!isSystemOpenable(stat.absPath)) return { ok: false, reason: "extension-not-allowed" };
|
|
4688
|
+
const { command, args } = deps.systemOpenArgv(stat.absPath);
|
|
4689
|
+
return { ok: true, command, args, absPath: stat.absPath };
|
|
4690
|
+
}
|
|
4691
|
+
};
|
|
4692
|
+
}
|
|
4693
|
+
var SYSTEM_OPENABLE_EXTENSIONS, LIST_TTL_MS;
|
|
4694
|
+
var init_workspace_files = __esm({
|
|
4695
|
+
"src/workspace-files.ts"() {
|
|
4696
|
+
SYSTEM_OPENABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4697
|
+
".doc",
|
|
4698
|
+
".docx",
|
|
4699
|
+
".xls",
|
|
4700
|
+
".xlsx",
|
|
4701
|
+
".ppt",
|
|
4702
|
+
".pptx",
|
|
4703
|
+
".rtf",
|
|
4704
|
+
".odt",
|
|
4705
|
+
".ods",
|
|
4706
|
+
".odp",
|
|
4707
|
+
".pages",
|
|
4708
|
+
".numbers",
|
|
4709
|
+
".key",
|
|
4710
|
+
".pdf",
|
|
4711
|
+
".tif",
|
|
4712
|
+
".tiff",
|
|
4713
|
+
".heic",
|
|
4714
|
+
".heif",
|
|
4715
|
+
".psd",
|
|
4716
|
+
".sketch",
|
|
4717
|
+
".fig",
|
|
4718
|
+
".mp4",
|
|
4719
|
+
".mov",
|
|
4720
|
+
".m4v",
|
|
4721
|
+
".mkv",
|
|
4722
|
+
".avi",
|
|
4723
|
+
".webm",
|
|
4724
|
+
".mp3",
|
|
4725
|
+
".wav",
|
|
4726
|
+
".m4a",
|
|
4727
|
+
".flac",
|
|
4728
|
+
".aac",
|
|
4729
|
+
".ogg"
|
|
4730
|
+
]);
|
|
4731
|
+
LIST_TTL_MS = 6e4;
|
|
4732
|
+
}
|
|
4733
|
+
});
|
|
4131
4734
|
function splitDirList(value) {
|
|
4132
4735
|
if (!value) return [];
|
|
4133
4736
|
return value.split(delimiter).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
@@ -4137,20 +4740,23 @@ function wireWorkspace(input) {
|
|
|
4137
4740
|
input.permission?.setExtraRoots(workspace.extra);
|
|
4138
4741
|
if (input.addDir.length === 0) return workspace;
|
|
4139
4742
|
for (const issue of issues) {
|
|
4140
|
-
if (issue.kind === "redundant") input.diags.skipped("
|
|
4141
|
-
else input.diags.warn("
|
|
4743
|
+
if (issue.kind === "redundant") input.diags.skipped("ExtraDirs", issue.detail);
|
|
4744
|
+
else input.diags.warn("ExtraDirs", issue.detail, `add-dir-${issue.kind}`);
|
|
4142
4745
|
}
|
|
4143
4746
|
if (workspace.extra.length > 0) {
|
|
4144
4747
|
input.diags.ok(
|
|
4145
4748
|
WORKSPACE_MODULE,
|
|
4146
|
-
|
|
4749
|
+
t("boot.workspace_extra", {
|
|
4750
|
+
count: workspace.extra.length,
|
|
4751
|
+
dirs: workspace.extra.join(", ")
|
|
4752
|
+
})
|
|
4147
4753
|
);
|
|
4148
4754
|
}
|
|
4149
4755
|
const unloaded = unloadedInstructionFiles(workspace);
|
|
4150
4756
|
if (unloaded.length > 0) {
|
|
4151
4757
|
input.diags.skipped(
|
|
4152
|
-
"
|
|
4153
|
-
|
|
4758
|
+
"ExtraDirInstructions",
|
|
4759
|
+
t("boot.workspace_extra_instructions", { paths: unloaded.join(", ") })
|
|
4154
4760
|
);
|
|
4155
4761
|
}
|
|
4156
4762
|
return workspace;
|
|
@@ -4161,10 +4767,6 @@ function workspaceDecisionLog(sessions, diags) {
|
|
|
4161
4767
|
try {
|
|
4162
4768
|
sessions.updateMeta(sessionId, {
|
|
4163
4769
|
workspaceState: decision.state,
|
|
4164
|
-
// `none` 那一档**不带路径**。写 `undefined` 在 `updateSessionMeta()` 里
|
|
4165
|
-
// 是「这一列不进 SET」(那儿第一行就 `if (v === undefined) continue`),
|
|
4166
|
-
// 不是「清空它」—— 而 `bound → none` 这条路被 server 那道 409 挡着,
|
|
4167
|
-
// 所以「留着一个陈旧的 root」在今天不可达
|
|
4168
4770
|
...decision.state === "bound" ? { workspaceRoot: decision.root } : {}
|
|
4169
4771
|
});
|
|
4170
4772
|
} catch (err) {
|
|
@@ -4210,51 +4812,10 @@ function inspectRoot(dir, lang) {
|
|
|
4210
4812
|
var WORKSPACE_MODULE, SessionWorkspaces;
|
|
4211
4813
|
var init_workspace = __esm({
|
|
4212
4814
|
"src/workspace.ts"() {
|
|
4213
|
-
WORKSPACE_MODULE = "
|
|
4815
|
+
WORKSPACE_MODULE = "Workspace";
|
|
4214
4816
|
SessionWorkspaces = class {
|
|
4215
|
-
/**
|
|
4216
|
-
* 谁绑在哪儿。**一张内存 Map,进程退出就没了 —— 这一条 2026-08-19 之后仍然成立。**
|
|
4217
|
-
*
|
|
4218
|
-
* 变的是它旁边多了一份 {@link WorkspaceDecisionLog}:绑成功的那一刻**另外**
|
|
4219
|
-
* 往会话库记一笔「用户当初选了哪儿」。两者不是一份数据的两个副本,时态不同 ——
|
|
4220
|
-
* 这张表答的是「它**现在**绑在哪儿」(因此 {@link bind} 那道闸门:路径校验 +
|
|
4221
|
-
* 信任判定),落盘那份答的是「用户**当初**决定了什么」(只读的事实,不带闸门,
|
|
4222
|
-
* 因为决定本身不需要重新判定)。
|
|
4223
|
-
*
|
|
4224
|
-
* 于是原来那两条反对理由,一条仍然拦着、一条被那个时态差绕开了:
|
|
4225
|
-
*
|
|
4226
|
-
* 1. **仍然成立**:落盘成一条**自动生效**的绑定,会和 [build.ts](./build.ts) 里
|
|
4227
|
-
* `/resume` 那一行 {@link seed} 顶上 —— 那儿的规矩是「接回来的会话跑在
|
|
4228
|
-
* **这个进程**的工作区里」。侧栏画着 A、引擎跑在 B 就是这么来的。所以装配时
|
|
4229
|
-
* **没有任何东西读那份记录去 `bind()`**;绑定发生在用户点开旧会话、敲下第一句
|
|
4230
|
-
* 话那一刻(`SessionFactory.revive()`),走的是这个类完整的 {@link bind};
|
|
4231
|
-
* 2. **不再成立**:「只读的事实不属于这张表」是对的,但结论错了 —— 它属于
|
|
4232
|
-
* **会话库那一行**,而不是「所以谁都别记」。不记的代价是重启之后一段会话
|
|
4233
|
-
* 说不出自己在哪儿干过活,判据全文(含哪条过期、哪条原样成立)在 web 的
|
|
4234
|
-
* `shell/session-list.tsx` 文件头。
|
|
4235
|
-
*/
|
|
4236
4817
|
bindings = /* @__PURE__ */ new Map();
|
|
4237
|
-
/**
|
|
4238
|
-
* 「建出来了,但还没选地盘」的那几个会话(方案 55 PR-1,2026-08-17)。
|
|
4239
|
-
*
|
|
4240
|
-
* **一个 `Set<sessionId>`,不是第二张绑定表**:这一档的全部内容就是「它在等
|
|
4241
|
-
* 一次选择」,没有路径、没有信任判定、没有 `ProjectContext`。用一张 Map 存的话
|
|
4242
|
-
* 那些格子只能是 null,而一个到处是 null 的绑定会被下一个人当成「绑了个空的」。
|
|
4243
|
-
*
|
|
4244
|
-
* 出集合有且只有两条路:{@link bind} 成功(转成 `bound`)、
|
|
4245
|
-
* 以及 {@link release}(会话没了)。「明确选不使用工作区」那一下在宿主那一层
|
|
4246
|
-
* 也走 `release` —— 判据见它。
|
|
4247
|
-
*/
|
|
4248
4818
|
deferred = /* @__PURE__ */ new Set();
|
|
4249
|
-
/**
|
|
4250
|
-
* 明确选过「不使用工作区」的那几个(会话库 v7,2026-08-19)。
|
|
4251
|
-
*
|
|
4252
|
-
* **{@link stateOf} 不看它**,那一档的语义一个字没变。这个集合存在只为回答
|
|
4253
|
-
* {@link decisionOf} 的第三种答案 `null`:`stateOf()` 把「明确不使用」和
|
|
4254
|
-
* 「这个进程从没听说过这个 id」都报成 `none`(那条 ⚠️ 就写在它的文档上),
|
|
4255
|
-
* 而落盘的时候这两者天差地别 —— 前者是一次决定,后者写下去就是**替用户
|
|
4256
|
-
* 编了一个他没做过的决定**。
|
|
4257
|
-
*/
|
|
4258
4819
|
noWorkspace = /* @__PURE__ */ new Set();
|
|
4259
4820
|
gate;
|
|
4260
4821
|
recent;
|
|
@@ -4266,51 +4827,17 @@ var init_workspace = __esm({
|
|
|
4266
4827
|
this.decisions = opts.decisions ?? null;
|
|
4267
4828
|
this.now = opts.now ?? Date.now;
|
|
4268
4829
|
}
|
|
4269
|
-
/** 这个会话绑在哪儿。没绑过 `null` —— 「不使用工作区」是真的一档,不是缺省值 */
|
|
4270
4830
|
of(sessionId) {
|
|
4271
4831
|
return this.bindings.get(sessionId) ?? null;
|
|
4272
4832
|
}
|
|
4273
|
-
/**
|
|
4274
|
-
* 记一笔「建出来了、还没选地盘」。判据全文在 {@link WorkspaceControl.defer}。
|
|
4275
|
-
*
|
|
4276
|
-
* **已经绑上的会话调它是空操作**,不是把它退回未选:那等于绕过决定 18 的
|
|
4277
|
-
* 「绑定后不给改」—— 一个宿主只要先 `defer()` 再 `bind()`,就能给一段跑过
|
|
4278
|
-
* 一整轮活的会话换地盘,而 `bind()` 那道 `locked` 闸拦的正是这件事。
|
|
4279
|
-
*/
|
|
4280
4833
|
defer(sessionId) {
|
|
4281
4834
|
if (this.bindings.has(sessionId)) return;
|
|
4282
4835
|
this.deferred.add(sessionId);
|
|
4283
4836
|
}
|
|
4284
|
-
/** 这个会话是哪一档。判据见 {@link WorkspaceBindingState} */
|
|
4285
4837
|
stateOf(sessionId) {
|
|
4286
4838
|
if (this.bindings.has(sessionId)) return "bound";
|
|
4287
4839
|
return this.deferred.has(sessionId) ? "unbound" : "none";
|
|
4288
4840
|
}
|
|
4289
|
-
/**
|
|
4290
|
-
* 装配时把**引导会话**的绑定记进来。
|
|
4291
|
-
*
|
|
4292
|
-
* 和 {@link bind} 分开是因为这一份已经算好了:`buildRuntime()` 手里的
|
|
4293
|
-
* `workspace`(含 `--add-dir`)、`services.trust`、`services.projectContext`
|
|
4294
|
-
* 就是引擎**真在用**的那三样。走 bind() 会重新 `discoverProject()`(要跑 git)
|
|
4295
|
-
* 算出第二份 —— 那第二份哪怕只差一个字节,界面上写的就不是引擎真在用的那个。
|
|
4296
|
-
*
|
|
4297
|
-
* `unloadedInstructions` **由这里补**,不要调用方给:它是 `workspace` 的纯函数,
|
|
4298
|
-
* 而让两个 `seed()` 调用点各算一遍的失败形态是其中一个漏了 ——
|
|
4299
|
-
* 结果是「resume 之后那条『额外目录的 CLAUDE.md 没加载』的提示凭空消失」。
|
|
4300
|
-
*
|
|
4301
|
-
* @param opts.decision 顺带把这一份**落盘**成用户的工作区决定(会话库 v7)。
|
|
4302
|
-
* **两个调用点一个给一个不给,这正是这个参数存在的全部理由**:
|
|
4303
|
-
*
|
|
4304
|
-
* - `buildRuntime()` 里引导会话那一句 —— **给**。用户在哪个目录里敲的
|
|
4305
|
-
* `epoch` / `epoch web`,那就是他为这段会话选的地盘,和在 picker 里点一下
|
|
4306
|
-
* 没有区别。不给的话「直接 `epoch web` 聊两句」这条最短路径重启后一律
|
|
4307
|
-
* 掉进「早先的会话」,而那正是这一轮要修的;
|
|
4308
|
-
* - `/resume` 那一句(`build.ts` 的 `SessionControl.resume` 回调)——
|
|
4309
|
-
* **不给**。那儿的规矩是「接回来的会话跑在**这个进程**的工作区里」,
|
|
4310
|
-
* 它是一次**临时接管**,不是那段会话的主人改了主意。落盘它的后果是:
|
|
4311
|
-
* 随手在 `/tmp` 里 `epoch --resume` 一段三周前的会话,就把它在库里的
|
|
4312
|
-
* 地盘永久改成了 `/tmp`。
|
|
4313
|
-
*/
|
|
4314
4841
|
seed(sessionId, bound, opts) {
|
|
4315
4842
|
this.bindings.set(sessionId, {
|
|
4316
4843
|
...bound,
|
|
@@ -4322,17 +4849,6 @@ var init_workspace = __esm({
|
|
|
4322
4849
|
this.decisions?.record(sessionId, { state: "bound", root: bound.workspace.root });
|
|
4323
4850
|
}
|
|
4324
4851
|
}
|
|
4325
|
-
/**
|
|
4326
|
-
* 把一个目录绑到会话上。
|
|
4327
|
-
*
|
|
4328
|
-
* 顺序是**先校验路径、再看有没有被锁**:一个写错的路径不管会话锁没锁都是写错的,
|
|
4329
|
-
* 而反过来的话「路径不存在」这条永远被 `locked` 盖住 —— 于是调用方收到的是
|
|
4330
|
-
* 「不能改」,而他真正的问题是「这个目录我打错了」。
|
|
4331
|
-
*
|
|
4332
|
-
* **只认绝对路径。** 相对路径要么相对服务进程的 cwd、要么相对用户以为的某个
|
|
4333
|
-
* 目录,两者不一致时没有任何迹象;而这条路的入口是网线(浏览器传字符串进来),
|
|
4334
|
-
* 那儿压根没有「当前目录」这个概念。
|
|
4335
|
-
*/
|
|
4336
4852
|
bind(sessionId, dir, lang) {
|
|
4337
4853
|
const invalid = inspectRoot(dir, lang);
|
|
4338
4854
|
if (invalid) return invalid;
|
|
@@ -4358,75 +4874,28 @@ var init_workspace = __esm({
|
|
|
4358
4874
|
this.decisions?.record(sessionId, { state: "bound", root });
|
|
4359
4875
|
return { ok: true, bound, changed: true };
|
|
4360
4876
|
}
|
|
4361
|
-
/** 记一笔「明确不使用工作区」。判据全文在 {@link WorkspaceControl.decideNone} */
|
|
4362
4877
|
decideNone(sessionId) {
|
|
4363
4878
|
if (this.bindings.has(sessionId)) return;
|
|
4364
4879
|
this.deferred.delete(sessionId);
|
|
4365
4880
|
this.noWorkspace.add(sessionId);
|
|
4366
4881
|
this.decisions?.record(sessionId, { state: "none" });
|
|
4367
4882
|
}
|
|
4368
|
-
/**
|
|
4369
|
-
* 用户为这个会话做过的工作区决定;**没做过是 `null`**。
|
|
4370
|
-
*
|
|
4371
|
-
* 这是 `SessionStore.ensure()` 建行那一刻要问的那一句(判据在
|
|
4372
|
-
* {@link WorkspaceDecisionLog.record} 的「实现方不许建行」那一节)。
|
|
4373
|
-
*
|
|
4374
|
-
* ⚠️ 和 {@link stateOf} 的差别只有一处,但那一处是全部意义所在:
|
|
4375
|
-
* `stateOf()` 把「明确不使用」和「没听说过这个 id」都答成 `none`,
|
|
4376
|
-
* 这里前者是 `{state:'none'}`、后者是 `null`。
|
|
4377
|
-
*/
|
|
4378
4883
|
decisionOf(sessionId) {
|
|
4379
4884
|
const bound = this.bindings.get(sessionId);
|
|
4380
4885
|
if (bound) return { state: "bound", root: bound.workspace.root };
|
|
4381
4886
|
return this.noWorkspace.has(sessionId) ? { state: "none" } : null;
|
|
4382
4887
|
}
|
|
4383
|
-
/**
|
|
4384
|
-
* 解绑(会话被删掉时)。
|
|
4385
|
-
*
|
|
4386
|
-
* **不动「已知工作区」清单**:那份记的是「我最近在哪几个目录里干过活」,
|
|
4387
|
-
* 删掉一段会话不代表那件事没发生过。
|
|
4388
|
-
*
|
|
4389
|
-
* ⚠️ 2026-08-17 起它也把 {@link defer} 那一笔清掉,于是「删掉一段还没选地盘的
|
|
4390
|
-
* 会话」不会在集合里留一个永远等不到选择的 id(方案 55 验收第 8 条)。
|
|
4391
|
-
*
|
|
4392
|
-
* ⚠️ **「明确选不使用工作区」那一下 2026-08-19 起不再走这里**,改走
|
|
4393
|
-
* {@link decideNone}。原来两者在内存里等价(摘掉 `defer` 那一笔之后
|
|
4394
|
-
* {@link stateOf} 就是 `none`),但落盘之后不再等价:这个方法**同时**是
|
|
4395
|
-
* 建会话失败的回滚路径和删会话的路径,在它里面记一次决定,等于让一段建到
|
|
4396
|
-
* 一半失败的会话在库里留下「用户选了不使用工作区」。判据全文在 `decideNone` 上。
|
|
4397
|
-
*
|
|
4398
|
-
* **它照旧把 `noWorkspace` 那一笔也摘掉** —— 会话都没了,那个决定连同它一起走。
|
|
4399
|
-
* ⚠️ 摘的只是内存这一份:库里那一行由 `DELETE /api/sessions/:id` 整行删掉,
|
|
4400
|
-
* 不是在这儿改字段。
|
|
4401
|
-
*/
|
|
4402
4888
|
release(sessionId) {
|
|
4403
4889
|
this.bindings.delete(sessionId);
|
|
4404
4890
|
this.deferred.delete(sessionId);
|
|
4405
4891
|
this.noWorkspace.delete(sessionId);
|
|
4406
4892
|
}
|
|
4407
|
-
/** 本机「已知工作区」清单,最近使用的在前 */
|
|
4408
4893
|
known() {
|
|
4409
4894
|
return this.recent.list();
|
|
4410
4895
|
}
|
|
4411
|
-
/**
|
|
4412
|
-
* 一个目录的信任判定。
|
|
4413
|
-
*
|
|
4414
|
-
* 和 {@link resolveBinding} 一样先归一到**项目根**再判 —— 两处分叉的表现是
|
|
4415
|
-
* 「清单里说未信任、绑上去之后指令文件却加载了」。
|
|
4416
|
-
*
|
|
4417
|
-
* 只查不写:`decide()` 是纯读(`TrustManager.check` + 降级规则),
|
|
4418
|
-
* 不往清单里记账,也不建绑定。
|
|
4419
|
-
*/
|
|
4420
4896
|
trustOf(dir) {
|
|
4421
4897
|
return this.gate.decide(resolveProjectRoot(dir));
|
|
4422
4898
|
}
|
|
4423
|
-
/**
|
|
4424
|
-
* 一个目录 → 完整的绑定。
|
|
4425
|
-
*
|
|
4426
|
-
* 信任按**项目根**判而不是按传进来那个目录判,和 `discoverProject()` 内部
|
|
4427
|
-
* 保持一致(在仓库子目录里绑定会继承仓库根的信任决定)。两处判据分叉的表现是
|
|
4428
|
-
* 「界面说未信任、指令文件却加载了」,或者反过来。
|
|
4429
|
-
*/
|
|
4430
4899
|
resolveBinding(root) {
|
|
4431
4900
|
const trust = this.gate.decide(resolveProjectRoot(root));
|
|
4432
4901
|
const { workspace } = resolveWorkspace(root);
|
|
@@ -4434,26 +4903,25 @@ var init_workspace = __esm({
|
|
|
4434
4903
|
workspace,
|
|
4435
4904
|
trust,
|
|
4436
4905
|
projectContext: discoverProject(root, trust),
|
|
4437
|
-
// 照旧探一遍而不是写死 `[]`:今天没有额外根所以它必然是空的,
|
|
4438
|
-
// 但「为什么是空的」这件事不该由一个字面量来记住
|
|
4439
4906
|
unloadedInstructions: unloadedInstructionFiles(workspace)
|
|
4440
4907
|
};
|
|
4441
4908
|
}
|
|
4442
4909
|
};
|
|
4443
4910
|
}
|
|
4444
4911
|
});
|
|
4445
|
-
|
|
4446
4912
|
// src/build.ts
|
|
4447
4913
|
var build_exports = {};
|
|
4448
4914
|
__export(build_exports, {
|
|
4449
4915
|
AgentRoleError: () => AgentRoleError,
|
|
4916
|
+
buildGoalCatalog: () => buildGoalCatalog,
|
|
4450
4917
|
buildRuntime: () => buildRuntime,
|
|
4451
4918
|
ensureSecretsReady: () => ensureSecretsReady
|
|
4452
4919
|
});
|
|
4453
4920
|
async function buildRuntime(opts = {}) {
|
|
4454
4921
|
const opened = { cleanups: null };
|
|
4922
|
+
const processes = createProcessTable();
|
|
4455
4923
|
try {
|
|
4456
|
-
return await assembleRuntime(opts, opened);
|
|
4924
|
+
return await assembleRuntime(opts, opened, processes);
|
|
4457
4925
|
} catch (err) {
|
|
4458
4926
|
for (const fn of [...opened.cleanups ?? []].reverse()) {
|
|
4459
4927
|
try {
|
|
@@ -4462,16 +4930,17 @@ async function buildRuntime(opts = {}) {
|
|
|
4462
4930
|
}
|
|
4463
4931
|
}
|
|
4464
4932
|
try {
|
|
4465
|
-
await
|
|
4933
|
+
await processes.release();
|
|
4466
4934
|
} catch {
|
|
4467
4935
|
}
|
|
4468
4936
|
throw err;
|
|
4469
4937
|
}
|
|
4470
4938
|
}
|
|
4471
|
-
async function assembleRuntime(opts, opened) {
|
|
4939
|
+
async function assembleRuntime(opts, opened, processes) {
|
|
4472
4940
|
const startedAt = Date.now();
|
|
4473
4941
|
const workDir = opts.workDir ?? process.cwd();
|
|
4474
4942
|
const sessionId = opts.resumeId || randomUUID();
|
|
4943
|
+
processes.claim(sessionId);
|
|
4475
4944
|
const diagnostics = new DiagnosticSink();
|
|
4476
4945
|
if (opts.hostPreset) {
|
|
4477
4946
|
await applyHostPreset({
|
|
@@ -4499,14 +4968,8 @@ async function assembleRuntime(opts, opened) {
|
|
|
4499
4968
|
...opts.interactive === void 0 ? {} : { interactive: opts.interactive },
|
|
4500
4969
|
...opts.logger ? { logger: opts.logger } : {},
|
|
4501
4970
|
...settingsPath ? { settingsPath } : {},
|
|
4502
|
-
// 宿主注入的技能目录(方案 44)。空数组也照传:`resolveSkillDirs` 对空数组
|
|
4503
|
-
// 和不给这个键的处理逐字相同,多一个分支只是多一处会走散的地方
|
|
4504
4971
|
hostSkillDirs: hostCaps.skillDirs,
|
|
4505
|
-
// 没有 `?? process.env.*` 的兜底,理由见 `BuildRuntimeOptions.managedSettingsPath`
|
|
4506
4972
|
...opts.managedSettingsPath ? { managedSettingsPath: opts.managedSettingsPath } : {},
|
|
4507
|
-
// **这一行必须紧跟着上面那次 `loadConfig()`**:`getConfigOrigins()` 报的是
|
|
4508
|
-
// 最近一次加载的账,中间隔一次别人的 `loadConfig()` 就串台了。递进去而不是
|
|
4509
|
-
// 让 `buildServices` 自己去问,判据写在 `ServiceOptions.configOrigins` 上
|
|
4510
4973
|
configOrigins
|
|
4511
4974
|
});
|
|
4512
4975
|
const config = services.config;
|
|
@@ -4545,21 +5008,25 @@ async function assembleRuntime(opts, opened) {
|
|
|
4545
5008
|
if (sandbox) {
|
|
4546
5009
|
cleanups.push(() => sandbox.destroy());
|
|
4547
5010
|
isolation = sandbox.getIsolation();
|
|
4548
|
-
diagnostics.ok("
|
|
5011
|
+
diagnostics.ok("SandboxIsolation", isolation.detail);
|
|
4549
5012
|
services.permission?.setSandboxIsolation(isolation.level);
|
|
4550
5013
|
}
|
|
4551
5014
|
if (services.permission) {
|
|
4552
5015
|
applyHeadless(services.permission, config, opts.headless, diagnostics, opts.interactive);
|
|
4553
5016
|
}
|
|
4554
5017
|
const budgetWiring = wireBudget(config, services.modelPricing, diagnostics);
|
|
5018
|
+
const bootstrapReveal = createToolRevealState();
|
|
4555
5019
|
const registry = registerLocalTools({
|
|
4556
5020
|
homeDir: config.homeDir,
|
|
5021
|
+
deferMcp: config.tools?.deferMcp === true,
|
|
5022
|
+
toolMode: foldToolMode(config.tools?.mode),
|
|
5023
|
+
bootstrapReveal,
|
|
4557
5024
|
trackerDb: services.trackerDb,
|
|
4558
5025
|
goals: services.goals,
|
|
4559
5026
|
memory: services.memory,
|
|
4560
5027
|
sandbox,
|
|
5028
|
+
skills: services.skill,
|
|
4561
5029
|
diags: diagnostics,
|
|
4562
|
-
// 合并后那份 config —— 项目级设置将来若放开 search 段,这里天然跟着走
|
|
4563
5030
|
...config.search ? { search: config.search } : {}
|
|
4564
5031
|
});
|
|
4565
5032
|
registerSessionSearch(registry, {
|
|
@@ -4568,11 +5035,20 @@ async function assembleRuntime(opts, opened) {
|
|
|
4568
5035
|
...config.sessionSearch ? { sessionSearch: config.sessionSearch } : {},
|
|
4569
5036
|
diags: diagnostics
|
|
4570
5037
|
});
|
|
4571
|
-
const mcpWiring = await registerMcpTools(
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
5038
|
+
const mcpWiring = await registerMcpTools(
|
|
5039
|
+
registry,
|
|
5040
|
+
config.homeDir,
|
|
5041
|
+
diagnostics,
|
|
5042
|
+
[
|
|
5043
|
+
{ source: "host", servers: hostCaps.mcpServers },
|
|
5044
|
+
{ source: "plugin", servers: wirePluginMcpServers(services.plugins.mcpPaths, diagnostics) }
|
|
5045
|
+
],
|
|
5046
|
+
config.tools?.deferMcp === true
|
|
5047
|
+
);
|
|
4575
5048
|
cleanups.push(mcpWiring.dispose);
|
|
5049
|
+
registry.registerTools(
|
|
5050
|
+
createMcpTools({ control: mcpWiring.control, servers: () => mcpWiring.status() })
|
|
5051
|
+
);
|
|
4576
5052
|
const bridges = new SessionBridges(opts.onApprovalRequest);
|
|
4577
5053
|
const interactive = !services.permission?.isNonInteractive();
|
|
4578
5054
|
const requestPlanApproval = (req, sessionId2) => bridges.approve(sessionId2, req);
|
|
@@ -4582,39 +5058,41 @@ async function assembleRuntime(opts, opened) {
|
|
|
4582
5058
|
requestPlanApproval,
|
|
4583
5059
|
interactive
|
|
4584
5060
|
);
|
|
5061
|
+
const defaultLevel = buildDefaultLevelSlot(services);
|
|
4585
5062
|
const guards = buildSessionGuards({
|
|
4586
5063
|
permission: services.permission,
|
|
4587
5064
|
planState,
|
|
4588
5065
|
managed: services.managedPolicy,
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
// `ToolContext.sessionId` 决定,在这儿另写一个闭包等于把那条路由绕过去
|
|
5066
|
+
deferredTools: () => registry.byExposure(ToolExposure.Deferred),
|
|
5067
|
+
bootstrapReveal,
|
|
5068
|
+
defaultLevel,
|
|
4593
5069
|
requestApproval: requestPlanApproval,
|
|
4594
5070
|
interactive
|
|
4595
5071
|
});
|
|
4596
5072
|
const bootstrapGuard = guards.bootstrap();
|
|
4597
5073
|
registerAskQuestion(registry, (req, sessionId2) => bridges.ask(sessionId2, req), interactive);
|
|
5074
|
+
registry.registerTools(
|
|
5075
|
+
dropOverlongToolNames(
|
|
5076
|
+
hostCaps.tools,
|
|
5077
|
+
HOST_CAPABILITIES_MODULE,
|
|
5078
|
+
"host-tool-name-too-long",
|
|
5079
|
+
diagnostics
|
|
5080
|
+
),
|
|
5081
|
+
"host"
|
|
5082
|
+
);
|
|
4598
5083
|
const commands = wireCommands({
|
|
4599
5084
|
homeDir: config.homeDir,
|
|
4600
5085
|
projectRoot: services.projectContext.rootDir,
|
|
4601
5086
|
trusted: services.trust.trusted,
|
|
4602
5087
|
pluginDirs: services.plugins.commandDirs,
|
|
4603
5088
|
toolProvider: registry.asProvider(),
|
|
4604
|
-
// 传的是**函数**:命令展开发生在用户敲下那一刻,而这之前他可能已经
|
|
4605
|
-
// `/model` 换过了。取值钉住就等于永远解析成启动时那个模型
|
|
4606
|
-
//
|
|
4607
|
-
// 只递 provider + model 两段:`origin` 讲的是**当前选择**怎么来的,
|
|
4608
|
-
// 而这里回答的是「工具模型是谁」。原样带过去,命令会展开出一个
|
|
4609
|
-
// `origin: 'config'` 的 ref,随后 `enterTurn` 再按 `command-frontmatter`
|
|
4610
|
-
// 盖一次 —— 同一个字段有两个说法,出事时看哪个都不对
|
|
4611
5089
|
utilityModel: () => {
|
|
4612
5090
|
const s = services.provider?.getUtilityInfo().selection;
|
|
4613
5091
|
return s ? { provider: s.provider, model: s.model } : void 0;
|
|
4614
5092
|
},
|
|
4615
5093
|
diags: diagnostics
|
|
4616
5094
|
});
|
|
4617
|
-
const checkpoints = new CheckpointManager({ workDir, homeDir: config.homeDir });
|
|
5095
|
+
const checkpoints = new CheckpointManager({ workDir: () => workDir, homeDir: config.homeDir });
|
|
4618
5096
|
const { providerBridge } = services;
|
|
4619
5097
|
const agentRoles = [];
|
|
4620
5098
|
const agentRoleNotices = [];
|
|
@@ -4633,8 +5111,11 @@ async function assembleRuntime(opts, opened) {
|
|
|
4633
5111
|
mainRole = pickMainRole(opts.agent ?? process.env.EPOCH_AGENT, roles);
|
|
4634
5112
|
if (mainRole) {
|
|
4635
5113
|
diagnostics.ok(
|
|
4636
|
-
"
|
|
4637
|
-
|
|
5114
|
+
"MainAgentRole",
|
|
5115
|
+
t("boot.main_role", {
|
|
5116
|
+
name: mainRole.name,
|
|
5117
|
+
tools: mainRole.tools ? t("boot.main_role_tools", { count: mainRole.tools.length }) : t("boot.main_role_no_narrow")
|
|
5118
|
+
})
|
|
4638
5119
|
);
|
|
4639
5120
|
}
|
|
4640
5121
|
}
|
|
@@ -4652,34 +5133,25 @@ async function assembleRuntime(opts, opened) {
|
|
|
4652
5133
|
provider: providerBridge,
|
|
4653
5134
|
models,
|
|
4654
5135
|
guards,
|
|
4655
|
-
// 引导会话的工作目录。**不能拿 `projectContext.rootDir` 顶替** ——
|
|
4656
|
-
// 在仓库子目录里启动是常态,顶替等于悄悄换了相对路径的解析基准
|
|
4657
5136
|
defaultWorkDir: workDir,
|
|
4658
|
-
// 收窄过的那一层(方案 23);未收窄那份给子 agent,判据见工厂的选项注释
|
|
4659
5137
|
toolProvider: commands.toolProvider,
|
|
4660
5138
|
rawToolProvider: registry.asProvider(),
|
|
5139
|
+
deferredTools: () => registry.byExposure(ToolExposure.Deferred),
|
|
5140
|
+
codeModeTools: () => registry.codeModeDeclared(),
|
|
4661
5141
|
telemetry,
|
|
5142
|
+
compliance: services.compliance,
|
|
4662
5143
|
budget: budgetWiring,
|
|
4663
5144
|
...opts.onApprovalRequest ? { hostApproval: opts.onApprovalRequest } : {},
|
|
4664
5145
|
workspaces,
|
|
4665
5146
|
bridges,
|
|
4666
5147
|
sessions: services.session,
|
|
4667
|
-
// 目标(方案 52)。**进程一份**,收窄成「这一段会话」发生在工厂里
|
|
4668
|
-
// (`buildGoalControl(…, () => session.sessionId)`)
|
|
4669
5148
|
goals: services.goals,
|
|
4670
5149
|
...opts.sessionSource ? { sessionSource: opts.sessionSource } : {},
|
|
4671
5150
|
mainRole: mainRole ?? null,
|
|
4672
5151
|
roles: () => agentRoles,
|
|
4673
|
-
// 后台任务的状态变化(方案 36 PR-2)。给函数不给数组 —— 任务状态每时每刻
|
|
4674
|
-
// 在变,快照拿到的是装配那一刻的。
|
|
4675
|
-
//
|
|
4676
|
-
// ⚠️ 2026-08-15 起它**按会话取**:工厂给每个会话拿它自己那一批(判据在
|
|
4677
|
-
// plugin-terminal 的 `background.ts` 文件头)。收窄之前每个会话每轮都被告知
|
|
4678
|
-
// 一批**它没起过**的命令在跑,而那条通知是直接进模型上下文的
|
|
4679
5152
|
backgroundTasks: (id) => listTasks(id),
|
|
4680
|
-
// 子任务跑完之后把它起的后台任务归还给派活的会话(判据在 `subRunner` 末尾)。
|
|
4681
|
-
// 这里是全仓唯一一处知道「任务表由 plugin-terminal 提供」的地方
|
|
4682
5153
|
rekeyBackgroundTasks: rekeyTasks,
|
|
5154
|
+
processes,
|
|
4683
5155
|
diags: diagnostics
|
|
4684
5156
|
});
|
|
4685
5157
|
if (providerBridge) {
|
|
@@ -4689,19 +5161,22 @@ async function assembleRuntime(opts, opened) {
|
|
|
4689
5161
|
for (const c of registry.getConflicts()) {
|
|
4690
5162
|
diagnostics.warn(
|
|
4691
5163
|
`Tool ${c.name}`,
|
|
4692
|
-
|
|
5164
|
+
t("boot.tool_conflict", { dropped: c.droppedFrom, kept: c.keptFrom }),
|
|
4693
5165
|
"tool-name-conflict"
|
|
4694
5166
|
);
|
|
4695
5167
|
}
|
|
4696
5168
|
}
|
|
5169
|
+
reportToolCost(registry, diagnostics);
|
|
4697
5170
|
const store = services.session ? new SessionStore(
|
|
4698
5171
|
services.session,
|
|
4699
5172
|
config.homeDir,
|
|
4700
|
-
services.projectContext.rootDir,
|
|
5173
|
+
() => services.projectContext.rootDir,
|
|
4701
5174
|
opts.sessionSource
|
|
4702
5175
|
) : void 0;
|
|
4703
5176
|
const history = opts.resumeId && store ? store.loadHistory(opts.resumeId) : [];
|
|
4704
|
-
if (history.length > 0)
|
|
5177
|
+
if (history.length > 0) {
|
|
5178
|
+
diagnostics.ok("History", t("boot.history_loaded", { count: history.length }));
|
|
5179
|
+
}
|
|
4705
5180
|
const bootstrap = factory.adopt(sessionId, history);
|
|
4706
5181
|
const session = bootstrap?.session ?? null;
|
|
4707
5182
|
const agent = bootstrap?.agent ?? null;
|
|
@@ -4728,7 +5203,7 @@ async function assembleRuntime(opts, opened) {
|
|
|
4728
5203
|
}
|
|
4729
5204
|
}
|
|
4730
5205
|
try {
|
|
4731
|
-
await
|
|
5206
|
+
await processes.release();
|
|
4732
5207
|
} catch {
|
|
4733
5208
|
}
|
|
4734
5209
|
};
|
|
@@ -4737,10 +5212,6 @@ async function assembleRuntime(opts, opened) {
|
|
|
4737
5212
|
const provider = services.provider;
|
|
4738
5213
|
const listTools = () => commands.toolProvider.list().map((tool) => ({
|
|
4739
5214
|
tool,
|
|
4740
|
-
// 注册表一直记着「谁注册的这个工具」,只是以前没人往外传。能力页的
|
|
4741
|
-
// 「内置连接器」那一组要按它分(方案 42 PR-1)—— 按工具名前缀猜
|
|
4742
|
-
// (`file_*` 归 plugin-file)会在第一个不按前缀命名的工具上错。
|
|
4743
|
-
// 取不到只可能是收窄层放进来一个 registry 没见过的名字,那时说 `builtin`
|
|
4744
5215
|
source: registry.sourceOf(tool.name) ?? "builtin"
|
|
4745
5216
|
}));
|
|
4746
5217
|
const schedules = createScheduleControl({
|
|
@@ -4748,30 +5219,35 @@ async function assembleRuntime(opts, opened) {
|
|
|
4748
5219
|
...opts.scheduleRunner ? { runner: opts.scheduleRunner } : {}
|
|
4749
5220
|
});
|
|
4750
5221
|
cleanups.push(schedules.dispose);
|
|
5222
|
+
if (schedules.openError) {
|
|
5223
|
+
diagnostics.failed("Schedules", `FAILED (${schedules.openError.message})`, "init-failed");
|
|
5224
|
+
} else {
|
|
5225
|
+
diagnostics.ok("Schedules");
|
|
5226
|
+
}
|
|
5227
|
+
const hostMarketplaces = opts.hostMarketplaces;
|
|
5228
|
+
if (hostMarketplaces) {
|
|
5229
|
+
await wireHostMarketplaces({
|
|
5230
|
+
hostMarketplaces,
|
|
5231
|
+
statePath: marketplacesPath(config.homeDir),
|
|
5232
|
+
diagnostics
|
|
5233
|
+
});
|
|
5234
|
+
}
|
|
5235
|
+
reportI18nHealth(diagnostics);
|
|
4751
5236
|
return {
|
|
4752
5237
|
agent,
|
|
4753
5238
|
session,
|
|
4754
|
-
// 多会话工厂(方案 30 §6.3)。`SessionFactory` 是收窄过的那一面 ——
|
|
4755
|
-
// `adopt` / `subRunner` 是装配层的特权,宿主拿到它就等于能绕过 `bind()`
|
|
4756
|
-
// 那两道门(判据同 `WorkspaceControl` 上刻意不露 `seed` 的那一条)
|
|
4757
5239
|
sessionFactory: factory,
|
|
5240
|
+
events: processEventBus,
|
|
4758
5241
|
sessionStore: store ?? null,
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
// `??` 那一支只在 provider 没起来时走得到(那时一个会话都装配不出来):
|
|
4765
|
-
// 这个字段不可为 null,而一个读得出空列表的控制面是那时唯一诚实的答案
|
|
5242
|
+
sessionReferences: services.session?.references ?? null,
|
|
5243
|
+
workspaceFiles: buildWorkspaceFilesControl(
|
|
5244
|
+
{ listFiles: listWorkspaceFiles, resolveInWorkspace, systemOpenArgv },
|
|
5245
|
+
services.permission
|
|
5246
|
+
),
|
|
4766
5247
|
checkpoints: bootstrap?.checkpoints ?? buildCheckpointControl(checkpoints, null, () => sessionId),
|
|
4767
|
-
// **引导会话自己那一份**,判据逐字同上面的 `checkpoints`:从工厂手里拿,
|
|
4768
|
-
// 不在这儿再包一个。`??` 那一支同理只在 provider 没起来时走得到 ——
|
|
4769
|
-
// 那时候没有会话,而目标是会话的状态,`null` 是那一刻唯一诚实的答案
|
|
4770
5248
|
goals: bootstrap?.goals ?? (services.goals ? buildGoalControl(services.goals, () => sessionId) : null),
|
|
5249
|
+
goalCatalog: services.goals ? buildGoalCatalog(services.goals) : null,
|
|
4771
5250
|
config,
|
|
4772
|
-
// 这次实际生效的数据目录,以及从它派生的 artifact 根目录(方案 54 §二)。
|
|
4773
|
-
// **调 `paths.ts` 而不是 `join(homeDir, 'artifacts')`**:路径真源只有一处,
|
|
4774
|
-
// 在这里拼字符串就是给同一个目录第二个说法 —— 而那正是这一节要修的病
|
|
4775
5251
|
homeDir: config.homeDir,
|
|
4776
5252
|
artifactsRoot: artifactsDir(void 0, config.homeDir),
|
|
4777
5253
|
sessionId,
|
|
@@ -4779,22 +5255,11 @@ async function assembleRuntime(opts, opened) {
|
|
|
4779
5255
|
schedules: schedules.control,
|
|
4780
5256
|
workspace,
|
|
4781
5257
|
workspaces,
|
|
4782
|
-
// 装配过程里到处在用的那个布尔(指令文件、角色文件、自定义命令都问它),
|
|
4783
|
-
// 原样露出来给 doctor —— 别在宿主侧重新判一遍,见字段的 JSDoc
|
|
4784
5258
|
trusted: services.trust.trusted,
|
|
4785
|
-
// 信任记录的读写口(方案 54 §五)。**零新逻辑** —— 转的就是装配时那个
|
|
4786
|
-
// manager 实例本身,宿主 `record()` 之后本进程新绑的工作区立刻按新判定走
|
|
4787
|
-
// (闸门每次现问 `check()`,中间没有缓存)。
|
|
4788
|
-
//
|
|
4789
|
-
// 转实例而不是包一层收窄:四个方法就是 protocol 的 `TrustManager` 契约,
|
|
4790
|
-
// 包一层只会多出一处「哪天签名变了这里忘了跟」。
|
|
4791
5259
|
trust: services.trustManager,
|
|
4792
5260
|
isolation,
|
|
4793
5261
|
policy: services.policy,
|
|
4794
|
-
|
|
4795
|
-
// 「模型现在看得见什么」,而一条写了 `allowed-tools` 的自定义命令正在跑的时候,
|
|
4796
|
-
// 模型看得见的就是白名单里那几个。读原始 registry 的话 `/tools` 会在收窄期间说假话。
|
|
4797
|
-
// getter 而不是快照:MCP 刷新(方案 15)和收窄进出都要能立刻反映出来
|
|
5262
|
+
hook: services.hook,
|
|
4798
5263
|
get tools() {
|
|
4799
5264
|
return listTools().map(({ tool, source }) => ({
|
|
4800
5265
|
name: tool.name,
|
|
@@ -4804,19 +5269,10 @@ async function assembleRuntime(opts, opened) {
|
|
|
4804
5269
|
},
|
|
4805
5270
|
usageScope: budgetWiring.budgetStore ? "session" : "run",
|
|
4806
5271
|
compression: compressionLine(config, services),
|
|
4807
|
-
// **getter 而不是快照**(和上面的 `tools` 同一个理由):方案 26 之后
|
|
4808
|
-
// `/model` 能在运行期换 provider,拍一张快照的话 `/model` 会一直显示
|
|
4809
|
-
// 启动时那家和那把 key —— 一个专门用来"确认我现在用的是什么"的面板在说假话
|
|
4810
5272
|
get providerInfo() {
|
|
4811
5273
|
return provider ? { provider: provider.getProviderType(), apiKeyHint: provider.getApiKeyHint() } : null;
|
|
4812
5274
|
},
|
|
4813
|
-
// **引导会话那一份**(2026-08-16)——它就是这个进程那个 router 的出口本体,
|
|
4814
|
-
// 所以 TUI 的 `/model`、CLI 的 `--model` 一个字节都没变。别的会话走
|
|
4815
|
-
// `sessionFactory.get(id).model`,判据见 [session-model.ts](./session-model.ts)
|
|
4816
5275
|
model: bootstrapModel?.control ?? null,
|
|
4817
|
-
// **不吃 `provider`**,所以 provider 层起不来时它照样在(判据在字段上)。
|
|
4818
|
-
// `configured` 写成 getter 而不是拍下来的两个值:base URL 只对配置里那一家
|
|
4819
|
-
// 有效,而那条判断住在 `model-catalog.ts` 里,不在这儿
|
|
4820
5276
|
modelCatalog: buildModelCatalogControl({
|
|
4821
5277
|
homeDir: config.homeDir,
|
|
4822
5278
|
configured: () => ({
|
|
@@ -4824,51 +5280,46 @@ async function assembleRuntime(opts, opened) {
|
|
|
4824
5280
|
...config.provider.baseUrl ? { baseUrl: config.provider.baseUrl } : {}
|
|
4825
5281
|
})
|
|
4826
5282
|
}),
|
|
5283
|
+
utility: provider ? buildUtilityControl({
|
|
5284
|
+
provider,
|
|
5285
|
+
...budgetWiring.budgetStore ? { store: budgetWiring.budgetStore } : {},
|
|
5286
|
+
...budgetWiring.pricingOf ? { pricingOf: budgetWiring.pricingOf } : {}
|
|
5287
|
+
}) : null,
|
|
4827
5288
|
agentRoles,
|
|
4828
5289
|
agentRoleNotices,
|
|
4829
5290
|
get skills() {
|
|
4830
5291
|
return services.skill?.list() ?? [];
|
|
4831
5292
|
},
|
|
4832
|
-
// 不计数的那条读(方案 56 §1.2)。`?? null` 而不是 `?? ''`:技能系统没起来
|
|
4833
|
-
// 和「这个技能的正文是空的」是两件事,合成同一个值之后界面画不出前者
|
|
4834
5293
|
skillBody: (name) => services.skill?.body(name) ?? null,
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
// ⚠️ `reload` 递的是上面那个 `reloadRoles` —— 建完当场重载,判据在
|
|
4840
|
-
// `role-write.ts` 文件头第二节(不重载的话这一下只做了一半)
|
|
5294
|
+
skillIndexResidency: (role) => services.skill?.indexResidency({
|
|
5295
|
+
only: role === null ? void 0 : findRole(agentRoles, role)?.skills
|
|
5296
|
+
}) ?? /* @__PURE__ */ new Map(),
|
|
5297
|
+
skillWrite: buildSkillControl(services),
|
|
4841
5298
|
roleWrite: createRoleWriteControl({
|
|
4842
5299
|
userDir: agentsDir(config.homeDir),
|
|
4843
5300
|
roles: () => agentRoles,
|
|
4844
|
-
// 给函数不给快照:MCP 连上 / 掉线都会让工具表变,而工具名要按**用户按下
|
|
4845
|
-
// 那一刻**的那份判(同 `mcpServers` 是个 getter 的判据)
|
|
4846
5301
|
tools: () => registry.asProvider(),
|
|
4847
5302
|
reload: reloadRoles
|
|
4848
5303
|
}),
|
|
4849
|
-
|
|
5304
|
+
plugins: hostMarketplaces ? createPluginControl({
|
|
5305
|
+
pluginsDir: pluginsDir(config.homeDir),
|
|
5306
|
+
statePath: pluginsStatePath(config.homeDir),
|
|
5307
|
+
marketplacesPath: marketplacesPath(config.homeDir),
|
|
5308
|
+
loaded: services.plugins.loaded,
|
|
5309
|
+
allowRemote: hostMarketplaces.allowRemote === true
|
|
5310
|
+
}) : null,
|
|
4850
5311
|
get mcpServers() {
|
|
4851
5312
|
return mcpWiring.status();
|
|
4852
5313
|
},
|
|
4853
|
-
// 收窄过的那一面(方案 56 §1.1)。**转的是 `McpWiring.control` 而不是
|
|
4854
|
-
// registry 本身** —— 判据在 `McpControl` 的文档上,同 `WorkspaceControl`
|
|
4855
|
-
// 刻意不露 `seed()` 那一条
|
|
4856
5314
|
mcp: mcpWiring.control,
|
|
4857
5315
|
memory: services.memory ? buildMemoryControl(services.memory) : null,
|
|
4858
|
-
// 引导会话那一份档位(`level` / `setLevel`)+ 三样进程级的读
|
|
4859
|
-
// (规则表 / 遮蔽 / 审计流水)。⚠️ 换档那三步(托管挡 bypass → `setLevel` →
|
|
4860
|
-
// `plan.forget()`)**不在这个函数里**:它们住在 `session-guard.ts`,
|
|
4861
|
-
// 因为它们现在要按会话跑 N 遍,而那条判定只能有一个实现
|
|
4862
5316
|
permissions: buildPermissionsControl(services, bootstrapGuard.permissions, listTools),
|
|
4863
|
-
// 设置页那一片。**收 `workspaces`** 是这一轮补的那笔账:③④ 跟着工作区走,
|
|
4864
|
-
// 而工作区一个会话绑一个(决定 18)—— 判据在 `SettingsControl` 的文档上
|
|
4865
5317
|
settings: buildSettingsControl(services, workspaces, {
|
|
4866
5318
|
...settingsPath ? { settingsPath } : {},
|
|
4867
5319
|
...opts.managedSettingsPath ? { managedSettingsPath: opts.managedSettingsPath } : {}
|
|
4868
5320
|
}),
|
|
4869
|
-
// 同 `model`:**引导会话那一份**,而它就是进程那个 `PlanModeState` 的出口本体
|
|
4870
5321
|
plan: bootstrapGuard.plan,
|
|
4871
|
-
sessions: services.session ? buildSessionControl(services.session, session, checkpoints, (from, id) => {
|
|
5322
|
+
sessions: services.session ? buildSessionControl(services.session, session, checkpoints, config.homeDir, (from, id) => {
|
|
4872
5323
|
planState?.restore(store?.loadPlan(id) ?? null);
|
|
4873
5324
|
workspaces.seed(id, {
|
|
4874
5325
|
workspace,
|
|
@@ -4879,7 +5330,6 @@ async function assembleRuntime(opts, opened) {
|
|
|
4879
5330
|
factory.rekey(source, id);
|
|
4880
5331
|
rekeyTasks(source, id);
|
|
4881
5332
|
}) : null,
|
|
4882
|
-
// getter 而不是快照,理由同 `tools` / `providerInfo`
|
|
4883
5333
|
get contextBreakdown() {
|
|
4884
5334
|
if (!agent) return null;
|
|
4885
5335
|
const historyText = (session?.getHistory() ?? []).map((m) => m.content).join("\n");
|
|
@@ -4889,18 +5339,12 @@ async function assembleRuntime(opts, opened) {
|
|
|
4889
5339
|
});
|
|
4890
5340
|
},
|
|
4891
5341
|
commands: commands.control,
|
|
4892
|
-
// 用 `registry` 而不是 `commands.toolProvider`:`!` 是**用户**敲的,
|
|
4893
|
-
// 不该被一条自定义命令的 `allowed-tools` 收窄掉(那条收窄的语义是
|
|
4894
|
-
// 「这一轮**模型**能用什么」)
|
|
4895
5342
|
userActions: createUserActions({
|
|
4896
5343
|
registry,
|
|
4897
5344
|
permission: services.permission,
|
|
4898
5345
|
workDir,
|
|
4899
5346
|
sessionId
|
|
4900
5347
|
}),
|
|
4901
|
-
// 三个通道,**一个真源**(那个 sink):字符串那份和「换个语言说一遍」那份
|
|
4902
|
-
// 都是投影,不可能漂移。前两个是快照(装配到这一步就定了),
|
|
4903
|
-
// 第三个是函数 —— 它要等请求到了才知道用哪个语言
|
|
4904
5348
|
diagnostics: diagnostics.toStrings(),
|
|
4905
5349
|
diagnosticList: diagnostics.toList(),
|
|
4906
5350
|
diagnosticsIn: (lang) => diagnostics.toList(lang),
|
|
@@ -4933,9 +5377,9 @@ function wireAgentRoles(config, services, registry, diags, hostRoles = []) {
|
|
|
4933
5377
|
});
|
|
4934
5378
|
const external = [...services.plugins.roles, ...hostRoles, ...load.overrides];
|
|
4935
5379
|
const { roles, notices } = mergeRoles(external);
|
|
4936
|
-
for (const n of notices) diags.warn(
|
|
5380
|
+
for (const n of notices) diags.warn(AGENT_ROLE_DIAG_MODULE, n.detail, n.code);
|
|
4937
5381
|
if (!load.projectSkipped) {
|
|
4938
|
-
diags.ok(
|
|
5382
|
+
diags.ok(AGENT_ROLE_DIAG_MODULE, (lang) => {
|
|
4939
5383
|
const parts = [
|
|
4940
5384
|
t("agent_roles.tally_builtin", { count: builtinRoles().length }, lang),
|
|
4941
5385
|
t("agent_roles.tally_user", { count: load.userCount }, lang),
|
|
@@ -4955,19 +5399,27 @@ function wireAgentRoles(config, services, registry, diags, hostRoles = []) {
|
|
|
4955
5399
|
const unknown = unknownRoleTools(provider, role, BLOCKED_TOOLS);
|
|
4956
5400
|
if (unknown.length > 0) {
|
|
4957
5401
|
diags.warn(
|
|
4958
|
-
|
|
4959
|
-
|
|
5402
|
+
AGENT_ROLE_DIAG_MODULE,
|
|
5403
|
+
t("boot.role_unknown_tools", { role: role.name, tools: unknown.join(", ") }),
|
|
4960
5404
|
"unknown-tool"
|
|
4961
5405
|
);
|
|
4962
5406
|
}
|
|
4963
5407
|
const hitBlocked = blockedRoleTools(role, BLOCKED_TOOLS);
|
|
4964
5408
|
if (hitBlocked.length > 0) {
|
|
4965
5409
|
diags.warn(
|
|
4966
|
-
|
|
4967
|
-
|
|
5410
|
+
AGENT_ROLE_DIAG_MODULE,
|
|
5411
|
+
t("boot.role_blocked_tools", { role: role.name, tools: hitBlocked.join(", ") }),
|
|
4968
5412
|
"role-blocked-tool"
|
|
4969
5413
|
);
|
|
4970
5414
|
}
|
|
5415
|
+
const unknownSkills = unknownRoleSkills(services.skill?.getNames() ?? [], role);
|
|
5416
|
+
if (unknownSkills.length > 0) {
|
|
5417
|
+
diags.warn(
|
|
5418
|
+
AGENT_ROLE_DIAG_MODULE,
|
|
5419
|
+
t("boot.role_unknown_skills", { role: role.name, skills: unknownSkills.join(", ") }),
|
|
5420
|
+
"unknown-skill"
|
|
5421
|
+
);
|
|
5422
|
+
}
|
|
4971
5423
|
}
|
|
4972
5424
|
return { roles, notices };
|
|
4973
5425
|
}
|
|
@@ -4977,7 +5429,9 @@ function pickMainRole(wanted, roles) {
|
|
|
4977
5429
|
const role = findRole(roles, name);
|
|
4978
5430
|
if (role) return role;
|
|
4979
5431
|
const available = roles.map((r) => r.name).join(", ");
|
|
4980
|
-
throw new AgentRoleError(
|
|
5432
|
+
throw new AgentRoleError(
|
|
5433
|
+
t("boot.role_not_found", { name, available: available || t("boot.role_none") })
|
|
5434
|
+
);
|
|
4981
5435
|
}
|
|
4982
5436
|
function makePricingLookup(homeDir) {
|
|
4983
5437
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -5005,18 +5459,18 @@ function wireBudget(config, modelPricing, diagnostics) {
|
|
|
5005
5459
|
);
|
|
5006
5460
|
const { maxCostUsd, maxTokens } = config.budget ?? {};
|
|
5007
5461
|
if (maxCostUsd === void 0 && maxTokens === void 0) {
|
|
5008
|
-
diagnostics.skipped("Budget", "
|
|
5462
|
+
diagnostics.skipped("Budget", t("boot.budget_none"));
|
|
5009
5463
|
} else {
|
|
5010
5464
|
const limits = [
|
|
5011
|
-
maxCostUsd !== void 0 ?
|
|
5465
|
+
maxCostUsd !== void 0 ? t("boot.budget_cost", { amount: maxCostUsd }) : "",
|
|
5012
5466
|
maxTokens !== void 0 ? `token ${maxTokens}` : ""
|
|
5013
5467
|
].filter(Boolean).join(" / ");
|
|
5014
|
-
diagnostics.ok("Budget",
|
|
5468
|
+
diagnostics.ok("Budget", t("boot.budget_limits", { limits }));
|
|
5015
5469
|
}
|
|
5016
5470
|
if (maxCostUsd !== void 0 && !modelPricing) {
|
|
5017
5471
|
diagnostics.warn(
|
|
5018
5472
|
"Budget",
|
|
5019
|
-
|
|
5473
|
+
t("boot.budget_no_pricing", { model: config.model }),
|
|
5020
5474
|
"no-pricing-data"
|
|
5021
5475
|
);
|
|
5022
5476
|
}
|
|
@@ -5024,8 +5478,6 @@ function wireBudget(config, modelPricing, diagnostics) {
|
|
|
5024
5478
|
...config.budget ? { budget: config.budget } : {},
|
|
5025
5479
|
...modelPricing ? { modelPricing } : {},
|
|
5026
5480
|
...budgetStore ? { budgetStore } : {},
|
|
5027
|
-
// **无条件给**,不看有没有配上限:闸门就算不拦截也要算钱(usage 事件里的
|
|
5028
|
-
// costUsd 有消费方),算错了一样是错的
|
|
5029
5481
|
pricingOf: makePricingLookup(config.homeDir)
|
|
5030
5482
|
};
|
|
5031
5483
|
}
|
|
@@ -5037,7 +5489,7 @@ function applyHeadless(permission, config, override, diagnostics, interactive) {
|
|
|
5037
5489
|
if (nonInteractive) {
|
|
5038
5490
|
diagnostics.warn(
|
|
5039
5491
|
"Headless",
|
|
5040
|
-
|
|
5492
|
+
t("boot.headless_preauth", { policy: describeHeadlessPolicy(merged) }),
|
|
5041
5493
|
"headless-preauthorized"
|
|
5042
5494
|
);
|
|
5043
5495
|
}
|
|
@@ -5062,16 +5514,17 @@ async function migrateOnce(store, diagnostics, override) {
|
|
|
5062
5514
|
envFilePath: envPath(homeDir),
|
|
5063
5515
|
store,
|
|
5064
5516
|
names: API_KEY_ENV_VARS,
|
|
5065
|
-
// 只到天:同一天多跑几次不该产生一堆备份文件,真重名时由 migrate.ts 自己让路
|
|
5066
5517
|
stamp: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "")
|
|
5067
5518
|
});
|
|
5068
5519
|
if (result.warning) diagnostics.warn("Secret", result.warning, "secret-migrate");
|
|
5069
5520
|
if (result.migrated.length > 0) {
|
|
5070
5521
|
diagnostics.ok(
|
|
5071
5522
|
"Secret",
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5523
|
+
t("boot.secret_migrated", {
|
|
5524
|
+
count: result.migrated.length,
|
|
5525
|
+
backend: store.detail,
|
|
5526
|
+
backup: result.backupPath ? t("boot.secret_migrated_backup", { path: result.backupPath }) : ""
|
|
5527
|
+
})
|
|
5075
5528
|
);
|
|
5076
5529
|
}
|
|
5077
5530
|
}
|
|
@@ -5093,7 +5546,7 @@ async function setupMcpDataKey(store, diagnostics) {
|
|
|
5093
5546
|
const why = err instanceof Error ? err.message : String(err);
|
|
5094
5547
|
diagnostics.warn(
|
|
5095
5548
|
"Secret",
|
|
5096
|
-
|
|
5549
|
+
t("boot.mcp_key_unavailable", { reason: why }),
|
|
5097
5550
|
"mcp-key-unavailable"
|
|
5098
5551
|
);
|
|
5099
5552
|
}
|
|
@@ -5129,6 +5582,34 @@ function buildSkillControl(services) {
|
|
|
5129
5582
|
const outcome = await importSkills(source, { ...opts, token });
|
|
5130
5583
|
if (outcome.ok && outcome.imported.length > 0) services.skill?.invalidateCache();
|
|
5131
5584
|
return outcome;
|
|
5585
|
+
},
|
|
5586
|
+
remove: async (name, lang) => {
|
|
5587
|
+
const skill = services.skill;
|
|
5588
|
+
if (!skill) {
|
|
5589
|
+
return {
|
|
5590
|
+
ok: false,
|
|
5591
|
+
reason: "missing",
|
|
5592
|
+
detail: t("skill.remove_no_system", void 0, lang)
|
|
5593
|
+
};
|
|
5594
|
+
}
|
|
5595
|
+
const found = skill.list().find((s) => s.name === name);
|
|
5596
|
+
if (!found) {
|
|
5597
|
+
return {
|
|
5598
|
+
ok: false,
|
|
5599
|
+
reason: "not-found",
|
|
5600
|
+
detail: t("skill.remove_unknown", { name }, lang)
|
|
5601
|
+
};
|
|
5602
|
+
}
|
|
5603
|
+
try {
|
|
5604
|
+
const path = await skill.delete(name);
|
|
5605
|
+
return { ok: true, name, path };
|
|
5606
|
+
} catch (err) {
|
|
5607
|
+
return {
|
|
5608
|
+
ok: false,
|
|
5609
|
+
reason: found.scope === "user" ? "failed" : "readonly",
|
|
5610
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
5611
|
+
};
|
|
5612
|
+
}
|
|
5132
5613
|
}
|
|
5133
5614
|
};
|
|
5134
5615
|
}
|
|
@@ -5136,19 +5617,25 @@ function buildMemoryControl(memory) {
|
|
|
5136
5617
|
return {
|
|
5137
5618
|
list: () => memory.getAllEntries().map((e) => ({
|
|
5138
5619
|
content: e.content,
|
|
5139
|
-
// `getAllEntries` 不带 target,而这一层要显示「记在哪一级」。
|
|
5140
|
-
// 拿不到就说 unknown,不猜 —— 猜错会让用户去错的文件里找
|
|
5141
5620
|
target: e.target ?? "unknown"
|
|
5142
5621
|
})),
|
|
5143
5622
|
remove: async (content) => {
|
|
5144
5623
|
const trimmed = content.trim();
|
|
5145
5624
|
const hit = memory.getAllEntries().find((e) => e.content.includes(trimmed));
|
|
5146
|
-
if (!hit) return { ok: false, message:
|
|
5625
|
+
if (!hit) return { ok: false, message: t("boot.memory_not_found", { query: trimmed }) };
|
|
5147
5626
|
try {
|
|
5148
5627
|
await memory.remove("memory", hit.content);
|
|
5149
|
-
return {
|
|
5628
|
+
return {
|
|
5629
|
+
ok: true,
|
|
5630
|
+
message: t("boot.memory_removed", { content: hit.content.slice(0, 60) })
|
|
5631
|
+
};
|
|
5150
5632
|
} catch (err) {
|
|
5151
|
-
return {
|
|
5633
|
+
return {
|
|
5634
|
+
ok: false,
|
|
5635
|
+
message: t("boot.memory_remove_failed", {
|
|
5636
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
5637
|
+
})
|
|
5638
|
+
};
|
|
5152
5639
|
}
|
|
5153
5640
|
}
|
|
5154
5641
|
};
|
|
@@ -5163,14 +5650,11 @@ function toSessionSummary(meta) {
|
|
|
5163
5650
|
messageCount: meta.messageCount,
|
|
5164
5651
|
model: meta.model,
|
|
5165
5652
|
costUsd: meta.estimatedCostUsd,
|
|
5166
|
-
// 两格原样转发,这一层**不判也不补**:库里是 NULL 就是 `undefined`
|
|
5167
|
-
// (`rowToMeta()` 在那两格上专门做了 `?? undefined` 的归一,判据写在那儿),
|
|
5168
|
-
// 而「没记过」和「明确不使用」在宿主那一侧是两个组
|
|
5169
5653
|
workspaceState: meta.workspaceState,
|
|
5170
5654
|
workspaceRoot: meta.workspaceRoot
|
|
5171
5655
|
};
|
|
5172
5656
|
}
|
|
5173
|
-
function buildSessionControl(sessions, session, checkpoints, onResumed) {
|
|
5657
|
+
function buildSessionControl(sessions, session, checkpoints, homeDir, onResumed) {
|
|
5174
5658
|
return {
|
|
5175
5659
|
resume: (sessionId) => {
|
|
5176
5660
|
const from = session?.sessionId ?? null;
|
|
@@ -5178,10 +5662,6 @@ function buildSessionControl(sessions, session, checkpoints, onResumed) {
|
|
|
5178
5662
|
onResumed?.(from, sessionId);
|
|
5179
5663
|
return restored;
|
|
5180
5664
|
},
|
|
5181
|
-
// ⚠️ `sortKey: 'updatedAt'` 这一行**没改,但含义变了**(会话库 v4):它以前
|
|
5182
|
-
// 在 `SessionDB.listSessions` 里静默退化成 `started_at`,现在真的按最后活动排。
|
|
5183
|
-
// 于是这个 `limit` 截出来的那批也变了 —— 一段两年前建、今天还在聊的会话
|
|
5184
|
-
// 从此进得了列表,而它以前必然被一批「新建但没说过话」的会话挤掉
|
|
5185
5665
|
list: (limit = 20) => sessions.list({ limit, sortKey: "updatedAt", direction: "desc" }).sessions.map(toSessionSummary),
|
|
5186
5666
|
get: (sessionId) => {
|
|
5187
5667
|
const found = sessions.get(sessionId);
|
|
@@ -5201,7 +5681,13 @@ function buildSessionControl(sessions, session, checkpoints, onResumed) {
|
|
|
5201
5681
|
} catch {
|
|
5202
5682
|
checkpointsRemoved = false;
|
|
5203
5683
|
}
|
|
5204
|
-
|
|
5684
|
+
let artifactsRemoved = true;
|
|
5685
|
+
try {
|
|
5686
|
+
await removeSessionArtifacts(sessionId, homeDir);
|
|
5687
|
+
} catch {
|
|
5688
|
+
artifactsRemoved = false;
|
|
5689
|
+
}
|
|
5690
|
+
return { deleted, checkpointsRemoved, artifactsRemoved };
|
|
5205
5691
|
},
|
|
5206
5692
|
search: (query, limit = 20) => sessions.search({ query, limit, scope: "all" }).results.flatMap(
|
|
5207
5693
|
(r) => r.matches.map((m) => ({
|
|
@@ -5238,15 +5724,17 @@ function buildPermissionsControl(services, bootstrap, tools) {
|
|
|
5238
5724
|
rules: effectiveRules,
|
|
5239
5725
|
shadows: () => checkShadowing(compileRuleLists(services.config.permissions)),
|
|
5240
5726
|
level: () => bootstrap?.level() ?? services.config.permission,
|
|
5241
|
-
// 配置里那一档 + 它赢在哪一层(方案 56 §1.3)。**和上面那一格是两个值** ——
|
|
5242
|
-
// 判据在接口上。读 `baseConfig` 是因为 `configOrigins` 记的就是它那一份
|
|
5243
|
-
// (而 `permission` 不在 `SettingsFileSchema` 白名单里,两份在这个键上恒等)
|
|
5244
5727
|
configured: () => ({
|
|
5245
5728
|
level: services.baseConfig.permission,
|
|
5246
5729
|
layer: originOf(services.configOrigins, "permission")
|
|
5247
5730
|
}),
|
|
5248
|
-
// 权限层起不来 = 什么都没判过,回空账而不是 null(见接口上那段注释)
|
|
5249
5731
|
audit: (scope) => services.permission?.getAudit(scope) ?? { entries: [], dropped: 0 },
|
|
5732
|
+
approvals: () => services.permission?.listApprovals() ?? [],
|
|
5733
|
+
revokeApproval: (id) => {
|
|
5734
|
+
const perm = services.permission;
|
|
5735
|
+
if (!perm) return { ok: false, reason: "no-permission-layer" };
|
|
5736
|
+
return perm.revokeApproval(id) ? { ok: true } : { ok: false, reason: "unknown-id" };
|
|
5737
|
+
},
|
|
5250
5738
|
managed: () => {
|
|
5251
5739
|
const p = services.managedPolicy;
|
|
5252
5740
|
return {
|
|
@@ -5257,10 +5745,7 @@ function buildPermissionsControl(services, bootstrap, tools) {
|
|
|
5257
5745
|
bypassDisabled: p.disableBypassPermissionsMode
|
|
5258
5746
|
};
|
|
5259
5747
|
},
|
|
5260
|
-
|
|
5261
|
-
// 权限层没起来时回 `{ok:true}` 而不是拒绝:那时 `level()` 读的是
|
|
5262
|
-
// `config.permission`,一个纯展示值,改它没有任何意义也没有任何危害
|
|
5263
|
-
setLevel: (level) => bootstrap?.setLevel(level) ?? { ok: true },
|
|
5748
|
+
setLevel: (level, opts) => bootstrap?.setLevel(level, opts) ?? { ok: true },
|
|
5264
5749
|
gates: (level) => {
|
|
5265
5750
|
const rules = compileRuleLists(services.config.permissions);
|
|
5266
5751
|
const isolation = services.permission?.getSandboxIsolation() ?? "process";
|
|
@@ -5283,12 +5768,7 @@ function compressionLine(config, services) {
|
|
|
5283
5768
|
const resolved = compressionOptions(config);
|
|
5284
5769
|
return {
|
|
5285
5770
|
contextLength: services.contextLength,
|
|
5286
|
-
// `.parse({})` 而不是 `defaultConfig()`:后者的返回类型标注成 `EpochConfig`,
|
|
5287
|
-
// 而那上面 `compression` 是可选的(嵌入宿主手搓那一份走的就是这条),
|
|
5288
|
-
// 于是这个回落自己又需要一个回落。schema 的产出类型上它是必有的(`.prefault({})`)
|
|
5289
5771
|
threshold: resolved.threshold ?? EpochConfigSchema.parse({}).compression.threshold,
|
|
5290
|
-
// `!== false` 而不是裸真值:判据同 `compressionOptions()` 里那一行 ——
|
|
5291
|
-
// 只有显式的 false 才关得掉,而这一条要写在脸上
|
|
5292
5772
|
auto: services.contextEngine !== null && resolved.enabled !== false
|
|
5293
5773
|
};
|
|
5294
5774
|
}
|
|
@@ -5310,20 +5790,20 @@ function buildSettingsControl(services, workspaces, opts) {
|
|
|
5310
5790
|
row.scalar ? { key: row.scalar, layers: scope.layers } : void 0
|
|
5311
5791
|
);
|
|
5312
5792
|
const overridable = row.scalar !== void 0;
|
|
5793
|
+
const writes = scope.paths === null ? [] : settingWriteTargets({
|
|
5794
|
+
key: row.key,
|
|
5795
|
+
winner: chain.layer,
|
|
5796
|
+
paths: scope.paths,
|
|
5797
|
+
trusted: scope.trusted
|
|
5798
|
+
});
|
|
5799
|
+
const kind = writes.length === 0 ? null : settingValueKind(row.key);
|
|
5800
|
+
const choices = writes.length === 0 ? null : settingValueChoices(row.key);
|
|
5313
5801
|
return {
|
|
5314
5802
|
...chain,
|
|
5315
5803
|
overridable,
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
// ⚠️ `paths` 和 `trusted` **来自同一个 `scope`**,那是硬约束不是巧合:
|
|
5320
|
-
// 判据在 `SettingsControl` 的文档和 core 的 `config/write.ts` 第四节
|
|
5321
|
-
writes: scope.paths === null ? [] : settingWriteTargets({
|
|
5322
|
-
winner: chain.layer,
|
|
5323
|
-
overridable,
|
|
5324
|
-
paths: scope.paths,
|
|
5325
|
-
trusted: scope.trusted
|
|
5326
|
-
})
|
|
5804
|
+
writes,
|
|
5805
|
+
...kind === null ? {} : { valueKind: kind },
|
|
5806
|
+
...choices === null ? {} : { choices }
|
|
5327
5807
|
};
|
|
5328
5808
|
});
|
|
5329
5809
|
};
|
|
@@ -5342,7 +5822,7 @@ function buildSettingsControl(services, workspaces, opts) {
|
|
|
5342
5822
|
value: input.value
|
|
5343
5823
|
});
|
|
5344
5824
|
if (!outcome.ok) return { ok: false, reason: outcome.reason, path };
|
|
5345
|
-
const target = rows(input.sessionId).find((row) => row.key === input.key)?.writes.find((
|
|
5825
|
+
const target = rows(input.sessionId).find((row) => row.key === input.key)?.writes.find((write2) => write2.layer === input.layer);
|
|
5346
5826
|
return {
|
|
5347
5827
|
ok: true,
|
|
5348
5828
|
key: input.key,
|
|
@@ -5393,6 +5873,31 @@ function writePathsOf(layers) {
|
|
|
5393
5873
|
}
|
|
5394
5874
|
return paths;
|
|
5395
5875
|
}
|
|
5876
|
+
function buildDefaultLevelSlot(services) {
|
|
5877
|
+
let current = services.permissionLevel;
|
|
5878
|
+
return {
|
|
5879
|
+
get: () => current,
|
|
5880
|
+
remember: (level) => {
|
|
5881
|
+
current = level;
|
|
5882
|
+
const paths = writePathsOf(services.settingsLayers);
|
|
5883
|
+
if (!paths) return { reason: "no-user-config" };
|
|
5884
|
+
const outcome = writeSettingValue({
|
|
5885
|
+
layer: "user",
|
|
5886
|
+
path: paths.user,
|
|
5887
|
+
key: "permission",
|
|
5888
|
+
value: level
|
|
5889
|
+
});
|
|
5890
|
+
if (!outcome.ok) {
|
|
5891
|
+
const reason2 = outcome.reason === "unwritable-file" ? "unwritable" : "io";
|
|
5892
|
+
return { reason: reason2, path: paths.user };
|
|
5893
|
+
}
|
|
5894
|
+
if (originOf(services.configOrigins, "permission") === "env") {
|
|
5895
|
+
return { reason: "env-pinned", path: paths.user };
|
|
5896
|
+
}
|
|
5897
|
+
return void 0;
|
|
5898
|
+
}
|
|
5899
|
+
};
|
|
5900
|
+
}
|
|
5396
5901
|
function valueAt(config, key) {
|
|
5397
5902
|
let cursor = config;
|
|
5398
5903
|
for (const segment of key.split(".")) {
|
|
@@ -5409,13 +5914,26 @@ function isSettingValue(value) {
|
|
|
5409
5914
|
function maybeValue(key, value) {
|
|
5410
5915
|
return value === void 0 ? {} : { [key]: value };
|
|
5411
5916
|
}
|
|
5917
|
+
function reportI18nHealth(diagnostics) {
|
|
5918
|
+
void t("i18n.locales_missing");
|
|
5919
|
+
const issues = i18nDiagnostics();
|
|
5920
|
+
if (issues.length === 0) {
|
|
5921
|
+
diagnostics.ok("I18n", "OK");
|
|
5922
|
+
return;
|
|
5923
|
+
}
|
|
5924
|
+
for (const issue of issues) diagnostics.warn("I18n", issue.detail, issue.code);
|
|
5925
|
+
}
|
|
5412
5926
|
var AgentRoleError, SETTING_ROWS;
|
|
5413
5927
|
var init_build = __esm({
|
|
5414
5928
|
"src/build.ts"() {
|
|
5415
5929
|
init_commands();
|
|
5930
|
+
init_events();
|
|
5416
5931
|
init_host_capabilities();
|
|
5932
|
+
init_host_marketplaces();
|
|
5417
5933
|
init_host_preset();
|
|
5934
|
+
init_mcp_tools();
|
|
5418
5935
|
init_model_catalog();
|
|
5936
|
+
init_plugin_control();
|
|
5419
5937
|
init_plugin_mcp_servers();
|
|
5420
5938
|
init_role_write();
|
|
5421
5939
|
init_control();
|
|
@@ -5428,7 +5946,10 @@ var init_build = __esm({
|
|
|
5428
5946
|
init_telemetry();
|
|
5429
5947
|
init_tools();
|
|
5430
5948
|
init_user_shell();
|
|
5949
|
+
init_utility();
|
|
5950
|
+
init_workspace_files();
|
|
5431
5951
|
init_workspace();
|
|
5952
|
+
init_session_goal();
|
|
5432
5953
|
AgentRoleError = class extends Error {
|
|
5433
5954
|
constructor(message) {
|
|
5434
5955
|
super(message);
|
|
@@ -5442,15 +5963,6 @@ var init_build = __esm({
|
|
|
5442
5963
|
{ key: "fallback.providers" },
|
|
5443
5964
|
{ key: "contextLength" },
|
|
5444
5965
|
{ key: "maxTurns", scalar: "maxTurns" },
|
|
5445
|
-
// 2026-08-16 合并时收的一笔(`web/src/settings/context-memory.tsx` 文件头那张 📮)。
|
|
5446
|
-
// 数组顺序就是界面上的顺序,所以紧挨着 `contextLength` —— 那一块要拿这三行
|
|
5447
|
-
// 算「压缩在多少 token 上开始」,缺一行整块不画。
|
|
5448
|
-
//
|
|
5449
|
-
// ⚠️ **两行都不带 `scalar`**,不是漏了:带 `scalar` 的含义是「这个键在
|
|
5450
|
-
// `SettingsFileSchema` 白名单里、项目级压得回去」,而 `compression.*` 判过
|
|
5451
|
-
// 不进那张白名单(三条判据在 `core/src/config/sources.ts` 文件头)。
|
|
5452
|
-
// 📮 那条还提醒了:顺手写 `scalar: 'compression.enabled'` 编译不过,
|
|
5453
|
-
// `ScalarKey` 是从 `SCALAR_KEYS` 派生的闭集合。那道闸是对的。
|
|
5454
5966
|
{ key: "compression.enabled" },
|
|
5455
5967
|
{ key: "compression.threshold" },
|
|
5456
5968
|
{ key: "budget.maxCostUsd" },
|
|
@@ -5460,12 +5972,12 @@ var init_build = __esm({
|
|
|
5460
5972
|
];
|
|
5461
5973
|
}
|
|
5462
5974
|
});
|
|
5463
|
-
|
|
5464
5975
|
// src/index.ts
|
|
5465
5976
|
init_build();
|
|
5466
5977
|
init_host_capabilities();
|
|
5467
5978
|
init_host_preset();
|
|
5468
5979
|
init_workspace();
|
|
5980
|
+
init_workspace_files();
|
|
5469
5981
|
init_executor();
|
|
5470
5982
|
init_control();
|
|
5471
5983
|
init_agent_session();
|
|
@@ -5475,11 +5987,12 @@ init_services();
|
|
|
5475
5987
|
init_user_shell();
|
|
5476
5988
|
init_commands();
|
|
5477
5989
|
init_role_scope();
|
|
5990
|
+
init_host_marketplaces();
|
|
5478
5991
|
init_serialize();
|
|
5479
5992
|
init_tools();
|
|
5480
5993
|
init_model_catalog();
|
|
5994
|
+
init_utility();
|
|
5481
5995
|
init_plugin_mcp_servers();
|
|
5482
|
-
|
|
5483
5996
|
// src/mcp-admin.ts
|
|
5484
5997
|
init_build();
|
|
5485
5998
|
init_mcp_config_write();
|
|
@@ -5498,7 +6011,7 @@ async function listMcpServers(opts = {}) {
|
|
|
5498
6011
|
async function mcpLogin(name, opts = {}) {
|
|
5499
6012
|
const config = await requireServer(name, opts);
|
|
5500
6013
|
if (!config.url) {
|
|
5501
|
-
throw new Error(`MCP server "${name}
|
|
6014
|
+
throw new Error(t("boot.mcp_stdio_no_oauth", { name: `MCP server "${name}` }));
|
|
5502
6015
|
}
|
|
5503
6016
|
const result = await loginToMcpServer({
|
|
5504
6017
|
server: name,
|
|
@@ -5522,7 +6035,7 @@ async function probeMcpServer(name, opts = {}) {
|
|
|
5522
6035
|
const tools = status?.connected ? await registry.getAllTools() : [];
|
|
5523
6036
|
return {
|
|
5524
6037
|
connected: status?.connected ?? false,
|
|
5525
|
-
toolNames: tools.map((
|
|
6038
|
+
toolNames: tools.map((t25) => t25.name),
|
|
5526
6039
|
...status?.lastError ? { error: status.lastError } : {},
|
|
5527
6040
|
needsLogin: status?.needsLogin ?? false
|
|
5528
6041
|
};
|
|
@@ -5535,14 +6048,15 @@ async function requireServer(name, opts) {
|
|
|
5535
6048
|
const { servers, configPath: configPath2 } = await listMcpServers(opts);
|
|
5536
6049
|
const found = servers.find((s) => s.config.name === name);
|
|
5537
6050
|
if (!found) {
|
|
5538
|
-
const known = servers.map((s) => s.config.name).join(", ") || "
|
|
5539
|
-
throw new Error(
|
|
6051
|
+
const known = servers.map((s) => s.config.name).join(", ") || t("boot.mcp_list_empty");
|
|
6052
|
+
throw new Error(t("boot.mcp_no_such_server", { path: configPath2, name, known }));
|
|
5540
6053
|
}
|
|
5541
6054
|
return found.config;
|
|
5542
6055
|
}
|
|
5543
|
-
|
|
5544
6056
|
// src/index.ts
|
|
5545
6057
|
init_mcp_config_file();
|
|
5546
6058
|
init_mcp_config_apply();
|
|
6059
|
+
init_mcp_config_edit();
|
|
6060
|
+
init_mcp_tools();
|
|
5547
6061
|
|
|
5548
|
-
export { AgentRoleError, AgentSession, ApprovalRelay, HOST_CAPABILITIES_MODULE, HOST_PRESET_MODULE, QuestionRelay, SCHEDULE_EXIT_CODES, SessionStore, SessionWorkspaces, applyHostPreset, applyMcpConfig, buildModelCatalogControl, buildRuntime, buildServices, collectFileChanges, createScheduleControl, createUserActions, ensureSecretsReady, exitCodeFor, fireSchedule, listMcpServers, mcpConfigRevision, mcpLogin, mcpLogout, probeMcpServer, readMcpConfigFile, registerAskQuestion, registerLocalTools, registerMcpTools, serializeStream, splitDirList, toSerializable, wireCommands, wireHostCapabilities, wirePluginMcpServers, wireWorkspace, withModelScope, withRoleScope, withToolScope, writeMcpConfigFile, writeMcpServer };
|
|
6062
|
+
export { AgentRoleError, AgentSession, ApprovalRelay, HOST_CAPABILITIES_MODULE, HOST_MARKETPLACE_MODULE, HOST_PRESET_MODULE, MCP_ADD_TOOL, MCP_LIST_TOOL, MCP_REMOVE_TOOL, MCP_UPDATE_TOOL, QuestionRelay, SCHEDULE_EXIT_CODES, SessionStore, SessionWorkspaces, applyHostPreset, applyMcpConfig, buildModelCatalogControl, buildRuntime, buildServices, buildWorkspaceFilesControl, collectFileChanges, createMcpTools, createScheduleControl, createUserActions, ensureSecretsReady, exitCodeFor, fireSchedule, isSystemOpenable, listMcpServers, mcpConfigRevision, mcpLogin, mcpLogout, probeMcpServer, providerLabel, readMcpConfigFile, registerAskQuestion, registerLocalTools, registerMcpTools, removeMcpServer, removeServerText, replaceServerText, serializeStream, splitDirList, toSerializable, updateMcpServer, utilityBudgetKey, wireCommands, wireHostCapabilities, wirePluginMcpServers, wireWorkspace, withModelScope, withRoleScope, withToolScope, writeMcpConfigFile, writeMcpServer };
|