@cjhyy/code-shell-core 0.6.0-rc.9 → 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 (168) 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 +8 -1
  20. package/dist/context/compaction.js +49 -4
  21. package/dist/context/manager.d.ts +16 -2
  22. package/dist/context/manager.js +103 -19
  23. package/dist/credentials/access.d.ts +56 -0
  24. package/dist/credentials/access.js +183 -0
  25. package/dist/credentials/index.d.ts +1 -0
  26. package/dist/credentials/index.js +1 -0
  27. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  28. package/dist/credentials/inject-credential-tool.js +30 -11
  29. package/dist/credentials/types.d.ts +2 -2
  30. package/dist/credentials/use-credential-tool.d.ts +9 -1
  31. package/dist/credentials/use-credential-tool.js +58 -45
  32. package/dist/engine/engine.d.ts +17 -1
  33. package/dist/engine/engine.js +247 -89
  34. package/dist/engine/image-policy.d.ts +6 -0
  35. package/dist/engine/image-policy.js +17 -6
  36. package/dist/engine/input-attachments.d.ts +13 -0
  37. package/dist/engine/input-attachments.js +255 -0
  38. package/dist/engine/model-facade.d.ts +5 -2
  39. package/dist/engine/model-facade.js +4 -4
  40. package/dist/engine/parse-task.d.ts +10 -0
  41. package/dist/engine/parse-task.js +5 -0
  42. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  43. package/dist/engine/streaming-tool-queue.js +11 -7
  44. package/dist/engine/turn-loop.d.ts +7 -1
  45. package/dist/engine/turn-loop.js +117 -27
  46. package/dist/engine/types.d.ts +8 -0
  47. package/dist/git/worktree/crud.d.ts +69 -0
  48. package/dist/git/worktree/crud.js +206 -0
  49. package/dist/git/worktree/diff.d.ts +14 -0
  50. package/dist/git/worktree/diff.js +82 -0
  51. package/dist/git/worktree/git-exec.d.ts +7 -0
  52. package/dist/git/worktree/git-exec.js +51 -0
  53. package/dist/git/worktree/index.d.ts +5 -0
  54. package/dist/git/worktree/index.js +5 -0
  55. package/dist/git/worktree/query.d.ts +42 -0
  56. package/dist/git/worktree/query.js +121 -0
  57. package/dist/git/worktree/slug.d.ts +11 -0
  58. package/dist/git/worktree/slug.js +58 -0
  59. package/dist/git/worktree.d.ts +1 -84
  60. package/dist/git/worktree.js +5 -230
  61. package/dist/index.d.ts +31 -28
  62. package/dist/index.js +27 -25
  63. package/dist/logging/logger.js +6 -6
  64. package/dist/logging/sanitize-messages.d.ts +10 -2
  65. package/dist/logging/sanitize-messages.js +21 -6
  66. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  67. package/dist/plugins/installer/checkUpdate.js +4 -2
  68. package/dist/plugins/installer/install.js +2 -0
  69. package/dist/plugins/installer/installFromSource.js +9 -1
  70. package/dist/plugins/installer/parseSource.d.ts +4 -1
  71. package/dist/plugins/installer/parseSource.js +28 -10
  72. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  73. package/dist/plugins/installer/sourcePath.js +50 -0
  74. package/dist/plugins/installer/update.d.ts +4 -1
  75. package/dist/plugins/installer/update.js +5 -3
  76. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  77. package/dist/plugins/parseMarketplaceInput.js +4 -3
  78. package/dist/plugins/pluginInstaller.js +24 -22
  79. package/dist/preset/index.d.ts +1 -0
  80. package/dist/preset/index.js +16 -9
  81. package/dist/prompt/sections/base.md +1 -1
  82. package/dist/protocol/chat-session-manager.d.ts +2 -0
  83. package/dist/protocol/chat-session-manager.js +38 -2
  84. package/dist/protocol/chat-session.d.ts +3 -0
  85. package/dist/protocol/chat-session.js +1 -0
  86. package/dist/protocol/client.d.ts +9 -4
  87. package/dist/protocol/client.js +18 -1
  88. package/dist/protocol/server.d.ts +30 -0
  89. package/dist/protocol/server.js +246 -46
  90. package/dist/protocol/types.d.ts +47 -0
  91. package/dist/protocol/types.js +6 -0
  92. package/dist/run/FileRunStore.js +10 -1
  93. package/dist/run/Heartbeat.js +12 -0
  94. package/dist/run/RunApprovalBackend.d.ts +3 -0
  95. package/dist/run/RunApprovalBackend.js +41 -6
  96. package/dist/run/RunLock.js +2 -0
  97. package/dist/run/RunManager.d.ts +2 -0
  98. package/dist/run/RunManager.js +64 -24
  99. package/dist/run/ids.d.ts +2 -0
  100. package/dist/run/ids.js +23 -0
  101. package/dist/runtime/background-shell.d.ts +1 -0
  102. package/dist/runtime/background-shell.js +23 -13
  103. package/dist/runtime/spawn-common.js +10 -0
  104. package/dist/services/auto-dream.d.ts +15 -4
  105. package/dist/services/auto-dream.js +20 -20
  106. package/dist/services/dream-consolidation.d.ts +2 -3
  107. package/dist/services/dream-consolidation.js +65 -15
  108. package/dist/services/extract-memories.d.ts +14 -5
  109. package/dist/services/extract-memories.js +20 -3
  110. package/dist/services/global-dream-promotion.d.ts +23 -0
  111. package/dist/services/global-dream-promotion.js +112 -0
  112. package/dist/services/memory-orchestrator.js +347 -34
  113. package/dist/session/memory.d.ts +61 -18
  114. package/dist/session/memory.js +342 -79
  115. package/dist/session/session-manager.d.ts +35 -1
  116. package/dist/session/session-manager.js +190 -3
  117. package/dist/session/transcript.d.ts +1 -1
  118. package/dist/session/transcript.js +17 -5
  119. package/dist/settings/manager.d.ts +1 -0
  120. package/dist/settings/manager.js +87 -37
  121. package/dist/settings/schema-export.d.ts +2 -3
  122. package/dist/settings/schema-export.js +2 -3
  123. package/dist/settings/schema.d.ts +21 -0
  124. package/dist/settings/schema.js +12 -0
  125. package/dist/skills/scanner.d.ts +3 -2
  126. package/dist/skills/scanner.js +12 -9
  127. package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
  128. package/dist/tool-system/builtin/background-jobs.js +12 -1
  129. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  130. package/dist/tool-system/builtin/background-work.js +51 -5
  131. package/dist/tool-system/builtin/config.d.ts +2 -1
  132. package/dist/tool-system/builtin/config.js +16 -11
  133. package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
  134. package/dist/tool-system/builtin/drive-claude-code.js +301 -47
  135. package/dist/tool-system/builtin/edit.js +5 -2
  136. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  137. package/dist/tool-system/builtin/generate-video.js +13 -4
  138. package/dist/tool-system/builtin/index.d.ts +8 -3
  139. package/dist/tool-system/builtin/index.js +37 -21
  140. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  141. package/dist/tool-system/builtin/lsp.js +6 -3
  142. package/dist/tool-system/builtin/memory.js +40 -8
  143. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  144. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  145. package/dist/tool-system/builtin/powershell.js +11 -7
  146. package/dist/tool-system/builtin/read.js +118 -6
  147. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  148. package/dist/tool-system/builtin/view-image.js +109 -19
  149. package/dist/tool-system/builtin/worktree.d.ts +4 -4
  150. package/dist/tool-system/builtin/worktree.js +297 -74
  151. package/dist/tool-system/builtin/write.js +5 -3
  152. package/dist/tool-system/context.d.ts +23 -1
  153. package/dist/tool-system/executor.d.ts +1 -5
  154. package/dist/tool-system/executor.js +94 -115
  155. package/dist/tool-system/mcp-manager.d.ts +18 -5
  156. package/dist/tool-system/mcp-manager.js +140 -74
  157. package/dist/tool-system/path-policy.d.ts +2 -0
  158. package/dist/tool-system/path-policy.js +41 -12
  159. package/dist/tool-system/permission.d.ts +28 -7
  160. package/dist/tool-system/permission.js +130 -49
  161. package/dist/tool-system/registry.js +11 -4
  162. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  163. package/dist/tool-system/tool-result-redaction.js +48 -0
  164. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  165. package/dist/tool-system/workspace-bridge.js +1 -0
  166. package/dist/types.d.ts +55 -4
  167. package/dist/utils/toolDisplay.js +1 -1
  168. package/package.json +4 -3
@@ -34,26 +34,27 @@ import { PLAN_MODE_ALLOWED_TOOLS } from "../tool-system/plan-mode-allowlist.js";
34
34
  import { PromptComposer } from "../prompt/composer.js";
35
35
  import { SessionManager } from "../session/session-manager.js";
36
36
  import { ModelFacade } from "./model-facade.js";
37
- import { logger, setCurrentSid, runWithSid, getCurrentSid } from "../logging/logger.js";
37
+ import { logger, runWithSid, getCurrentSid } from "../logging/logger.js";
38
38
  import { recordSessionStart, recordSessionEnd } from "../logging/session-recorder.js";
39
39
  import { sanitizeContent, sanitizeTaskString } from "../logging/sanitize-messages.js";
40
40
  import { TurnLoop } from "./turn-loop.js";
41
41
  import { MCPManager } from "../tool-system/mcp-manager.js";
42
42
  import { SettingsManager, userHome } from "../settings/manager.js";
43
- import { CredentialStore } from "../credentials/store.js";
43
+ import { getCredentialAccess } from "../credentials/access.js";
44
44
  import { isFeatureEnabled, resolveFeatureFlags, } from "../settings/feature-flags.js";
45
- import { effectiveDisabledList, effectiveBuiltinLists, } from "../capability-control/overlay.js";
45
+ import { effectiveDisabledList, effectiveBuiltinLists } from "../capability-control/overlay.js";
46
46
  import { computeEffectiveDisabledLists } from "../capability-control/disabled-lists.js";
47
47
  import { FileHistory } from "../session/file-history.js";
48
48
  import { patchBackupTargets } from "../tool-system/builtin/apply-patch/backup-targets.js";
49
49
  import { resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
50
- import { resolveAgentPreset, resolveBuiltinToolNames, } from "../preset/index.js";
50
+ import { resolveAgentPreset, resolveBuiltinToolNames } from "../preset/index.js";
51
51
  import { ModelPool } from "../llm/model-pool.js";
52
52
  import { AgentDefinitionRegistry } from "../agent/agent-definition-registry.js";
53
53
  import { defaultCacheDir } from "../llm/model-cache.js";
54
54
  import { detectProviderFromApiKey, buildModelPool } from "../onboarding.js";
55
55
  import { detectPastedNoise } from "../utils/task-sanitizer.js";
56
56
  import { parseTaskWithImages } from "./parse-task.js";
57
+ import { buildInputAttachmentContext } from "./input-attachments.js";
57
58
  import { enforceImagePolicy, byteLengthFromBase64, dropOversizedImages, collectAttachedImagePaths, } from "./image-policy.js";
58
59
  import { tryCompressImages } from "./image-compression.js";
59
60
  import { buildSessionTitle } from "./session-title.js";
@@ -129,7 +130,7 @@ export function resolveChildLlm(modelKey, pool, parentLlm) {
129
130
  * Names in `disabledAgents` are filtered out so the LLM never sees them.
130
131
  */
131
132
  /**
132
- * Resolve the working directory for a run. Precedence:
133
+ * Resolve the working directory for a run. Precedence for legacy sessions:
133
134
  * options.cwd > resumed session's state.cwd > config.cwd > process.cwd()
134
135
  *
135
136
  * The session-cwd tier is what stops a project-bound session from being
@@ -445,6 +446,7 @@ export class Engine {
445
446
  new ToolRegistry({
446
447
  builtinTools: resolveBuiltinToolNames({
447
448
  preset: this.preset.name,
449
+ host: config.builtinToolHost,
448
450
  enabledBuiltinTools: builtinLists.enabledBuiltinTools,
449
451
  disabledBuiltinTools: builtinLists.disabledBuiltinTools,
450
452
  }),
@@ -635,6 +637,10 @@ export class Engine {
635
637
  setBrowserBridge(bridge) {
636
638
  this.config.browserBridge = bridge;
637
639
  }
640
+ /** Inject the host-backed workspace bridge after construction. */
641
+ setWorkspaceBridge(bridge) {
642
+ this.config.workspaceBridge = bridge;
643
+ }
638
644
  /**
639
645
  * Queue a user message to be spliced into the in-flight run for `sessionId`
640
646
  * at the next turn-loop step boundary — the 不打断 steering path (vs cancel +
@@ -735,20 +741,45 @@ export class Engine {
735
741
  * Run a task from start to finish.
736
742
  */
737
743
  async run(task, options) {
738
- // When the caller omits cwd but is resuming an existing session, recover
739
- // that session's bound cwd from disk so a project-bound session keeps
740
- // loading its own agents/settings/memory even if the host's UI repo
741
- // selection has drifted to null. Only probe on omission — an explicit cwd
742
- // always wins, and a fresh session has nothing to recover.
743
- const sessionCwd = options?.cwd === undefined && options?.sessionId
744
- ? this.sessionManager.readCwd(options.sessionId)
744
+ const workspaceResume = options?.sessionId && this.sessionManager.exists(options.sessionId)
745
+ ? await this.sessionManager.resolveSessionWorkspaceForResume(options.sessionId)
745
746
  : undefined;
746
- const cwd = resolveRunCwd({
747
- optionCwd: options?.cwd,
748
- sessionCwd,
749
- configCwd: this.config.cwd,
750
- processCwd: process.cwd(),
751
- });
747
+ if (workspaceResume && !workspaceResume.ok) {
748
+ return {
749
+ text: `ERROR: ${workspaceResume.message}`,
750
+ reason: "completed",
751
+ sessionId: options.sessionId,
752
+ turnCount: 0,
753
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
754
+ };
755
+ }
756
+ if (workspaceResume?.ok &&
757
+ workspaceResume.reason === "worktree_missing_branch_gone" &&
758
+ workspaceResume.message) {
759
+ return {
760
+ text: workspaceResume.message,
761
+ reason: "completed",
762
+ sessionId: options.sessionId,
763
+ turnCount: 0,
764
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
765
+ };
766
+ }
767
+ // Existing P1 sessions resolve cwd from SessionWorkspace, even if the host
768
+ // passes a stale cwd. Legacy sessions without workspace keep the historical
769
+ // explicit-cwd precedence for backward compatibility.
770
+ const workspaceCwd = workspaceResume?.ok && workspaceResume.reason !== "legacy" ? workspaceResume.cwd : undefined;
771
+ const sessionCwd = workspaceCwd === undefined && options?.cwd === undefined && options?.sessionId
772
+ ? workspaceResume?.ok
773
+ ? workspaceResume.cwd
774
+ : this.sessionManager.readCwd(options.sessionId)
775
+ : undefined;
776
+ const cwd = workspaceCwd ??
777
+ resolveRunCwd({
778
+ optionCwd: options?.cwd,
779
+ sessionCwd,
780
+ configCwd: this.config.cwd,
781
+ processCwd: process.cwd(),
782
+ });
752
783
  // Wrap the caller's onStream so we can intercept `task_update`
753
784
  // events emitted by TodoWrite and keep an in-engine snapshot.
754
785
  // TaskGuard reads this snapshot at turn end to decide whether to
@@ -801,8 +832,32 @@ export class Engine {
801
832
  usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
802
833
  };
803
834
  }
835
+ const cap = capabilitiesFor((this.config.llm.providerKind ?? this.config.llm.provider), this.config.llm.model);
836
+ const attachmentContext = await buildInputAttachmentContext(options?.attachments, cwd, {
837
+ includeImageBytes: cap.supportsVision,
838
+ expectedSessionId: options?.sessionId,
839
+ });
840
+ if (attachmentContext.errors.length > 0) {
841
+ const detail = attachmentContext.errors.join("; ");
842
+ logger.warn("engine.run.input_attachment_failed", { error: detail });
843
+ return {
844
+ text: `ERROR: input attachment could not be read (${detail}). Re-attach it or choose a path inside the workspace.`,
845
+ reason: "image_error",
846
+ sessionId: options?.sessionId ?? "input-attachment-failed",
847
+ turnCount: 0,
848
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
849
+ };
850
+ }
851
+ if (attachmentContext.text || attachmentContext.hasStructuredImageAttachments) {
852
+ parsedTask = {
853
+ text: [parsedTask.text, attachmentContext.text].filter(Boolean).join("\n\n"),
854
+ images: [...parsedTask.images, ...attachmentContext.images],
855
+ hasImages: parsedTask.hasImages ||
856
+ attachmentContext.images.length > 0 ||
857
+ attachmentContext.hasStructuredImageAttachments,
858
+ };
859
+ }
804
860
  if (parsedTask.hasImages) {
805
- const cap = capabilitiesFor((this.config.llm.providerKind ?? this.config.llm.provider), this.config.llm.model);
806
861
  if (!cap.supportsVision) {
807
862
  logger.warn("engine.run.vision_not_supported", {
808
863
  provider: this.config.llm.provider,
@@ -884,7 +939,7 @@ export class Engine {
884
939
  // *text* portion only — base64 bytes count as "noise" by the heuristic
885
940
  // and would also bloat the transcript by megabytes per image. Image
886
941
  // bytes ride in parsedTask.images and re-enter the message tree below.
887
- const taskText = parsedTask.hasImages ? parsedTask.text : task;
942
+ const taskText = parsedTask.text;
888
943
  const noise = detectPastedNoise(taskText);
889
944
  if (noise.isNoise) {
890
945
  const hint = `Your input looks like pasted terminal output (${noise.reason}). ` +
@@ -946,6 +1001,7 @@ export class Engine {
946
1001
  preset: this.preset.name,
947
1002
  enabledBuiltinTools: childEnabled,
948
1003
  disabledBuiltinTools: childDisabled,
1004
+ builtinToolHost: this.config.builtinToolHost,
949
1005
  customSystemPrompt: this.config.customSystemPrompt,
950
1006
  appendSystemPrompt: [this.config.appendSystemPrompt, req.appendSystemPrompt].filter(Boolean).join("\n\n") ||
951
1007
  undefined,
@@ -1013,23 +1069,7 @@ export class Engine {
1013
1069
  },
1014
1070
  sessionExists: (sessionId) => this.sessionManager.exists(sessionId),
1015
1071
  };
1016
- // Priority: config.sandbox → project settings.sandbox → global → per-run
1017
- // default. Read UNMERGED per-scope (getForScope) so a project that wrote no
1018
- // sandbox genuinely follows global, rather than inheriting global's mode and
1019
- // looking like it set one. Fixes "项目级配了不生效" + the scope model.
1020
- let projectSandbox;
1021
- let globalSandbox;
1022
- try {
1023
- const sm = this.getSettingsManager();
1024
- if (this.config.isSubAgent !== true) {
1025
- projectSandbox = sm.getForScope("project", cwd).sandbox;
1026
- }
1027
- globalSandbox = sm.getForScope("user").sandbox;
1028
- }
1029
- catch {
1030
- // settings unavailable → fall through to per-run default
1031
- }
1032
- const sandboxConfig = resolveSandboxConfig(this.config.sandbox, projectSandbox, globalSandbox, this.config.headless === true);
1072
+ const sandboxConfig = this.resolveSandboxConfigForCwd(cwd);
1033
1073
  // A2: explicit sandbox modes (seatbelt, bwrap) must fail closed
1034
1074
  // per standard §S4. resolveSandboxBackend throws when an explicit
1035
1075
  // mode is unavailable on this host; we let it propagate. The
@@ -1070,12 +1110,16 @@ export class Engine {
1070
1110
  ? sandboxBackend
1071
1111
  : { ...sandboxBackend, network: sandboxConfig.network },
1072
1112
  cwd,
1113
+ shellEnv: this.readShellEnv(cwd),
1073
1114
  // TodoWrite reads this to push task_update events independently
1074
1115
  // of its return value, so the UI's pinned task panel refreshes
1075
1116
  // immediately rather than after the LLM next surfaces the
1076
1117
  // snapshot. wrappedOnStream snoops the same channel to keep
1077
1118
  // latestTodos current for TaskGuard.
1078
1119
  streamCallback: options?.onStream,
1120
+ setCwd(nextCwd) {
1121
+ toolCtx.cwd = nextCwd;
1122
+ },
1079
1123
  };
1080
1124
  logger.info("engine.run", {
1081
1125
  task: taskText.slice(0, 200),
@@ -1134,6 +1178,7 @@ export class Engine {
1134
1178
  let session;
1135
1179
  let messages;
1136
1180
  let freshImageMessage;
1181
+ let resumedFromDisk = false;
1137
1182
  const claimedClientMessageIds = new Set();
1138
1183
  const claimClientMessageId = (bundle, clientMessageId, source) => {
1139
1184
  if (!clientMessageId)
@@ -1151,6 +1196,7 @@ export class Engine {
1151
1196
  return true;
1152
1197
  };
1153
1198
  if (options?.sessionId && this.sessionManager.exists(options.sessionId)) {
1199
+ resumedFromDisk = true;
1154
1200
  session = this.sessionManager.resume(options.sessionId);
1155
1201
  const cachedCompacted = this.compactedMessagesBySession.get(options.sessionId);
1156
1202
  messages = cachedCompacted ? [...cachedCompacted] : session.transcript.toMessages();
@@ -1233,21 +1279,6 @@ export class Engine {
1233
1279
  // with this value so `/undo` reverts exactly this turn's file changes.
1234
1280
  // (Both resume and cold-start paths converge here.)
1235
1281
  session.state.turnSeq = (session.state.turnSeq ?? 0) + 1;
1236
- // Stamp the resolved session id for downstream logging.
1237
- //
1238
- // `setCurrentSid` updates the module-level fallback so any code path
1239
- // running outside an ALS scope (bootstrap, /sid before a run starts)
1240
- // still sees the latest sid.
1241
- //
1242
- // The real isolation comes from wrapping the rest of `run` in
1243
- // `runWithSid(sid, async () => { ... })`: every `getCurrentSid()`
1244
- // call inside that closure — including those inside `await`ed child
1245
- // Engine.run() calls — reads sid from this scope's
1246
- // AsyncLocalStorage binding, not the module global. Sibling Engines
1247
- // running concurrently each get their own scope; an `enterSid` inside
1248
- // a child mutates that child's scope only and doesn't leak back to
1249
- // the parent's chain after `await child.run(...)` returns.
1250
- setCurrentSid(session.state.sessionId);
1251
1282
  // B2 / Gate 1: stamp the resolved sid onto the tool context so
1252
1283
  // session-scoped side effects (background-agent completion
1253
1284
  // notifications) attribute to the right session. toolCtx is created
@@ -1255,6 +1286,9 @@ export class Engine {
1255
1286
  // the first point we can set it. After this assignment treat the
1256
1287
  // field as immutable for the rest of the run.
1257
1288
  toolCtx.sessionId = session.state.sessionId;
1289
+ toolCtx.setSessionWorkspace = (workspace) => {
1290
+ session.state.workspace = workspace;
1291
+ };
1258
1292
  return runWithSid(session.state.sessionId, async () => {
1259
1293
  recordSessionStart(session.state.sessionId, {
1260
1294
  // Strip <codeshell-image> base64 payloads before they reach
@@ -1265,7 +1299,7 @@ export class Engine {
1265
1299
  model: this.config.llm.model,
1266
1300
  provider: this.config.llm.provider,
1267
1301
  permissionMode: this.config.permissionMode ?? "acceptEdits",
1268
- resumed: !!options?.sessionId,
1302
+ resumed: resumedFromDisk,
1269
1303
  });
1270
1304
  // Session-level hook: fired once per Engine.run() entry, regardless of
1271
1305
  // cold-start vs resume. Handlers can return `messages` to inject a
@@ -1275,7 +1309,8 @@ export class Engine {
1275
1309
  const sessionStartHook = await this.emitHook("on_session_start", {
1276
1310
  sessionId: session.state.sessionId,
1277
1311
  cwd,
1278
- resumed: !!options?.sessionId,
1312
+ resumed: resumedFromDisk,
1313
+ source: resumedFromDisk ? "resume" : "startup",
1279
1314
  });
1280
1315
  // Per-turn hook: fired every time a new user prompt enters the loop.
1281
1316
  // Equivalent to CC's UserPromptSubmit. Handlers can inject lightweight
@@ -1289,7 +1324,7 @@ export class Engine {
1289
1324
  // and silently leaking attachment bytes through hooks is the kind of
1290
1325
  // exfiltration risk a curious user-installed shell hook shouldn't carry.
1291
1326
  prompt: taskText,
1292
- resumed: !!options?.sessionId,
1327
+ resumed: resumedFromDisk,
1293
1328
  });
1294
1329
  // updatedPrompt: handler rewrote the user's prompt text. Replace the
1295
1330
  // last user message we just pushed (cold-start: line ~511; resume:
@@ -1307,22 +1342,49 @@ export class Engine {
1307
1342
  messages[lastIdx] = { role: "user", content: promptSubmitHook.updatedPrompt };
1308
1343
  }
1309
1344
  }
1310
- // Rough token estimate of the full prompt so the UI's ctx bar isn't 0%
1311
- // before the first real usage_update arrives. The authoritative count
1345
+ const contextManager = new ContextManager({
1346
+ maxTokens: this.resolveMaxContextTokens(),
1347
+ // Drop undefined fields so they don't clobber ContextManager defaults
1348
+ // (spread of `{x: undefined}` would override the default with undefined).
1349
+ ...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
1350
+ });
1351
+ this.lastContextManager = contextManager;
1352
+ const persistedContextAnchor = session.state.contextUsageAnchor;
1353
+ const contextAnchorCompatible = persistedContextAnchor !== undefined &&
1354
+ (persistedContextAnchor.provider === undefined ||
1355
+ persistedContextAnchor.provider === this.config.llm.provider) &&
1356
+ (persistedContextAnchor.model === undefined ||
1357
+ persistedContextAnchor.model === this.config.llm.model) &&
1358
+ (persistedContextAnchor.messageCount <= messages.length ||
1359
+ persistedContextAnchor.estimateAtAnchor !== undefined);
1360
+ if (contextAnchorCompatible) {
1361
+ contextManager.seedActualUsage(persistedContextAnchor);
1362
+ }
1363
+ // Best-effort token estimate of the full prompt so the UI's ctx bar isn't
1364
+ // 0% before the first real usage_update arrives. The authoritative count
1312
1365
  // comes from `usage.promptTokens` after the first LLM response — this is
1313
- // just a display-friendly approximation for the first frame.
1366
+ // just a display-friendly approximation for the first frame, annotated
1367
+ // with source/confidence so consumers don't treat heuristics as truth.
1314
1368
  //
1315
1369
  // Only seed once per (process, sid). On subsequent turns the UI already
1316
- // shows the previous turn's accurate ctx; overwriting it with this rough
1317
- // char/4 estimate would make the bar visibly drop on every submit.
1370
+ // shows the previous turn's accurate ctx; overwriting it with a fresh
1371
+ // best-effort estimate would make the bar visibly drop on every submit.
1318
1372
  const sid = session.state.sessionId;
1319
1373
  const needsCtxSeed = !this.ctxSeedSent.has(sid);
1320
- const roughPromptTokens = needsCtxSeed
1321
- ? messages.reduce((sum, m) => {
1322
- const text = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
1323
- return sum + Math.ceil(text.length / 4);
1324
- }, 0)
1325
- : 0;
1374
+ const ctxSeed = needsCtxSeed
1375
+ ? (() => {
1376
+ const checked = contextManager.checkLimits(messages);
1377
+ return {
1378
+ tokens: checked.tokens,
1379
+ source: checked.promptTokensSource,
1380
+ confidence: checked.promptTokensConfidence,
1381
+ };
1382
+ })()
1383
+ : {
1384
+ tokens: 0,
1385
+ source: "heuristic_estimate",
1386
+ confidence: "low",
1387
+ };
1326
1388
  if (needsCtxSeed)
1327
1389
  this.ctxSeedSent.add(sid);
1328
1390
  // Tell the client the sid *now* instead of waiting for run() to resolve.
@@ -1331,7 +1393,9 @@ export class Engine {
1331
1393
  options?.onStream?.({
1332
1394
  type: "session_started",
1333
1395
  sessionId: sid,
1334
- promptTokens: roughPromptTokens,
1396
+ promptTokens: ctxSeed.tokens,
1397
+ promptTokensSource: ctxSeed.source,
1398
+ promptTokensConfidence: ctxSeed.confidence,
1335
1399
  });
1336
1400
  // Replay the last TodoWrite snapshot on resume so the UI's pinned
1337
1401
  // task panel re-hydrates without the LLM needing to call TodoWrite
@@ -1358,12 +1422,14 @@ export class Engine {
1358
1422
  // in this same session don't re-prompt. Headless/auto backends skip
1359
1423
  // this — they don't prompt, so there are no project rules to persist.
1360
1424
  if (approvalBackend instanceof InteractiveApprovalBackend) {
1361
- approvalBackend.setCwd(cwd);
1362
- approvalBackend.setOnProjectRules((rules) => {
1363
- // Prepend the *full* accumulated list of session-saved project rules
1364
- // so user approvals win over defaults and earlier approvals aren't
1365
- // dropped when later ones come in.
1366
- permission.reconfigure(mode, approvalBackend, [...rules, ...defaultRules]);
1425
+ approvalBackend.setSessionContext(session.state.sessionId, {
1426
+ cwd,
1427
+ onProjectRules: (rules) => {
1428
+ // Prepend the *full* accumulated list of session-saved project rules
1429
+ // so user approvals win over defaults and earlier approvals aren't
1430
+ // dropped when later ones come in.
1431
+ permission.reconfigure(mode, approvalBackend, [...rules, ...defaultRules]);
1432
+ },
1367
1433
  });
1368
1434
  }
1369
1435
  const toolExecutor = new ToolExecutor(this.toolRegistry, permission, this.hooks);
@@ -1379,13 +1445,6 @@ export class Engine {
1379
1445
  // Wire abort signal for cascading cancellation + per-Engine ToolContext
1380
1446
  toolExecutor.setSignal(options?.signal);
1381
1447
  toolExecutor.setContext(toolCtx);
1382
- const contextManager = new ContextManager({
1383
- maxTokens: this.resolveMaxContextTokens(),
1384
- // Drop undefined fields so they don't clobber ContextManager defaults
1385
- // (spread of `{x: undefined}` would override the default with undefined).
1386
- ...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
1387
- });
1388
- this.lastContextManager = contextManager;
1389
1448
  const { disabledSkills, disabledPlugins } = this.readDisabledLists();
1390
1449
  const promptComposer = new PromptComposer({
1391
1450
  cwd,
@@ -1447,6 +1506,7 @@ export class Engine {
1447
1506
  (normalizeGoal(options?.goal) !== undefined ||
1448
1507
  session.state.activeGoal !== undefined ||
1449
1508
  normalizeGoal(this.config.goal) !== undefined),
1509
+ settingsScope: this.config.settingsScope ?? "project",
1450
1510
  };
1451
1511
  toolCtx.toolVisibility = toolVisibility;
1452
1512
  // #7: per-turn project builtin override. The toolRegistry's builtin tool
@@ -1757,6 +1817,13 @@ export class Engine {
1757
1817
  consumeSteer: (source) => this.consumeSteer(sid, source),
1758
1818
  claimClientMessageId: (clientMessageId, source) => claimClientMessageId(session, clientMessageId, source),
1759
1819
  recordCumulativeUsage,
1820
+ recordContextUsageAnchor: (anchor) => {
1821
+ session.state.contextUsageAnchor = {
1822
+ ...anchor,
1823
+ provider: this.config.llm.provider,
1824
+ model: this.config.llm.model,
1825
+ };
1826
+ },
1760
1827
  // Clear the persisted goal for a self-reported completion / confirmed
1761
1828
  // cancel. Clears the in-RAM session's activeGoal (so THIS run's later
1762
1829
  // turns don't re-arm) AND persists it, and drops the in-flight stop
@@ -1816,6 +1883,8 @@ export class Engine {
1816
1883
  options?.onStream?.({
1817
1884
  type: "usage_update",
1818
1885
  promptTokens: cumulative.cumulativePromptTokens,
1886
+ promptTokensSource: "session_cumulative",
1887
+ promptTokensConfidence: "high",
1819
1888
  cumulativePromptTokens: cumulative.cumulativePromptTokens,
1820
1889
  cumulativeCacheReadTokens: cumulative.cumulativeCacheReadTokens,
1821
1890
  cumulativeCacheCreationTokens: cumulative.cumulativeCacheCreationTokens,
@@ -2355,7 +2424,6 @@ export class Engine {
2355
2424
  refreshRuntimeConfig(patch, version) {
2356
2425
  if (version <= this.lastAppliedConfigVersion)
2357
2426
  return;
2358
- const prevServers = this.config.mcpServers ?? {};
2359
2427
  const prevPresetName = this.preset.name;
2360
2428
  this.config = { ...this.config, ...patch };
2361
2429
  // #2: re-resolve the prompt-affecting preset so the next-turn PromptComposer
@@ -2366,11 +2434,17 @@ export class Engine {
2366
2434
  // The builtin tool SET is ctor-frozen and may be shared via runtime — we
2367
2435
  // do NOT rebuild it here. If the new preset implies a different builtin
2368
2436
  // tool set, that part of the change only lands on session restart.
2369
- const prevTools = resolveBuiltinToolNames({ preset: prevPresetName })
2437
+ const prevTools = resolveBuiltinToolNames({
2438
+ preset: prevPresetName,
2439
+ host: this.config.builtinToolHost,
2440
+ })
2370
2441
  .slice()
2371
2442
  .sort()
2372
2443
  .join(",");
2373
- const nextTools = resolveBuiltinToolNames({ preset: nextPreset.name })
2444
+ const nextTools = resolveBuiltinToolNames({
2445
+ preset: nextPreset.name,
2446
+ host: this.config.builtinToolHost,
2447
+ })
2374
2448
  .slice()
2375
2449
  .sort()
2376
2450
  .join(",");
@@ -2457,6 +2531,41 @@ export class Engine {
2457
2531
  }
2458
2532
  return had;
2459
2533
  }
2534
+ /**
2535
+ * Reset a session's workspace pointer back to its main root. If the session is
2536
+ * actively running, mutate that live SessionBundle first so the run's next
2537
+ * saveState cannot resurrect a stale worktree pointer.
2538
+ */
2539
+ releaseSessionWorkspace(sessionId) {
2540
+ if (!sessionId || !this.sessionManager.exists(sessionId))
2541
+ return null;
2542
+ const mainRoot = this.sessionManager.readCwd(sessionId) ??
2543
+ (this.activeRunSession?.state.sessionId === sessionId
2544
+ ? this.activeRunSession.state.cwd
2545
+ : undefined);
2546
+ if (!mainRoot)
2547
+ return null;
2548
+ const workspace = { root: mainRoot, kind: "main" };
2549
+ if (this.activeRunSession?.state.sessionId === sessionId) {
2550
+ this.activeRunSession.state.workspace = workspace;
2551
+ }
2552
+ try {
2553
+ const bundle = this.activeRunSession?.state.sessionId === sessionId
2554
+ ? this.activeRunSession
2555
+ : this.sessionManager.resume(sessionId);
2556
+ bundle.state.workspace = workspace;
2557
+ this.sessionManager.saveState(bundle.state);
2558
+ }
2559
+ catch {
2560
+ try {
2561
+ this.sessionManager.setSessionWorkspace(sessionId, workspace);
2562
+ }
2563
+ catch {
2564
+ return null;
2565
+ }
2566
+ }
2567
+ return workspace;
2568
+ }
2460
2569
  injectContext(sessionId, content) {
2461
2570
  const session = this.sessionManager.resume(sessionId);
2462
2571
  session.transcript.appendMessage("assistant", content);
@@ -2575,10 +2684,10 @@ export class Engine {
2575
2684
  buildPermissionConfig(mode, cwd) {
2576
2685
  const rules = [...this.preset.defaultPermissionRules];
2577
2686
  // Memory tools: dream scope is the LLM's own workspace, so save/delete
2578
- // there go through without prompting. user-scope save/delete fall through
2579
- // to the tool's permissionDefault ("ask"), forcing the user to confirm
2580
- // any modification of memories they own. Read tools are listed in the
2581
- // tool definition as permissionDefault: "allow" no rule needed here.
2687
+ // there go through without prompting. user-scope save/delete have no
2688
+ // explicit allow rule here, so default-mode classifier fallback asks the
2689
+ // user to confirm modifications. RegisteredTool.permissionDefault is only
2690
+ // UI/metadata and is not read by the classifier.
2582
2691
  rules.push({
2583
2692
  tool: "MemorySave",
2584
2693
  argsPattern: { scope: "^dream$" },
@@ -2837,6 +2946,25 @@ export class Engine {
2837
2946
  }
2838
2947
  return cached;
2839
2948
  }
2949
+ resolveSandboxConfigForCwd(cwd) {
2950
+ // Priority: config.sandbox → project settings.sandbox → global → per-run
2951
+ // default. Read UNMERGED per-scope (getForScope) so a project that wrote no
2952
+ // sandbox genuinely follows global, rather than inheriting global's mode and
2953
+ // looking like it set one. Fixes "项目级配了不生效" + the scope model.
2954
+ let projectSandbox;
2955
+ let globalSandbox;
2956
+ try {
2957
+ const sm = this.getSettingsManager();
2958
+ if (this.config.isSubAgent !== true) {
2959
+ projectSandbox = sm.getForScope("project", cwd).sandbox;
2960
+ }
2961
+ globalSandbox = sm.getForScope("user").sandbox;
2962
+ }
2963
+ catch {
2964
+ // settings unavailable → fall through to per-run default
2965
+ }
2966
+ return resolveSandboxConfig(this.config.sandbox, projectSandbox, globalSandbox, this.config.headless === true);
2967
+ }
2840
2968
  /**
2841
2969
  * Build the shell env layered onto the Bash tool / background shells (see
2842
2970
  * mergeShellEnv). Three user-configured sources, merged lowest → highest:
@@ -2894,7 +3022,7 @@ export class Engine {
2894
3022
  // the host user's credentials (same isolation contract as top-level env).
2895
3023
  // Placed below settings.env so an explicit `env` entry can still override.
2896
3024
  const credScope = (this.config.settingsScope ?? "project") === "full" ? "full" : "project";
2897
- layer(new CredentialStore(cwd).envExposures(credScope));
3025
+ layer(getCredentialAccess().envExposures(cwd, credScope));
2898
3026
  layer(settings.env); // top-level env (global ⊕ project) wins
2899
3027
  }
2900
3028
  catch {
@@ -2931,9 +3059,34 @@ export class Engine {
2931
3059
  return undefined;
2932
3060
  }
2933
3061
  }
3062
+ readWorktreeBranchPrefix(cwd) {
3063
+ if (this.config.isSubAgent === true || !cwd)
3064
+ return undefined;
3065
+ try {
3066
+ const settings = this.getSettingsManager().get();
3067
+ return settings.worktree?.branchPrefix;
3068
+ }
3069
+ catch {
3070
+ return undefined;
3071
+ }
3072
+ }
3073
+ async resolveWorktreeSetupSandbox(cwd) {
3074
+ if (!cwd)
3075
+ return undefined;
3076
+ const sandboxConfig = this.resolveSandboxConfigForCwd(cwd);
3077
+ const sandboxBackend = this.runtime
3078
+ ? await this.runtime.resolveSandbox(sandboxConfig, cwd)
3079
+ : await this.resolveSandboxWithoutRuntime(sandboxConfig, cwd);
3080
+ return sandboxBackend.name === "off"
3081
+ ? sandboxBackend
3082
+ : { ...sandboxBackend, network: sandboxConfig.network };
3083
+ }
3084
+ readWorktreeSetupShellEnv(cwd) {
3085
+ return this.readShellEnv(cwd);
3086
+ }
2934
3087
  buildToolContext() {
2935
3088
  const { disabledSkills, disabledPlugins } = this.readDisabledLists();
2936
- return {
3089
+ const ctx = {
2937
3090
  shellEnv: this.readShellEnv(this.config.cwd),
2938
3091
  cwd: this.config.cwd ?? process.cwd(),
2939
3092
  llmConfig: this.config.llm,
@@ -2941,6 +3094,7 @@ export class Engine {
2941
3094
  toolRegistry: this.toolRegistry,
2942
3095
  askUser: this.config.askUser,
2943
3096
  browser: this.config.browserBridge,
3097
+ workspace: this.config.workspaceBridge,
2944
3098
  injectCredentialToBrowser: this.config.injectCredentialToBrowser,
2945
3099
  isSubAgent: this.config.isSubAgent === true,
2946
3100
  // Credential tools narrow their disk reads to this scope: a project/
@@ -2961,6 +3115,10 @@ export class Engine {
2961
3115
  // opts out via config. Otherwise allowed.
2962
3116
  allowBackgroundShells: this.config.isSubAgent === true ? false : this.config.allowBackgroundShells !== false,
2963
3117
  };
3118
+ ctx.setCwd = (cwd) => {
3119
+ ctx.cwd = cwd;
3120
+ };
3121
+ return ctx;
2964
3122
  }
2965
3123
  /**
2966
3124
  * Read settings.disabledSkills + settings.disabledPlugins in a single
@@ -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