@cjhyy/code-shell-core 0.8.13 → 0.9.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 (91) hide show
  1. package/dist/automation/desktop-authority-client.d.ts +9 -0
  2. package/dist/automation/desktop-authority-client.js +47 -0
  3. package/dist/automation/scheduler.d.ts +8 -0
  4. package/dist/automation/scheduler.js +37 -1
  5. package/dist/automation/store.js +14 -1
  6. package/dist/capabilities/index.d.ts +1 -0
  7. package/dist/cli/agent-server-stdio.js +8 -4
  8. package/dist/engine/engine-workspace-authority.d.ts +35 -0
  9. package/dist/engine/engine-workspace-authority.js +136 -0
  10. package/dist/engine/engine.d.ts +5 -10
  11. package/dist/engine/engine.js +108 -112
  12. package/dist/engine/input-attachments.d.ts +1 -0
  13. package/dist/engine/input-attachments.js +6 -2
  14. package/dist/engine/run-environment.d.ts +8 -3
  15. package/dist/engine/run-environment.js +33 -8
  16. package/dist/engine/run-image-input.d.ts +1 -0
  17. package/dist/engine/run-image-input.js +1 -0
  18. package/dist/engine/run-session-open.d.ts +2 -1
  19. package/dist/engine/run-session-open.js +7 -1
  20. package/dist/engine/run-setup.d.ts +1 -0
  21. package/dist/engine/run-setup.js +2 -1
  22. package/dist/engine/run-tooling.d.ts +2 -0
  23. package/dist/engine/run-tooling.js +3 -0
  24. package/dist/engine/run-types.d.ts +4 -0
  25. package/dist/engine/run-workspace.d.ts +6 -1
  26. package/dist/engine/run-workspace.js +47 -0
  27. package/dist/engine/subagent-spawner.d.ts +1 -0
  28. package/dist/engine/subagent-spawner.js +1 -0
  29. package/dist/engine/turn-loop.js +17 -0
  30. package/dist/engine/types.d.ts +2 -0
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.extension.d.ts +1 -1
  33. package/dist/index.internal.d.ts +3 -2
  34. package/dist/index.internal.js +2 -0
  35. package/dist/index.js +2 -2
  36. package/dist/llm/client-base.d.ts +2 -1
  37. package/dist/llm/client-base.js +3 -1
  38. package/dist/llm/providers/anthropic.js +24 -25
  39. package/dist/llm/providers/openai.d.ts +4 -1
  40. package/dist/llm/providers/openai.js +76 -13
  41. package/dist/panel-apps/index.d.ts +1 -1
  42. package/dist/panel-apps/index.js +1 -1
  43. package/dist/panel-apps/installer.d.ts +29 -0
  44. package/dist/panel-apps/installer.js +124 -15
  45. package/dist/plugins/pluginAutomationTemplates.d.ts +2 -0
  46. package/dist/plugins/pluginAutomationTemplates.js +4 -0
  47. package/dist/plugins/pluginCatalog.d.ts +6 -0
  48. package/dist/plugins/pluginCatalog.js +7 -2
  49. package/dist/plugins/pluginContent.d.ts +1 -1
  50. package/dist/plugins/pluginContent.js +2 -10
  51. package/dist/prompt/composer.d.ts +4 -1
  52. package/dist/prompt/composer.js +11 -3
  53. package/dist/protocol/chat-session-manager.d.ts +42 -1
  54. package/dist/protocol/chat-session-manager.js +167 -4
  55. package/dist/protocol/chat-session.d.ts +11 -1
  56. package/dist/protocol/chat-session.js +20 -2
  57. package/dist/protocol/mobile-remote-types.d.ts +15 -0
  58. package/dist/protocol/server.d.ts +1 -2
  59. package/dist/protocol/server.js +28 -51
  60. package/dist/protocol/session-workspace-rpc.d.ts +23 -0
  61. package/dist/protocol/session-workspace-rpc.js +171 -0
  62. package/dist/protocol/types.d.ts +47 -1
  63. package/dist/protocol/types.js +4 -0
  64. package/dist/session/session-manager.d.ts +25 -0
  65. package/dist/session/session-manager.js +106 -6
  66. package/dist/session/transcript.d.ts +9 -0
  67. package/dist/session/transcript.js +81 -0
  68. package/dist/settings/manager.d.ts +12 -0
  69. package/dist/settings/manager.js +31 -0
  70. package/dist/tool-system/builtin/configure-model-connection.d.ts +36 -0
  71. package/dist/tool-system/builtin/configure-model-connection.js +396 -0
  72. package/dist/tool-system/builtin/cron.d.ts +11 -0
  73. package/dist/tool-system/builtin/cron.js +27 -2
  74. package/dist/tool-system/builtin/edit-model-catalog.d.ts +4 -6
  75. package/dist/tool-system/builtin/edit-model-catalog.js +5 -8
  76. package/dist/tool-system/builtin/edit.js +5 -3
  77. package/dist/tool-system/builtin/index.js +14 -0
  78. package/dist/tool-system/builtin/install-capability.js +22 -8
  79. package/dist/tool-system/builtin/settings-changed.d.ts +9 -0
  80. package/dist/tool-system/builtin/settings-changed.js +18 -0
  81. package/dist/tool-system/builtin/write.js +5 -3
  82. package/dist/tool-system/context.d.ts +12 -3
  83. package/dist/tool-system/executor.js +1 -1
  84. package/dist/tool-system/path-policy.d.ts +11 -3
  85. package/dist/tool-system/path-policy.js +56 -36
  86. package/dist/types.d.ts +11 -0
  87. package/dist/workspace/canonical-key.d.ts +7 -0
  88. package/dist/workspace/canonical-key.js +39 -0
  89. package/dist/workspace/workspace-context.d.ts +26 -0
  90. package/dist/workspace/workspace-context.js +112 -0
  91. package/package.json +1 -1
@@ -9,6 +9,7 @@ import { modelEntriesFromConnections } from "./model-connections-pool.js";
9
9
  import { cumulativeCacheHitRate, foldRunUsage, normalizeCumulativeUsageCounters, } from "../session/usage.js";
10
10
  import { enqueueSteerItem, consumeSteerItems, removeSteerItem, } from "./steer-queue.js";
11
11
  import { RunEnvironmentResolver } from "./run-environment.js";
12
+ import { legacySingleRootWorkspace } from "../workspace/workspace-context.js";
12
13
  import { asyncAgentRegistry } from "../tool-system/builtin/agent-registry.js";
13
14
  import { skillToolDef } from "../tool-system/builtin/skill.js";
14
15
  import { backgroundShellManager } from "../runtime/background-shell.js";
@@ -21,7 +22,8 @@ import { runShellHook, shellHookMatches } from "../hooks/shell-runner.js";
21
22
  import { ContextManager } from "../context/manager.js";
22
23
  import { CONTEXT_PACKAGE_MAX_OUTPUT_TOKENS, buildAnchoredSummaryMessage, buildContextPackagePromptFromSerialized, estimateTokens, groupMessagesByApiRound, serializeContextPackageMessages, clampContextRatios as clampContextRatiosImpl, } from "../context/compaction.js";
23
24
  import { PromptComposer } from "../prompt/composer.js";
24
- import { SessionManager, assertSafeSessionId, isEphemeralSessionState, sessionsRoot, } from "../session/session-manager.js";
25
+ import { SessionManager, isEphemeralSessionState, sessionsRoot, } from "../session/session-manager.js";
26
+ import { createAuthorizedSessionMessageService, migrateOwnedSessionMainRoot, releaseOwnedSessionWorkspace, SessionWorkspaceAuthorityTracker, setOwnedSessionWorkspace, } from "./engine-workspace-authority.js";
25
27
  import { createRunUsageAccounting, wireRunModelFacade } from "./run-accounting.js";
26
28
  import { logger, runWithSid } from "../logging/logger.js";
27
29
  import { recordSessionStart } from "../logging/session-recorder.js";
@@ -198,6 +200,7 @@ export class Engine {
198
200
  lastMessages;
199
201
  lastSessionId;
200
202
  compactedMessagesBySession = new Map();
203
+ workspaceAuthorities = new SessionWorkspaceAuthorityTracker();
201
204
  /**
202
205
  * SIDs whose ctx-bar seed we've already emitted in this process. The seed
203
206
  * is a rough char/4 estimate; only useful before the first real
@@ -260,6 +263,7 @@ export class Engine {
260
263
  * writers are additionally fenced by SessionManager's persisted revision CAS.
261
264
  */
262
265
  runInProgress = false;
266
+ disposed = false;
263
267
  agentControlStateListener;
264
268
  agentDirectionsDeliveredListener;
265
269
  /** Permission update requested while runInProgress. Applied in run() finally. */
@@ -404,8 +408,7 @@ export class Engine {
404
408
  throw new ConfigError("EngineConfig.composition is mutually exclusive with modules");
405
409
  }
406
410
  // Single composition fact source for every module contribution.
407
- this.composition =
408
- config.composition ?? compileComposition({ modules: config.modules ?? [] });
411
+ this.composition = config.composition ?? compileComposition({ modules: config.modules ?? [] });
409
412
  this.toolCatalog = compositionToolCatalog(this.composition);
410
413
  this.toolGuards = new Map(this.toolCatalog.flatMap((tool) => tool.exposure.availability
411
414
  ? [[tool.definition.name, tool.exposure.availability]]
@@ -941,6 +944,8 @@ export class Engine {
941
944
  * policy (for example ChatSession's FIFO queue).
942
945
  */
943
946
  async run(task, options) {
947
+ if (this.disposed)
948
+ throw new Error("Engine has been disposed");
944
949
  if (this.runInProgress) {
945
950
  throw new Error("Engine.run() cannot start while another run is in progress");
946
951
  }
@@ -987,12 +992,16 @@ export class Engine {
987
992
  resolveBehaviorProfile: (kind, mode) => this.resolveBehaviorProfile(kind, mode),
988
993
  configPermissionMode: this.config.permissionMode,
989
994
  configCwd: this.config.cwd,
995
+ configWorkspaceContext: this.config.workspaceContext,
990
996
  settings: this.getSettingsManager(),
991
997
  processCwd: process.cwd(),
992
998
  });
993
999
  if (!workspaceResolved.ok)
994
1000
  return workspaceResolved.result;
995
- const { sessionKind, sessionWorkspaceProfile, profile, profileParams, runPermissionMode, runPlanMode, cwd, profileState: { workspaceProfile: runWorkspaceProfile, sessionProfileOverrides, profileMemoryDir, }, } = workspaceResolved.resolution;
1001
+ const { sessionKind, sessionWorkspaceProfile, profile, profileParams, runPermissionMode, runPlanMode, cwd, workspaceContext, authoritativeWorkspaceContext, profileState: { workspaceProfile: runWorkspaceProfile, sessionProfileOverrides, profileMemoryDir, }, } = workspaceResolved.resolution;
1002
+ if (options?.sessionId) {
1003
+ this.workspaceAuthorities.remember(options.sessionId, workspaceContext);
1004
+ }
996
1005
  /** Structured results the profile's run services report; keyed per profile contract. */
997
1006
  let profileReportedResults;
998
1007
  // Wrap the caller's onStream so we can intercept `task_update`
@@ -1018,6 +1027,7 @@ export class Engine {
1018
1027
  llm: this.config.llm,
1019
1028
  sessionId: options?.sessionId,
1020
1029
  attachments: options?.attachments,
1030
+ workspaceContext,
1021
1031
  });
1022
1032
  if (!imageInput.ok)
1023
1033
  return imageInput.result;
@@ -1039,6 +1049,7 @@ export class Engine {
1039
1049
  const toolCtx = await this.wireRunSandboxToolContext({
1040
1050
  options,
1041
1051
  cwd,
1052
+ workspaceContext,
1042
1053
  runPermissionMode,
1043
1054
  runPlanMode,
1044
1055
  profile,
@@ -1080,6 +1091,12 @@ export class Engine {
1080
1091
  cwd,
1081
1092
  sessionKind,
1082
1093
  sessionWorkspaceProfile,
1094
+ projectBinding: authoritativeWorkspaceContext
1095
+ ? {
1096
+ projectId: workspaceContext.projectId,
1097
+ mainRootId: workspaceContext.sessionMainRootId,
1098
+ }
1099
+ : undefined,
1083
1100
  ...(options?.sessionBrief ? { sessionBrief: options.sessionBrief } : {}),
1084
1101
  llmModel: this.config.llm.model,
1085
1102
  llmProvider: this.config.llm.provider,
@@ -1104,7 +1121,7 @@ export class Engine {
1104
1121
  // the entire old topic. Fail open to full history if summarization fails.
1105
1122
  if (options?.archiveBeforeCurrentTurn && options.clientMessageId) {
1106
1123
  try {
1107
- const archived = await this.archiveTurnRange(session.state.sessionId, { start: 0, end: 0 }, {
1124
+ const anchors = {
1108
1125
  toClientMessageId: options.clientMessageId,
1109
1126
  ...(options.archiveBeforeCurrentTurn.fromClientMessageId
1110
1127
  ? {
@@ -1114,7 +1131,24 @@ export class Engine {
1114
1131
  ...(options.archiveBeforeCurrentTurn.segmentId
1115
1132
  ? { segmentId: options.archiveBeforeCurrentTurn.segmentId }
1116
1133
  : {}),
1117
- });
1134
+ };
1135
+ const before = estimateTokens(messages);
1136
+ if (options.archiveBeforeCurrentTurn.summary) {
1137
+ await this.appendArchiveMarker(session.state.sessionId, {
1138
+ ...anchors,
1139
+ summary: options.archiveBeforeCurrentTurn.summary,
1140
+ });
1141
+ }
1142
+ else {
1143
+ await this.archiveTurnRange(session.state.sessionId, { start: 0, end: 0 }, anchors);
1144
+ }
1145
+ // openRunSession captured the pre-marker replay. Rebuild from the
1146
+ // persisted marker so this very first post-boundary model call gets
1147
+ // the archived view rather than waiting until the following turn.
1148
+ messages =
1149
+ this.compactedMessagesBySession.get(session.state.sessionId) ??
1150
+ this.sessionManager.resumeForRun(session.state.sessionId).transcript.toMessages();
1151
+ const archived = { before, after: estimateTokens(messages) };
1118
1152
  if (archived.before > archived.after) {
1119
1153
  options.onStream?.({
1120
1154
  type: "context_compact",
@@ -1123,12 +1157,6 @@ export class Engine {
1123
1157
  after: archived.after,
1124
1158
  });
1125
1159
  }
1126
- // openRunSession captured the pre-marker replay. Rebuild from the
1127
- // persisted marker so this very first post-boundary model call gets
1128
- // the archived view rather than waiting until the following turn.
1129
- messages =
1130
- this.compactedMessagesBySession.get(session.state.sessionId) ??
1131
- this.sessionManager.resume(session.state.sessionId).transcript.toMessages();
1132
1160
  }
1133
1161
  catch (error) {
1134
1162
  logger.warn("engine.pre_run_archive.failed", {
@@ -1170,6 +1198,7 @@ export class Engine {
1170
1198
  options,
1171
1199
  session,
1172
1200
  cwd,
1201
+ workspaceContext,
1173
1202
  toolCtx,
1174
1203
  profile,
1175
1204
  profileParams,
@@ -1452,66 +1481,12 @@ export class Engine {
1452
1481
  }
1453
1482
  /** Attach a closed-set, host-routed Session message sender to this run. */
1454
1483
  attachSessionMessageService(toolCtx, session, options) {
1455
- const sourceSessionId = session.state.sessionId;
1456
- const sourceRoot = this.sessionManager.readSessionMainRoot(sourceSessionId);
1457
- if (!sourceRoot)
1458
- return;
1459
- if (!this.sessionMessageRouter)
1460
- return;
1461
- const catalog = [];
1462
- const seen = new Set();
1463
- const rawTargets = Array.isArray(options?.sessionMessageTargets)
1464
- ? [...options.sessionMessageTargets]
1465
- : [];
1466
- for (const raw of rawTargets.slice(0, 100)) {
1467
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
1468
- continue;
1469
- const candidate = raw;
1470
- const sessionId = typeof candidate.sessionId === "string" ? candidate.sessionId : "";
1471
- try {
1472
- assertSafeSessionId(sessionId);
1473
- }
1474
- catch {
1475
- continue;
1476
- }
1477
- if (seen.has(sessionId))
1478
- continue;
1479
- if (candidate.workspaceRoot !== sourceRoot)
1480
- continue;
1481
- const title = typeof candidate.title === "string" ? candidate.title.trim() : "";
1482
- if (!title || title.length > 512)
1483
- continue;
1484
- const workspaceProfile = typeof candidate.workspaceProfile === "string"
1485
- ? candidate.workspaceProfile.trim().slice(0, 256)
1486
- : "";
1487
- seen.add(sessionId);
1488
- catalog.push({
1489
- sessionId,
1490
- title,
1491
- workspaceRoot: sourceRoot,
1492
- ...(workspaceProfile ? { workspaceProfile } : {}),
1493
- });
1494
- }
1495
- const targets = catalog.filter((target) => target.sessionId !== sourceSessionId);
1496
- toolCtx.sessionMessages = {
1497
- targets,
1498
- send: async ({ targetSessionId, message }) => {
1499
- const target = targets.find((candidate) => candidate.sessionId === targetSessionId);
1500
- if (!target)
1501
- throw new Error("target Session is not in the host-authorized project list");
1502
- if (!message.trim())
1503
- throw new Error("message is required");
1504
- if (message.length > 48_000)
1505
- throw new Error("message exceeds 48000 characters");
1506
- await this.sessionMessageRouter({
1507
- sourceSessionId,
1508
- target,
1509
- message,
1510
- catalog,
1511
- });
1512
- return target;
1513
- },
1514
- };
1484
+ toolCtx.sessionMessages = createAuthorizedSessionMessageService({
1485
+ sessionManager: this.sessionManager,
1486
+ router: this.sessionMessageRouter,
1487
+ sourceSessionId: session.state.sessionId,
1488
+ rawTargets: options?.sessionMessageTargets,
1489
+ });
1515
1490
  }
1516
1491
  /**
1517
1492
  * Await the parallel prompt/context assembly (LLM client handshake, system
@@ -1645,12 +1620,13 @@ export class Engine {
1645
1620
  * lazily via `getSession` because it is opened after this wiring runs.
1646
1621
  */
1647
1622
  async wireRunSandboxToolContext(args) {
1648
- const { options, cwd, runPermissionMode, runPlanMode, profile, profileParams, sessionProfileOverrides, profileMemoryDir, getSession, reportResult, } = args;
1623
+ const { options, cwd, workspaceContext, runPermissionMode, runPlanMode, profile, profileParams, sessionProfileOverrides, profileMemoryDir, getSession, reportResult, } = args;
1649
1624
  // Resolve before constructing the child spawner: a parent sandbox may come
1650
1625
  // solely from project/user settings rather than config.sandbox, while a
1651
1626
  // child intentionally skips project settings. Passing the complete
1652
1627
  // effective config is what makes undefined role sandbox mean inherit.
1653
- const sandboxConfig = this.runEnvironmentResolver.resolveSandboxConfig(cwd);
1628
+ const sandboxRun = { cwd, workspaceContext };
1629
+ const sandboxConfig = this.runEnvironmentResolver.resolveSandboxConfig(sandboxRun);
1654
1630
  // Build the per-Engine ToolContext that will be threaded through every
1655
1631
  // tool call. Replaces the old module-level singleton setters used by
1656
1632
  // built-ins and product capabilities.
@@ -1659,6 +1635,7 @@ export class Engine {
1659
1635
  parentSandbox: sandboxConfig,
1660
1636
  presetName: this.preset.name,
1661
1637
  cwd,
1638
+ workspaceContext,
1662
1639
  permissionMode: runPermissionMode,
1663
1640
  modelPool: this.modelPool,
1664
1641
  parentStream: options?.onStream,
@@ -1685,7 +1662,7 @@ export class Engine {
1685
1662
  //
1686
1663
  // Backend is cached per runtime/engine so the capability probe runs once
1687
1664
  // per (mode, cwd) instead of every turn.
1688
- const sandboxBackend = await this.runEnvironmentResolver.resolveSandbox(cwd);
1665
+ const sandboxBackend = await this.runEnvironmentResolver.resolveSandbox(sandboxRun);
1689
1666
  // Observability: surface what sandbox actually applied this run — the
1690
1667
  // configured mode vs the resolved backend (auto may downgrade to off when
1691
1668
  // no OS backend is available) + the network policy. Without this you can't
@@ -1709,6 +1686,7 @@ export class Engine {
1709
1686
  ? sandboxBackend
1710
1687
  : { ...sandboxBackend, network: sandboxConfig.network },
1711
1688
  cwd,
1689
+ workspace: workspaceContext,
1712
1690
  shellEnv: this.runEnvironmentResolver.readShellEnv(cwd),
1713
1691
  profile,
1714
1692
  profileParams,
@@ -1965,7 +1943,7 @@ export class Engine {
1965
1943
  * this method — only the composer and tool defs cross back out.
1966
1944
  */
1967
1945
  async wireRunTooling(args) {
1968
- const { options, session, cwd, toolCtx, profile, profileParams, runWorkspaceProfile, profileMemoryDir, sessionProfileOverrides, runPlanMode, } = args;
1946
+ const { options, session, cwd, workspaceContext, toolCtx, profile, profileParams, runWorkspaceProfile, profileMemoryDir, sessionProfileOverrides, runPlanMode, } = args;
1969
1947
  const visibilityExplicitGoal = normalizeGoal(options?.goal);
1970
1948
  const visibilityLifecycle = session.state.goalLifecycle;
1971
1949
  const visibilityStoredGoal = visibilityLifecycle && isGoalLifecycleCurrent(visibilityLifecycle)
@@ -1984,6 +1962,7 @@ export class Engine {
1984
1962
  const profileCanUseSkills = !runAllowedToolNames || runAllowedToolNames.has(skillToolDef.name);
1985
1963
  const promptComposer = new PromptComposer(buildPromptComposerConfig({
1986
1964
  cwd,
1965
+ workspace: workspaceContext,
1987
1966
  model: this.config.llm.model,
1988
1967
  preset: this.preset,
1989
1968
  customSystemPrompt: profile?.disableInstructions
@@ -2018,9 +1997,11 @@ export class Engine {
2018
1997
  runAllowedToolNames.has("cancel_goal")
2019
1998
  ? { hasGoal: hasRunnableGoal }
2020
1999
  : undefined,
2021
- capabilityPromptSections: profile?.disableCapabilityContext
2022
- ? {}
2023
- : this.capabilityPromptSections,
2000
+ // Static sections named by the active preset are part of that preset's
2001
+ // contract. `disableCapabilityContext` suppresses ambient/dynamic
2002
+ // capability context, but removing these definitions leaves presets
2003
+ // with dangling section names (for example terminal-coding → coding).
2004
+ capabilityPromptSections: this.capabilityPromptSections,
2024
2005
  dynamicContextProviders: this.capabilityDynamicContextProviders,
2025
2006
  getSettingsManager: () => this.getSettingsManager(),
2026
2007
  toolCatalog: this.toolCatalog,
@@ -2144,6 +2125,7 @@ export class Engine {
2144
2125
  llm: this.config.llm,
2145
2126
  sessionId: sid,
2146
2127
  attachments: item.attachments,
2128
+ workspaceContext: toolCtx.workspace,
2147
2129
  });
2148
2130
  if (!steerImageInput.ok) {
2149
2131
  throw new Error(steerImageInput.result.text);
@@ -2513,6 +2495,27 @@ export class Engine {
2513
2495
  getConfig() {
2514
2496
  return this.config;
2515
2497
  }
2498
+ /** Release Engine-local registrations after its owning ChatSession is replaced. */
2499
+ async dispose() {
2500
+ if (this.disposed)
2501
+ return;
2502
+ if (this.runInProgress)
2503
+ throw new Error("Engine cannot be disposed while a run is in progress");
2504
+ this.disposed = true;
2505
+ try {
2506
+ const mcp = this.runtime?.mcpPool ?? this.mcpManager;
2507
+ await mcp?.unregisterOwner(this);
2508
+ }
2509
+ finally {
2510
+ this.hooks.clear();
2511
+ this.settingsHookHandles = [];
2512
+ this.workspaceAuthorities.clear();
2513
+ this.steerQueueBySid.clear();
2514
+ this.compactedMessagesBySession.clear();
2515
+ this.agentDefsCache = undefined;
2516
+ this.lastContextManager = undefined;
2517
+ }
2518
+ }
2516
2519
  /**
2517
2520
  * Config hot-reload "layer 2": merge a disk-default config patch into this
2518
2521
  * ALREADY-RUNNING session's `this.config`, reload settings hooks, and
@@ -2784,40 +2787,30 @@ export class Engine {
2784
2787
  }
2785
2788
  return had;
2786
2789
  }
2787
- /**
2788
- * Persist a workspace pointer through the Engine that owns the live bundle.
2789
- * Host-side workspace actions use this RPC-facing seam so advancing the disk
2790
- * revision also rebases the active run before its next progress write.
2791
- */
2792
2790
  setSessionWorkspace(sessionId, workspace) {
2793
- if (!sessionId || !this.sessionManager.exists(sessionId))
2794
- return null;
2795
- try {
2796
- const stateRevision = this.sessionManager.setSessionWorkspace(sessionId, workspace);
2797
- if (this.activeRunSession?.state.sessionId === sessionId) {
2798
- Object.assign(this.activeRunSession.state, { workspace, stateRevision });
2799
- }
2800
- return workspace;
2801
- }
2802
- catch {
2803
- return null;
2804
- }
2791
+ return setOwnedSessionWorkspace({
2792
+ sessionManager: this.sessionManager,
2793
+ activeState: this.activeRunSession?.state,
2794
+ sessionId,
2795
+ workspace,
2796
+ });
2797
+ }
2798
+ migrateSessionMainRoot(sessionId, project, mainRoot) {
2799
+ return migrateOwnedSessionMainRoot({
2800
+ sessionManager: this.sessionManager,
2801
+ activeState: this.activeRunSession?.state,
2802
+ authorities: this.workspaceAuthorities,
2803
+ sessionId,
2804
+ project,
2805
+ mainRoot,
2806
+ });
2805
2807
  }
2806
- /**
2807
- * Reset a session's workspace pointer back to its main root. If the session is
2808
- * actively running, mutate that live SessionBundle first so the run's next
2809
- * saveState cannot resurrect a stale worktree pointer.
2810
- */
2811
2808
  releaseSessionWorkspace(sessionId) {
2812
- if (!sessionId || !this.sessionManager.exists(sessionId))
2813
- return null;
2814
- const mainRoot = this.sessionManager.readSessionMainRoot(sessionId) ??
2815
- (this.activeRunSession?.state.sessionId === sessionId
2816
- ? this.activeRunSession.state.cwd
2817
- : undefined);
2818
- if (!mainRoot)
2819
- return null;
2820
- return this.setSessionWorkspace(sessionId, { root: mainRoot, kind: "main" });
2809
+ return releaseOwnedSessionWorkspace({
2810
+ sessionManager: this.sessionManager,
2811
+ activeState: this.activeRunSession?.state,
2812
+ sessionId,
2813
+ });
2821
2814
  }
2822
2815
  injectContext(sessionId, content) {
2823
2816
  const session = this.sessionManager.resume(sessionId);
@@ -3282,7 +3275,10 @@ export class Engine {
3282
3275
  const service = value({
3283
3276
  isSubAgent: this.config.isSubAgent === true,
3284
3277
  settings: this.getSettingsManager(),
3285
- resolveSandbox: (cwd) => this.runEnvironmentResolver.resolveSandbox(cwd),
3278
+ resolveSandbox: (cwd) => this.runEnvironmentResolver.resolveSandbox({
3279
+ cwd,
3280
+ workspaceContext: legacySingleRootWorkspace(cwd),
3281
+ }),
3286
3282
  readShellEnv: (cwd) => this.runEnvironmentResolver.readShellEnv(cwd),
3287
3283
  getSessionManager: () => this.sessionManager,
3288
3284
  });
@@ -3298,7 +3294,7 @@ export class Engine {
3298
3294
  capabilityServices,
3299
3295
  askUser: this.config.askUser,
3300
3296
  browser: this.config.browserBridge,
3301
- workspace: this.config.workspaceBridge,
3297
+ workspaceBridge: this.config.workspaceBridge,
3302
3298
  panels: this.config.panelBridge,
3303
3299
  injectCredentialToBrowser: this.config.injectCredentialToBrowser,
3304
3300
  isSubAgent: this.config.isSubAgent === true,
@@ -9,5 +9,6 @@ export interface InputAttachmentContext {
9
9
  export interface BuildInputAttachmentContextOptions {
10
10
  includeImageBytes?: boolean;
11
11
  expectedSessionId?: string;
12
+ workspaceRoots?: readonly string[];
12
13
  }
13
14
  export declare function buildInputAttachmentContext(attachments: readonly InputAttachmentMeta[] | undefined, cwd: string, options?: BuildInputAttachmentContextOptions): Promise<InputAttachmentContext>;
@@ -113,7 +113,11 @@ export async function buildInputAttachmentContext(attachments, cwd, options = {}
113
113
  });
114
114
  continue;
115
115
  }
116
- const policy = classifyPath(resolved, { workspaceRoot: cwdReal, operation: "read" });
116
+ const policy = classifyPath(resolved, {
117
+ workspaceRoot: cwdReal,
118
+ workspaceRoots: options.workspaceRoots,
119
+ operation: "read",
120
+ });
117
121
  if (policy.decision !== "allow") {
118
122
  errors.push(`attachment ${id === "(unknown)" ? displayPath : id} blocked by path policy: ${policy.reason}`);
119
123
  diagnostics.push({
@@ -141,7 +145,7 @@ export async function buildInputAttachmentContext(attachments, cwd, options = {}
141
145
  continue;
142
146
  }
143
147
  if (attachment.kind === "directory" || info.isDirectory()) {
144
- const tree = await directoryTree(realPath, cwdReal).catch((err) => ({
148
+ const tree = await directoryTree(realPath, policy.matchedRoot ?? cwdReal).catch((err) => ({
145
149
  lines: [`(directory tree unavailable: ${err.message})`],
146
150
  truncated: true,
147
151
  entryCount: 0,
@@ -2,6 +2,7 @@ import type { CredentialAccess } from "../credentials/access.js";
2
2
  import { type SandboxBackend, type SandboxConfig } from "../tool-system/sandbox/index.js";
3
3
  import type { EngineRuntime } from "./runtime.js";
4
4
  import type { EngineConfig } from "./types.js";
5
+ import type { WorkspaceContext } from "../workspace/workspace-context.js";
5
6
  type EnvironmentSettings = {
6
7
  get(): unknown;
7
8
  getForScope(scope: "user" | "project", cwd?: string): unknown;
@@ -13,14 +14,18 @@ export interface RunEnvironmentResolverDeps {
13
14
  runtime?: Pick<EngineRuntime, "resolveSandbox">;
14
15
  resolveBackend?: (config: SandboxConfig, cwd: string) => Promise<SandboxBackend>;
15
16
  }
17
+ export interface RunEnvironmentInput {
18
+ cwd: string;
19
+ workspaceContext: WorkspaceContext;
20
+ }
16
21
  export declare class RunEnvironmentResolver {
17
22
  private readonly deps;
18
23
  private readonly sandboxCache;
19
24
  private readonly resolveBackend;
20
25
  constructor(deps: RunEnvironmentResolverDeps);
21
- resolveSandboxConfig(cwd: string): SandboxConfig;
22
- resolveSandbox(cwd: string): Promise<SandboxBackend>;
23
- resolve(cwd: string): Promise<{
26
+ resolveSandboxConfig(run: RunEnvironmentInput): SandboxConfig;
27
+ resolveSandbox(run: RunEnvironmentInput): Promise<SandboxBackend>;
28
+ resolve(run: RunEnvironmentInput): Promise<{
24
29
  sandbox: SandboxBackend;
25
30
  sandboxConfig: SandboxConfig;
26
31
  shellEnv?: Record<string, string>;
@@ -1,6 +1,28 @@
1
- import { resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
1
+ import { expandPath, resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
2
2
  import { resolveSandboxConfig } from "./sandbox-config.js";
3
3
  import { sandboxCacheKey } from "./sandbox-cache-key.js";
4
+ import { canonicalKey, canonicalPath } from "../workspace/canonical-key.js";
5
+ function appendWorkspaceRoots(config, run) {
6
+ const writableRoots = [];
7
+ const seen = new Set();
8
+ // Key configured roots after placeholder expansion so `${workspace}` wins
9
+ // over the equivalent primary path. Keep the first spelling and order.
10
+ for (const root of config.writableRoots) {
11
+ const key = canonicalKey(expandPath(root, run.cwd));
12
+ if (seen.has(key))
13
+ continue;
14
+ seen.add(key);
15
+ writableRoots.push(root);
16
+ }
17
+ for (const root of run.workspaceContext.roots) {
18
+ const key = canonicalKey(root.path);
19
+ if (seen.has(key))
20
+ continue;
21
+ seen.add(key);
22
+ writableRoots.push(canonicalPath(root.path));
23
+ }
24
+ return { ...config, writableRoots };
25
+ }
4
26
  export class RunEnvironmentResolver {
5
27
  deps;
6
28
  sandboxCache = new Map();
@@ -9,7 +31,8 @@ export class RunEnvironmentResolver {
9
31
  this.deps = deps;
10
32
  this.resolveBackend = deps.resolveBackend ?? resolveSandboxBackend;
11
33
  }
12
- resolveSandboxConfig(cwd) {
34
+ resolveSandboxConfig(run) {
35
+ const { cwd } = run;
13
36
  const config = this.deps.config();
14
37
  let projectSandbox;
15
38
  let globalSandbox;
@@ -24,10 +47,11 @@ export class RunEnvironmentResolver {
24
47
  catch {
25
48
  // Missing settings fall through to the run default.
26
49
  }
27
- return resolveSandboxConfig(config.sandbox, projectSandbox, globalSandbox, config.headless === true);
50
+ return appendWorkspaceRoots(resolveSandboxConfig(config.sandbox, projectSandbox, globalSandbox, config.headless === true), run);
28
51
  }
29
- resolveSandbox(cwd) {
30
- const config = this.resolveSandboxConfig(cwd);
52
+ resolveSandbox(run) {
53
+ const { cwd } = run;
54
+ const config = this.resolveSandboxConfig(run);
31
55
  if (this.deps.runtime)
32
56
  return this.deps.runtime.resolveSandbox(config, cwd);
33
57
  const key = sandboxCacheKey(config, cwd);
@@ -42,9 +66,10 @@ export class RunEnvironmentResolver {
42
66
  }
43
67
  return cached;
44
68
  }
45
- async resolve(cwd) {
46
- const sandboxConfig = this.resolveSandboxConfig(cwd);
47
- const backend = await this.resolveSandbox(cwd);
69
+ async resolve(run) {
70
+ const { cwd } = run;
71
+ const sandboxConfig = this.resolveSandboxConfig(run);
72
+ const backend = await this.resolveSandbox(run);
48
73
  return {
49
74
  sandbox: backend.name === "off" ? backend : { ...backend, network: sandboxConfig.network },
50
75
  sandboxConfig,
@@ -18,5 +18,6 @@ export declare function prepareRunImageInput(args: {
18
18
  llm: Pick<LLMConfig, "provider" | "providerKind" | "model">;
19
19
  sessionId?: string;
20
20
  attachments?: readonly InputAttachmentMeta[];
21
+ workspaceContext?: import("../workspace/workspace-context.js").WorkspaceContext;
21
22
  }): Promise<PrepareRunImageInputResult>;
22
23
  export declare function buildRunUserMessageContent(parsedTask: ParsedTask, cwd: string, taskText: string): string | ContentBlock[];
@@ -38,6 +38,7 @@ export async function prepareRunImageInput(args) {
38
38
  attachmentContext = await buildInputAttachmentContext(args.attachments, cwd, {
39
39
  includeImageBytes: cap.supportsVision,
40
40
  expectedSessionId: sessionId,
41
+ workspaceRoots: args.workspaceContext?.roots.map((root) => root.path),
41
42
  });
42
43
  }
43
44
  catch (err) {
@@ -1,4 +1,4 @@
1
- import type { Message, SessionKind } from "../types.js";
1
+ import type { Message, SessionKind, SessionProjectBinding } from "../types.js";
2
2
  import type { SessionBundle, SessionManager } from "../session/session-manager.js";
3
3
  import type { ParsedTask } from "./parse-task.js";
4
4
  import type { buildRunUserMessageContent } from "./run-image-input.js";
@@ -13,6 +13,7 @@ export interface OpenRunSessionArgs {
13
13
  cwd: string;
14
14
  sessionKind: SessionKind;
15
15
  sessionWorkspaceProfile: string | undefined;
16
+ projectBinding?: SessionProjectBinding;
16
17
  /** Standing brief resent by the host each turn until the Session persists it. */
17
18
  sessionBrief?: string;
18
19
  llmModel: string;
@@ -29,7 +29,10 @@ export function openRunSession(args) {
29
29
  };
30
30
  if (options?.sessionId && args.sessionManager.exists(options.sessionId)) {
31
31
  resumedFromDisk = true;
32
- session = args.sessionManager.resume(options.sessionId);
32
+ session = args.sessionManager.resumeForRun(options.sessionId);
33
+ if (!session.state.project && args.projectBinding) {
34
+ args.sessionManager.saveStateOrUpdateFields(session.state, { project: args.projectBinding });
35
+ }
33
36
  const cachedCompacted = args.cachedCompactedMessages;
34
37
  messages = cachedCompacted ? [...cachedCompacted] : session.transcript.toMessages();
35
38
  // If the previous run was Ctrl+C'd or crashed between an assistant
@@ -114,6 +117,9 @@ export function openRunSession(args) {
114
117
  // Cold start: shape (2) reuses the host-supplied sid; shape (3)
115
118
  // lets sessionManager generate one with nanoid.
116
119
  session = args.sessionManager.create(args.cwd, args.llmModel, args.llmProvider, options?.sessionId, args.isSubAgent ? getCurrentSid() : undefined, args.isSubAgent ? "subagent" : args.origin, args.sessionKind, options?.ephemeral === true);
120
+ if (args.projectBinding) {
121
+ args.sessionManager.saveStateOrUpdateFields(session.state, { project: args.projectBinding });
122
+ }
117
123
  const userMsg = { role: "user", content: args.userMessageContent };
118
124
  claimClientMessageId(session, options?.clientMessageId, "submit");
119
125
  if (args.parsedTask.hasImages)
@@ -16,6 +16,7 @@ export declare function resolveRunProfileState(args: {
16
16
  }): RunProfileState;
17
17
  export interface RunPromptComposerConfigInput {
18
18
  cwd: ComposerOptions["cwd"];
19
+ workspace: ComposerOptions["workspace"];
19
20
  /** Standing brief persisted on the Session (see SessionState.sessionBrief). */
20
21
  sessionBrief?: ComposerOptions["sessionBrief"];
21
22
  model: ComposerOptions["model"];
@@ -23,9 +23,10 @@ export function resolveRunProfileState(args) {
23
23
  }
24
24
  /** Build the prompt-composer options for a run without capturing the Engine facade. */
25
25
  export function buildPromptComposerConfig(args) {
26
- const { cwd, model, preset, customSystemPrompt, appendSystemPrompt, responseLanguage, userProfile, workspaceProfile, sessionBrief, profileMemoryDir, instructionCompatFileNames, instructionBoundaryFinder, disabledSkills, disabledPlugins, skillAllowlist, memoriesMaxAgeDays, memoryCurrentProjectOnly, disableInstructions, disableMemoryContext, disableCapabilityContext, disableSourcesContext, goalToolState, capabilityPromptSections, dynamicContextProviders, getSettingsManager, toolCatalog, } = args;
26
+ const { cwd, workspace, model, preset, customSystemPrompt, appendSystemPrompt, responseLanguage, userProfile, workspaceProfile, sessionBrief, profileMemoryDir, instructionCompatFileNames, instructionBoundaryFinder, disabledSkills, disabledPlugins, skillAllowlist, memoriesMaxAgeDays, memoryCurrentProjectOnly, disableInstructions, disableMemoryContext, disableCapabilityContext, disableSourcesContext, goalToolState, capabilityPromptSections, dynamicContextProviders, getSettingsManager, toolCatalog, } = args;
27
27
  return {
28
28
  cwd,
29
+ workspace,
29
30
  model,
30
31
  preset,
31
32
  customSystemPrompt,
@@ -29,6 +29,7 @@ export declare function buildRunToolContext(args: {
29
29
  agentDefinitions: ToolContext["agentDefinitions"];
30
30
  sandbox: ToolContext["sandbox"];
31
31
  cwd: string;
32
+ workspace: ToolContext["workspace"];
32
33
  shellEnv: ToolContext["shellEnv"];
33
34
  profile: RunBehaviorProfile | undefined;
34
35
  profileParams: Readonly<Record<string, unknown>>;
@@ -73,6 +74,7 @@ export declare function connectRunMcp(args: {
73
74
  */
74
75
  export interface ToolVisibilityInputs {
75
76
  cwd: string;
77
+ workspace?: ToolContext["workspace"];
76
78
  hasGoal: boolean;
77
79
  sessionId?: string;
78
80
  settingsScope?: SettingsScope;