@cjhyy/code-shell-core 0.6.0-rc.8 → 0.7.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) 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 +2 -0
  7. package/dist/cc-orchestrator/agent-adapter.js +7 -1
  8. package/dist/cc-orchestrator/codex-session-history.js +1 -1
  9. package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
  10. package/dist/cc-orchestrator/cwd-normalize.js +19 -0
  11. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  12. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  13. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  14. package/dist/cc-orchestrator/external-agent-driver.js +102 -51
  15. package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
  16. package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
  17. package/dist/cc-orchestrator/session-history.js +2 -2
  18. package/dist/cli/agent-server-stdio.js +9 -2
  19. package/dist/context/compaction.d.ts +38 -1
  20. package/dist/context/compaction.js +138 -0
  21. package/dist/context/manager.d.ts +34 -2
  22. package/dist/context/manager.js +254 -58
  23. package/dist/context/token-counter.js +13 -0
  24. package/dist/credentials/access.d.ts +56 -0
  25. package/dist/credentials/access.js +183 -0
  26. package/dist/credentials/index.d.ts +1 -0
  27. package/dist/credentials/index.js +1 -0
  28. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  29. package/dist/credentials/inject-credential-tool.js +30 -11
  30. package/dist/credentials/types.d.ts +2 -2
  31. package/dist/credentials/use-credential-tool.d.ts +9 -1
  32. package/dist/credentials/use-credential-tool.js +58 -45
  33. package/dist/engine/engine.d.ts +38 -12
  34. package/dist/engine/engine.js +477 -163
  35. package/dist/engine/image-policy.d.ts +6 -0
  36. package/dist/engine/image-policy.js +17 -6
  37. package/dist/engine/input-attachments.d.ts +13 -0
  38. package/dist/engine/input-attachments.js +255 -0
  39. package/dist/engine/model-facade.d.ts +5 -2
  40. package/dist/engine/model-facade.js +6 -16
  41. package/dist/engine/parse-task.d.ts +10 -0
  42. package/dist/engine/parse-task.js +5 -0
  43. package/dist/engine/query.js +2 -0
  44. package/dist/engine/session-usage.d.ts +12 -0
  45. package/dist/engine/session-usage.js +56 -0
  46. package/dist/engine/steer-queue.d.ts +2 -1
  47. package/dist/engine/steer-queue.js +2 -2
  48. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  49. package/dist/engine/streaming-tool-queue.js +11 -7
  50. package/dist/engine/turn-loop.d.ts +34 -2
  51. package/dist/engine/turn-loop.js +269 -52
  52. package/dist/engine/types.d.ts +8 -0
  53. package/dist/git/worktree/crud.d.ts +69 -0
  54. package/dist/git/worktree/crud.js +206 -0
  55. package/dist/git/worktree/diff.d.ts +14 -0
  56. package/dist/git/worktree/diff.js +82 -0
  57. package/dist/git/worktree/git-exec.d.ts +7 -0
  58. package/dist/git/worktree/git-exec.js +51 -0
  59. package/dist/git/worktree/index.d.ts +5 -0
  60. package/dist/git/worktree/index.js +5 -0
  61. package/dist/git/worktree/query.d.ts +42 -0
  62. package/dist/git/worktree/query.js +121 -0
  63. package/dist/git/worktree/slug.d.ts +11 -0
  64. package/dist/git/worktree/slug.js +58 -0
  65. package/dist/git/worktree.d.ts +1 -84
  66. package/dist/git/worktree.js +5 -230
  67. package/dist/index.d.ts +31 -28
  68. package/dist/index.js +27 -25
  69. package/dist/logging/logger.js +6 -6
  70. package/dist/logging/sanitize-messages.d.ts +10 -2
  71. package/dist/logging/sanitize-messages.js +21 -6
  72. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  73. package/dist/plugins/installer/checkUpdate.js +4 -2
  74. package/dist/plugins/installer/install.js +2 -0
  75. package/dist/plugins/installer/installFromSource.js +9 -1
  76. package/dist/plugins/installer/parseSource.d.ts +4 -1
  77. package/dist/plugins/installer/parseSource.js +28 -10
  78. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  79. package/dist/plugins/installer/sourcePath.js +50 -0
  80. package/dist/plugins/installer/update.d.ts +4 -1
  81. package/dist/plugins/installer/update.js +5 -3
  82. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  83. package/dist/plugins/parseMarketplaceInput.js +4 -3
  84. package/dist/plugins/pluginInstaller.js +24 -22
  85. package/dist/preset/index.d.ts +1 -0
  86. package/dist/preset/index.js +16 -9
  87. package/dist/prompt/sections/base.md +1 -1
  88. package/dist/protocol/chat-session-manager.d.ts +2 -0
  89. package/dist/protocol/chat-session-manager.js +38 -2
  90. package/dist/protocol/chat-session.d.ts +5 -0
  91. package/dist/protocol/chat-session.js +2 -0
  92. package/dist/protocol/client.d.ts +13 -5
  93. package/dist/protocol/client.js +26 -3
  94. package/dist/protocol/server.d.ts +43 -12
  95. package/dist/protocol/server.js +326 -104
  96. package/dist/protocol/types.d.ts +51 -0
  97. package/dist/protocol/types.js +6 -0
  98. package/dist/run/FileRunStore.js +10 -1
  99. package/dist/run/Heartbeat.js +12 -0
  100. package/dist/run/RunApprovalBackend.d.ts +3 -0
  101. package/dist/run/RunApprovalBackend.js +41 -6
  102. package/dist/run/RunLock.js +2 -0
  103. package/dist/run/RunManager.d.ts +2 -0
  104. package/dist/run/RunManager.js +64 -24
  105. package/dist/run/ids.d.ts +2 -0
  106. package/dist/run/ids.js +23 -0
  107. package/dist/runtime/background-shell.d.ts +1 -0
  108. package/dist/runtime/background-shell.js +23 -13
  109. package/dist/runtime/safe-spawn.js +74 -11
  110. package/dist/runtime/spawn-common.js +10 -0
  111. package/dist/services/auto-dream.d.ts +15 -4
  112. package/dist/services/auto-dream.js +20 -20
  113. package/dist/services/dream-consolidation.d.ts +2 -3
  114. package/dist/services/dream-consolidation.js +65 -15
  115. package/dist/services/extract-memories.d.ts +14 -5
  116. package/dist/services/extract-memories.js +20 -3
  117. package/dist/services/global-dream-promotion.d.ts +23 -0
  118. package/dist/services/global-dream-promotion.js +112 -0
  119. package/dist/services/memory-orchestrator.js +347 -34
  120. package/dist/session/memory.d.ts +61 -18
  121. package/dist/session/memory.js +342 -79
  122. package/dist/session/session-manager.d.ts +35 -1
  123. package/dist/session/session-manager.js +197 -4
  124. package/dist/session/transcript.d.ts +5 -1
  125. package/dist/session/transcript.js +38 -5
  126. package/dist/settings/manager.d.ts +1 -0
  127. package/dist/settings/manager.js +87 -37
  128. package/dist/settings/schema-export.d.ts +2 -3
  129. package/dist/settings/schema-export.js +2 -3
  130. package/dist/settings/schema.d.ts +21 -0
  131. package/dist/settings/schema.js +12 -0
  132. package/dist/skills/scanner.d.ts +3 -2
  133. package/dist/skills/scanner.js +12 -9
  134. package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
  135. package/dist/tool-system/builtin/background-jobs.js +12 -1
  136. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  137. package/dist/tool-system/builtin/background-work.js +51 -5
  138. package/dist/tool-system/builtin/config.d.ts +2 -1
  139. package/dist/tool-system/builtin/config.js +16 -11
  140. package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
  141. package/dist/tool-system/builtin/drive-claude-code.js +301 -47
  142. package/dist/tool-system/builtin/edit.js +5 -2
  143. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  144. package/dist/tool-system/builtin/generate-video.js +13 -4
  145. package/dist/tool-system/builtin/index.d.ts +8 -3
  146. package/dist/tool-system/builtin/index.js +37 -21
  147. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  148. package/dist/tool-system/builtin/lsp.js +6 -3
  149. package/dist/tool-system/builtin/memory.js +40 -8
  150. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  151. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  152. package/dist/tool-system/builtin/powershell.js +11 -7
  153. package/dist/tool-system/builtin/read.js +118 -6
  154. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  155. package/dist/tool-system/builtin/view-image.js +109 -19
  156. package/dist/tool-system/builtin/worktree.d.ts +4 -4
  157. package/dist/tool-system/builtin/worktree.js +297 -74
  158. package/dist/tool-system/builtin/write.js +5 -3
  159. package/dist/tool-system/context.d.ts +23 -1
  160. package/dist/tool-system/executor.d.ts +1 -5
  161. package/dist/tool-system/executor.js +94 -115
  162. package/dist/tool-system/mcp-manager.d.ts +18 -5
  163. package/dist/tool-system/mcp-manager.js +157 -74
  164. package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
  165. package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
  166. package/dist/tool-system/path-policy.d.ts +2 -0
  167. package/dist/tool-system/path-policy.js +41 -12
  168. package/dist/tool-system/permission.d.ts +28 -7
  169. package/dist/tool-system/permission.js +130 -49
  170. package/dist/tool-system/registry.js +11 -4
  171. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  172. package/dist/tool-system/tool-result-redaction.js +48 -0
  173. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  174. package/dist/tool-system/workspace-bridge.js +1 -0
  175. package/dist/types.d.ts +86 -5
  176. package/dist/utils/toolDisplay.js +1 -1
  177. package/package.json +4 -3
@@ -149,6 +149,12 @@ export interface DropOversizedResult {
149
149
  droppedCount: number;
150
150
  }
151
151
  export declare function dropOversizedImages(images: readonly ParsedImage[]): DropOversizedResult;
152
+ export interface ImagePolicyByteInput {
153
+ name: string;
154
+ mime: string;
155
+ bytes: number;
156
+ }
157
+ export declare function enforceImageBytePolicy(images: readonly ImagePolicyByteInput[]): ImagePolicyVerdict;
152
158
  export declare function enforceImagePolicy(images: readonly ParsedImage[]): ImagePolicyVerdict;
153
159
  /**
154
160
  * Collect the workspace file paths of any attached images that came from a
@@ -107,15 +107,13 @@ export function dropOversizedImages(images) {
107
107
  if (dropped.length === 0) {
108
108
  return { kept, placeholder: "", droppedCount: 0 };
109
109
  }
110
- const list = dropped
111
- .map((d) => `「${d.name}」(~${fmtMB(d.bytes)})`)
112
- .join("、");
110
+ const list = dropped.map((d) => `「${d.name}」(~${fmtMB(d.bytes)})`).join("、");
113
111
  const placeholder = `[已自动跳过 ${dropped.length} 张超大图片:${list};` +
114
112
  `单图上限 ${fmtMB(IMAGE_LIMITS.maxBytesPerImage)}。` +
115
113
  `图片未进入对话历史,本轮其余内容继续。]`;
116
114
  return { kept, placeholder, droppedCount: dropped.length };
117
115
  }
118
- export function enforceImagePolicy(images) {
116
+ export function enforceImageBytePolicy(images) {
119
117
  if (images.length === 0)
120
118
  return { ok: true };
121
119
  if (images.length > IMAGE_LIMITS.maxImagesPerTurn) {
@@ -126,13 +124,13 @@ export function enforceImagePolicy(images) {
126
124
  `本次有 ${images.length} 张。请删掉一些再发。`,
127
125
  totals: {
128
126
  imageCount: images.length,
129
- totalBytes: images.reduce((s, i) => s + byteLengthFromBase64(i.base64), 0),
127
+ totalBytes: images.reduce((s, i) => s + i.bytes, 0),
130
128
  },
131
129
  };
132
130
  }
133
131
  let totalBytes = 0;
134
132
  for (const img of images) {
135
- const bytes = byteLengthFromBase64(img.base64);
133
+ const bytes = img.bytes;
136
134
  if (bytes > IMAGE_LIMITS.maxBytesPerImage) {
137
135
  return {
138
136
  ok: false,
@@ -163,6 +161,13 @@ export function enforceImagePolicy(images) {
163
161
  }
164
162
  return { ok: true };
165
163
  }
164
+ export function enforceImagePolicy(images) {
165
+ return enforceImageBytePolicy(images.map((img) => ({
166
+ name: img.name,
167
+ mime: img.mime,
168
+ bytes: byteLengthFromBase64(img.base64),
169
+ })));
170
+ }
166
171
  /**
167
172
  * Collect the workspace file paths of any attached images that came from a
168
173
  * real file (the desktop composer's path-attach flow sets ParsedImage.name to
@@ -178,6 +183,12 @@ export function enforceImagePolicy(images) {
178
183
  export function collectAttachedImagePaths(images, resolve, exists) {
179
184
  const out = [];
180
185
  for (const img of images) {
186
+ const path = img.path?.trim();
187
+ if (path) {
188
+ if (exists(resolve(path)))
189
+ out.push(path);
190
+ continue;
191
+ }
181
192
  const name = img.name?.trim();
182
193
  if (!name)
183
194
  continue;
@@ -0,0 +1,13 @@
1
+ import type { InputAttachmentMeta } from "../protocol/types.js";
2
+ import type { ParsedImage } from "./parse-task.js";
3
+ export interface InputAttachmentContext {
4
+ text: string;
5
+ images: ParsedImage[];
6
+ errors: string[];
7
+ hasStructuredImageAttachments: boolean;
8
+ }
9
+ export interface BuildInputAttachmentContextOptions {
10
+ includeImageBytes?: boolean;
11
+ expectedSessionId?: string;
12
+ }
13
+ export declare function buildInputAttachmentContext(attachments: readonly InputAttachmentMeta[] | undefined, cwd: string, options?: BuildInputAttachmentContextOptions): Promise<InputAttachmentContext>;
@@ -0,0 +1,255 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, readFile, realpath, stat } from "node:fs/promises";
3
+ import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { classifyPath } from "../tool-system/path-policy.js";
5
+ import { enforceImageBytePolicy } from "./image-policy.js";
6
+ const IMAGE_MIME_BY_EXT = {
7
+ ".png": "image/png",
8
+ ".jpg": "image/jpeg",
9
+ ".jpeg": "image/jpeg",
10
+ ".gif": "image/gif",
11
+ ".webp": "image/webp",
12
+ };
13
+ const MAX_DIRECTORY_TREE_ENTRIES = 200;
14
+ const MAX_DIRECTORY_TREE_DEPTH = 2;
15
+ export async function buildInputAttachmentContext(attachments, cwd, options = {}) {
16
+ if (!attachments || attachments.length === 0) {
17
+ return { text: "", images: [], errors: [], hasStructuredImageAttachments: false };
18
+ }
19
+ const cwdReal = await realpath(cwd);
20
+ const includeImageBytes = options.includeImageBytes ?? true;
21
+ const expectedSessionId = options.expectedSessionId;
22
+ const textBlocks = [];
23
+ const images = [];
24
+ const errors = [];
25
+ const pendingImages = [];
26
+ let hasStructuredImageAttachments = false;
27
+ if (!expectedSessionId) {
28
+ return {
29
+ text: "",
30
+ images: [],
31
+ errors: ["input attachments require an expected sessionId"],
32
+ hasStructuredImageAttachments: false,
33
+ };
34
+ }
35
+ if (!isSafeSessionPathSegment(expectedSessionId)) {
36
+ return {
37
+ text: "",
38
+ images: [],
39
+ errors: [`expected sessionId "${expectedSessionId}" is not a safe path segment`],
40
+ hasStructuredImageAttachments: false,
41
+ };
42
+ }
43
+ for (const attachment of attachments) {
44
+ if (!attachment || typeof attachment !== "object")
45
+ continue;
46
+ const displayPath = attachment.path || attachment.relPath || attachment.absPath;
47
+ if (!displayPath) {
48
+ errors.push(`attachment ${attachment.id || "(unknown)"} has no path`);
49
+ continue;
50
+ }
51
+ if (attachment.sessionId !== expectedSessionId) {
52
+ errors.push(`attachment ${attachment.id || displayPath} session mismatch: expected ${expectedSessionId}, got ${attachment.sessionId}`);
53
+ continue;
54
+ }
55
+ const resolved = resolveAttachmentPath(attachment, cwd);
56
+ let info;
57
+ try {
58
+ info = await stat(resolved);
59
+ }
60
+ catch (err) {
61
+ errors.push(`attachment ${attachment.id || displayPath} stat failed: ${err.message}`);
62
+ continue;
63
+ }
64
+ const policy = classifyPath(resolved, { workspaceRoot: cwdReal, operation: "read" });
65
+ if (policy.decision !== "allow") {
66
+ errors.push(`attachment ${attachment.id || displayPath} blocked by path policy: ${policy.reason}`);
67
+ continue;
68
+ }
69
+ const realPath = policy.resolvedPath;
70
+ const stagedPathError = await validateStagedAttachmentPath(attachment, realPath, cwdReal, expectedSessionId, displayPath);
71
+ if (stagedPathError) {
72
+ errors.push(stagedPathError);
73
+ continue;
74
+ }
75
+ if (attachment.kind === "directory" || info.isDirectory()) {
76
+ const tree = await directoryTree(realPath, cwdReal).catch((err) => ({
77
+ lines: [`(directory tree unavailable: ${err.message})`],
78
+ truncated: true,
79
+ entryCount: 0,
80
+ }));
81
+ textBlocks.push([
82
+ `<attached-directory path="${escapeText(displayPath)}" entries="${tree.entryCount}" truncated="${tree.truncated}">`,
83
+ ...tree.lines,
84
+ `</attached-directory>`,
85
+ ].join("\n"));
86
+ continue;
87
+ }
88
+ if (!info.isFile()) {
89
+ errors.push(`attachment ${attachment.id || displayPath} is not a regular file or directory`);
90
+ continue;
91
+ }
92
+ const mime = attachment.mime || IMAGE_MIME_BY_EXT[extname(realPath).toLowerCase()];
93
+ if (attachment.kind === "image") {
94
+ hasStructuredImageAttachments = true;
95
+ if (!includeImageBytes || attachment.vision?.include === false) {
96
+ textBlocks.push(formatFileMetadata(attachment, displayPath, realPath, info.size, mime));
97
+ continue;
98
+ }
99
+ if (!mime ||
100
+ !mime.startsWith("image/") ||
101
+ !IMAGE_MIME_BY_EXT[extname(realPath).toLowerCase()]) {
102
+ errors.push(`image attachment ${attachment.id || displayPath} has unsupported image type`);
103
+ continue;
104
+ }
105
+ pendingImages.push({
106
+ attachment,
107
+ displayPath,
108
+ realPath,
109
+ mime,
110
+ size: info.size,
111
+ });
112
+ continue;
113
+ }
114
+ textBlocks.push(formatFileMetadata(attachment, displayPath, realPath, info.size, mime));
115
+ }
116
+ if (errors.length === 0 && includeImageBytes && pendingImages.length > 0) {
117
+ const verdict = enforceImageBytePolicy(pendingImages.map((image) => ({
118
+ name: image.attachment.originalName || image.displayPath,
119
+ mime: image.mime,
120
+ bytes: image.size,
121
+ })));
122
+ if (!verdict.ok) {
123
+ errors.push(`image attachment size policy failed: ${verdict.message}`);
124
+ }
125
+ else {
126
+ for (const image of pendingImages) {
127
+ let bytes;
128
+ try {
129
+ bytes = await readFile(image.realPath);
130
+ }
131
+ catch (err) {
132
+ errors.push(`image attachment ${image.attachment.id || image.displayPath} read failed: ${err.message}`);
133
+ continue;
134
+ }
135
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
136
+ const base64 = bytes.toString("base64");
137
+ images.push({
138
+ mime: image.mime,
139
+ name: image.attachment.originalName || image.displayPath,
140
+ dataUrl: `data:${image.mime};base64,${base64}`,
141
+ base64,
142
+ path: image.displayPath,
143
+ hash: `sha256:${sha256}`,
144
+ size: image.size,
145
+ origin: image.attachment.origin,
146
+ sessionId: image.attachment.sessionId,
147
+ });
148
+ }
149
+ }
150
+ }
151
+ return { text: textBlocks.join("\n\n"), images, errors, hasStructuredImageAttachments };
152
+ }
153
+ function resolveAttachmentPath(attachment, cwd) {
154
+ const candidate = attachment.vision?.mediaPath || attachment.absPath || attachment.relPath || attachment.path;
155
+ if (isAbsolute(candidate))
156
+ return candidate;
157
+ return resolve(cwd, candidate);
158
+ }
159
+ async function validateStagedAttachmentPath(attachment, realPath, cwdReal, expectedSessionId, displayPath) {
160
+ if (!isStagedAttachmentReference(attachment, realPath, cwdReal))
161
+ return undefined;
162
+ const expectedDir = resolve(cwdReal, ".code-shell", "attachments", expectedSessionId);
163
+ let expectedDirReal;
164
+ try {
165
+ expectedDirReal = await realpath(expectedDir);
166
+ }
167
+ catch (err) {
168
+ return `attachment ${attachment.id || displayPath} staged session directory is unavailable: ${err.message}`;
169
+ }
170
+ if (isPathInside(realPath, expectedDirReal))
171
+ return undefined;
172
+ return `attachment ${attachment.id || displayPath} staged path is outside .code-shell/attachments/${expectedSessionId}`;
173
+ }
174
+ function isStagedAttachmentReference(attachment, realPath, cwdReal) {
175
+ const fields = [
176
+ attachment.path,
177
+ attachment.absPath,
178
+ attachment.relPath,
179
+ attachment.vision?.mediaPath,
180
+ ].filter((value) => typeof value === "string" && value.length > 0);
181
+ if (fields.some((value) => looksLikeStagedAttachmentPath(value)))
182
+ return true;
183
+ return isPathInside(realPath, resolve(cwdReal, ".code-shell", "attachments"));
184
+ }
185
+ function looksLikeStagedAttachmentPath(value) {
186
+ const normalized = normalizePath(value);
187
+ return (normalized === ".code-shell/attachments" ||
188
+ normalized.startsWith(".code-shell/attachments/") ||
189
+ normalized.includes("/.code-shell/attachments/"));
190
+ }
191
+ function isPathInside(child, parent) {
192
+ const rel = relative(parent, child);
193
+ return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
194
+ }
195
+ function isSafeSessionPathSegment(sessionId) {
196
+ if (!sessionId)
197
+ return false;
198
+ return (!sessionId.includes("/") && !sessionId.includes("\\") && sessionId !== "." && sessionId !== "..");
199
+ }
200
+ function normalizePath(value) {
201
+ return value.replace(/\\/g, "/");
202
+ }
203
+ function formatFileMetadata(attachment, displayPath, realPath, size, mime) {
204
+ const lines = [
205
+ `<attached-file path="${escapeText(displayPath)}">`,
206
+ `absolutePath: ${realPath}`,
207
+ ...(mime ? [`mime: ${mime}`] : []),
208
+ `size: ${size}`,
209
+ `sha256: ${attachment.sha256}`,
210
+ `origin: ${attachment.origin}`,
211
+ `</attached-file>`,
212
+ ];
213
+ return lines.join("\n");
214
+ }
215
+ async function directoryTree(dir, cwdReal) {
216
+ const lines = [];
217
+ let entryCount = 0;
218
+ let truncated = false;
219
+ async function walk(current, depth) {
220
+ if (entryCount >= MAX_DIRECTORY_TREE_ENTRIES) {
221
+ truncated = true;
222
+ return;
223
+ }
224
+ const entries = await readdir(current, { withFileTypes: true });
225
+ entries.sort((a, b) => a.name.localeCompare(b.name));
226
+ for (const entry of entries) {
227
+ if (entryCount >= MAX_DIRECTORY_TREE_ENTRIES) {
228
+ truncated = true;
229
+ return;
230
+ }
231
+ if (entry.name === ".git" || entry.name === "node_modules")
232
+ continue;
233
+ const abs = join(current, entry.name);
234
+ const real = await realpath(abs).catch(() => null);
235
+ if (!real)
236
+ continue;
237
+ const rel = relative(cwdReal, real);
238
+ if (rel === ".." || rel.startsWith(`..${sep}`))
239
+ continue;
240
+ entryCount += 1;
241
+ lines.push(`${" ".repeat(depth)}${entry.isDirectory() ? "dir " : "file "}${slash(rel)}`);
242
+ if (entry.isDirectory() && depth + 1 < MAX_DIRECTORY_TREE_DEPTH) {
243
+ await walk(real, depth + 1);
244
+ }
245
+ }
246
+ }
247
+ await walk(dir, 0);
248
+ return { lines, truncated, entryCount };
249
+ }
250
+ function escapeText(value) {
251
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
252
+ }
253
+ function slash(value) {
254
+ return sep === "\\" ? value.replace(/\\/g, "/") : value;
255
+ }
@@ -4,6 +4,9 @@
4
4
  import type { LLMClientBase } from "../llm/client-base.js";
5
5
  import type { Message, ToolDefinition, LLMResponse, StreamCallback } from "../types.js";
6
6
  import { Transcript } from "../session/transcript.js";
7
+ export interface ModelCallRecordingOptions {
8
+ sensitiveToolResultRedactions?: ReadonlyMap<string, string>;
9
+ }
7
10
  /**
8
11
  * Prompt-cache hit rate for one request, as CC computes it:
9
12
  * cacheRead / (cacheRead + cacheCreation + uncachedInput)
@@ -18,11 +21,11 @@ export declare class ModelFacade {
18
21
  private readonly client;
19
22
  private readonly transcript;
20
23
  constructor(client: LLMClientBase, transcript: Transcript);
21
- call(systemPrompt: string, messages: Message[], tools: ToolDefinition[], onStream?: StreamCallback, signal?: AbortSignal): Promise<LLMResponse>;
24
+ call(systemPrompt: string, messages: Message[], tools: ToolDefinition[], onStream?: StreamCallback, signal?: AbortSignal, recordingOptions?: ModelCallRecordingOptions): Promise<LLMResponse>;
22
25
  /**
23
26
  * Call without streaming — used as fallback when streaming fails.
24
27
  */
25
- callWithoutStreaming(systemPrompt: string, messages: Message[], tools: ToolDefinition[], signal?: AbortSignal): Promise<LLMResponse>;
28
+ callWithoutStreaming(systemPrompt: string, messages: Message[], tools: ToolDefinition[], signal?: AbortSignal, recordingOptions?: ModelCallRecordingOptions): Promise<LLMResponse>;
26
29
  /** Record API timing and token usage to bootstrap state. */
27
30
  private recordUsage;
28
31
  /** Optional summarize function for tool use summaries. */
@@ -5,6 +5,7 @@ import { logger, getCurrentSid } from "../logging/logger.js";
5
5
  import { recordLLMError, recordLLMRequest, recordLLMResponse, } from "../logging/session-recorder.js";
6
6
  import { sanitizeMessages } from "../logging/sanitize-messages.js";
7
7
  import { addAPIDuration, addToModelUsage, addInputTokens, addOutputTokens } from "../state.js";
8
+ import { cacheHitRateFromUsage } from "./session-usage.js";
8
9
  let _reqSeq = 0;
9
10
  function nextReqId() {
10
11
  _reqSeq += 1;
@@ -20,18 +21,7 @@ function nextReqId() {
20
21
  * any caching tuning (docs/todo/prompt-cache-optimization.md §四 step 1).
21
22
  */
22
23
  export function cacheHitRate(usage) {
23
- if (!usage)
24
- return undefined;
25
- const read = usage.cacheReadTokens ?? 0;
26
- const creation = usage.cacheCreationTokens ?? 0;
27
- if (read === 0 && creation === 0)
28
- return undefined;
29
- const prompt = usage.promptTokens ?? 0;
30
- const uncached = Math.max(0, prompt - read - creation);
31
- const denom = read + creation + uncached;
32
- if (denom === 0)
33
- return undefined;
34
- return read / denom;
24
+ return cacheHitRateFromUsage(usage);
35
25
  }
36
26
  export class ModelFacade {
37
27
  client;
@@ -40,7 +30,7 @@ export class ModelFacade {
40
30
  this.client = client;
41
31
  this.transcript = transcript;
42
32
  }
43
- async call(systemPrompt, messages, tools, onStream, signal) {
33
+ async call(systemPrompt, messages, tools, onStream, signal, recordingOptions) {
44
34
  const startMs = Date.now();
45
35
  const msgCount = messages.length;
46
36
  const sid = getCurrentSid();
@@ -52,7 +42,7 @@ export class ModelFacade {
52
42
  // Image base64 payloads stay out of recorded prompts — see
53
43
  // logging/sanitize-messages.ts. Transcripts keep the full bytes
54
44
  // (needed for replay); logs do not.
55
- messages: sanitizeMessages(messages),
45
+ messages: sanitizeMessages(messages, recordingOptions),
56
46
  tools,
57
47
  systemPrompt,
58
48
  }, reqId);
@@ -121,7 +111,7 @@ export class ModelFacade {
121
111
  /**
122
112
  * Call without streaming — used as fallback when streaming fails.
123
113
  */
124
- async callWithoutStreaming(systemPrompt, messages, tools, signal) {
114
+ async callWithoutStreaming(systemPrompt, messages, tools, signal, recordingOptions) {
125
115
  const startMs = Date.now();
126
116
  const msgCount = messages.length;
127
117
  const sid = getCurrentSid();
@@ -132,7 +122,7 @@ export class ModelFacade {
132
122
  stream: false,
133
123
  // Image base64 payloads stay out of recorded prompts — same rule
134
124
  // as the streaming path above.
135
- messages: sanitizeMessages(messages),
125
+ messages: sanitizeMessages(messages, recordingOptions),
136
126
  tools,
137
127
  systemPrompt,
138
128
  }, reqId);
@@ -29,6 +29,16 @@ export interface ParsedImage {
29
29
  dataUrl: string;
30
30
  /** Just the base64 payload, with the data-URL prefix stripped. */
31
31
  base64: string;
32
+ /** Stable on-disk path, usually cwd-relative for staged desktop attachments. */
33
+ path?: string;
34
+ /** Wire hash attr, usually `sha256:<hex>`. */
35
+ hash?: string;
36
+ /** Size in bytes from wire metadata. Core must still verify with stat when reading. */
37
+ size?: number;
38
+ /** Source channel that produced this attachment. */
39
+ origin?: string;
40
+ /** Session directory that owns the staged attachment. */
41
+ sessionId?: string;
32
42
  }
33
43
  export interface ParsedTask {
34
44
  /** Remaining plain-text portion (image blocks removed, surrounding whitespace trimmed). */
@@ -117,6 +117,11 @@ export function parseTaskWithImages(task) {
117
117
  name: attrs.name ?? "",
118
118
  dataUrl: `data:${finalMime};base64,${base64}`,
119
119
  base64,
120
+ ...(attrs.path ? { path: attrs.path } : {}),
121
+ ...(attrs.hash ? { hash: attrs.hash } : {}),
122
+ ...(attrs.size && Number.isFinite(Number(attrs.size)) ? { size: Number(attrs.size) } : {}),
123
+ ...(attrs.origin ? { origin: attrs.origin } : {}),
124
+ ...(attrs.sessionid ? { sessionId: attrs.sessionid } : {}),
120
125
  });
121
126
  return "";
122
127
  });
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { TurnLoop } from "./turn-loop.js";
11
11
  import { logger } from "../logging/logger.js";
12
+ import { messageHasBase64ImagePayload } from "../context/compaction.js";
12
13
  // ─── Query generator ────────────────────────────────────────────────
13
14
  /**
14
15
  * Run an agentic query and yield stream events as they occur.
@@ -37,6 +38,7 @@ export async function* query(params) {
37
38
  maxToolCallsPerTurn,
38
39
  onStream,
39
40
  signal,
41
+ freshImageMessages: messages.filter(messageHasBase64ImagePayload),
40
42
  };
41
43
  // Local overhead store — query() is a standalone entry point without a real
42
44
  // session id, so per-call in-memory state is enough.
@@ -1,5 +1,17 @@
1
1
  import type { TokenUsage } from "../types.js";
2
2
  import type { LLMUsageTracker } from "../llm/types.js";
3
+ export interface CumulativeUsageCounters {
4
+ cumulativePromptTokens: number;
5
+ cumulativeCacheReadTokens: number;
6
+ cumulativeCacheCreationTokens: number;
7
+ }
8
+ export declare function emptyCumulativeUsageCounters(): CumulativeUsageCounters;
9
+ export declare function normalizeCumulativeUsageCounters(counters: Partial<CumulativeUsageCounters> | undefined, legacyUsage?: TokenUsage): CumulativeUsageCounters;
10
+ export declare function addCumulativeUsage(counters: Partial<CumulativeUsageCounters> | undefined, usage: TokenUsage): CumulativeUsageCounters;
11
+ export declare function addTokenUsage(left: TokenUsage, right: TokenUsage): TokenUsage;
12
+ export declare function cacheHitRateFromTokens(promptTokens: number, cacheReadTokens: number | undefined, cacheCreationTokens: number | undefined): number | undefined;
13
+ export declare function cacheHitRateFromUsage(usage: TokenUsage | undefined): number | undefined;
14
+ export declare function cumulativeCacheHitRate(counters: CumulativeUsageCounters): number | undefined;
3
15
  /**
4
16
  * Fold one run's cumulative usage onto a session baseline, producing the new
5
17
  * session-cumulative TokenUsage.
@@ -1,3 +1,59 @@
1
+ export function emptyCumulativeUsageCounters() {
2
+ return {
3
+ cumulativePromptTokens: 0,
4
+ cumulativeCacheReadTokens: 0,
5
+ cumulativeCacheCreationTokens: 0,
6
+ };
7
+ }
8
+ export function normalizeCumulativeUsageCounters(counters, legacyUsage) {
9
+ return {
10
+ cumulativePromptTokens: typeof counters?.cumulativePromptTokens === "number"
11
+ ? counters.cumulativePromptTokens
12
+ : (legacyUsage?.promptTokens ?? 0),
13
+ cumulativeCacheReadTokens: typeof counters?.cumulativeCacheReadTokens === "number"
14
+ ? counters.cumulativeCacheReadTokens
15
+ : (legacyUsage?.cacheReadTokens ?? 0),
16
+ cumulativeCacheCreationTokens: typeof counters?.cumulativeCacheCreationTokens === "number"
17
+ ? counters.cumulativeCacheCreationTokens
18
+ : (legacyUsage?.cacheCreationTokens ?? 0),
19
+ };
20
+ }
21
+ export function addCumulativeUsage(counters, usage) {
22
+ const current = normalizeCumulativeUsageCounters(counters);
23
+ return {
24
+ cumulativePromptTokens: current.cumulativePromptTokens + (usage.promptTokens ?? 0),
25
+ cumulativeCacheReadTokens: current.cumulativeCacheReadTokens + (usage.cacheReadTokens ?? 0),
26
+ cumulativeCacheCreationTokens: current.cumulativeCacheCreationTokens + (usage.cacheCreationTokens ?? 0),
27
+ };
28
+ }
29
+ export function addTokenUsage(left, right) {
30
+ return {
31
+ promptTokens: left.promptTokens + (right.promptTokens ?? 0),
32
+ completionTokens: left.completionTokens + (right.completionTokens ?? 0),
33
+ totalTokens: left.totalTokens + (right.totalTokens ?? 0),
34
+ cacheReadTokens: (left.cacheReadTokens ?? 0) + (right.cacheReadTokens ?? 0),
35
+ cacheCreationTokens: (left.cacheCreationTokens ?? 0) + (right.cacheCreationTokens ?? 0),
36
+ };
37
+ }
38
+ export function cacheHitRateFromTokens(promptTokens, cacheReadTokens, cacheCreationTokens) {
39
+ const read = cacheReadTokens ?? 0;
40
+ const creation = cacheCreationTokens ?? 0;
41
+ if (read === 0 && creation === 0)
42
+ return undefined;
43
+ const uncached = Math.max(0, promptTokens - read - creation);
44
+ const denom = read + creation + uncached;
45
+ if (denom === 0)
46
+ return undefined;
47
+ return read / denom;
48
+ }
49
+ export function cacheHitRateFromUsage(usage) {
50
+ if (!usage)
51
+ return undefined;
52
+ return cacheHitRateFromTokens(usage.promptTokens ?? 0, usage.cacheReadTokens, usage.cacheCreationTokens);
53
+ }
54
+ export function cumulativeCacheHitRate(counters) {
55
+ return cacheHitRateFromTokens(counters.cumulativePromptTokens, counters.cumulativeCacheReadTokens, counters.cumulativeCacheCreationTokens);
56
+ }
1
57
  /**
2
58
  * Fold one run's cumulative usage onto a session baseline, producing the new
3
59
  * session-cumulative TokenUsage.
@@ -9,9 +9,10 @@
9
9
  export interface SteerItem {
10
10
  id: string;
11
11
  text: string;
12
+ clientMessageId?: string;
12
13
  }
13
14
  /** Append a steer entry. Blank text is dropped (returns the list unchanged). */
14
- export declare function enqueueSteerItem(list: SteerItem[], id: string, text: string): SteerItem[];
15
+ export declare function enqueueSteerItem(list: SteerItem[], id: string, text: string, clientMessageId?: string): SteerItem[];
15
16
  /**
16
17
  * Take everything currently queued and clear the list. Returns the drained
17
18
  * entries (in order) and the now-empty remainder. The turn loop calls this at
@@ -1,9 +1,9 @@
1
1
  /** Append a steer entry. Blank text is dropped (returns the list unchanged). */
2
- export function enqueueSteerItem(list, id, text) {
2
+ export function enqueueSteerItem(list, id, text, clientMessageId) {
3
3
  const t = text?.trim();
4
4
  if (!id || !t)
5
5
  return list;
6
- return [...list, { id, text: t }];
6
+ return [...list, { id, text: t, ...(clientMessageId ? { clientMessageId } : {}) }];
7
7
  }
8
8
  /**
9
9
  * Take everything currently queued and clear the list. Returns the drained
@@ -1,13 +1,16 @@
1
1
  /**
2
- * Streaming tool queue enqueues tools for execution during streaming,
3
- * starts concurrency-safe tools immediately, queues unsafe tools for
4
- * sequential execution.
2
+ * Tool execution queue for a completed model response.
3
+ *
4
+ * Despite the historical name, TurnLoop currently enqueues tool calls only
5
+ * after `callModelWithFallback()` returns a complete LLMResponse. The queue's
6
+ * job is concurrency policy inside that post-response batch: start
7
+ * concurrency-safe tools as soon as they are enqueued, keep unsafe tools
8
+ * sequential, and return results in original tool-call order.
5
9
  *
6
10
  * Usage:
7
11
  * const queue = new StreamingToolQueue(executor);
8
- * // During streaming, as tool_use blocks arrive:
12
+ * // After the full LLMResponse is available:
9
13
  * queue.enqueue(toolCall);
10
- * // After streaming completes:
11
14
  * const results = await queue.drain();
12
15
  */
13
16
  import type { ToolCall, ToolResult } from "../types.js";
@@ -21,8 +24,9 @@ export declare class StreamingToolQueue {
21
24
  private draining;
22
25
  constructor(executor: ToolExecutor);
23
26
  /**
24
- * Enqueue a tool for execution. Concurrency-safe tools start immediately;
25
- * unsafe tools are queued for sequential execution during drain().
27
+ * Enqueue a tool from the completed response. Concurrency-safe tools start
28
+ * immediately within this post-response phase; unsafe tools are queued for
29
+ * sequential execution during drain().
26
30
  */
27
31
  enqueue(call: ToolCall): void;
28
32
  /**
@@ -1,13 +1,16 @@
1
1
  /**
2
- * Streaming tool queue enqueues tools for execution during streaming,
3
- * starts concurrency-safe tools immediately, queues unsafe tools for
4
- * sequential execution.
2
+ * Tool execution queue for a completed model response.
3
+ *
4
+ * Despite the historical name, TurnLoop currently enqueues tool calls only
5
+ * after `callModelWithFallback()` returns a complete LLMResponse. The queue's
6
+ * job is concurrency policy inside that post-response batch: start
7
+ * concurrency-safe tools as soon as they are enqueued, keep unsafe tools
8
+ * sequential, and return results in original tool-call order.
5
9
  *
6
10
  * Usage:
7
11
  * const queue = new StreamingToolQueue(executor);
8
- * // During streaming, as tool_use blocks arrive:
12
+ * // After the full LLMResponse is available:
9
13
  * queue.enqueue(toolCall);
10
- * // After streaming completes:
11
14
  * const results = await queue.drain();
12
15
  */
13
16
  export class StreamingToolQueue {
@@ -21,8 +24,9 @@ export class StreamingToolQueue {
21
24
  this.executor = executor;
22
25
  }
23
26
  /**
24
- * Enqueue a tool for execution. Concurrency-safe tools start immediately;
25
- * unsafe tools are queued for sequential execution during drain().
27
+ * Enqueue a tool from the completed response. Concurrency-safe tools start
28
+ * immediately within this post-response phase; unsafe tools are queued for
29
+ * sequential execution during drain().
26
30
  */
27
31
  enqueue(call) {
28
32
  this.callOrder.push(call.id);