@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.
Files changed (81) hide show
  1. package/dist/capability-control/service.d.ts +2 -0
  2. package/dist/capability-control/service.js +4 -2
  3. package/dist/cc-orchestrator/codex-session-history.js +1 -1
  4. package/dist/cc-orchestrator/external-agent-bindings.js +1 -1
  5. package/dist/cc-orchestrator/external-agent-session-store.js +3 -1
  6. package/dist/cc-orchestrator/session-history.js +2 -2
  7. package/dist/cli/agent-server-stdio.js +7 -2
  8. package/dist/context/manager.d.ts +11 -2
  9. package/dist/context/manager.js +64 -3
  10. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  11. package/dist/credentials/inject-credential-tool.js +29 -10
  12. package/dist/credentials/use-credential-tool.d.ts +1 -0
  13. package/dist/credentials/use-credential-tool.js +3 -0
  14. package/dist/engine/engine.d.ts +9 -0
  15. package/dist/engine/engine.js +96 -17
  16. package/dist/engine/turn-loop.d.ts +3 -1
  17. package/dist/engine/turn-loop.js +3 -1
  18. package/dist/engine/types.d.ts +8 -0
  19. package/dist/git/worktree/crud.d.ts +69 -0
  20. package/dist/git/worktree/crud.js +206 -0
  21. package/dist/git/worktree/diff.d.ts +14 -0
  22. package/dist/git/worktree/diff.js +82 -0
  23. package/dist/git/worktree/git-exec.d.ts +7 -0
  24. package/dist/git/worktree/git-exec.js +51 -0
  25. package/dist/git/worktree/index.d.ts +5 -0
  26. package/dist/git/worktree/index.js +5 -0
  27. package/dist/git/worktree/query.d.ts +42 -0
  28. package/dist/git/worktree/query.js +121 -0
  29. package/dist/git/worktree/slug.d.ts +11 -0
  30. package/dist/git/worktree/slug.js +58 -0
  31. package/dist/git/worktree.d.ts +1 -127
  32. package/dist/git/worktree.js +5 -464
  33. package/dist/index.d.ts +28 -27
  34. package/dist/index.js +24 -24
  35. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  36. package/dist/plugins/installer/checkUpdate.js +4 -2
  37. package/dist/plugins/installer/install.js +2 -0
  38. package/dist/plugins/installer/parseSource.d.ts +4 -1
  39. package/dist/plugins/installer/parseSource.js +28 -10
  40. package/dist/plugins/installer/update.d.ts +4 -1
  41. package/dist/plugins/installer/update.js +5 -3
  42. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  43. package/dist/plugins/parseMarketplaceInput.js +4 -3
  44. package/dist/preset/index.d.ts +1 -0
  45. package/dist/preset/index.js +6 -0
  46. package/dist/prompt/sections/base.md +1 -1
  47. package/dist/protocol/chat-session-manager.js +7 -0
  48. package/dist/protocol/server.d.ts +10 -0
  49. package/dist/protocol/server.js +88 -4
  50. package/dist/protocol/types.d.ts +6 -0
  51. package/dist/protocol/types.js +2 -0
  52. package/dist/runtime/background-shell.js +2 -3
  53. package/dist/services/auto-dream.d.ts +15 -4
  54. package/dist/services/auto-dream.js +20 -20
  55. package/dist/services/dream-consolidation.d.ts +2 -3
  56. package/dist/services/dream-consolidation.js +65 -15
  57. package/dist/services/extract-memories.d.ts +14 -5
  58. package/dist/services/extract-memories.js +20 -3
  59. package/dist/services/global-dream-promotion.d.ts +23 -0
  60. package/dist/services/global-dream-promotion.js +112 -0
  61. package/dist/services/memory-orchestrator.js +347 -34
  62. package/dist/session/memory.d.ts +56 -17
  63. package/dist/session/memory.js +337 -78
  64. package/dist/session/session-manager.d.ts +1 -1
  65. package/dist/session/session-manager.js +6 -6
  66. package/dist/settings/manager.js +43 -12
  67. package/dist/settings/schema.d.ts +21 -0
  68. package/dist/settings/schema.js +12 -0
  69. package/dist/tool-system/builtin/index.js +18 -8
  70. package/dist/tool-system/builtin/memory.js +40 -8
  71. package/dist/tool-system/builtin/worktree.d.ts +2 -0
  72. package/dist/tool-system/builtin/worktree.js +59 -11
  73. package/dist/tool-system/context.d.ts +11 -1
  74. package/dist/tool-system/mcp-manager.d.ts +8 -5
  75. package/dist/tool-system/mcp-manager.js +85 -55
  76. package/dist/tool-system/path-policy.d.ts +2 -0
  77. package/dist/tool-system/path-policy.js +28 -12
  78. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  79. package/dist/tool-system/workspace-bridge.js +1 -0
  80. package/dist/types.d.ts +14 -0
  81. 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, } from "./overlay.js";
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.slice(0, 4000) });
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.slice(0, 4000) });
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.slice(0, 4000), tools: tools.length ? tools : undefined });
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[]): void;
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.
@@ -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
- this.lastActualTokens = inputTokens;
76
- this.lastActualAtMessageCount = messageCount;
77
- this.lastActualAnchorEstimate = messages ? estimateTokens(messages) : undefined;
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
- function readAutoApprove(cwd) {
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).listMasked().some((c) => c.type === "cookie");
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({ kind: "error", error: "缺少 id。先用 UseCredential(无参)列出凭证,再用 cookie 凭证的 id 调本工具。" });
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({ kind: "error", error: `凭证不存在: "${id}"。调用 UseCredential(无参)可列出可用凭证。` });
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 ? (q, opts) => ctx.askUser(q, opts) : undefined;
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;
@@ -203,3 +203,6 @@ function json(r) {
203
203
  export function __resetCredentialSessionAllowForTests() {
204
204
  sessionAllowByEngine.clear();
205
205
  }
206
+ export function clearCredentialSessionAllow(sessionId) {
207
+ sessionAllowByEngine.delete(sessionId);
208
+ }
@@ -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;
@@ -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, } from "../capability-control/overlay.js";
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, } from "../preset/index.js";
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
- ? messages.reduce((sum, m) => {
1326
- const text = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
1327
- return sum + Math.ceil(text.length / 4);
1328
- }, 0)
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({ preset: prevPresetName })
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({ preset: nextPreset.name })
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
@@ -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