@cjhyy/code-shell-core 0.6.0-rc.14 → 0.6.0-rc.15
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/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- package/dist/cc-orchestrator/external-agent-bindings.js +1 -1
- package/dist/cc-orchestrator/external-agent-session-store.js +3 -1
- package/dist/cc-orchestrator/session-history.js +2 -2
- package/dist/cli/agent-server-stdio.js +7 -2
- package/dist/context/manager.d.ts +11 -2
- package/dist/context/manager.js +64 -3
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +29 -10
- package/dist/credentials/use-credential-tool.d.ts +1 -0
- package/dist/credentials/use-credential-tool.js +3 -0
- package/dist/engine/engine.d.ts +9 -0
- package/dist/engine/engine.js +96 -17
- package/dist/engine/turn-loop.d.ts +3 -1
- package/dist/engine/turn-loop.js +3 -1
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -127
- package/dist/git/worktree.js +5 -464
- package/dist/index.d.ts +28 -27
- package/dist/index.js +24 -24
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +6 -0
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.js +7 -0
- package/dist/protocol/server.d.ts +10 -0
- package/dist/protocol/server.js +88 -4
- package/dist/protocol/types.d.ts +6 -0
- package/dist/protocol/types.js +2 -0
- package/dist/runtime/background-shell.js +2 -3
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +56 -17
- package/dist/session/memory.js +337 -78
- package/dist/session/session-manager.d.ts +1 -1
- package/dist/session/session-manager.js +6 -6
- package/dist/settings/manager.js +43 -12
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/tool-system/builtin/index.js +18 -8
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/worktree.d.ts +2 -0
- package/dist/tool-system/builtin/worktree.js +59 -11
- package/dist/tool-system/context.d.ts +11 -1
- package/dist/tool-system/mcp-manager.d.ts +8 -5
- package/dist/tool-system/mcp-manager.js +85 -55
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +28 -12
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
|
@@ -31,9 +31,11 @@ export interface CapabilityServiceDeps {
|
|
|
31
31
|
readInstalledPlugins: () => InstalledPluginsV2;
|
|
32
32
|
resolveBuiltinToolNames: (o?: {
|
|
33
33
|
preset?: string;
|
|
34
|
+
host?: "desktop";
|
|
34
35
|
enabledBuiltinTools?: string[];
|
|
35
36
|
disabledBuiltinTools?: string[];
|
|
36
37
|
}) => string[];
|
|
38
|
+
builtinToolHost?: "desktop";
|
|
37
39
|
}
|
|
38
40
|
export declare class CapabilityService {
|
|
39
41
|
private readonly deps;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { CapabilityNotFoundError } from "./types.js";
|
|
15
15
|
import { projectBuiltin, projectMcp, projectSkills, projectPlugins, projectAgents, } from "./project.js";
|
|
16
|
-
import { applyOverride, bucketForKind, overrideTokenForId, overrideFor
|
|
16
|
+
import { applyOverride, bucketForKind, overrideTokenForId, overrideFor } from "./overlay.js";
|
|
17
17
|
export class CapabilityService {
|
|
18
18
|
deps;
|
|
19
19
|
constructor(deps) {
|
|
@@ -30,12 +30,14 @@ export class CapabilityService {
|
|
|
30
30
|
const agent = (s.agent ?? {});
|
|
31
31
|
const tools = this.deps.registry.listToolsDetailed();
|
|
32
32
|
const preset = agent.preset;
|
|
33
|
+
const host = this.deps.builtinToolHost;
|
|
33
34
|
const base = [
|
|
34
35
|
...projectBuiltin({
|
|
35
36
|
tools: tools.filter((t) => t.source === "builtin"),
|
|
36
|
-
presetDefaults: this.deps.resolveBuiltinToolNames({ preset }),
|
|
37
|
+
presetDefaults: this.deps.resolveBuiltinToolNames({ preset, host }),
|
|
37
38
|
effective: this.deps.resolveBuiltinToolNames({
|
|
38
39
|
preset,
|
|
40
|
+
host,
|
|
39
41
|
enabledBuiltinTools: agent.enabledBuiltinTools ?? [],
|
|
40
42
|
disabledBuiltinTools: agent.disabledBuiltinTools ?? [],
|
|
41
43
|
}),
|
|
@@ -52,7 +52,7 @@ export function readCodexRecentHistory(cwd, threadId, limit, codexHome = join(ho
|
|
|
52
52
|
const t = textOf(p.content).trim();
|
|
53
53
|
if (!t || t.startsWith("<environment_context>"))
|
|
54
54
|
continue;
|
|
55
|
-
all.push({ role: p.role, text: t
|
|
55
|
+
all.push({ role: p.role, text: t });
|
|
56
56
|
}
|
|
57
57
|
else if (p.type === "function_call" || p.type === "custom_tool_call") {
|
|
58
58
|
const tool = { name: typeof p.name === "string" ? p.name : "tool", summary: summaryOf(p), args: argsOf(p) };
|
|
@@ -43,7 +43,7 @@ export class ExternalAgentBindingStore {
|
|
|
43
43
|
}
|
|
44
44
|
catch (err) {
|
|
45
45
|
if (opts.failOnCorrupt) {
|
|
46
|
-
throw new Error(`external agent bindings file is corrupt or unreadable: ${err instanceof Error ? err.message : String(err)}
|
|
46
|
+
throw new Error(`external agent bindings file is corrupt or unreadable: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
47
47
|
}
|
|
48
48
|
return { bindings: {} };
|
|
49
49
|
}
|
|
@@ -90,7 +90,9 @@ export class ExternalAgentSessionStore {
|
|
|
90
90
|
if (removeStaleLock(lockDir))
|
|
91
91
|
continue;
|
|
92
92
|
if (Date.now() >= deadline) {
|
|
93
|
-
throw new Error(`timed out waiting for external agent session store lock: ${lockDir}
|
|
93
|
+
throw new Error(`timed out waiting for external agent session store lock: ${lockDir}`, {
|
|
94
|
+
cause: err,
|
|
95
|
+
});
|
|
94
96
|
}
|
|
95
97
|
sleepSync(LOCK_POLL_MS);
|
|
96
98
|
}
|
|
@@ -51,14 +51,14 @@ export function readRecentHistory(cwd, sessionId, limit, claudeHome = join(homed
|
|
|
51
51
|
const t = textOf(d.message?.content).trim();
|
|
52
52
|
if (!t || NOISE.some((n) => t.startsWith(n)))
|
|
53
53
|
continue;
|
|
54
|
-
all.push({ role: "user", text: t
|
|
54
|
+
all.push({ role: "user", text: t });
|
|
55
55
|
}
|
|
56
56
|
else if (d.type === "assistant") {
|
|
57
57
|
const t = textOf(d.message?.content).trim();
|
|
58
58
|
const tools = toolsOf(d.message?.content);
|
|
59
59
|
if (!t && tools.length === 0)
|
|
60
60
|
continue;
|
|
61
|
-
all.push({ role: "assistant", text: t
|
|
61
|
+
all.push({ role: "assistant", text: t, tools: tools.length ? tools : undefined });
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
const lim = limit > 0 ? limit : 20;
|
|
@@ -106,8 +106,7 @@ catch (err) {
|
|
|
106
106
|
// knob; imageDetail from settings.images.detail).
|
|
107
107
|
const seedLlm = resolveLLMConfigForTag(settings, "text", settings.defaults?.text);
|
|
108
108
|
if (!seedLlm) {
|
|
109
|
-
console.error(`[agent-server] 没有可用的文本模型连接(defaults.text=${settings.defaults?.text ?? "未设置"})。` +
|
|
110
|
-
`请在「连接」页添加并填写凭证。`);
|
|
109
|
+
console.error(`[agent-server] 没有可用的文本模型连接(defaults.text=${settings.defaults?.text ?? "未设置"})。` + `请在「连接」页添加并填写凭证。`);
|
|
111
110
|
process.exit(1);
|
|
112
111
|
}
|
|
113
112
|
const llmConfig = seedLlm;
|
|
@@ -115,6 +114,10 @@ const llmConfig = seedLlm;
|
|
|
115
114
|
const seedEngine = new Engine({
|
|
116
115
|
llm: llmConfig,
|
|
117
116
|
cwd,
|
|
117
|
+
preset: settings.agent.preset,
|
|
118
|
+
enabledBuiltinTools: settings.agent.enabledBuiltinTools,
|
|
119
|
+
disabledBuiltinTools: settings.agent.disabledBuiltinTools,
|
|
120
|
+
builtinToolHost: "desktop",
|
|
118
121
|
settingsScope: "full",
|
|
119
122
|
// No runtime — Engine.populateModelPoolFromSettings() runs in ctor.
|
|
120
123
|
});
|
|
@@ -217,6 +220,7 @@ const chatManager = new ChatSessionManager({
|
|
|
217
220
|
// This stdio worker exists only to serve the desktop app, so every
|
|
218
221
|
// session it creates is a desktop-origin session.
|
|
219
222
|
origin: "desktop",
|
|
223
|
+
builtinToolHost: "desktop",
|
|
220
224
|
// Inherit full scope so spawned subagents read user config too.
|
|
221
225
|
settingsScope: "full",
|
|
222
226
|
// MCP servers from settings — the worker reads the full disk
|
|
@@ -286,6 +290,7 @@ const goalDiskReader = new SessionManager();
|
|
|
286
290
|
const agentServer = new AgentServer({
|
|
287
291
|
chatManager,
|
|
288
292
|
transport: stdioTransport,
|
|
293
|
+
workspaceBridge: true,
|
|
289
294
|
// Config hot-reload (layer 2) reads disk through the SAME closure the
|
|
290
295
|
// engineFactory uses for new sessions, so a reloaded running session and a
|
|
291
296
|
// newly-created session converge on identical disk config (no divergence).
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Tier 2: LLM summary (async) — generate summary of older messages via model call
|
|
6
6
|
* Tier 3: window compact (sync, emergency) — aggressive truncation fallback
|
|
7
7
|
*/
|
|
8
|
-
import type { Message } from "../types.js";
|
|
8
|
+
import type { Message, ContextUsageAnchor } from "../types.js";
|
|
9
9
|
export interface ContextManagerConfig {
|
|
10
10
|
maxTokens: number;
|
|
11
11
|
compactAtRatio: number;
|
|
@@ -50,6 +50,9 @@ export declare class ContextManager {
|
|
|
50
50
|
private lastActualAtMessageCount;
|
|
51
51
|
/** Heuristic token estimate for the same messages as lastActualTokens. */
|
|
52
52
|
private lastActualAnchorEstimate;
|
|
53
|
+
private lastActualRecordedAt;
|
|
54
|
+
private lastActualProvider;
|
|
55
|
+
private lastActualModel;
|
|
53
56
|
/** Path to session transcript — passed to summary compaction for on-demand access. */
|
|
54
57
|
private transcriptPath;
|
|
55
58
|
/** Notified whenever any compaction tier fires, including microcompact. */
|
|
@@ -67,7 +70,13 @@ export declare class ContextManager {
|
|
|
67
70
|
* Record actual token usage from API response.
|
|
68
71
|
* Used for hybrid estimation: actual + estimate for new messages.
|
|
69
72
|
*/
|
|
70
|
-
recordActualUsage(inputTokens: number, messageCount: number, messages?: Message[]):
|
|
73
|
+
recordActualUsage(inputTokens: number, messageCount: number, messages?: Message[]): ContextUsageAnchor | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Seed actual prompt-token usage from persisted session state.
|
|
76
|
+
* Returns the normalized anchor when accepted; invalid legacy/tampered data is ignored.
|
|
77
|
+
*/
|
|
78
|
+
seedActualUsage(anchor: ContextUsageAnchor | undefined): ContextUsageAnchor | undefined;
|
|
79
|
+
getActualUsageAnchor(): ContextUsageAnchor | undefined;
|
|
71
80
|
/**
|
|
72
81
|
* Best-effort token estimate: uses actual API usage as base if available,
|
|
73
82
|
* plus estimation for messages added since the last API call.
|
package/dist/context/manager.js
CHANGED
|
@@ -37,6 +37,9 @@ const DEFAULT_CONFIG = {
|
|
|
37
37
|
function defaultKeepRecent(maxTokens) {
|
|
38
38
|
return Math.max(5, Math.floor(maxTokens / 100_000));
|
|
39
39
|
}
|
|
40
|
+
function positiveFinite(value) {
|
|
41
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
42
|
+
}
|
|
40
43
|
export class ContextManager {
|
|
41
44
|
config;
|
|
42
45
|
summarizeFn;
|
|
@@ -50,6 +53,9 @@ export class ContextManager {
|
|
|
50
53
|
lastActualAtMessageCount;
|
|
51
54
|
/** Heuristic token estimate for the same messages as lastActualTokens. */
|
|
52
55
|
lastActualAnchorEstimate;
|
|
56
|
+
lastActualRecordedAt;
|
|
57
|
+
lastActualProvider;
|
|
58
|
+
lastActualModel;
|
|
53
59
|
/** Path to session transcript — passed to summary compaction for on-demand access. */
|
|
54
60
|
transcriptPath;
|
|
55
61
|
/** Notified whenever any compaction tier fires, including microcompact. */
|
|
@@ -72,9 +78,53 @@ export class ContextManager {
|
|
|
72
78
|
* Used for hybrid estimation: actual + estimate for new messages.
|
|
73
79
|
*/
|
|
74
80
|
recordActualUsage(inputTokens, messageCount, messages) {
|
|
75
|
-
|
|
76
|
-
this.
|
|
77
|
-
|
|
81
|
+
const estimateAtAnchor = messages ? estimateTokens(messages) : undefined;
|
|
82
|
+
return this.seedActualUsage({
|
|
83
|
+
promptTokens: inputTokens,
|
|
84
|
+
messageCount,
|
|
85
|
+
...(estimateAtAnchor !== undefined ? { estimateAtAnchor } : {}),
|
|
86
|
+
recordedAt: Date.now(),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Seed actual prompt-token usage from persisted session state.
|
|
91
|
+
* Returns the normalized anchor when accepted; invalid legacy/tampered data is ignored.
|
|
92
|
+
*/
|
|
93
|
+
seedActualUsage(anchor) {
|
|
94
|
+
if (!anchor)
|
|
95
|
+
return undefined;
|
|
96
|
+
if (!positiveFinite(anchor.promptTokens))
|
|
97
|
+
return undefined;
|
|
98
|
+
if (!Number.isSafeInteger(anchor.messageCount) || anchor.messageCount <= 0) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
if (anchor.estimateAtAnchor !== undefined && !positiveFinite(anchor.estimateAtAnchor)) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
this.lastActualTokens = anchor.promptTokens;
|
|
105
|
+
this.lastActualAtMessageCount = anchor.messageCount;
|
|
106
|
+
this.lastActualAnchorEstimate = anchor.estimateAtAnchor;
|
|
107
|
+
this.lastActualRecordedAt = positiveFinite(anchor.recordedAt) ? anchor.recordedAt : Date.now();
|
|
108
|
+
this.lastActualProvider = anchor.provider;
|
|
109
|
+
this.lastActualModel = anchor.model;
|
|
110
|
+
return this.getActualUsageAnchor();
|
|
111
|
+
}
|
|
112
|
+
getActualUsageAnchor() {
|
|
113
|
+
if (this.lastActualTokens === undefined ||
|
|
114
|
+
this.lastActualAtMessageCount === undefined ||
|
|
115
|
+
this.lastActualRecordedAt === undefined) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
promptTokens: this.lastActualTokens,
|
|
120
|
+
messageCount: this.lastActualAtMessageCount,
|
|
121
|
+
...(this.lastActualAnchorEstimate !== undefined
|
|
122
|
+
? { estimateAtAnchor: this.lastActualAnchorEstimate }
|
|
123
|
+
: {}),
|
|
124
|
+
recordedAt: this.lastActualRecordedAt,
|
|
125
|
+
...(this.lastActualProvider ? { provider: this.lastActualProvider } : {}),
|
|
126
|
+
...(this.lastActualModel ? { model: this.lastActualModel } : {}),
|
|
127
|
+
};
|
|
78
128
|
}
|
|
79
129
|
/**
|
|
80
130
|
* Best-effort token estimate: uses actual API usage as base if available,
|
|
@@ -306,6 +356,17 @@ export class ContextManager {
|
|
|
306
356
|
result = this.truncateToolResults(result);
|
|
307
357
|
// Tier 0c: Aggregate tool result budget (per-message)
|
|
308
358
|
result = applyToolResultBudget(result);
|
|
359
|
+
// Tier 0d: same always-on waste-removal passes as manage().
|
|
360
|
+
const dedup = dedupeFileReads(result);
|
|
361
|
+
if (dedup.clearedCount > 0) {
|
|
362
|
+
result = dedup.messages;
|
|
363
|
+
logger.info("context.dedupe_file_reads", { cleared: dedup.clearedCount });
|
|
364
|
+
}
|
|
365
|
+
const masked = maskOldObservations(result);
|
|
366
|
+
if (masked.maskedCount > 0) {
|
|
367
|
+
result = masked.messages;
|
|
368
|
+
logger.info("context.mask_browser_snapshots", { masked: masked.maskedCount });
|
|
369
|
+
}
|
|
309
370
|
// Tier 1: microcompact — see manage() for the rationale on the floor.
|
|
310
371
|
const preTier1Tokens = this.estimateTokensHybrid(result);
|
|
311
372
|
const microFloorGate = this.config.maxTokens * this.config.microcompactFloorRatio;
|
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { ToolDefinition } from "../types.js";
|
|
14
14
|
import type { ToolContext } from "../tool-system/context.js";
|
|
15
|
+
import { type SettingsScope } from "../settings/manager.js";
|
|
15
16
|
export declare const injectCredentialToolDef: ToolDefinition;
|
|
16
17
|
/** 该工具仅在有 cookie 凭证 且 宿主接了注入回调时才可见(BUILTIN_TOOL_GUARDS)。 */
|
|
17
|
-
export declare function isInjectCredentialAvailable(cwd: string): boolean;
|
|
18
|
+
export declare function isInjectCredentialAvailable(cwd: string, settingsScope?: SettingsScope): boolean;
|
|
18
19
|
export declare function injectCredentialTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
19
20
|
/** 测试钩子:清空会话 allow 集。 */
|
|
20
21
|
export declare function __resetInjectCredentialSessionAllowForTests(): void;
|
|
22
|
+
export declare function clearInjectCredentialSessionAllow(sessionId: string): void;
|
|
@@ -62,9 +62,14 @@ function sessionAllowFor(ctx) {
|
|
|
62
62
|
}
|
|
63
63
|
return set;
|
|
64
64
|
}
|
|
65
|
-
|
|
65
|
+
// CredentialStore only distinguishes full user+project from project-only.
|
|
66
|
+
// Isolated engines must be at least as restrictive as project-scoped engines.
|
|
67
|
+
function credentialScope(scope) {
|
|
68
|
+
return scope === "full" || scope === undefined ? "full" : "project";
|
|
69
|
+
}
|
|
70
|
+
function readAutoApprove(cwd, scope) {
|
|
66
71
|
try {
|
|
67
|
-
const s = new SettingsManager(cwd, "full").get();
|
|
72
|
+
const s = new SettingsManager(cwd, scope === "full" ? "full" : "project").get();
|
|
68
73
|
return s.credentialUse?.autoApprove === true;
|
|
69
74
|
}
|
|
70
75
|
catch {
|
|
@@ -72,9 +77,11 @@ function readAutoApprove(cwd) {
|
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
/** 该工具仅在有 cookie 凭证 且 宿主接了注入回调时才可见(BUILTIN_TOOL_GUARDS)。 */
|
|
75
|
-
export function isInjectCredentialAvailable(cwd) {
|
|
80
|
+
export function isInjectCredentialAvailable(cwd, settingsScope) {
|
|
76
81
|
try {
|
|
77
|
-
return new CredentialStore(cwd)
|
|
82
|
+
return new CredentialStore(cwd)
|
|
83
|
+
.listMasked(credentialScope(settingsScope))
|
|
84
|
+
.some((c) => c.type === "cookie");
|
|
78
85
|
}
|
|
79
86
|
catch {
|
|
80
87
|
return false;
|
|
@@ -82,10 +89,14 @@ export function isInjectCredentialAvailable(cwd) {
|
|
|
82
89
|
}
|
|
83
90
|
export async function injectCredentialTool(args, ctx) {
|
|
84
91
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
92
|
+
const scope = credentialScope(ctx?.settingsScope);
|
|
85
93
|
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
86
94
|
const purpose = typeof args.purpose === "string" ? args.purpose : undefined;
|
|
87
95
|
if (!id) {
|
|
88
|
-
return json({
|
|
96
|
+
return json({
|
|
97
|
+
kind: "error",
|
|
98
|
+
error: "缺少 id。先用 UseCredential(无参)列出凭证,再用 cookie 凭证的 id 调本工具。",
|
|
99
|
+
});
|
|
89
100
|
}
|
|
90
101
|
if (!ctx?.injectCredentialToBrowser) {
|
|
91
102
|
return json({
|
|
@@ -93,17 +104,22 @@ export async function injectCredentialTool(args, ctx) {
|
|
|
93
104
|
error: "当前环境无内置浏览器(headless/无面板),无法注入。请改用 UseCredential 取 cookie 走 HTTP 请求。",
|
|
94
105
|
});
|
|
95
106
|
}
|
|
96
|
-
const cred = new CredentialStore(cwd).resolve(id);
|
|
107
|
+
const cred = new CredentialStore(cwd).resolve(id, scope);
|
|
97
108
|
if (!cred) {
|
|
98
|
-
return json({
|
|
109
|
+
return json({
|
|
110
|
+
kind: "error",
|
|
111
|
+
error: `凭证不存在: "${id}"。调用 UseCredential(无参)可列出可用凭证。`,
|
|
112
|
+
});
|
|
99
113
|
}
|
|
100
114
|
if (cred.type !== "cookie") {
|
|
101
115
|
return json({ kind: "error", error: `凭证「${cred.label}」不是 cookie 类型,不能注入浏览器。` });
|
|
102
116
|
}
|
|
103
117
|
// 过门:复用三档,但用该凭证的 autoInjectByAI(不是 autoUseByAI)。
|
|
104
|
-
const ask = ctx.askUser
|
|
118
|
+
const ask = ctx.askUser
|
|
119
|
+
? (q, opts) => ctx.askUser(q, opts)
|
|
120
|
+
: undefined;
|
|
105
121
|
const decision = await credentialUseGate({ id: cred.id, label: cred.label, purpose }, {
|
|
106
|
-
autoApprove: readAutoApprove(cwd),
|
|
122
|
+
autoApprove: readAutoApprove(cwd, scope),
|
|
107
123
|
credentialAutoUse: cred.autoInjectByAI === true,
|
|
108
124
|
sessionAllow: sessionAllowFor(ctx),
|
|
109
125
|
ask,
|
|
@@ -115,7 +131,7 @@ export async function injectCredentialTool(args, ctx) {
|
|
|
115
131
|
return json({ kind: "error", error: msg });
|
|
116
132
|
}
|
|
117
133
|
// 跨进程触发宿主注入。
|
|
118
|
-
const res = await ctx.injectCredentialToBrowser(cred.id);
|
|
134
|
+
const res = await ctx.injectCredentialToBrowser(cred.id, scope);
|
|
119
135
|
if (!res.ok) {
|
|
120
136
|
return json({ kind: "error", error: res.error ?? "注入浏览器失败(宿主未返回成功)。" });
|
|
121
137
|
}
|
|
@@ -128,3 +144,6 @@ function json(r) {
|
|
|
128
144
|
export function __resetInjectCredentialSessionAllowForTests() {
|
|
129
145
|
injectSessionAllowByEngine.clear();
|
|
130
146
|
}
|
|
147
|
+
export function clearInjectCredentialSessionAllow(sessionId) {
|
|
148
|
+
injectSessionAllowByEngine.delete(sessionId);
|
|
149
|
+
}
|
|
@@ -27,3 +27,4 @@ export declare function sweepStaleCredentialCookies(now?: number): void;
|
|
|
27
27
|
export declare function useCredentialTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
28
28
|
/** 测试钩子:清空会话 allow 集(避免跨用例污染)。 */
|
|
29
29
|
export declare function __resetCredentialSessionAllowForTests(): void;
|
|
30
|
+
export declare function clearCredentialSessionAllow(sessionId: string): void;
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -262,6 +262,8 @@ export declare class Engine {
|
|
|
262
262
|
* clear "no browser panel" error.
|
|
263
263
|
*/
|
|
264
264
|
setBrowserBridge(bridge: import("../tool-system/browser-bridge.js").BrowserBridge | undefined): void;
|
|
265
|
+
/** Inject the host-backed workspace bridge after construction. */
|
|
266
|
+
setWorkspaceBridge(bridge: import("../tool-system/workspace-bridge.js").WorkspaceBridge | undefined): void;
|
|
265
267
|
/**
|
|
266
268
|
* Queue a user message to be spliced into the in-flight run for `sessionId`
|
|
267
269
|
* at the next turn-loop step boundary — the 不打断 steering path (vs cancel +
|
|
@@ -461,6 +463,12 @@ export declare class Engine {
|
|
|
461
463
|
* session with no active goal is a no-op returning false.
|
|
462
464
|
*/
|
|
463
465
|
clearGoal(sessionId: string): boolean;
|
|
466
|
+
/**
|
|
467
|
+
* Reset a session's workspace pointer back to its main root. If the session is
|
|
468
|
+
* actively running, mutate that live SessionBundle first so the run's next
|
|
469
|
+
* saveState cannot resurrect a stale worktree pointer.
|
|
470
|
+
*/
|
|
471
|
+
releaseSessionWorkspace(sessionId: string): import("../types.js").SessionWorkspace | null;
|
|
464
472
|
injectContext(sessionId: string, content: string): void;
|
|
465
473
|
/**
|
|
466
474
|
* Force context compaction on a session.
|
|
@@ -624,6 +632,7 @@ export declare class Engine {
|
|
|
624
632
|
linux?: string;
|
|
625
633
|
windows?: string;
|
|
626
634
|
} | undefined;
|
|
635
|
+
readWorktreeBranchPrefix(cwd?: string): string | undefined;
|
|
627
636
|
resolveWorktreeSetupSandbox(cwd: string): Promise<SandboxBackend | undefined>;
|
|
628
637
|
readWorktreeSetupShellEnv(cwd?: string): Record<string, string> | undefined;
|
|
629
638
|
buildToolContext(): ToolContext;
|
package/dist/engine/engine.js
CHANGED
|
@@ -42,12 +42,12 @@ import { MCPManager } from "../tool-system/mcp-manager.js";
|
|
|
42
42
|
import { SettingsManager, userHome } from "../settings/manager.js";
|
|
43
43
|
import { CredentialStore } from "../credentials/store.js";
|
|
44
44
|
import { isFeatureEnabled, resolveFeatureFlags, } from "../settings/feature-flags.js";
|
|
45
|
-
import { effectiveDisabledList, effectiveBuiltinLists
|
|
45
|
+
import { effectiveDisabledList, effectiveBuiltinLists } from "../capability-control/overlay.js";
|
|
46
46
|
import { computeEffectiveDisabledLists } from "../capability-control/disabled-lists.js";
|
|
47
47
|
import { FileHistory } from "../session/file-history.js";
|
|
48
48
|
import { patchBackupTargets } from "../tool-system/builtin/apply-patch/backup-targets.js";
|
|
49
49
|
import { resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
|
|
50
|
-
import { resolveAgentPreset, resolveBuiltinToolNames
|
|
50
|
+
import { resolveAgentPreset, resolveBuiltinToolNames } from "../preset/index.js";
|
|
51
51
|
import { ModelPool } from "../llm/model-pool.js";
|
|
52
52
|
import { AgentDefinitionRegistry } from "../agent/agent-definition-registry.js";
|
|
53
53
|
import { defaultCacheDir } from "../llm/model-cache.js";
|
|
@@ -445,6 +445,7 @@ export class Engine {
|
|
|
445
445
|
new ToolRegistry({
|
|
446
446
|
builtinTools: resolveBuiltinToolNames({
|
|
447
447
|
preset: this.preset.name,
|
|
448
|
+
host: config.builtinToolHost,
|
|
448
449
|
enabledBuiltinTools: builtinLists.enabledBuiltinTools,
|
|
449
450
|
disabledBuiltinTools: builtinLists.disabledBuiltinTools,
|
|
450
451
|
}),
|
|
@@ -635,6 +636,10 @@ export class Engine {
|
|
|
635
636
|
setBrowserBridge(bridge) {
|
|
636
637
|
this.config.browserBridge = bridge;
|
|
637
638
|
}
|
|
639
|
+
/** Inject the host-backed workspace bridge after construction. */
|
|
640
|
+
setWorkspaceBridge(bridge) {
|
|
641
|
+
this.config.workspaceBridge = bridge;
|
|
642
|
+
}
|
|
638
643
|
/**
|
|
639
644
|
* Queue a user message to be spliced into the in-flight run for `sessionId`
|
|
640
645
|
* at the next turn-loop step boundary — the 不打断 steering path (vs cancel +
|
|
@@ -736,7 +741,7 @@ export class Engine {
|
|
|
736
741
|
*/
|
|
737
742
|
async run(task, options) {
|
|
738
743
|
const workspaceResume = options?.sessionId && this.sessionManager.exists(options.sessionId)
|
|
739
|
-
? this.sessionManager.resolveSessionWorkspaceForResume(options.sessionId)
|
|
744
|
+
? await this.sessionManager.resolveSessionWorkspaceForResume(options.sessionId)
|
|
740
745
|
: undefined;
|
|
741
746
|
if (workspaceResume && !workspaceResume.ok) {
|
|
742
747
|
return {
|
|
@@ -971,6 +976,7 @@ export class Engine {
|
|
|
971
976
|
preset: this.preset.name,
|
|
972
977
|
enabledBuiltinTools: childEnabled,
|
|
973
978
|
disabledBuiltinTools: childDisabled,
|
|
979
|
+
builtinToolHost: this.config.builtinToolHost,
|
|
974
980
|
customSystemPrompt: this.config.customSystemPrompt,
|
|
975
981
|
appendSystemPrompt: [this.config.appendSystemPrompt, req.appendSystemPrompt].filter(Boolean).join("\n\n") ||
|
|
976
982
|
undefined,
|
|
@@ -1311,6 +1317,24 @@ export class Engine {
|
|
|
1311
1317
|
messages[lastIdx] = { role: "user", content: promptSubmitHook.updatedPrompt };
|
|
1312
1318
|
}
|
|
1313
1319
|
}
|
|
1320
|
+
const contextManager = new ContextManager({
|
|
1321
|
+
maxTokens: this.resolveMaxContextTokens(),
|
|
1322
|
+
// Drop undefined fields so they don't clobber ContextManager defaults
|
|
1323
|
+
// (spread of `{x: undefined}` would override the default with undefined).
|
|
1324
|
+
...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
|
|
1325
|
+
});
|
|
1326
|
+
this.lastContextManager = contextManager;
|
|
1327
|
+
const persistedContextAnchor = session.state.contextUsageAnchor;
|
|
1328
|
+
const contextAnchorCompatible = persistedContextAnchor !== undefined &&
|
|
1329
|
+
(persistedContextAnchor.provider === undefined ||
|
|
1330
|
+
persistedContextAnchor.provider === this.config.llm.provider) &&
|
|
1331
|
+
(persistedContextAnchor.model === undefined ||
|
|
1332
|
+
persistedContextAnchor.model === this.config.llm.model) &&
|
|
1333
|
+
(persistedContextAnchor.messageCount <= messages.length ||
|
|
1334
|
+
persistedContextAnchor.estimateAtAnchor !== undefined);
|
|
1335
|
+
const seededContextAnchor = contextAnchorCompatible
|
|
1336
|
+
? contextManager.seedActualUsage(persistedContextAnchor)
|
|
1337
|
+
: undefined;
|
|
1314
1338
|
// Rough token estimate of the full prompt so the UI's ctx bar isn't 0%
|
|
1315
1339
|
// before the first real usage_update arrives. The authoritative count
|
|
1316
1340
|
// comes from `usage.promptTokens` after the first LLM response — this is
|
|
@@ -1322,10 +1346,12 @@ export class Engine {
|
|
|
1322
1346
|
const sid = session.state.sessionId;
|
|
1323
1347
|
const needsCtxSeed = !this.ctxSeedSent.has(sid);
|
|
1324
1348
|
const roughPromptTokens = needsCtxSeed
|
|
1325
|
-
?
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1349
|
+
? seededContextAnchor
|
|
1350
|
+
? contextManager.checkLimits(messages).tokens
|
|
1351
|
+
: messages.reduce((sum, m) => {
|
|
1352
|
+
const text = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
|
|
1353
|
+
return sum + Math.ceil(text.length / 4);
|
|
1354
|
+
}, 0)
|
|
1329
1355
|
: 0;
|
|
1330
1356
|
if (needsCtxSeed)
|
|
1331
1357
|
this.ctxSeedSent.add(sid);
|
|
@@ -1383,13 +1409,6 @@ export class Engine {
|
|
|
1383
1409
|
// Wire abort signal for cascading cancellation + per-Engine ToolContext
|
|
1384
1410
|
toolExecutor.setSignal(options?.signal);
|
|
1385
1411
|
toolExecutor.setContext(toolCtx);
|
|
1386
|
-
const contextManager = new ContextManager({
|
|
1387
|
-
maxTokens: this.resolveMaxContextTokens(),
|
|
1388
|
-
// Drop undefined fields so they don't clobber ContextManager defaults
|
|
1389
|
-
// (spread of `{x: undefined}` would override the default with undefined).
|
|
1390
|
-
...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
|
|
1391
|
-
});
|
|
1392
|
-
this.lastContextManager = contextManager;
|
|
1393
1412
|
const { disabledSkills, disabledPlugins } = this.readDisabledLists();
|
|
1394
1413
|
const promptComposer = new PromptComposer({
|
|
1395
1414
|
cwd,
|
|
@@ -1451,6 +1470,7 @@ export class Engine {
|
|
|
1451
1470
|
(normalizeGoal(options?.goal) !== undefined ||
|
|
1452
1471
|
session.state.activeGoal !== undefined ||
|
|
1453
1472
|
normalizeGoal(this.config.goal) !== undefined),
|
|
1473
|
+
settingsScope: this.config.settingsScope ?? "project",
|
|
1454
1474
|
};
|
|
1455
1475
|
toolCtx.toolVisibility = toolVisibility;
|
|
1456
1476
|
// #7: per-turn project builtin override. The toolRegistry's builtin tool
|
|
@@ -1761,6 +1781,13 @@ export class Engine {
|
|
|
1761
1781
|
consumeSteer: (source) => this.consumeSteer(sid, source),
|
|
1762
1782
|
claimClientMessageId: (clientMessageId, source) => claimClientMessageId(session, clientMessageId, source),
|
|
1763
1783
|
recordCumulativeUsage,
|
|
1784
|
+
recordContextUsageAnchor: (anchor) => {
|
|
1785
|
+
session.state.contextUsageAnchor = {
|
|
1786
|
+
...anchor,
|
|
1787
|
+
provider: this.config.llm.provider,
|
|
1788
|
+
model: this.config.llm.model,
|
|
1789
|
+
};
|
|
1790
|
+
},
|
|
1764
1791
|
// Clear the persisted goal for a self-reported completion / confirmed
|
|
1765
1792
|
// cancel. Clears the in-RAM session's activeGoal (so THIS run's later
|
|
1766
1793
|
// turns don't re-arm) AND persists it, and drops the in-flight stop
|
|
@@ -2359,7 +2386,6 @@ export class Engine {
|
|
|
2359
2386
|
refreshRuntimeConfig(patch, version) {
|
|
2360
2387
|
if (version <= this.lastAppliedConfigVersion)
|
|
2361
2388
|
return;
|
|
2362
|
-
const prevServers = this.config.mcpServers ?? {};
|
|
2363
2389
|
const prevPresetName = this.preset.name;
|
|
2364
2390
|
this.config = { ...this.config, ...patch };
|
|
2365
2391
|
// #2: re-resolve the prompt-affecting preset so the next-turn PromptComposer
|
|
@@ -2370,11 +2396,17 @@ export class Engine {
|
|
|
2370
2396
|
// The builtin tool SET is ctor-frozen and may be shared via runtime — we
|
|
2371
2397
|
// do NOT rebuild it here. If the new preset implies a different builtin
|
|
2372
2398
|
// tool set, that part of the change only lands on session restart.
|
|
2373
|
-
const prevTools = resolveBuiltinToolNames({
|
|
2399
|
+
const prevTools = resolveBuiltinToolNames({
|
|
2400
|
+
preset: prevPresetName,
|
|
2401
|
+
host: this.config.builtinToolHost,
|
|
2402
|
+
})
|
|
2374
2403
|
.slice()
|
|
2375
2404
|
.sort()
|
|
2376
2405
|
.join(",");
|
|
2377
|
-
const nextTools = resolveBuiltinToolNames({
|
|
2406
|
+
const nextTools = resolveBuiltinToolNames({
|
|
2407
|
+
preset: nextPreset.name,
|
|
2408
|
+
host: this.config.builtinToolHost,
|
|
2409
|
+
})
|
|
2378
2410
|
.slice()
|
|
2379
2411
|
.sort()
|
|
2380
2412
|
.join(",");
|
|
@@ -2461,6 +2493,41 @@ export class Engine {
|
|
|
2461
2493
|
}
|
|
2462
2494
|
return had;
|
|
2463
2495
|
}
|
|
2496
|
+
/**
|
|
2497
|
+
* Reset a session's workspace pointer back to its main root. If the session is
|
|
2498
|
+
* actively running, mutate that live SessionBundle first so the run's next
|
|
2499
|
+
* saveState cannot resurrect a stale worktree pointer.
|
|
2500
|
+
*/
|
|
2501
|
+
releaseSessionWorkspace(sessionId) {
|
|
2502
|
+
if (!sessionId || !this.sessionManager.exists(sessionId))
|
|
2503
|
+
return null;
|
|
2504
|
+
const mainRoot = this.sessionManager.readCwd(sessionId) ??
|
|
2505
|
+
(this.activeRunSession?.state.sessionId === sessionId
|
|
2506
|
+
? this.activeRunSession.state.cwd
|
|
2507
|
+
: undefined);
|
|
2508
|
+
if (!mainRoot)
|
|
2509
|
+
return null;
|
|
2510
|
+
const workspace = { root: mainRoot, kind: "main" };
|
|
2511
|
+
if (this.activeRunSession?.state.sessionId === sessionId) {
|
|
2512
|
+
this.activeRunSession.state.workspace = workspace;
|
|
2513
|
+
}
|
|
2514
|
+
try {
|
|
2515
|
+
const bundle = this.activeRunSession?.state.sessionId === sessionId
|
|
2516
|
+
? this.activeRunSession
|
|
2517
|
+
: this.sessionManager.resume(sessionId);
|
|
2518
|
+
bundle.state.workspace = workspace;
|
|
2519
|
+
this.sessionManager.saveState(bundle.state);
|
|
2520
|
+
}
|
|
2521
|
+
catch {
|
|
2522
|
+
try {
|
|
2523
|
+
this.sessionManager.setSessionWorkspace(sessionId, workspace);
|
|
2524
|
+
}
|
|
2525
|
+
catch {
|
|
2526
|
+
return null;
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
return workspace;
|
|
2530
|
+
}
|
|
2464
2531
|
injectContext(sessionId, content) {
|
|
2465
2532
|
const session = this.sessionManager.resume(sessionId);
|
|
2466
2533
|
session.transcript.appendMessage("assistant", content);
|
|
@@ -2954,6 +3021,17 @@ export class Engine {
|
|
|
2954
3021
|
return undefined;
|
|
2955
3022
|
}
|
|
2956
3023
|
}
|
|
3024
|
+
readWorktreeBranchPrefix(cwd) {
|
|
3025
|
+
if (this.config.isSubAgent === true || !cwd)
|
|
3026
|
+
return undefined;
|
|
3027
|
+
try {
|
|
3028
|
+
const settings = this.getSettingsManager().get();
|
|
3029
|
+
return settings.worktree?.branchPrefix;
|
|
3030
|
+
}
|
|
3031
|
+
catch {
|
|
3032
|
+
return undefined;
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
2957
3035
|
async resolveWorktreeSetupSandbox(cwd) {
|
|
2958
3036
|
if (!cwd)
|
|
2959
3037
|
return undefined;
|
|
@@ -2978,6 +3056,7 @@ export class Engine {
|
|
|
2978
3056
|
toolRegistry: this.toolRegistry,
|
|
2979
3057
|
askUser: this.config.askUser,
|
|
2980
3058
|
browser: this.config.browserBridge,
|
|
3059
|
+
workspace: this.config.workspaceBridge,
|
|
2981
3060
|
injectCredentialToBrowser: this.config.injectCredentialToBrowser,
|
|
2982
3061
|
isSubAgent: this.config.isSubAgent === true,
|
|
2983
3062
|
// Credential tools narrow their disk reads to this scope: a project/
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Following Claude Code's po_() pattern:
|
|
5
5
|
* pre_check → model_call → post_check → tool_exec → context_mgmt → hook_notify → next turn
|
|
6
6
|
*/
|
|
7
|
-
import type { Message, StreamCallback, TerminalReason, ContentBlock, ToolResult, TokenUsage } from "../types.js";
|
|
7
|
+
import type { Message, StreamCallback, TerminalReason, ContentBlock, ToolResult, TokenUsage, ContextUsageAnchor } from "../types.js";
|
|
8
8
|
import type { SteerItem } from "./steer-queue.js";
|
|
9
9
|
import { ModelFacade } from "./model-facade.js";
|
|
10
10
|
import { ToolExecutor } from "../tool-system/executor.js";
|
|
@@ -88,6 +88,8 @@ export interface TurnLoopDeps {
|
|
|
88
88
|
* Returns the updated counters so usage_update can carry both metric scopes.
|
|
89
89
|
*/
|
|
90
90
|
recordCumulativeUsage?: (usage: TokenUsage) => CumulativeUsageCounters;
|
|
91
|
+
/** Persist the latest context-estimation anchor derived from provider usage. */
|
|
92
|
+
recordContextUsageAnchor?: (anchor: ContextUsageAnchor) => void;
|
|
91
93
|
/**
|
|
92
94
|
* Reads/clears any user messages queued for THIS session via the steering
|
|
93
95
|
* channel (Engine.enqueueSteer) while a run is in flight. Consumed at the top
|
package/dist/engine/turn-loop.js
CHANGED
|
@@ -553,7 +553,9 @@ export class TurnLoop {
|
|
|
553
553
|
// compaction decisions use hybrid (actual + delta) estimation rather than
|
|
554
554
|
// pure heuristics. Without this the manager falls back to char/4 estimates.
|
|
555
555
|
if (response.usage?.promptTokens !== undefined) {
|
|
556
|
-
this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length, messages);
|
|
556
|
+
const anchor = this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length, messages);
|
|
557
|
+
if (anchor)
|
|
558
|
+
this.deps.recordContextUsageAnchor?.(anchor);
|
|
557
559
|
}
|
|
558
560
|
messages = this.markPendingImagesConsumed(messages);
|
|
559
561
|
// Truncation that cut off a TOOL CALL: the model overflowed
|