@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
@@ -21,6 +21,17 @@ export declare class AutoApprovalBackend implements ApprovalBackend {
21
21
  requestApproval(req: ApprovalRequest): Promise<ApprovalResult>;
22
22
  private isSafeOperation;
23
23
  }
24
+ /**
25
+ * Closed-session tombstones are a bounded replay guard. A prompt that started
26
+ * before close is still protected by the state-object identity check in
27
+ * isActiveSessionState(): clearSession deletes the captured state, so the late
28
+ * result cannot write into a new bucket even if an old tombstone has aged out.
29
+ * The tombstone only blocks fresh post-close calls carrying a recently closed
30
+ * sessionId from creating an empty bucket. 4096 is 256x the default
31
+ * ChatSessionManager.maxSessions (16), which covers normal late-result bursts
32
+ * while keeping long-lived processes from retaining every historical id.
33
+ */
34
+ export declare const CLOSED_SESSION_TOMBSTONE_LIMIT = 4096;
24
35
  /**
25
36
  * Interactive approval backend — prompts the user via a callback.
26
37
  *
@@ -34,13 +45,11 @@ export declare class AutoApprovalBackend implements ApprovalBackend {
34
45
  * the next time the user opens the project.
35
46
  */
36
47
  export declare class InteractiveApprovalBackend implements ApprovalBackend {
37
- private sessionAllowRules;
38
- private sessionDenyRules;
48
+ private sessionStateById;
49
+ private closedSessionIds;
39
50
  private promptFn;
40
- private cwd;
41
- private savedProjectRules;
42
- private onProjectRules;
43
- private promptTurn;
51
+ private legacyPromptTurn;
52
+ private legacyContext;
44
53
  setPromptFn(fn: (request: ApprovalRequest) => Promise<ApprovalResult>): void;
45
54
  /**
46
55
  * Has someone installed a real prompt callback? Engine consults this
@@ -59,8 +68,18 @@ export declare class InteractiveApprovalBackend implements ApprovalBackend {
59
68
  * earlier approvals.
60
69
  */
61
70
  setOnProjectRules(fn: (rules: PermissionRule[]) => void): void;
71
+ setSessionContext(sessionId: string, context: {
72
+ cwd: string;
73
+ onProjectRules: (rules: PermissionRule[]) => void;
74
+ }): void;
75
+ openSession(sessionId: string): void;
76
+ clearSession(sessionId: string): void;
77
+ private rememberClosedSession;
78
+ private makeSessionState;
79
+ private getSessionState;
80
+ private isActiveSessionState;
62
81
  requestApproval(req: ApprovalRequest): Promise<ApprovalResult>;
63
- /** Session-rule lookup — operation-scoped (see sessionAllowRules doc). Deny
82
+ /** Session-rule lookup — operation-scoped (see sessionStateById doc). Deny
64
83
  * wins over allow if both somehow match (conservative). Null = no rule. */
65
84
  private checkSessionRules;
66
85
  /** The actual interactive ask + rule recording (runs inside the prompt turn). */
@@ -89,6 +108,8 @@ export declare function pathRuleArgsPattern(absPath: string, scope: "file" | "di
89
108
  */
90
109
  export declare function ruleMatches(rule: PermissionRule, toolName: string, args: Record<string, unknown>): boolean;
91
110
  export declare function getInteractiveApprovalBackend(): InteractiveApprovalBackend;
111
+ export declare function openInteractiveApprovalSession(sessionId: string): void;
112
+ export declare function clearInteractiveApprovalSession(sessionId: string): void;
92
113
  export declare function setInteractiveApprovalFn(fn: (request: ApprovalRequest) => Promise<ApprovalResult>): void;
93
114
  type BashSafetyLevel = "safe-read" | "safe-write" | "unsafe" | "dangerous";
94
115
  /**
@@ -2,7 +2,7 @@
2
2
  * Permission system — classifier + approval backend.
3
3
  */
4
4
  import { resolve as resolvePath, dirname } from "node:path";
5
- import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { logger as rootPermLogger } from "../logging/logger.js";
8
8
  import { READ_ONLY_TOOLS } from "./plan-mode-allowlist.js";
@@ -93,6 +93,17 @@ export class AutoApprovalBackend {
93
93
  return false;
94
94
  }
95
95
  }
96
+ /**
97
+ * Closed-session tombstones are a bounded replay guard. A prompt that started
98
+ * before close is still protected by the state-object identity check in
99
+ * isActiveSessionState(): clearSession deletes the captured state, so the late
100
+ * result cannot write into a new bucket even if an old tombstone has aged out.
101
+ * The tombstone only blocks fresh post-close calls carrying a recently closed
102
+ * sessionId from creating an empty bucket. 4096 is 256x the default
103
+ * ChatSessionManager.maxSessions (16), which covers normal late-result bursts
104
+ * while keeping long-lived processes from retaining every historical id.
105
+ */
106
+ export const CLOSED_SESSION_TOMBSTONE_LIMIT = 4096;
96
107
  /**
97
108
  * Interactive approval backend — prompts the user via a callback.
98
109
  *
@@ -106,29 +117,21 @@ export class AutoApprovalBackend {
106
117
  * the next time the user opens the project.
107
118
  */
108
119
  export class InteractiveApprovalBackend {
109
- // Session-scoped grants, keyed on the OPERATION (tool + narrowed argsPattern
110
- // via buildProjectRule), NOT just the tool name. Approving `git status` for
111
- // the session must not auto-allow `rm -rf /` (both are "Bash"). Allow and
112
- // deny are tracked separately so a one-off deny of `curl evil` never blocks
113
- // an unrelated `git status`. (TODO 5.1 会话级权限缓存.)
114
- sessionAllowRules = [];
115
- sessionDenyRules = [];
120
+ // Session-scoped grants are bucketed by ApprovalRequest.sessionId and then
121
+ // keyed on the OPERATION (tool + narrowed argsPattern via buildProjectRule),
122
+ // NOT just the tool name. Approving `git status` for one session must not
123
+ // auto-allow either `rm -rf /` in that same session or `git ...` in another.
124
+ // Allow and deny are tracked separately so a one-off deny of `curl evil`
125
+ // never blocks an unrelated `git status`.
126
+ sessionStateById = new Map();
127
+ closedSessionIds = new Set();
116
128
  promptFn = null;
117
- cwd = null;
118
- // Project rules saved during this session. Kept in-memory (not re-read from
119
- // settings.local.json) so a stream of approvals all stay applied — the
120
- // previous version passed only the freshly-added rule to the classifier,
121
- // which silently dropped earlier approvals from the live classifier within
122
- // the same session. We accumulate here and hand the full list to the callback.
123
- savedProjectRules = [];
124
- onProjectRules = null;
125
- // Serialize prompts so a burst of parallel tool calls doesn't queue N
126
- // identical cards: while one ask is outstanding, later requests wait here
127
- // and RE-CHECK the session rules when their turn comes — the first
128
- // "本会话一直允许" answer then silently absorbs the queued duplicates.
129
- // (All callers were already serialized at the UI anyway — the renderer
130
- // shows one approval card at a time.)
131
- promptTurn = Promise.resolve();
129
+ legacyPromptTurn = Promise.resolve();
130
+ legacyContext = {
131
+ cwd: null,
132
+ savedProjectRules: [],
133
+ onProjectRules: null,
134
+ };
132
135
  setPromptFn(fn) {
133
136
  this.promptFn = fn;
134
137
  }
@@ -144,7 +147,7 @@ export class InteractiveApprovalBackend {
144
147
  }
145
148
  /** Inject the project root so persistence writes to the right settings file. */
146
149
  setCwd(cwd) {
147
- this.cwd = cwd;
150
+ this.legacyContext.cwd = cwd;
148
151
  }
149
152
  /**
150
153
  * Inject a callback fired when the user approves "for this project". The
@@ -153,11 +156,68 @@ export class InteractiveApprovalBackend {
153
156
  * earlier approvals.
154
157
  */
155
158
  setOnProjectRules(fn) {
156
- this.onProjectRules = fn;
159
+ this.legacyContext.onProjectRules = fn;
160
+ }
161
+ setSessionContext(sessionId, context) {
162
+ const state = this.getSessionState(sessionId, true);
163
+ if (!state)
164
+ return;
165
+ state.cwd = context.cwd;
166
+ state.onProjectRules = context.onProjectRules;
167
+ }
168
+ openSession(sessionId) {
169
+ if (!sessionId)
170
+ return;
171
+ this.closedSessionIds.delete(sessionId);
172
+ }
173
+ clearSession(sessionId) {
174
+ if (!sessionId)
175
+ return;
176
+ this.sessionStateById.delete(sessionId);
177
+ this.rememberClosedSession(sessionId);
178
+ }
179
+ rememberClosedSession(sessionId) {
180
+ this.closedSessionIds.delete(sessionId);
181
+ this.closedSessionIds.add(sessionId);
182
+ while (this.closedSessionIds.size > CLOSED_SESSION_TOMBSTONE_LIMIT) {
183
+ const oldest = this.closedSessionIds.values().next().value;
184
+ if (oldest === undefined)
185
+ break;
186
+ this.closedSessionIds.delete(oldest);
187
+ }
188
+ }
189
+ makeSessionState() {
190
+ return {
191
+ allowRules: [],
192
+ denyRules: [],
193
+ promptTurn: Promise.resolve(),
194
+ cwd: null,
195
+ savedProjectRules: [],
196
+ onProjectRules: null,
197
+ };
198
+ }
199
+ getSessionState(sessionId, create) {
200
+ if (!sessionId)
201
+ return null;
202
+ if (this.closedSessionIds.has(sessionId))
203
+ return null;
204
+ const existing = this.sessionStateById.get(sessionId);
205
+ if (existing || !create)
206
+ return existing ?? null;
207
+ const next = this.makeSessionState();
208
+ this.sessionStateById.set(sessionId, next);
209
+ return next;
210
+ }
211
+ isActiveSessionState(sessionId, state) {
212
+ return (!!sessionId &&
213
+ !!state &&
214
+ !this.closedSessionIds.has(sessionId) &&
215
+ this.sessionStateById.get(sessionId) === state);
157
216
  }
158
217
  async requestApproval(req) {
218
+ const state = this.getSessionState(req.sessionId, true);
159
219
  // Fast path: the operation may already be covered by a session rule.
160
- const cached = this.checkSessionRules(req);
220
+ const cached = state ? this.checkSessionRules(req, state) : null;
161
221
  if (cached)
162
222
  return cached;
163
223
  if (!this.promptFn) {
@@ -167,51 +227,68 @@ export class InteractiveApprovalBackend {
167
227
  // field doc): a burst of parallel tool calls all passes the fast path
168
228
  // before the first decision lands; without the re-check the user had to
169
229
  // answer one card per duplicate.
170
- const prevTurn = this.promptTurn;
230
+ const prevTurn = state ? state.promptTurn : this.legacyPromptTurn;
171
231
  let release;
172
- this.promptTurn = new Promise((r) => (release = r));
232
+ const currentTurn = new Promise((r) => (release = r));
233
+ if (state) {
234
+ state.promptTurn = currentTurn;
235
+ }
236
+ else {
237
+ this.legacyPromptTurn = currentTurn;
238
+ }
173
239
  try {
174
240
  await prevTurn;
175
- const nowCached = this.checkSessionRules(req);
241
+ const activeState = this.isActiveSessionState(req.sessionId, state) ? state : null;
242
+ const nowCached = activeState ? this.checkSessionRules(req, activeState) : null;
176
243
  if (nowCached)
177
244
  return nowCached;
178
- return await this.promptAndRecord(req);
245
+ return await this.promptAndRecord(req, activeState);
179
246
  }
180
247
  finally {
181
248
  release();
182
249
  }
183
250
  }
184
- /** Session-rule lookup — operation-scoped (see sessionAllowRules doc). Deny
251
+ /** Session-rule lookup — operation-scoped (see sessionStateById doc). Deny
185
252
  * wins over allow if both somehow match (conservative). Null = no rule. */
186
- checkSessionRules(req) {
187
- if (this.sessionDenyRules.some((r) => ruleMatches(r, req.toolName, req.args))) {
253
+ checkSessionRules(req, state) {
254
+ if (state.denyRules.some((r) => ruleMatches(r, req.toolName, req.args))) {
188
255
  return { approved: false };
189
256
  }
190
- if (this.sessionAllowRules.some((r) => ruleMatches(r, req.toolName, req.args))) {
257
+ if (state.allowRules.some((r) => ruleMatches(r, req.toolName, req.args))) {
191
258
  return { approved: true };
192
259
  }
193
260
  return null;
194
261
  }
195
262
  /** The actual interactive ask + rule recording (runs inside the prompt turn). */
196
- async promptAndRecord(req) {
263
+ async promptAndRecord(req, state) {
197
264
  if (!this.promptFn) {
198
265
  return { approved: false, reason: "interactive approval backend has no prompt function" };
199
266
  }
200
267
  const result = await this.promptFn(req);
201
268
  const scope = result.scope ?? (result.always ? "session" : "once");
269
+ const activeState = this.isActiveSessionState(req.sessionId, state) ? state : null;
270
+ const context = activeState ?? (!req.sessionId ? this.legacyContext : null);
202
271
  // Path narrowing only rides on an APPROVE: a path-scoped deny is confusing
203
272
  // (deny stays tool-wide). pathScope is ignored by buildProjectRule for
204
273
  // non-file tools / when absent.
205
274
  const ruleOpts = result.approved
206
- ? { pathScope: result.pathScope, cwd: this.cwd ?? undefined }
275
+ ? { pathScope: result.pathScope, cwd: context?.cwd ?? undefined }
207
276
  : undefined;
208
277
  if (scope === "session" && result.always) {
278
+ if (!activeState) {
279
+ rootPermLogger.warn("permission.session_remember_ignored", {
280
+ cat: "permission",
281
+ tool: req.toolName,
282
+ reason: req.sessionId ? "session_closed" : "missing_session_id",
283
+ });
284
+ return result;
285
+ }
209
286
  // Narrow to the operation (Bash → head command, file tools → path scope)
210
287
  // so the session grant is scoped, not tool-wide. A rule with no
211
288
  // argsPattern keeps the prior tool-granularity behavior.
212
289
  const rule = buildProjectRule(req.toolName, req.args, ruleOpts);
213
290
  if (rule) {
214
- const target = result.approved ? this.sessionAllowRules : this.sessionDenyRules;
291
+ const target = result.approved ? activeState.allowRules : activeState.denyRules;
215
292
  const dup = target.some((r) => r.tool === rule.tool &&
216
293
  JSON.stringify(r.argsPattern) === JSON.stringify(rule.argsPattern));
217
294
  if (!dup) {
@@ -225,23 +302,23 @@ export class InteractiveApprovalBackend {
225
302
  // We only persist allow rules — denies stay session-only because a
226
303
  // persisted deny is harder to recover from than a session deny.
227
304
  const rule = buildProjectRule(req.toolName, req.args, ruleOpts);
228
- if (rule && this.cwd) {
305
+ if (rule && context?.cwd) {
229
306
  try {
230
- persistProjectRule(this.cwd, rule);
307
+ persistProjectRule(context.cwd, rule);
231
308
  // Dedup against the in-memory list using the same equality used by
232
309
  // persistProjectRule so re-prompts on the same tool/argsPattern
233
310
  // don't bloat the live rule set.
234
- const dup = this.savedProjectRules.some((r) => r.tool === rule.tool &&
311
+ const dup = context.savedProjectRules.some((r) => r.tool === rule.tool &&
235
312
  r.decision === rule.decision &&
236
313
  JSON.stringify(r.argsPattern) === JSON.stringify(rule.argsPattern));
237
314
  if (!dup)
238
- this.savedProjectRules.push(rule);
239
- this.onProjectRules?.(this.savedProjectRules);
315
+ context.savedProjectRules.push(rule);
316
+ context.onProjectRules?.(context.savedProjectRules);
240
317
  rootPermLogger.info("permission.persist", {
241
318
  cat: "permission",
242
319
  tool: rule.tool,
243
320
  decision: rule.decision,
244
- totalProjectRules: this.savedProjectRules.length,
321
+ totalProjectRules: context.savedProjectRules.length,
245
322
  duplicate: dup,
246
323
  });
247
324
  }
@@ -258,11 +335,11 @@ export class InteractiveApprovalBackend {
258
335
  // Also seed the session allow list (operation-scoped) so the rest of
259
336
  // this REPL session benefits even if the classifier path doesn't pick
260
337
  // the rule up immediately.
261
- if (rule) {
262
- const dup = this.sessionAllowRules.some((r) => r.tool === rule.tool &&
338
+ if (rule && activeState) {
339
+ const dup = activeState.allowRules.some((r) => r.tool === rule.tool &&
263
340
  JSON.stringify(r.argsPattern) === JSON.stringify(rule.argsPattern));
264
341
  if (!dup)
265
- this.sessionAllowRules.push(rule);
342
+ activeState.allowRules.push(rule);
266
343
  }
267
344
  }
268
345
  return result;
@@ -452,6 +529,12 @@ export function getInteractiveApprovalBackend() {
452
529
  }
453
530
  return _interactiveBackend;
454
531
  }
532
+ export function openInteractiveApprovalSession(sessionId) {
533
+ getInteractiveApprovalBackend().openSession(sessionId);
534
+ }
535
+ export function clearInteractiveApprovalSession(sessionId) {
536
+ getInteractiveApprovalBackend().clearSession(sessionId);
537
+ }
455
538
  export function setInteractiveApprovalFn(fn) {
456
539
  getInteractiveApprovalBackend().setPromptFn(fn);
457
540
  }
@@ -896,9 +979,7 @@ export class PermissionClassifier {
896
979
  let result;
897
980
  try {
898
981
  const baseDescription = this.describeToolCall(toolName, args);
899
- const description = reason
900
- ? `${baseDescription}\n\nReason (from pre_tool_use hook): ${reason}`
901
- : baseDescription;
982
+ const description = reason ? `${baseDescription}\n\nReason:\n${reason}` : baseDescription;
902
983
  result = await this.approvalBackend.requestApproval({
903
984
  ...(opts?.sessionId ? { sessionId: opts.sessionId } : {}),
904
985
  toolName,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Unified tool registry — built-in tools + MCP tools.
3
3
  */
4
- import { ConfigError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError } from "../exceptions.js";
4
+ import { ConfigError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, } from "../exceptions.js";
5
5
  import { BUILTIN_TOOLS } from "./builtin/index.js";
6
6
  import { validateToolMetadata } from "./validate-tool-metadata.js";
7
7
  /**
@@ -22,7 +22,9 @@ export class ToolRegistry {
22
22
  if (selectedNames) {
23
23
  const unknown = [...selectedNames].filter((name) => !availableNames.has(name));
24
24
  if (unknown.length > 0) {
25
- throw new ConfigError(`Unknown built-in tool(s): ${unknown.join(", ")}`, { unknownBuiltinTools: unknown });
25
+ throw new ConfigError(`Unknown built-in tool(s): ${unknown.join(", ")}`, {
26
+ unknownBuiltinTools: unknown,
27
+ });
26
28
  }
27
29
  }
28
30
  for (const tool of BUILTIN_TOOLS) {
@@ -120,12 +122,18 @@ export class ToolRegistry {
120
122
  // 各取所有,缺的字段为 undefined。
121
123
  const contentBlocks = "contentBlocks" in result ? result.contentBlocks : undefined;
122
124
  const sandbox = "sandbox" in result ? result.sandbox : undefined;
125
+ const sensitive = "sensitive" in result ? result.sensitive : undefined;
126
+ const displayResult = "displayResult" in result ? result.displayResult : undefined;
127
+ const transcriptResult = "transcriptResult" in result ? result.transcriptResult : undefined;
123
128
  return {
124
129
  id,
125
130
  toolName: name,
126
131
  result: result.result ?? (contentBlocks ? "(image)" : ""),
127
132
  contentBlocks,
128
133
  sandbox,
134
+ sensitive,
135
+ displayResult,
136
+ transcriptResult,
129
137
  };
130
138
  }
131
139
  catch (err) {
@@ -133,8 +141,7 @@ export class ToolRegistry {
133
141
  parentSignal?.removeEventListener("abort", onParentAbort);
134
142
  // Always return error as ToolResult, never throw
135
143
  let errorMsg;
136
- const isAbort = err?.name === "AbortError" ||
137
- parentSignal?.aborted === true;
144
+ const isAbort = err?.name === "AbortError" || parentSignal?.aborted === true;
138
145
  if (err instanceof ToolTimeoutError) {
139
146
  errorMsg = `Tool timed out after ${timeout}ms: ${name}`;
140
147
  }
@@ -0,0 +1,7 @@
1
+ import type { Message, ToolResult } from "../types.js";
2
+ export declare const SENSITIVE_TOOL_RESULT_PLACEHOLDER = "[credential value withheld]";
3
+ export declare function toolResultDisplayText(result: ToolResult): string | undefined;
4
+ export declare function toolResultTranscriptText(result: ToolResult): string | undefined;
5
+ export declare function toolResultForDisplay(result: ToolResult): ToolResult;
6
+ export declare function toolResultsForDisplay(results: ToolResult[]): ToolResult[];
7
+ export declare function redactSensitiveToolResultsInMessages(messages: Message[], redactions: ReadonlyMap<string, string>): Message[];
@@ -0,0 +1,48 @@
1
+ export const SENSITIVE_TOOL_RESULT_PLACEHOLDER = "[credential value withheld]";
2
+ export function toolResultDisplayText(result) {
3
+ if (!result.sensitive)
4
+ return result.result;
5
+ return result.displayResult ?? result.transcriptResult ?? SENSITIVE_TOOL_RESULT_PLACEHOLDER;
6
+ }
7
+ export function toolResultTranscriptText(result) {
8
+ if (!result.sensitive)
9
+ return result.result;
10
+ return result.transcriptResult ?? result.displayResult ?? SENSITIVE_TOOL_RESULT_PLACEHOLDER;
11
+ }
12
+ export function toolResultForDisplay(result) {
13
+ if (!result.sensitive)
14
+ return result;
15
+ const { displayResult: _displayResult, transcriptResult: _transcriptResult, ...rest } = result;
16
+ return {
17
+ ...rest,
18
+ result: toolResultDisplayText(result),
19
+ contentBlocks: undefined,
20
+ };
21
+ }
22
+ export function toolResultsForDisplay(results) {
23
+ return results.map((result) => toolResultForDisplay(result));
24
+ }
25
+ export function redactSensitiveToolResultsInMessages(messages, redactions) {
26
+ if (redactions.size === 0)
27
+ return messages;
28
+ let changed = false;
29
+ const out = messages.map((message) => {
30
+ if (!Array.isArray(message.content))
31
+ return message;
32
+ let contentChanged = false;
33
+ const content = message.content.map((block) => {
34
+ if (block.type !== "tool_result")
35
+ return block;
36
+ const replacement = typeof block.tool_use_id === "string" ? redactions.get(block.tool_use_id) : undefined;
37
+ if (replacement === undefined)
38
+ return block;
39
+ contentChanged = true;
40
+ return { ...block, content: replacement };
41
+ });
42
+ if (!contentChanged)
43
+ return message;
44
+ changed = true;
45
+ return { ...message, content };
46
+ });
47
+ return changed ? out : messages;
48
+ }
@@ -0,0 +1,11 @@
1
+ import type { SessionWorkspace } from "../types.js";
2
+ /**
3
+ * WorkspaceBridge — host-backed session workspace switching.
4
+ *
5
+ * Desktop implements this through Electron main so model-initiated switches use
6
+ * the same service path as the UI workspace switcher. Undefined in headless or
7
+ * non-desktop contexts.
8
+ */
9
+ export interface WorkspaceBridge {
10
+ switch(target: string): Promise<SessionWorkspace>;
11
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/types.d.ts CHANGED
@@ -43,7 +43,15 @@ export interface ToolCall {
43
43
  export interface ToolResult {
44
44
  id: string;
45
45
  toolName: string;
46
+ /**
47
+ * Sensitive tool results keep `result` as the model-facing value for the
48
+ * current model round only. Persisted/displayed/streamed observers must use
49
+ * `transcriptResult`/`displayResult` (or the standard placeholder) instead.
50
+ */
51
+ sensitive?: boolean;
46
52
  result?: string;
53
+ displayResult?: string;
54
+ transcriptResult?: string;
47
55
  /**
48
56
  * 结构化结果块(目前仅图片)。存在时优先于 `result` 用作发给 LLM 的
49
57
  * tool_result content —— view_image 用它把本地图片以 image ContentBlock
@@ -54,13 +62,13 @@ export interface ToolResult {
54
62
  error?: string;
55
63
  isError?: boolean;
56
64
  /**
57
- * Set by tools that execute under the sandbox (Bash / background shell /
58
- * worktree) so the UI can show whether THIS call was isolated. `backend`
65
+ * Set by command-executing tools with sandbox visibility (Bash / background
66
+ * shell / worktree / PowerShell) so the UI can show whether THIS call was isolated. `backend`
59
67
  * "off" means the command ran un-sandboxed (we surface it explicitly so the
60
68
  * user sees "未隔离" rather than guessing from an absent badge); "seatbelt"
61
69
  * / "bwrap" mean OS-level isolation applied. `network` is the policy that
62
- * was in force (absent when off). Tools that don't touch the sandbox leave
63
- * this undefined and the UI renders no badge.
70
+ * was in force (absent when off). Tools with no sandbox visibility leave this
71
+ * undefined and the UI renders no badge.
64
72
  */
65
73
  sandbox?: {
66
74
  backend: "off" | "seatbelt" | "bwrap";
@@ -97,6 +105,12 @@ export interface RegisteredTool {
97
105
  inputSchema: Record<string, unknown>;
98
106
  source: ToolSource;
99
107
  serverName?: string;
108
+ /**
109
+ * Declarative UI/metadata hint for hosts, docs, and capability listings.
110
+ * It is not an execution-policy input: PermissionClassifier does not read
111
+ * RegisteredTool and runtime decisions come from explicit rules, permission
112
+ * mode, approval backend, and tool-specific gates.
113
+ */
100
114
  permissionDefault: PermissionDecision;
101
115
  isConcurrencySafe?: boolean;
102
116
  isReadOnly?: boolean;
@@ -139,13 +153,51 @@ export type SessionStatus = "active" | "paused" | TerminalReason;
139
153
  /** Which host/context created a session — used by the desktop disk-rebuild to
140
154
  * filter the sidebar (only `desktop` + `automation` are shown). */
141
155
  export type SessionOrigin = "desktop" | "tui" | "automation" | "subagent";
156
+ export interface SessionWorkspace {
157
+ root: string;
158
+ kind: "main" | "worktree";
159
+ worktree?: {
160
+ path: string;
161
+ branch: string;
162
+ baseRef: string;
163
+ createdBy: "codeshell";
164
+ };
165
+ }
166
+ export interface ContextUsageAnchor {
167
+ promptTokens: number;
168
+ messageCount: number;
169
+ estimateAtAnchor?: number;
170
+ recordedAt: number;
171
+ provider?: string;
172
+ model?: string;
173
+ }
142
174
  export interface SessionState {
143
175
  sessionId: string;
144
176
  cwd: string;
177
+ /** Current session workspace pointer. Absent only on legacy state.json files. */
178
+ workspace?: SessionWorkspace;
145
179
  startedAt: number;
146
180
  model: string;
147
181
  provider: string;
182
+ /**
183
+ * Legacy/model-scoped usage summary. Some flows reset this accounting window
184
+ * (for example model switches), so it must not drive whole-session metrics.
185
+ */
148
186
  tokenUsage: TokenUsage;
187
+ /**
188
+ * Last provider-reported prompt-token count for the current context window.
189
+ * Separate from cumulative tokenUsage; used only to seed context-size estimates
190
+ * before the next provider response.
191
+ */
192
+ contextUsageAnchor?: ContextUsageAnchor;
193
+ /**
194
+ * Monotonic prompt-cache counters for the whole session. These only increase
195
+ * from session start and are separate from the resettable tokenUsage window.
196
+ * Optional on legacy state.json files; SessionManager normalizes them on load.
197
+ */
198
+ cumulativePromptTokens?: number;
199
+ cumulativeCacheReadTokens?: number;
200
+ cumulativeCacheCreationTokens?: number;
149
201
  turnCount: number;
150
202
  /**
151
203
  * Conversation-turn counter where ONE user message = one turn (incremented in
@@ -281,10 +333,14 @@ export interface TaskInfo {
281
333
  activeForm?: string;
282
334
  status: "pending" | "in_progress" | "completed" | "stopped";
283
335
  }
336
+ export type PromptTokenSource = "provider_usage" | "anchor_delta" | "anchor_rescale" | "calibrated_estimate" | "heuristic_estimate" | "session_cumulative";
337
+ export type PromptTokenConfidence = "high" | "medium" | "low";
284
338
  export type StreamEvent = {
285
339
  type: "session_started";
286
340
  sessionId: string;
287
341
  promptTokens: number;
342
+ promptTokensSource?: PromptTokenSource;
343
+ promptTokensConfidence?: PromptTokenConfidence;
288
344
  } | {
289
345
  type: "session_title";
290
346
  sessionId: string;
@@ -292,6 +348,7 @@ export type StreamEvent = {
292
348
  } | {
293
349
  type: "stream_request_start";
294
350
  turnNumber: number;
351
+ messageId?: string;
295
352
  agentId?: string;
296
353
  } | {
297
354
  type: "steer_injected";
@@ -318,6 +375,7 @@ export type StreamEvent = {
318
375
  } | {
319
376
  type: "assistant_message";
320
377
  message: Message;
378
+ messageId?: string;
321
379
  agentId?: string;
322
380
  } | {
323
381
  type: "turn_complete";
@@ -348,6 +406,7 @@ export type StreamEvent = {
348
406
  } | {
349
407
  type: "tombstone";
350
408
  messageId: string;
409
+ agentId?: string;
351
410
  } | {
352
411
  type: "task_update";
353
412
  tasks: TaskInfo[];
@@ -389,15 +448,19 @@ export type StreamEvent = {
389
448
  } | {
390
449
  type: "tool_summary";
391
450
  summary: string;
451
+ toolCallIds?: string[];
452
+ agentId?: string;
392
453
  } | {
393
454
  type: "context_compact";
394
- strategy: "micro" | "summary" | "window" | "snip" | "emergency";
455
+ strategy: "micro" | "summary" | "window" | "snip" | "emergency" | "compacted";
395
456
  before: number;
396
457
  after: number;
397
458
  agentId?: string;
398
459
  } | {
399
460
  type: "usage_update";
400
461
  promptTokens: number;
462
+ promptTokensSource?: PromptTokenSource;
463
+ promptTokensConfidence?: PromptTokenConfidence;
401
464
  /**
402
465
  * Provider-reported prompt-cache counts, forwarded so the UI can show a
403
466
  * cache hit rate in the context-ring tooltip. Present only when the LLM
@@ -407,6 +470,24 @@ export type StreamEvent = {
407
470
  */
408
471
  cacheReadTokens?: number;
409
472
  cacheCreationTokens?: number;
473
+ /**
474
+ * Prompt-cache metrics for the current turn only. These are reset at the
475
+ * start of each turn-loop iteration and sum every LLM response in that
476
+ * turn, including max-token continuations.
477
+ */
478
+ singleTurnPromptTokens?: number;
479
+ singleTurnCacheReadTokens?: number;
480
+ singleTurnCacheCreationTokens?: number;
481
+ singleTurnCacheHitRate?: number;
482
+ /**
483
+ * Whole-session monotonic prompt-cache counters/rate. Unlike the legacy
484
+ * session* fields below, these are not derived from the resettable
485
+ * tokenUsage accounting window.
486
+ */
487
+ cumulativePromptTokens?: number;
488
+ cumulativeCacheReadTokens?: number;
489
+ cumulativeCacheCreationTokens?: number;
490
+ cumulativeCacheHitRate?: number;
410
491
  /**
411
492
  * SESSION-CUMULATIVE cache counts (sum across every LLM response this
412
493
  * session, across runs and turns), emitted from the engine's turn