@cjhyy/code-shell-core 0.6.0-rc.9 → 0.7.0

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 (193) hide show
  1. package/THIRD_PARTY_NOTICES.md +206 -0
  2. package/dist/automation/scheduler.d.ts +13 -7
  3. package/dist/automation/scheduler.js +116 -37
  4. package/dist/capability-control/service.d.ts +2 -0
  5. package/dist/capability-control/service.js +4 -2
  6. package/dist/cc-orchestrator/agent-adapter.d.ts +4 -0
  7. package/dist/cc-orchestrator/agent-adapter.js +11 -1
  8. package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
  9. package/dist/cc-orchestrator/codex-session-history.js +65 -5
  10. package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
  11. package/dist/cc-orchestrator/cwd-normalize.js +19 -0
  12. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  13. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  14. package/dist/cc-orchestrator/external-agent-changes.js +22 -5
  15. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  16. package/dist/cc-orchestrator/external-agent-driver.js +265 -50
  17. package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
  18. package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
  19. package/dist/cc-orchestrator/session-history.d.ts +35 -0
  20. package/dist/cc-orchestrator/session-history.js +98 -15
  21. package/dist/cli/agent-server-stdio.js +9 -2
  22. package/dist/context/compaction.d.ts +8 -1
  23. package/dist/context/compaction.js +49 -4
  24. package/dist/context/manager.d.ts +16 -2
  25. package/dist/context/manager.js +103 -19
  26. package/dist/credentials/access.d.ts +57 -0
  27. package/dist/credentials/access.js +185 -0
  28. package/dist/credentials/index.d.ts +3 -1
  29. package/dist/credentials/index.js +2 -0
  30. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  31. package/dist/credentials/inject-credential-tool.js +30 -11
  32. package/dist/credentials/oauth.d.ts +20 -0
  33. package/dist/credentials/oauth.js +114 -0
  34. package/dist/credentials/store.d.ts +1 -0
  35. package/dist/credentials/store.js +3 -1
  36. package/dist/credentials/types.d.ts +49 -3
  37. package/dist/credentials/use-credential-tool.d.ts +9 -1
  38. package/dist/credentials/use-credential-tool.js +58 -45
  39. package/dist/engine/engine.d.ts +23 -3
  40. package/dist/engine/engine.js +380 -255
  41. package/dist/engine/goal.d.ts +17 -0
  42. package/dist/engine/goal.js +16 -6
  43. package/dist/engine/image-policy.d.ts +6 -0
  44. package/dist/engine/image-policy.js +17 -6
  45. package/dist/engine/input-attachments.d.ts +13 -0
  46. package/dist/engine/input-attachments.js +398 -0
  47. package/dist/engine/model-facade.d.ts +5 -2
  48. package/dist/engine/model-facade.js +4 -4
  49. package/dist/engine/parse-task.d.ts +10 -0
  50. package/dist/engine/parse-task.js +5 -0
  51. package/dist/engine/run-image-input.d.ts +22 -0
  52. package/dist/engine/run-image-input.js +195 -0
  53. package/dist/engine/steer-queue.d.ts +3 -1
  54. package/dist/engine/steer-queue.js +10 -2
  55. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  56. package/dist/engine/streaming-tool-queue.js +11 -7
  57. package/dist/engine/turn-loop.d.ts +37 -2
  58. package/dist/engine/turn-loop.js +228 -43
  59. package/dist/engine/types.d.ts +8 -0
  60. package/dist/git/worktree/crud.d.ts +69 -0
  61. package/dist/git/worktree/crud.js +206 -0
  62. package/dist/git/worktree/diff.d.ts +14 -0
  63. package/dist/git/worktree/diff.js +82 -0
  64. package/dist/git/worktree/git-exec.d.ts +7 -0
  65. package/dist/git/worktree/git-exec.js +51 -0
  66. package/dist/git/worktree/index.d.ts +5 -0
  67. package/dist/git/worktree/index.js +5 -0
  68. package/dist/git/worktree/query.d.ts +42 -0
  69. package/dist/git/worktree/query.js +121 -0
  70. package/dist/git/worktree/slug.d.ts +11 -0
  71. package/dist/git/worktree/slug.js +58 -0
  72. package/dist/git/worktree.d.ts +1 -84
  73. package/dist/git/worktree.js +5 -230
  74. package/dist/hooks/goal-stop-hook.d.ts +33 -1
  75. package/dist/hooks/goal-stop-hook.js +202 -34
  76. package/dist/index.d.ts +31 -28
  77. package/dist/index.js +27 -25
  78. package/dist/logging/logger.js +6 -6
  79. package/dist/logging/sanitize-messages.d.ts +10 -2
  80. package/dist/logging/sanitize-messages.js +21 -6
  81. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  82. package/dist/plugins/installer/checkUpdate.js +4 -2
  83. package/dist/plugins/installer/install.js +2 -0
  84. package/dist/plugins/installer/installFromSource.js +9 -1
  85. package/dist/plugins/installer/parseSource.d.ts +4 -1
  86. package/dist/plugins/installer/parseSource.js +28 -10
  87. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  88. package/dist/plugins/installer/sourcePath.js +50 -0
  89. package/dist/plugins/installer/update.d.ts +4 -1
  90. package/dist/plugins/installer/update.js +5 -3
  91. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  92. package/dist/plugins/parseMarketplaceInput.js +4 -3
  93. package/dist/plugins/pluginInstaller.js +24 -22
  94. package/dist/preset/index.d.ts +1 -0
  95. package/dist/preset/index.js +30 -13
  96. package/dist/prompt/sections/base.md +1 -1
  97. package/dist/protocol/chat-session-manager.d.ts +2 -0
  98. package/dist/protocol/chat-session-manager.js +38 -2
  99. package/dist/protocol/chat-session.d.ts +3 -0
  100. package/dist/protocol/chat-session.js +1 -0
  101. package/dist/protocol/client.d.ts +9 -4
  102. package/dist/protocol/client.js +18 -1
  103. package/dist/protocol/server.d.ts +30 -0
  104. package/dist/protocol/server.js +247 -47
  105. package/dist/protocol/types.d.ts +49 -0
  106. package/dist/protocol/types.js +6 -0
  107. package/dist/run/FileRunStore.js +10 -1
  108. package/dist/run/Heartbeat.js +12 -0
  109. package/dist/run/RunApprovalBackend.d.ts +3 -0
  110. package/dist/run/RunApprovalBackend.js +41 -6
  111. package/dist/run/RunLock.js +2 -0
  112. package/dist/run/RunManager.d.ts +2 -0
  113. package/dist/run/RunManager.js +64 -24
  114. package/dist/run/ids.d.ts +2 -0
  115. package/dist/run/ids.js +23 -0
  116. package/dist/runtime/background-shell.d.ts +1 -0
  117. package/dist/runtime/background-shell.js +23 -13
  118. package/dist/runtime/spawn-common.js +10 -0
  119. package/dist/services/auto-dream.d.ts +15 -4
  120. package/dist/services/auto-dream.js +20 -20
  121. package/dist/services/dream-consolidation.d.ts +2 -3
  122. package/dist/services/dream-consolidation.js +65 -15
  123. package/dist/services/extract-memories.d.ts +14 -5
  124. package/dist/services/extract-memories.js +20 -3
  125. package/dist/services/global-dream-promotion.d.ts +23 -0
  126. package/dist/services/global-dream-promotion.js +112 -0
  127. package/dist/services/memory-orchestrator.js +347 -34
  128. package/dist/session/memory.d.ts +61 -18
  129. package/dist/session/memory.js +342 -79
  130. package/dist/session/session-manager.d.ts +35 -1
  131. package/dist/session/session-manager.js +224 -4
  132. package/dist/session/transcript.d.ts +1 -1
  133. package/dist/session/transcript.js +17 -5
  134. package/dist/settings/manager.d.ts +1 -0
  135. package/dist/settings/manager.js +87 -37
  136. package/dist/settings/schema-export.d.ts +2 -3
  137. package/dist/settings/schema-export.js +2 -3
  138. package/dist/settings/schema.d.ts +21 -0
  139. package/dist/settings/schema.js +12 -0
  140. package/dist/skills/scanner.d.ts +3 -2
  141. package/dist/skills/scanner.js +12 -9
  142. package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
  143. package/dist/tool-system/builtin/agent-notifications.js +19 -7
  144. package/dist/tool-system/builtin/background-jobs.d.ts +35 -6
  145. package/dist/tool-system/builtin/background-jobs.js +116 -7
  146. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  147. package/dist/tool-system/builtin/background-work.js +51 -5
  148. package/dist/tool-system/builtin/config.d.ts +2 -1
  149. package/dist/tool-system/builtin/config.js +16 -11
  150. package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
  151. package/dist/tool-system/builtin/cron-list.definition.js +6 -0
  152. package/dist/tool-system/builtin/cron.d.ts +1 -2
  153. package/dist/tool-system/builtin/cron.js +9 -7
  154. package/dist/tool-system/builtin/drive-claude-code.d.ts +23 -2
  155. package/dist/tool-system/builtin/drive-claude-code.js +589 -48
  156. package/dist/tool-system/builtin/edit.js +5 -2
  157. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  158. package/dist/tool-system/builtin/generate-video.js +13 -4
  159. package/dist/tool-system/builtin/index.d.ts +8 -3
  160. package/dist/tool-system/builtin/index.js +50 -22
  161. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  162. package/dist/tool-system/builtin/lsp.js +6 -3
  163. package/dist/tool-system/builtin/memory.js +40 -8
  164. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  165. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  166. package/dist/tool-system/builtin/powershell.js +11 -7
  167. package/dist/tool-system/builtin/read.js +118 -6
  168. package/dist/tool-system/builtin/sleep.d.ts +1 -2
  169. package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
  170. package/dist/tool-system/builtin/sleep.definition.js +28 -0
  171. package/dist/tool-system/builtin/sleep.js +1 -22
  172. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  173. package/dist/tool-system/builtin/view-image.js +109 -19
  174. package/dist/tool-system/builtin/worktree.d.ts +4 -4
  175. package/dist/tool-system/builtin/worktree.js +297 -74
  176. package/dist/tool-system/builtin/write.js +5 -3
  177. package/dist/tool-system/context.d.ts +41 -1
  178. package/dist/tool-system/executor.d.ts +1 -5
  179. package/dist/tool-system/executor.js +94 -115
  180. package/dist/tool-system/mcp-manager.d.ts +31 -6
  181. package/dist/tool-system/mcp-manager.js +193 -78
  182. package/dist/tool-system/path-policy.d.ts +2 -0
  183. package/dist/tool-system/path-policy.js +41 -12
  184. package/dist/tool-system/permission.d.ts +28 -7
  185. package/dist/tool-system/permission.js +130 -49
  186. package/dist/tool-system/registry.js +11 -4
  187. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  188. package/dist/tool-system/tool-result-redaction.js +48 -0
  189. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  190. package/dist/tool-system/workspace-bridge.js +1 -0
  191. package/dist/types.d.ts +78 -11
  192. package/dist/utils/toolDisplay.js +1 -1
  193. package/package.json +4 -3
@@ -19,23 +19,28 @@ function toolsOf(content) {
19
19
  const inp = p.input ?? {};
20
20
  const summary = inp.command ?? inp.file_path ?? inp.path ?? inp.url ?? inp.pattern ?? inp.query ?? "";
21
21
  const args = inp && typeof inp === "object" && Object.keys(inp).length > 0 ? inp : undefined;
22
- out.push({ name: typeof p.name === "string" ? p.name : "tool", summary: String(summary).slice(0, 120), args });
22
+ out.push({
23
+ name: typeof p.name === "string" ? p.name : "tool",
24
+ summary: String(summary).slice(0, 120),
25
+ args,
26
+ });
23
27
  }
24
28
  }
25
29
  return out;
26
30
  }
27
- /** Read the last `limit` user/assistant messages from a claude session jsonl. */
28
- export function readRecentHistory(cwd, sessionId, limit, claudeHome = join(homedir(), ".claude")) {
29
- const file = join(claudeHome, "projects", encodeCwd(cwd), `${sessionId}.jsonl`);
30
- if (!existsSync(file))
31
- return { messages: [], hasMore: false, totalCount: 0 };
32
- let raw;
33
- try {
34
- raw = readFileSync(file, "utf-8");
35
- }
36
- catch {
37
- return { messages: [], hasMore: false, totalCount: 0 };
38
- }
31
+ function toolResultText(content) {
32
+ if (typeof content === "string")
33
+ return content;
34
+ if (!Array.isArray(content))
35
+ return "";
36
+ return content
37
+ .map((part) => typeof part === "string" ? part : typeof part?.text === "string" ? part.text : "")
38
+ .join("");
39
+ }
40
+ /** Parse a bounded/raw Claude Code transcript snapshot. Exported so a live
41
+ * follower can take its initial snapshot and EOF cursor from the same read,
42
+ * eliminating the snapshot→subscribe race. */
43
+ export function parseRecentHistory(raw, limit) {
39
44
  const all = [];
40
45
  for (const line of raw.split("\n")) {
41
46
  if (!line.trim())
@@ -51,17 +56,95 @@ export function readRecentHistory(cwd, sessionId, limit, claudeHome = join(homed
51
56
  const t = textOf(d.message?.content).trim();
52
57
  if (!t || NOISE.some((n) => t.startsWith(n)))
53
58
  continue;
54
- all.push({ role: "user", text: t.slice(0, 4000) });
59
+ all.push({ role: "user", text: t });
55
60
  }
56
61
  else if (d.type === "assistant") {
57
62
  const t = textOf(d.message?.content).trim();
58
63
  const tools = toolsOf(d.message?.content);
59
64
  if (!t && tools.length === 0)
60
65
  continue;
61
- all.push({ role: "assistant", text: t.slice(0, 4000), tools: tools.length ? tools : undefined });
66
+ all.push({ role: "assistant", text: t, tools: tools.length ? tools : undefined });
62
67
  }
63
68
  }
64
69
  const lim = limit > 0 ? limit : 20;
65
70
  const start = Math.max(0, all.length - lim);
66
71
  return { messages: all.slice(start), hasMore: start > 0, totalCount: all.length };
67
72
  }
73
+ /** Parse one newly-appended Claude Code transcript JSONL line into the compact
74
+ * event vocabulary consumed by the desktop room follower. */
75
+ export function parseClaudeTranscriptLine(line) {
76
+ let d;
77
+ try {
78
+ d = JSON.parse(line);
79
+ }
80
+ catch {
81
+ return [];
82
+ }
83
+ const out = [];
84
+ if (d.type === "user") {
85
+ const content = d.message?.content;
86
+ if (Array.isArray(content)) {
87
+ for (const part of content) {
88
+ if (part?.type !== "tool_result")
89
+ continue;
90
+ out.push({
91
+ type: "tool_result",
92
+ id: typeof part.tool_use_id === "string" ? part.tool_use_id : undefined,
93
+ result: toolResultText(part.content).slice(0, 4000),
94
+ isError: Boolean(part.is_error),
95
+ });
96
+ }
97
+ }
98
+ const text = textOf(content).trim();
99
+ if (text && !NOISE.some((noise) => text.startsWith(noise))) {
100
+ out.unshift({ type: "user", text });
101
+ }
102
+ return out;
103
+ }
104
+ if (d.type === "assistant" && Array.isArray(d.message?.content)) {
105
+ for (const part of d.message.content) {
106
+ if (part?.type === "text" && typeof part.text === "string" && part.text) {
107
+ out.push({ type: "assistant", text: part.text });
108
+ }
109
+ else if (part?.type === "tool_use") {
110
+ const input = part.input && typeof part.input === "object"
111
+ ? part.input
112
+ : undefined;
113
+ out.push({
114
+ type: "tool",
115
+ id: typeof part.id === "string" ? part.id : undefined,
116
+ name: typeof part.name === "string" ? part.name : "tool",
117
+ summary: toolsOf([part])[0]?.summary ?? "",
118
+ args: input,
119
+ });
120
+ }
121
+ }
122
+ if (d.message.stop_reason === "end_turn") {
123
+ out.push({ type: "turn_end", reason: "completed" });
124
+ }
125
+ return out;
126
+ }
127
+ if (d.type === "result") {
128
+ return [
129
+ {
130
+ type: "turn_end",
131
+ reason: typeof d.subtype === "string" ? d.subtype : "completed",
132
+ },
133
+ ];
134
+ }
135
+ return [];
136
+ }
137
+ /** Read the last `limit` user/assistant messages from a claude session jsonl. */
138
+ export function readRecentHistory(cwd, sessionId, limit, claudeHome = join(homedir(), ".claude")) {
139
+ const file = join(claudeHome, "projects", encodeCwd(cwd), `${sessionId}.jsonl`);
140
+ if (!existsSync(file))
141
+ return { messages: [], hasMore: false, totalCount: 0 };
142
+ let raw;
143
+ try {
144
+ raw = readFileSync(file, "utf-8");
145
+ }
146
+ catch {
147
+ return { messages: [], hasMore: false, totalCount: 0 };
148
+ }
149
+ return parseRecentHistory(raw, limit);
150
+ }
@@ -50,6 +50,7 @@ import { logger } from "../logging/logger.js";
50
50
  import { cronScheduler } from "../automation/scheduler.js";
51
51
  import { CronStore, defaultCronStorePath } from "../automation/store.js";
52
52
  import { resolveLLMConfigForTag } from "../engine/resolve-llm-config.js";
53
+ import { createIpcCredentialAccess, setDefaultCredentialAccess } from "../credentials/access.js";
53
54
  /**
54
55
  * Resolve per-session agent config: protocol slice overrides win, else fall
55
56
  * back to disk settings.agent.*. Fixes the bug where settings.agent.* never
@@ -106,8 +107,7 @@ catch (err) {
106
107
  // knob; imageDetail from settings.images.detail).
107
108
  const seedLlm = resolveLLMConfigForTag(settings, "text", settings.defaults?.text);
108
109
  if (!seedLlm) {
109
- console.error(`[agent-server] 没有可用的文本模型连接(defaults.text=${settings.defaults?.text ?? "未设置"})。` +
110
- `请在「连接」页添加并填写凭证。`);
110
+ console.error(`[agent-server] 没有可用的文本模型连接(defaults.text=${settings.defaults?.text ?? "未设置"})。` + `请在「连接」页添加并填写凭证。`);
111
111
  process.exit(1);
112
112
  }
113
113
  const llmConfig = seedLlm;
@@ -115,6 +115,10 @@ const llmConfig = seedLlm;
115
115
  const seedEngine = new Engine({
116
116
  llm: llmConfig,
117
117
  cwd,
118
+ preset: settings.agent.preset,
119
+ enabledBuiltinTools: settings.agent.enabledBuiltinTools,
120
+ disabledBuiltinTools: settings.agent.disabledBuiltinTools,
121
+ builtinToolHost: "desktop",
118
122
  settingsScope: "full",
119
123
  // No runtime — Engine.populateModelPoolFromSettings() runs in ctor.
120
124
  });
@@ -217,6 +221,7 @@ const chatManager = new ChatSessionManager({
217
221
  // This stdio worker exists only to serve the desktop app, so every
218
222
  // session it creates is a desktop-origin session.
219
223
  origin: "desktop",
224
+ builtinToolHost: "desktop",
220
225
  // Inherit full scope so spawned subagents read user config too.
221
226
  settingsScope: "full",
222
227
  // MCP servers from settings — the worker reads the full disk
@@ -270,6 +275,7 @@ cronScheduler.setExecutionEnabled(false);
270
275
  cronScheduler.loadJobs();
271
276
  // ─── Step 5: AgentServer over stdio ──────────────────────────────
272
277
  const stdioTransport = new StdioTransport(process.stdin, process.stdout);
278
+ setDefaultCredentialAccess(createIpcCredentialAccess(stdioTransport));
273
279
  // Cron jobs are persisted by this worker but only main arms/executes their
274
280
  // timers (this worker keeps setExecutionEnabled(false) above). When an AI tool
275
281
  // creates/deletes a cron job here, notify main over stdio so it reloads the
@@ -286,6 +292,7 @@ const goalDiskReader = new SessionManager();
286
292
  const agentServer = new AgentServer({
287
293
  chatManager,
288
294
  transport: stdioTransport,
295
+ workspaceBridge: true,
289
296
  // Config hot-reload (layer 2) reads disk through the SAME closure the
290
297
  // engineFactory uses for new sessions, so a reloaded running session and a
291
298
  // newly-created session converge on identical disk config (no divergence).
@@ -10,7 +10,7 @@
10
10
  * All slicing functions use adjustIndexToPreserveAPIInvariants()
11
11
  * to prevent splitting tool_use / tool_result pairs.
12
12
  */
13
- import type { Message } from "../types.js";
13
+ import type { ContentBlock, Message } from "../types.js";
14
14
  /**
15
15
  * Estimate token count from messages.
16
16
  * Uses per-block-type estimation with 33% overhead padding.
@@ -34,6 +34,12 @@ export interface DowngradeImageHistoryResult {
34
34
  messages: Message[];
35
35
  replacedCount: number;
36
36
  }
37
+ export interface Base64ImageHistoryEntry {
38
+ imageNumber: number;
39
+ block: ContentBlock;
40
+ }
41
+ export declare function collectBase64Images(messages: Message[]): Base64ImageHistoryEntry[];
42
+ export declare function findImageByNumber(messages: Message[], imageNumber: number): Base64ImageHistoryEntry | undefined;
37
43
  /**
38
44
  * Replace already-consumed image payload blocks with compact text markers.
39
45
  *
@@ -45,6 +51,7 @@ export interface DowngradeImageHistoryResult {
45
51
  */
46
52
  export declare function downgradeImagePayloadsInHistory(messages: Message[], options?: DowngradeImageHistoryOptions): DowngradeImageHistoryResult;
47
53
  export declare function messageHasBase64ImagePayload(message: Message): boolean;
54
+ export declare function isBase64ImageBlock(block: ContentBlock): boolean;
48
55
  /**
49
56
  * Reconcile user-supplied compaction ratios into a safe ordering.
50
57
  *
@@ -20,6 +20,36 @@ export function estimateTokens(messages) {
20
20
  }
21
21
  export const IMAGE_HISTORY_PLACEHOLDER_PREFIX = "[image #";
22
22
  export const IMAGE_HISTORY_PLACEHOLDER_SUFFIX = ", 已处理 / already provided earlier]";
23
+ export function collectBase64Images(messages) {
24
+ const entries = [];
25
+ let nextImageNumber = 1;
26
+ const visitBlocks = (blocks) => {
27
+ for (const block of blocks) {
28
+ const placeholderNumber = imageHistoryPlaceholderNumber(block);
29
+ if (placeholderNumber !== undefined) {
30
+ nextImageNumber = Math.max(nextImageNumber, placeholderNumber + 1);
31
+ continue;
32
+ }
33
+ if (isBase64ImageBlock(block)) {
34
+ entries.push({ imageNumber: nextImageNumber++, block });
35
+ continue;
36
+ }
37
+ if (block.type === "tool_result" && Array.isArray(block.content)) {
38
+ visitBlocks(block.content);
39
+ }
40
+ }
41
+ };
42
+ for (const msg of messages) {
43
+ if (Array.isArray(msg.content))
44
+ visitBlocks(msg.content);
45
+ }
46
+ return entries;
47
+ }
48
+ export function findImageByNumber(messages, imageNumber) {
49
+ if (!Number.isSafeInteger(imageNumber) || imageNumber <= 0)
50
+ return undefined;
51
+ return collectBase64Images(messages).find((entry) => entry.imageNumber === imageNumber);
52
+ }
23
53
  /**
24
54
  * Replace already-consumed image payload blocks with compact text markers.
25
55
  *
@@ -30,9 +60,23 @@ export const IMAGE_HISTORY_PLACEHOLDER_SUFFIX = ", 已处理 / already provided
30
60
  * nested inside tool_result.content arrays (view_image / browser screenshots).
31
61
  */
32
62
  export function downgradeImagePayloadsInHistory(messages, options = {}) {
33
- let nextImageNumber = 1;
34
63
  let replacedCount = 0;
35
64
  let changed = false;
65
+ const imageNumberQueues = new WeakMap();
66
+ for (const entry of collectBase64Images(messages)) {
67
+ const key = entry.block;
68
+ const existing = imageNumberQueues.get(key);
69
+ if (existing) {
70
+ existing.push(entry.imageNumber);
71
+ }
72
+ else {
73
+ imageNumberQueues.set(key, [entry.imageNumber]);
74
+ }
75
+ }
76
+ const takeImageNumber = (block) => {
77
+ const queue = imageNumberQueues.get(block);
78
+ return queue?.shift();
79
+ };
36
80
  const placeholderFor = (imageNumber) => ({
37
81
  type: "text",
38
82
  text: `${IMAGE_HISTORY_PLACEHOLDER_PREFIX}${imageNumber}${IMAGE_HISTORY_PLACEHOLDER_SUFFIX}`,
@@ -42,11 +86,12 @@ export function downgradeImagePayloadsInHistory(messages, options = {}) {
42
86
  const out = blocks.map((block) => {
43
87
  const placeholderNumber = imageHistoryPlaceholderNumber(block);
44
88
  if (placeholderNumber !== undefined) {
45
- nextImageNumber = Math.max(nextImageNumber, placeholderNumber + 1);
46
89
  return block;
47
90
  }
48
91
  if (isBase64ImageBlock(block)) {
49
- const imageNumber = nextImageNumber++;
92
+ const imageNumber = takeImageNumber(block);
93
+ if (imageNumber === undefined)
94
+ return block;
50
95
  if (preserve)
51
96
  return block;
52
97
  replacedCount++;
@@ -99,7 +144,7 @@ function imageHistoryPlaceholderNumber(block) {
99
144
  const n = Number(match[1]);
100
145
  return Number.isSafeInteger(n) && n > 0 ? n : undefined;
101
146
  }
102
- function isBase64ImageBlock(block) {
147
+ export function isBase64ImageBlock(block) {
103
148
  if (block.type === "image" &&
104
149
  block.source?.type === "base64" &&
105
150
  typeof block.source.data === "string" &&
@@ -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, PromptTokenConfidence, PromptTokenSource } from "../types.js";
9
9
  export interface ContextManagerConfig {
10
10
  maxTokens: number;
11
11
  compactAtRatio: number;
@@ -48,6 +48,11 @@ export declare class ContextManager {
48
48
  private lastActualTokens;
49
49
  /** Message count at the time lastActualTokens was recorded. */
50
50
  private lastActualAtMessageCount;
51
+ /** Heuristic token estimate for the same messages as lastActualTokens. */
52
+ private lastActualAnchorEstimate;
53
+ private lastActualRecordedAt;
54
+ private lastActualProvider;
55
+ private lastActualModel;
51
56
  /** Path to session transcript — passed to summary compaction for on-demand access. */
52
57
  private transcriptPath;
53
58
  /** Notified whenever any compaction tier fires, including microcompact. */
@@ -65,11 +70,18 @@ export declare class ContextManager {
65
70
  * Record actual token usage from API response.
66
71
  * Used for hybrid estimation: actual + estimate for new messages.
67
72
  */
68
- recordActualUsage(inputTokens: number, messageCount: number): 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;
69
80
  /**
70
81
  * Best-effort token estimate: uses actual API usage as base if available,
71
82
  * plus estimation for messages added since the last API call.
72
83
  */
84
+ private estimateTokensHybridInfo;
73
85
  private estimateTokensHybrid;
74
86
  /**
75
87
  * Set the summarize function (injected by Engine).
@@ -139,5 +151,7 @@ export declare class ContextManager {
139
151
  ratio: number;
140
152
  needsCompact: boolean;
141
153
  needsEmergency: boolean;
154
+ promptTokensSource: PromptTokenSource;
155
+ promptTokensConfidence: PromptTokenConfidence;
142
156
  };
143
157
  }
@@ -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;
@@ -48,6 +51,11 @@ export class ContextManager {
48
51
  lastActualTokens;
49
52
  /** Message count at the time lastActualTokens was recorded. */
50
53
  lastActualAtMessageCount;
54
+ /** Heuristic token estimate for the same messages as lastActualTokens. */
55
+ lastActualAnchorEstimate;
56
+ lastActualRecordedAt;
57
+ lastActualProvider;
58
+ lastActualModel;
51
59
  /** Path to session transcript — passed to summary compaction for on-demand access. */
52
60
  transcriptPath;
53
61
  /** Notified whenever any compaction tier fires, including microcompact. */
@@ -69,23 +77,84 @@ export class ContextManager {
69
77
  * Record actual token usage from API response.
70
78
  * Used for hybrid estimation: actual + estimate for new messages.
71
79
  */
72
- recordActualUsage(inputTokens, messageCount) {
73
- this.lastActualTokens = inputTokens;
74
- this.lastActualAtMessageCount = messageCount;
80
+ recordActualUsage(inputTokens, messageCount, messages) {
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
+ };
75
128
  }
76
129
  /**
77
130
  * Best-effort token estimate: uses actual API usage as base if available,
78
131
  * plus estimation for messages added since the last API call.
79
132
  */
80
- estimateTokensHybrid(messages) {
133
+ estimateTokensHybridInfo(messages) {
134
+ const currentEstimate = estimateTokens(messages);
81
135
  if (this.lastActualTokens !== undefined &&
82
- this.lastActualAtMessageCount !== undefined &&
83
- this.lastActualAtMessageCount < messages.length) {
84
- const newMessages = messages.slice(this.lastActualAtMessageCount);
85
- const newTokens = estimateTokens(newMessages);
86
- return this.lastActualTokens + newTokens;
136
+ this.lastActualAtMessageCount !== undefined) {
137
+ if (this.lastActualAtMessageCount < messages.length) {
138
+ const newMessages = messages.slice(this.lastActualAtMessageCount);
139
+ const newTokens = estimateTokens(newMessages);
140
+ return {
141
+ tokens: this.lastActualTokens + newTokens,
142
+ source: "anchor_delta",
143
+ confidence: "medium",
144
+ };
145
+ }
146
+ if (this.lastActualAnchorEstimate !== undefined && this.lastActualAnchorEstimate > 0) {
147
+ return {
148
+ tokens: Math.round(this.lastActualTokens * (currentEstimate / this.lastActualAnchorEstimate)),
149
+ source: "anchor_rescale",
150
+ confidence: "medium",
151
+ };
152
+ }
87
153
  }
88
- return estimateTokens(messages);
154
+ return { tokens: currentEstimate, source: "heuristic_estimate", confidence: "low" };
155
+ }
156
+ estimateTokensHybrid(messages) {
157
+ return this.estimateTokensHybridInfo(messages).tokens;
89
158
  }
90
159
  /**
91
160
  * Set the summarize function (injected by Engine).
@@ -119,7 +188,7 @@ export class ContextManager {
119
188
  return { messages, tokens: before, compacted: false, noProgress: true };
120
189
  }
121
190
  const compacted = applySummaryCompaction(messages, summary, keepRecentN, this.transcriptPath);
122
- const after = estimateTokens(compacted);
191
+ const after = this.estimateTokensHybrid(compacted);
123
192
  if (after >= before) {
124
193
  this.consecutiveSummaryFailures++;
125
194
  logger.warn("context.summary_no_progress", {
@@ -298,6 +367,17 @@ export class ContextManager {
298
367
  result = this.truncateToolResults(result);
299
368
  // Tier 0c: Aggregate tool result budget (per-message)
300
369
  result = applyToolResultBudget(result);
370
+ // Tier 0d: same always-on waste-removal passes as manage().
371
+ const dedup = dedupeFileReads(result);
372
+ if (dedup.clearedCount > 0) {
373
+ result = dedup.messages;
374
+ logger.info("context.dedupe_file_reads", { cleared: dedup.clearedCount });
375
+ }
376
+ const masked = maskOldObservations(result);
377
+ if (masked.maskedCount > 0) {
378
+ result = masked.messages;
379
+ logger.info("context.mask_browser_snapshots", { masked: masked.maskedCount });
380
+ }
301
381
  // Tier 1: microcompact — see manage() for the rationale on the floor.
302
382
  const preTier1Tokens = this.estimateTokensHybrid(result);
303
383
  const microFloorGate = this.config.maxTokens * this.config.microcompactFloorRatio;
@@ -342,6 +422,9 @@ export class ContextManager {
342
422
  ratio >= this.config.microcompactFloorRatio &&
343
423
  ratio < this.config.compactAtRatio;
344
424
  const shouldEscalateNoOpMicro = noOpMicroSpinBand && !this.suppressNoOpMicroSummaryUntilCompact;
425
+ const snipGate = this.config.maxTokens * this.config.compactAtRatio;
426
+ const windowGate = this.config.maxTokens * (this.config.compactAtRatio + 0.05);
427
+ const emergencyGate = this.config.maxTokens * this.config.summarizeAtRatio;
345
428
  // Tier 2: LLM summary if approaching limit, or if micro was the only tier
346
429
  // available in the 0.70-0.85 band and it freed nothing.
347
430
  if (ratio >= this.config.compactAtRatio || shouldEscalateNoOpMicro) {
@@ -350,7 +433,8 @@ export class ContextManager {
350
433
  tokens = summarized.tokens;
351
434
  if (summarized.compacted) {
352
435
  this.suppressNoOpMicroSummaryUntilCompact = false;
353
- return result;
436
+ if (tokens <= snipGate)
437
+ return result;
354
438
  }
355
439
  if (shouldEscalateNoOpMicro && summarized.noProgress) {
356
440
  this.suppressNoOpMicroSummaryUntilCompact = true;
@@ -362,13 +446,10 @@ export class ContextManager {
362
446
  });
363
447
  }
364
448
  }
365
- // Reuse the `tokens` we already computed above. We only get here if no
366
- // summary compacted the prompt (no summarizeFn, too many failures, a
367
- // thrown error, or a generated summary that did not shrink anything).
449
+ // Reuse the `tokens` we already computed above. We get here if no summary
450
+ // compacted the prompt, or if summary helped but the hybrid estimate is
451
+ // still above the fallback gate.
368
452
  let live = tokens;
369
- const snipGate = this.config.maxTokens * this.config.compactAtRatio;
370
- const windowGate = this.config.maxTokens * (this.config.compactAtRatio + 0.05);
371
- const emergencyGate = this.config.maxTokens * this.config.summarizeAtRatio;
372
453
  // Tier 2 fallback: snip first (cheapest sync option)
373
454
  if (live > snipGate) {
374
455
  const before = live;
@@ -507,13 +588,16 @@ export class ContextManager {
507
588
  * Check if context is approaching limits.
508
589
  */
509
590
  checkLimits(messages) {
510
- const tokens = this.estimateTokensHybrid(messages);
591
+ const estimate = this.estimateTokensHybridInfo(messages);
592
+ const tokens = estimate.tokens;
511
593
  const ratio = tokens / this.config.maxTokens;
512
594
  return {
513
595
  tokens,
514
596
  ratio,
515
597
  needsCompact: ratio >= this.config.compactAtRatio,
516
598
  needsEmergency: ratio >= this.config.summarizeAtRatio,
599
+ promptTokensSource: estimate.source,
600
+ promptTokensConfidence: estimate.confidence,
517
601
  };
518
602
  }
519
603
  }
@@ -0,0 +1,57 @@
1
+ import type { Credential, CredentialType } from "./types.js";
2
+ import type { SettingsScope } from "../settings/manager.js";
3
+ import type { Transport } from "../protocol/transport.js";
4
+ export type CredentialAccessScope = "full" | "project";
5
+ export interface CredentialMetadata {
6
+ id: string;
7
+ type: CredentialType;
8
+ label: string;
9
+ autoUseByAI?: boolean;
10
+ autoInjectByAI?: boolean;
11
+ exposeAsEnv?: string;
12
+ meta?: Credential["meta"];
13
+ hasSecret: boolean;
14
+ secretHint?: string;
15
+ oauthStatus?: import("./types.js").OAuthCredentialPublicStatus;
16
+ }
17
+ export interface CredentialAccess {
18
+ listMasked(cwd: string | undefined, scope: CredentialAccessScope): CredentialMetadata[];
19
+ resolveMeta(cwd: string | undefined, id: string, scope: CredentialAccessScope): CredentialMetadata | undefined;
20
+ envExposures(cwd: string | undefined, scope: CredentialAccessScope): Record<string, string>;
21
+ resolveValue?(req: {
22
+ cwd?: string;
23
+ id: string;
24
+ scope: CredentialAccessScope;
25
+ purpose: "use" | "mcp";
26
+ }): Promise<string>;
27
+ materializeCookie?(req: {
28
+ cwd?: string;
29
+ id: string;
30
+ scope: CredentialAccessScope;
31
+ }): Promise<{
32
+ cookiesFile: string;
33
+ count: number;
34
+ }>;
35
+ }
36
+ export interface CredentialSnapshotEntry {
37
+ cwd?: string;
38
+ full: CredentialMetadata[];
39
+ project: CredentialMetadata[];
40
+ envFull: Record<string, string>;
41
+ envProject: Record<string, string>;
42
+ }
43
+ export interface CredentialSnapshot {
44
+ revision: number;
45
+ entries: CredentialSnapshotEntry[];
46
+ }
47
+ export declare function setDefaultCredentialAccess(access: CredentialAccess | null | undefined): void;
48
+ export declare function getCredentialAccess(): CredentialAccess;
49
+ export declare function createIpcCredentialAccess(transport: Pick<Transport, "send" | "onMessage">): CredentialAccess;
50
+ export declare function credentialAccessScope(scope: SettingsScope | undefined): CredentialAccessScope;
51
+ export declare function isCredentialSecretAvailable(secret: string | undefined): secret is string;
52
+ export declare const localCredentialAccess: CredentialAccess;
53
+ export declare function materializeCookieSecret(credentialId: string, secret: string): {
54
+ cookiesFile: string;
55
+ count: number;
56
+ };
57
+ export declare function sweepStaleCredentialCookieFiles(now?: number): void;