@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
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { initialTurnState, newTurnId } from "./turn-state.js";
8
8
  import { formatFriendlyError } from "./friendly-error.js";
9
+ import { projectGoalJudgeToolResult, } from "../hooks/goal-stop-hook.js";
9
10
  import { wrapHookMessages } from "../hooks/inject.js";
10
11
  import { ContextLimitError } from "../exceptions.js";
11
12
  import { logger } from "../logging/logger.js";
@@ -17,8 +18,9 @@ import { isAbortError } from "../llm/client-base.js";
17
18
  import { crossedReactiveThreshold } from "./reactive-threshold.js";
18
19
  import { COMPLETE_GOAL_TOOL_NAME } from "../tool-system/builtin/complete-goal.js";
19
20
  import { CANCEL_GOAL_TOOL_NAME } from "../tool-system/builtin/cancel-goal.js";
21
+ import { redactSensitiveToolResultsInMessages, toolResultForDisplay, toolResultTranscriptText, toolResultsForDisplay, } from "../tool-system/tool-result-redaction.js";
20
22
  import { addTokenUsage, cacheHitRateFromUsage, cumulativeCacheHitRate, } from "./session-usage.js";
21
- import { createGoalBudgetTracker, recordGoalUsage, goalBudgetExceeded, applyGoalExtension, limitProximity, GOAL_DEFAULT_MAX_STOP_BLOCKS, } from "./goal.js";
23
+ import { createGoalBudgetTracker, recordGoalUsage, goalBudgetTerminationReason, applyGoalExtension, limitProximity, GOAL_DEFAULT_MAX_STOP_BLOCKS, } from "./goal.js";
22
24
  /**
23
25
  * 把一个 ToolResult 映射成发给 LLM 的 tool_result ContentBlock。
24
26
  * 有 contentBlocks(view_image 的图片块)就原样用作 content;否则
@@ -59,7 +61,9 @@ export class TurnLoop {
59
61
  cacheCreationTokens: 0,
60
62
  };
61
63
  currentCumulativeUsage;
64
+ sensitiveToolResultRedactions = new Map();
62
65
  pendingImageMessages = new Set();
66
+ volatileContextMessages = new Set();
63
67
  /**
64
68
  * Consecutive on_stop blocks (Goal mode kept the agent going). Reset to 0
65
69
  * on any unblocked completion. When it reaches config.maxStopBlocks the
@@ -158,6 +162,9 @@ export class TurnLoop {
158
162
  for (const msg of this.config.freshImageMessages ?? []) {
159
163
  this.pendingImageMessages.add(msg);
160
164
  }
165
+ for (const msg of this.config.volatileContextMessages ?? []) {
166
+ this.volatileContextMessages.add(msg);
167
+ }
161
168
  // Wrap onStream so a single throwing handler can't silently break
162
169
  // the channel for the rest of the run. A 2026-05-25 incident saw a
163
170
  // sub-agent's events stop reaching the renderer ~23s into its run —
@@ -222,6 +229,23 @@ export class TurnLoop {
222
229
  }
223
230
  return result.messages;
224
231
  }
232
+ stripVolatileContextMessages(messages) {
233
+ if (this.volatileContextMessages.size === 0)
234
+ return messages;
235
+ let changed = false;
236
+ const stripped = messages.filter((message) => {
237
+ const keep = !this.volatileContextMessages.has(message);
238
+ if (!keep)
239
+ changed = true;
240
+ return keep;
241
+ });
242
+ return changed ? stripped : messages;
243
+ }
244
+ appendVolatileContextMessages(messages) {
245
+ if (this.volatileContextMessages.size === 0)
246
+ return messages;
247
+ return [...this.stripVolatileContextMessages(messages), ...this.volatileContextMessages];
248
+ }
225
249
  markPendingImagesConsumed(messages) {
226
250
  if (this.pendingImageMessages.size === 0)
227
251
  return messages;
@@ -237,6 +261,20 @@ export class TurnLoop {
237
261
  }
238
262
  return result.messages;
239
263
  }
264
+ redactConsumedSensitiveToolResults(messages) {
265
+ if (this.sensitiveToolResultRedactions.size === 0)
266
+ return messages;
267
+ const redacted = redactSensitiveToolResultsInMessages(messages, this.sensitiveToolResultRedactions);
268
+ this.sensitiveToolResultRedactions.clear();
269
+ return redacted;
270
+ }
271
+ modelCallRecordingOptions() {
272
+ if (this.sensitiveToolResultRedactions.size === 0)
273
+ return undefined;
274
+ return {
275
+ sensitiveToolResultRedactions: new Map(this.sensitiveToolResultRedactions),
276
+ };
277
+ }
240
278
  trackFreshImageMessage(message) {
241
279
  if (messageHasBase64ImagePayload(message)) {
242
280
  this.pendingImageMessages.add(message);
@@ -281,11 +319,17 @@ export class TurnLoop {
281
319
  if (ctx === this.lastCtxEmit)
282
320
  return;
283
321
  this.lastCtxEmit = ctx;
284
- this.config.onStream({ type: "usage_update", promptTokens: ctx });
322
+ this.config.onStream({
323
+ type: "usage_update",
324
+ promptTokens: ctx,
325
+ promptTokensSource: overhead > 0 ? "calibrated_estimate" : "heuristic_estimate",
326
+ promptTokensConfidence: overhead > 0 ? "medium" : "low",
327
+ });
285
328
  }
286
329
  recordResponseUsage(usage) {
287
330
  this.currentTurnUsage = addTokenUsage(this.currentTurnUsage, usage);
288
331
  this.currentCumulativeUsage = this.deps.recordCumulativeUsage?.(usage);
332
+ this.deps.recordCacheReadDiagnostics?.(usage);
289
333
  }
290
334
  emitCtxFromUsage(usage, messages) {
291
335
  if (!this.config.onStream)
@@ -320,6 +364,8 @@ export class TurnLoop {
320
364
  this.config.onStream({
321
365
  type: "usage_update",
322
366
  promptTokens,
367
+ promptTokensSource: "provider_usage",
368
+ promptTokensConfidence: "high",
323
369
  ...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),
324
370
  ...(usage.cacheCreationTokens !== undefined
325
371
  ? { cacheCreationTokens: usage.cacheCreationTokens }
@@ -349,9 +395,17 @@ export class TurnLoop {
349
395
  * Run the multi-turn agent loop until completion.
350
396
  */
351
397
  async run(initialMessages) {
398
+ const result = await this.runUnredacted(initialMessages);
399
+ return { ...result, messages: this.redactConsumedSensitiveToolResults(result.messages) };
400
+ }
401
+ async runUnredacted(initialMessages) {
352
402
  let messages = [...initialMessages];
353
403
  let finalText = "";
354
404
  const budgetTracker = createBudgetTracker();
405
+ // Recent execution evidence for the built-in Goal judge. Values enter this
406
+ // window only after irreversible sensitive-data removal, non-text omission,
407
+ // and per-item truncation at the current-round ToolResult boundary.
408
+ const goalToolResults = [];
355
409
  // Goal-mode run-scoped budget tracker (P0). Null when no goal. Stamps a
356
410
  // wall-clock start now and accumulates prompt+completion tokens across
357
411
  // every turn; the guardrail below force-stops the run once any configured
@@ -398,7 +452,7 @@ export class TurnLoop {
398
452
  // they join THIS step's request — no abort, no lost in-flight work. Same
399
453
  // loop-top user-push pattern as turnStartInjection / turn-limit warnings
400
454
  // below. Push to transcript too so they persist + survive resume.
401
- this.consumeQueuedSteer(messages, "normal_step");
455
+ await this.consumeQueuedSteer(messages, "normal_step");
402
456
  const state = initialTurnState(this.turnCount);
403
457
  // Per-turn correlation ID. Every log written through `tlog` (or any
404
458
  // child derived from it) is stamped with `turn` + `turnId`, so
@@ -407,13 +461,18 @@ export class TurnLoop {
407
461
  // returns; instead, each return-causing branch logs its own terminal
408
462
  // event (model_error, completed, etc.).
409
463
  const turnId = newTurnId();
464
+ const assistantMessageId = `assistant_${turnId}`;
410
465
  const tlog = logger.child({ turn: this.turnCount, turnId });
411
466
  this.currentTurnLog = tlog;
412
467
  const turnStartedAt = Date.now();
413
468
  tlog.info("turn.start", { cat: "turn", messageCount: messages.length });
414
469
  // Tag downstream tool-exec / permission lines with this turn's IDs.
415
470
  this.deps.toolExecutor.setLogger(tlog);
416
- this.config.onStream?.({ type: "stream_request_start", turnNumber: this.turnCount });
471
+ this.config.onStream?.({
472
+ type: "stream_request_start",
473
+ turnNumber: this.turnCount,
474
+ messageId: assistantMessageId,
475
+ });
417
476
  const turnStartHook = await this.emitHook("on_turn_start", {
418
477
  turnNumber: this.turnCount,
419
478
  });
@@ -453,16 +512,26 @@ export class TurnLoop {
453
512
  // Pre-check: downgrade image payloads that have already had their one
454
513
  // model-consumption turn, then run context management. Fresh images in
455
514
  // pendingImageMessages are preserved through this next model request.
515
+ const hasPendingSensitiveToolResults = this.sensitiveToolResultRedactions.size > 0;
456
516
  messages = this.prepareMessagesForModel(messages);
457
- // Context management (async — may trigger LLM summarization)
458
- messages = await this.deps.contextManager.manageAsync(messages);
459
- // manageAsync can itself issue an LLM summarization call lasting several
460
- // seconds; if the signal aborted during it, stop here rather than
461
- // proceeding into the (expensive) main model call. Belt to the loop-top
462
- // brace: this catches an abort that landed *inside* context management.
463
- if (this.config.signal?.aborted) {
464
- this.markStopped();
465
- return { text: finalText, reason: "aborted_streaming", messages };
517
+ messages = this.stripVolatileContextMessages(messages);
518
+ if (hasPendingSensitiveToolResults) {
519
+ tlog.info("turn.sensitive_tool_result_context_management_skipped", {
520
+ cat: "turn",
521
+ count: this.sensitiveToolResultRedactions.size,
522
+ });
523
+ }
524
+ else {
525
+ // Context management (async may trigger LLM summarization)
526
+ messages = await this.deps.contextManager.manageAsync(messages);
527
+ // manageAsync can itself issue an LLM summarization call lasting several
528
+ // seconds; if the signal aborted during it, stop here rather than
529
+ // proceeding into the (expensive) main model call. Belt to the loop-top
530
+ // brace: this catches an abort that landed *inside* context management.
531
+ if (this.config.signal?.aborted) {
532
+ this.markStopped();
533
+ return { text: finalText, reason: "aborted_streaming", messages };
534
+ }
466
535
  }
467
536
  // No pre-llm ctx emit here: the messages-only estimate would be ~16k
468
537
  // smaller than the real prompt (system + tools not included), making
@@ -476,7 +545,9 @@ export class TurnLoop {
476
545
  // call. Microcompact is lossless (just clearing redundant
477
546
  // tool_results) so we suppress hook emits for it to keep token
478
547
  // overhead down.
479
- const pending = this.deps.consumePendingCompactInfo?.();
548
+ const pending = hasPendingSensitiveToolResults
549
+ ? null
550
+ : this.deps.consumePendingCompactInfo?.();
480
551
  if (pending && pending.strategy !== "micro") {
481
552
  const compactHook = await this.emitHook("post_compact", {
482
553
  strategy: pending.strategy,
@@ -488,14 +559,16 @@ export class TurnLoop {
488
559
  messages.push(compactInjection);
489
560
  }
490
561
  }
562
+ messages = this.appendVolatileContextMessages(messages);
491
563
  // Model call (with streaming fallback and max_output_tokens continuation)
492
564
  // Track tool IDs streamed during this turn to avoid duplicate UI events
493
565
  this.streamedToolIds.clear();
494
- // Streaming tool queue: start concurrency-safe tools during streaming
566
+ // Tool queue is created before the call, but enqueue happens only after
567
+ // the complete LLMResponse is available below.
495
568
  const streamingQueue = new StreamingToolQueue(this.deps.toolExecutor);
496
569
  let response;
497
570
  try {
498
- response = await this.callModelWithFallback(messages);
571
+ response = await this.callModelWithFallback(messages, assistantMessageId);
499
572
  }
500
573
  catch (err) {
501
574
  if (err instanceof ContextLimitError) {
@@ -506,13 +579,14 @@ export class TurnLoop {
506
579
  tlog.warn("turn.ptl_recovery", { cat: "turn", retry, roundsToDrop: retry });
507
580
  messages = dropOldestRounds(messages, retry);
508
581
  try {
509
- response = await this.callModelWithFallback(messages);
582
+ response = await this.callModelWithFallback(messages, assistantMessageId);
510
583
  recovered = true;
511
584
  break;
512
585
  }
513
586
  catch (retryErr) {
514
587
  if (!(retryErr instanceof ContextLimitError)) {
515
588
  this.config.onStream?.({ type: "error", error: formatFriendlyError(retryErr) });
589
+ messages = this.redactConsumedSensitiveToolResults(messages);
516
590
  return { text: finalText, reason: "model_error", messages };
517
591
  }
518
592
  }
@@ -523,6 +597,7 @@ export class TurnLoop {
523
597
  type: "error",
524
598
  error: "Context limit exceeded after 3 recovery attempts",
525
599
  });
600
+ messages = this.redactConsumedSensitiveToolResults(messages);
526
601
  return { text: finalText, reason: "prompt_too_long", messages };
527
602
  }
528
603
  }
@@ -536,14 +611,17 @@ export class TurnLoop {
536
611
  // interrupted turn folds behind the process-card header on reload).
537
612
  this.patchOrphanedToolUses(messages);
538
613
  this.markStopped();
614
+ messages = this.redactConsumedSensitiveToolResults(messages);
539
615
  return { text: finalText, reason: "aborted_streaming", messages };
540
616
  }
541
617
  else {
542
618
  this.patchOrphanedToolUses(messages);
543
619
  this.config.onStream?.({ type: "error", error: formatFriendlyError(err) });
620
+ messages = this.redactConsumedSensitiveToolResults(messages);
544
621
  return { text: finalText, reason: "model_error", messages };
545
622
  }
546
623
  }
624
+ messages = this.redactConsumedSensitiveToolResults(messages);
547
625
  // Record the response once into current-turn and whole-session counters.
548
626
  if (response.usage?.promptTokens !== undefined) {
549
627
  this.recordResponseUsage(response.usage);
@@ -553,7 +631,10 @@ export class TurnLoop {
553
631
  // compaction decisions use hybrid (actual + delta) estimation rather than
554
632
  // pure heuristics. Without this the manager falls back to char/4 estimates.
555
633
  if (response.usage?.promptTokens !== undefined) {
556
- this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length);
634
+ const contextAnchorMessages = this.stripVolatileContextMessages(messages);
635
+ const anchor = this.deps.contextManager.recordActualUsage(response.usage.promptTokens, contextAnchorMessages.length, contextAnchorMessages);
636
+ if (anchor)
637
+ this.deps.recordContextUsageAnchor?.(anchor);
557
638
  }
558
639
  messages = this.markPendingImagesConsumed(messages);
559
640
  // Truncation that cut off a TOOL CALL: the model overflowed
@@ -641,7 +722,10 @@ export class TurnLoop {
641
722
  // is force-stopped regardless of what the model wants to do next (stop OR
642
723
  // continue with tool calls). This is the unattended-safety backstop, so
643
724
  // it sits BEFORE the "tool calls?" branch — both paths pass the gate.
644
- if (goalTracker && goalBudgetExceeded(goalTracker, Date.now())) {
725
+ const budgetTermination = goalTracker
726
+ ? goalBudgetTerminationReason(goalTracker, Date.now())
727
+ : undefined;
728
+ if (goalTracker && budgetTermination) {
645
729
  tlog.info("turn.goal_budget_exhausted", {
646
730
  cat: "goal",
647
731
  tokensUsed: goalTracker.tokensUsed,
@@ -650,18 +734,25 @@ export class TurnLoop {
650
734
  });
651
735
  this.config.onStream?.({
652
736
  type: "assistant_message",
737
+ messageId: assistantMessageId,
653
738
  message: {
654
739
  role: "assistant",
655
740
  content: "(Goal 预算已耗尽,强制停止。)",
656
741
  },
657
742
  });
658
- return { text: finalText, reason: "goal_budget_exhausted", messages };
743
+ return {
744
+ text: finalText,
745
+ reason: "goal_budget_exhausted",
746
+ messages,
747
+ goalTermination: budgetTermination,
748
+ };
659
749
  }
660
750
  // Post-check: tool calls?
661
751
  if (response.toolCalls.length === 0) {
662
752
  // No tool use — final answer
663
753
  this.config.onStream?.({
664
754
  type: "assistant_message",
755
+ messageId: assistantMessageId,
665
756
  message: { role: "assistant", content: finalText },
666
757
  });
667
758
  await this.emitHook("on_turn_end", {
@@ -669,7 +760,7 @@ export class TurnLoop {
669
760
  hasToolUse: false,
670
761
  });
671
762
  messages.push({ role: "assistant", content: finalText });
672
- if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
763
+ if (await this.consumeQueuedSteer(messages, "finalize_backfill")) {
673
764
  continue;
674
765
  }
675
766
  // on_stop seam: the model wants to stop. Give handlers (Goal mode)
@@ -678,6 +769,23 @@ export class TurnLoop {
678
769
  // we run another turn instead of returning. Bounded by
679
770
  // maxStopBlocks (consecutive) and the outer maxTurns ceiling.
680
771
  const maxStopBlocks = this.config.maxStopBlocks ?? GOAL_DEFAULT_MAX_STOP_BLOCKS;
772
+ if (goalTracker) {
773
+ this.deps.updateGoalJudgeContext?.({
774
+ toolResults: goalToolResults.map((item) => ({
775
+ ...item,
776
+ })),
777
+ progress: {
778
+ turnCount: this.turnCount,
779
+ stopRound: this.stopBlockCount + 1,
780
+ elapsedMs: Math.max(0, Date.now() - goalTracker.startedAtMs),
781
+ tokensUsed: goalTracker.tokensUsed,
782
+ tokenBudget: goalTracker.goal.tokenBudget,
783
+ timeBudgetMs: goalTracker.goal.timeBudgetMs,
784
+ maxTurns: this.config.maxTurns,
785
+ maxStopBlocks,
786
+ },
787
+ });
788
+ }
681
789
  const stopHook = await this.emitHook("on_stop", {
682
790
  goal: this.config.goal,
683
791
  finalText,
@@ -722,6 +830,7 @@ export class TurnLoop {
722
830
  });
723
831
  continue;
724
832
  }
833
+ let goalTermination;
725
834
  if (stopHook.continueSession && this.stopBlockCount >= maxStopBlocks) {
726
835
  // Cap hit: stop anyway, but tell the user why we're not looping
727
836
  // forever on an unsatisfiable goal.
@@ -735,8 +844,10 @@ export class TurnLoop {
735
844
  status: "exhausted",
736
845
  round: this.stopBlockCount,
737
846
  });
847
+ goalTermination = "stop_blocks_exhausted";
738
848
  this.config.onStream?.({
739
849
  type: "assistant_message",
850
+ messageId: assistantMessageId,
740
851
  message: {
741
852
  role: "assistant",
742
853
  content: `(Goal 续跑已达 ${maxStopBlocks} 次上限,先停下。)`,
@@ -753,10 +864,11 @@ export class TurnLoop {
753
864
  });
754
865
  }
755
866
  this.stopBlockCount = 0;
756
- if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
867
+ if (await this.consumeQueuedSteer(messages, "finalize_backfill")) {
757
868
  continue;
758
869
  }
759
- return { text: finalText, reason: "completed", messages };
870
+ messages = this.redactConsumedSensitiveToolResults(messages);
871
+ return { text: finalText, reason: "completed", messages, goalTermination };
760
872
  }
761
873
  // Tool execution phase
762
874
  tlog.info("turn.tool_use", {
@@ -799,9 +911,21 @@ export class TurnLoop {
799
911
  // Record results in transcript and stream
800
912
  const resultBlocks = [];
801
913
  for (const result of results) {
914
+ if (goalTracker) {
915
+ goalToolResults.push(projectGoalJudgeToolResult(result, this.turnCount));
916
+ // The prompt renderer keeps at most 12, but retaining a few extra
917
+ // bounded projections lets its total-char budget choose the newest useful subset.
918
+ if (goalToolResults.length > 20)
919
+ goalToolResults.splice(0, goalToolResults.length - 20);
920
+ }
802
921
  resultBlocks.push(toolResultToBlock(result));
803
- this.deps.transcript.appendToolResult(result.id, result.toolName, result.result, result.error);
804
- this.config.onStream?.({ type: "tool_result", result });
922
+ const streamResult = toolResultForDisplay(result);
923
+ const transcriptResult = toolResultTranscriptText(result);
924
+ if (result.sensitive && transcriptResult !== undefined) {
925
+ this.sensitiveToolResultRedactions.set(result.id, transcriptResult);
926
+ }
927
+ this.deps.transcript.appendToolResult(result.id, result.toolName, transcriptResult, result.error, result.sensitive ? undefined : result.contentBlocks);
928
+ this.config.onStream?.({ type: "tool_result", result: streamResult });
805
929
  }
806
930
  // Fire-and-forget tool use summary (non-blocking). The whole chain is
807
931
  // best-effort observability — a thrown onStream handler, a failed dynamic
@@ -812,9 +936,13 @@ export class TurnLoop {
812
936
  .then(({ generateToolUseSummary }) => {
813
937
  if (!this.deps.model.summarize)
814
938
  return;
815
- return generateToolUseSummary(toolCalls, results, this.deps.model.summarize).then((summary) => {
939
+ return generateToolUseSummary(toolCalls, toolResultsForDisplay(results), this.deps.model.summarize).then((summary) => {
816
940
  if (summary) {
817
- this.config.onStream?.({ type: "tool_summary", summary });
941
+ this.config.onStream?.({
942
+ type: "tool_summary",
943
+ summary,
944
+ toolCallIds: toolCalls.map((toolCall) => toolCall.id),
945
+ });
818
946
  }
819
947
  });
820
948
  })
@@ -866,9 +994,10 @@ export class TurnLoop {
866
994
  tlog.info("turn.goal_self_reported_complete", { cat: "goal" });
867
995
  this.stopBlockCount = 0;
868
996
  this.deps.clearPersistedGoal?.();
869
- if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
997
+ if (await this.consumeQueuedSteer(messages, "finalize_backfill")) {
870
998
  continue;
871
999
  }
1000
+ messages = this.redactConsumedSensitiveToolResults(messages);
872
1001
  return { text: finalText, reason: "completed", messages };
873
1002
  }
874
1003
  // Goal mode: user-initiated cancellation. cancel_goal is the "strong
@@ -881,7 +1010,7 @@ export class TurnLoop {
881
1010
  tlog.info("turn.goal_user_cancelled", { cat: "goal" });
882
1011
  this.stopBlockCount = 0;
883
1012
  this.deps.clearPersistedGoal?.();
884
- if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
1013
+ if (await this.consumeQueuedSteer(messages, "finalize_backfill")) {
885
1014
  continue;
886
1015
  }
887
1016
  return { text: finalText, reason: "completed", messages };
@@ -897,12 +1026,14 @@ export class TurnLoop {
897
1026
  });
898
1027
  this.config.onStream?.({
899
1028
  type: "assistant_message",
1029
+ messageId: assistantMessageId,
900
1030
  message: { role: "assistant", content: finalText },
901
1031
  });
902
1032
  messages.push({ role: "assistant", content: finalText });
903
- if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
1033
+ if (await this.consumeQueuedSteer(messages, "finalize_backfill")) {
904
1034
  continue;
905
1035
  }
1036
+ messages = this.redactConsumedSensitiveToolResults(messages);
906
1037
  return { text: finalText, reason: "completed", messages };
907
1038
  }
908
1039
  if (budgetDecision === "nudge") {
@@ -963,6 +1094,7 @@ export class TurnLoop {
963
1094
  // no error event, so the UI shows only the "你停止了本轮" line.
964
1095
  if (isAbortError(err) || this.config.signal?.aborted) {
965
1096
  this.markStopped();
1097
+ messages = this.redactConsumedSensitiveToolResults(messages);
966
1098
  return { text: finalText, reason: "aborted_streaming", messages };
967
1099
  }
968
1100
  this.currentTurnLog.error("turn.unhandled_error", {
@@ -971,6 +1103,7 @@ export class TurnLoop {
971
1103
  stack: err.stack?.split("\n").slice(0, 4).join("\n"),
972
1104
  });
973
1105
  this.config.onStream?.({ type: "error", error: formatFriendlyError(err) });
1106
+ messages = this.redactConsumedSensitiveToolResults(messages);
974
1107
  return { text: finalText, reason: "model_error", messages };
975
1108
  }
976
1109
  // Max turns reached — do one final summarization call (no tools)
@@ -979,9 +1112,19 @@ export class TurnLoop {
979
1112
  maxTurns: this.config.maxTurns,
980
1113
  turnCount: this.turnCount,
981
1114
  });
982
- this.consumeQueuedSteer(messages, "finalize_backfill");
1115
+ await this.consumeQueuedSteer(messages, "finalize_backfill");
1116
+ const hasPendingSensitiveToolResults = this.sensitiveToolResultRedactions.size > 0;
983
1117
  messages = this.prepareMessagesForModel(messages);
984
- messages = this.deps.contextManager.manage(messages);
1118
+ if (hasPendingSensitiveToolResults) {
1119
+ logger.info("turn.sensitive_tool_result_context_management_skipped", {
1120
+ cat: "turn",
1121
+ count: this.sensitiveToolResultRedactions.size,
1122
+ phase: "max_turns_summary",
1123
+ });
1124
+ }
1125
+ else {
1126
+ messages = this.deps.contextManager.manage(messages);
1127
+ }
985
1128
  messages.push({
986
1129
  role: "user",
987
1130
  content: "<system-reminder>Turn limit reached. Provide a final summary of what you accomplished and what remains to be done. Do NOT call any tools.</system-reminder>",
@@ -989,7 +1132,7 @@ export class TurnLoop {
989
1132
  this.emitCtxFromMessages(messages);
990
1133
  try {
991
1134
  const summaryResponse = await this.deps.model.call(this.deps.systemPrompt, this.prepareMessagesForModel(messages), [], // No tools available for summary turn
992
- this.config.onStream, this.config.signal);
1135
+ this.config.onStream, this.config.signal, this.modelCallRecordingOptions());
993
1136
  messages = this.markPendingImagesConsumed(messages);
994
1137
  if (summaryResponse.text) {
995
1138
  finalText = summaryResponse.text;
@@ -1006,14 +1149,19 @@ export class TurnLoop {
1006
1149
  });
1007
1150
  messages.push({ role: "assistant", content: finalText });
1008
1151
  }
1009
- this.config.onStream?.({ type: "turn_complete", reason: "max_turns" });
1010
- return { text: finalText, reason: "max_turns", messages };
1152
+ messages = this.redactConsumedSensitiveToolResults(messages);
1153
+ return {
1154
+ text: finalText,
1155
+ reason: "max_turns",
1156
+ messages,
1157
+ ...(this.config.goal ? { goalTermination: "max_turns_exhausted" } : {}),
1158
+ };
1011
1159
  }
1012
1160
  /**
1013
1161
  * Call model with streaming fallback.
1014
1162
  * If streaming fails, emit tombstone and retry non-streaming.
1015
1163
  */
1016
- async callModelWithFallback(messages) {
1164
+ async callModelWithFallback(messages, assistantMessageId) {
1017
1165
  // Wrap stream callback to track tool_use_start events and reactive compaction
1018
1166
  let streamingResponseTokens = 0;
1019
1167
  let reactiveBucket = -1;
@@ -1045,7 +1193,7 @@ export class TurnLoop {
1045
1193
  }
1046
1194
  : undefined;
1047
1195
  try {
1048
- return await this.deps.model.call(this.deps.systemPrompt, messages, this.deps.tools, wrappedStream, this.config.signal);
1196
+ return await this.deps.model.call(this.deps.systemPrompt, messages, this.deps.tools, wrappedStream, this.config.signal, this.modelCallRecordingOptions());
1049
1197
  }
1050
1198
  catch (err) {
1051
1199
  // If it's a context or rate limit error, don't fallback — propagate
@@ -1065,23 +1213,36 @@ export class TurnLoop {
1065
1213
  // really just a cancel. The signal is the authoritative cancel source.
1066
1214
  if (isAbortError(err) || this.config.signal?.aborted)
1067
1215
  throw err;
1216
+ // Sensitive tool results are model-facing exactly once. A streaming
1217
+ // fallback would re-send the same pending plaintext in a second request,
1218
+ // so fail the turn and let the unified exit redact returned history.
1219
+ if (this.sensitiveToolResultRedactions.size > 0) {
1220
+ this.config.onStream?.({ type: "tombstone", messageId: assistantMessageId });
1221
+ this.currentTurnLog.warn("turn.streaming_fallback_skipped_sensitive", {
1222
+ cat: "turn",
1223
+ error: err.message,
1224
+ });
1225
+ throw err;
1226
+ }
1068
1227
  // Streaming might have partially emitted — send tombstone to revoke
1069
- this.config.onStream?.({ type: "tombstone", messageId: `turn_${this.turnCount}` });
1228
+ this.config.onStream?.({ type: "tombstone", messageId: assistantMessageId });
1070
1229
  this.currentTurnLog.warn("turn.streaming_fallback", {
1071
1230
  cat: "turn",
1072
1231
  error: err.message,
1073
1232
  });
1074
1233
  // Retry without streaming
1075
- return await this.deps.model.callWithoutStreaming(this.deps.systemPrompt, messages, this.deps.tools, this.config.signal);
1234
+ return await this.deps.model.callWithoutStreaming(this.deps.systemPrompt, messages, this.deps.tools, this.config.signal, this.modelCallRecordingOptions());
1076
1235
  }
1077
1236
  }
1078
1237
  get currentTurn() {
1079
1238
  return this.turnCount;
1080
1239
  }
1081
- consumeQueuedSteer(messages, source) {
1240
+ async consumeQueuedSteer(messages, source) {
1082
1241
  const steered = this.deps.consumeSteer?.(source) ?? [];
1083
1242
  let consumed = false;
1084
- for (const { id, text, clientMessageId } of steered) {
1243
+ for (let index = 0; index < steered.length; index++) {
1244
+ const item = steered[index];
1245
+ const { id, text, clientMessageId } = item;
1085
1246
  if (!text)
1086
1247
  continue;
1087
1248
  if (clientMessageId && this.deps.claimClientMessageId?.(clientMessageId, "steer") === false) {
@@ -1093,9 +1254,33 @@ export class TurnLoop {
1093
1254
  });
1094
1255
  continue;
1095
1256
  }
1257
+ let content;
1258
+ try {
1259
+ content = this.deps.buildSteerUserMessageContent
1260
+ ? await this.deps.buildSteerUserMessageContent(item)
1261
+ : text;
1262
+ }
1263
+ catch (err) {
1264
+ if (clientMessageId)
1265
+ this.deps.releaseClientMessageId?.(clientMessageId);
1266
+ const pendingSuffix = steered.slice(index);
1267
+ this.deps.restoreSteer?.(pendingSuffix);
1268
+ logger.warn("steer.prepare.failed_requeued", {
1269
+ clientMessageId,
1270
+ steerId: id,
1271
+ sessionId: this.deps.sessionId,
1272
+ source,
1273
+ restoredCount: pendingSuffix.length,
1274
+ error: err.message,
1275
+ });
1276
+ break;
1277
+ }
1096
1278
  consumed = true;
1097
- messages.push({ role: "user", content: text });
1098
- this.deps.transcript.appendMessage("user", text, { steerId: id, clientMessageId });
1279
+ this.deps.setOriginClientMessageId?.(clientMessageId);
1280
+ const message = { role: "user", content };
1281
+ messages.push(message);
1282
+ this.trackFreshImageMessage(message);
1283
+ this.deps.transcript.appendMessage("user", content, { steerId: id, clientMessageId });
1099
1284
  this.config.onStream?.({ type: "steer_injected", text, id });
1100
1285
  }
1101
1286
  return consumed;
@@ -75,6 +75,8 @@ export interface EngineConfig {
75
75
  /** Browser automation bridge (browser_* tools). Wired by the host (desktop)
76
76
  * after construction via setBrowserBridge. Undefined → tools degrade. */
77
77
  browserBridge?: import("../tool-system/browser-bridge.js").BrowserBridge;
78
+ /** Host-backed workspace switch bridge (desktop only). */
79
+ workspaceBridge?: import("../tool-system/workspace-bridge.js").WorkspaceBridge;
78
80
  /** Inject a cookie credential into the built-in browser (InjectCredential
79
81
  * tool). Wired by the host after construction via setInjectCredential.
80
82
  * Undefined → the tool degrades with a clear "no browser" error. */
@@ -127,6 +129,12 @@ export interface EngineConfig {
127
129
  * automation shown; tui hidden). Sub-agents override to "subagent".
128
130
  */
129
131
  origin?: SessionOrigin;
132
+ /**
133
+ * Host-specific builtin tool surface. Desktop replaces the legacy core
134
+ * worktree tools with SwitchSessionWorkspace because Electron main owns the
135
+ * authoritative workspace switch service and UI refresh path.
136
+ */
137
+ builtinToolHost?: "desktop";
130
138
  /**
131
139
  * Optional shared EngineRuntime providing pre-constructed shared resources
132
140
  * (modelPool, toolRegistry, etc.). When provided, Engine uses these instead