@cjhyy/code-shell-core 0.9.6 → 0.9.7

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 (55) hide show
  1. package/dist/automation/scheduler.d.ts +3 -0
  2. package/dist/automation/scheduler.js +21 -0
  3. package/dist/context/manager.d.ts +8 -2
  4. package/dist/context/manager.js +20 -4
  5. package/dist/context/notes.d.ts +39 -0
  6. package/dist/context/notes.js +314 -0
  7. package/dist/engine/engine.d.ts +5 -4
  8. package/dist/engine/engine.js +79 -11
  9. package/dist/engine/run-tooling.d.ts +3 -0
  10. package/dist/engine/run-tooling.js +25 -23
  11. package/dist/engine/run-types.d.ts +4 -0
  12. package/dist/engine/subagent-spawner.d.ts +3 -0
  13. package/dist/engine/subagent-spawner.js +48 -17
  14. package/dist/engine/turn-loop.d.ts +10 -0
  15. package/dist/engine/turn-loop.js +91 -19
  16. package/dist/engine/types.d.ts +19 -0
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/prompt/section-loader.js +1 -0
  20. package/dist/prompt/sections/browser.md +4 -2
  21. package/dist/prompt/sections/context-notes.md +9 -0
  22. package/dist/protocol/server.d.ts +1 -0
  23. package/dist/protocol/server.js +207 -19
  24. package/dist/protocol/types.d.ts +2 -0
  25. package/dist/session/session-manager.js +8 -7
  26. package/dist/session/transcript.d.ts +17 -0
  27. package/dist/session/transcript.js +271 -16
  28. package/dist/settings/schema.d.ts +9 -0
  29. package/dist/settings/schema.js +4 -0
  30. package/dist/themes/paths.js +20 -1
  31. package/dist/tool-system/browser-bridge.d.ts +3 -1
  32. package/dist/tool-system/browser-discovery.d.ts +6 -0
  33. package/dist/tool-system/browser-discovery.js +17 -0
  34. package/dist/tool-system/builtin/browser-tools.js +12 -8
  35. package/dist/tool-system/builtin/context-notes.d.ts +12 -0
  36. package/dist/tool-system/builtin/context-notes.js +188 -0
  37. package/dist/tool-system/builtin/index.js +47 -0
  38. package/dist/tool-system/builtin/mcp-tools.d.ts +5 -3
  39. package/dist/tool-system/builtin/mcp-tools.js +10 -10
  40. package/dist/tool-system/builtin/tool-search.js +15 -3
  41. package/dist/tool-system/context.d.ts +9 -0
  42. package/dist/tool-system/executor.js +5 -3
  43. package/dist/tool-system/mcp-compat.d.ts +3 -0
  44. package/dist/tool-system/mcp-compat.js +51 -0
  45. package/dist/tool-system/mcp-manager.d.ts +27 -26
  46. package/dist/tool-system/mcp-manager.js +273 -111
  47. package/dist/tool-system/mcp-workspace.d.ts +18 -0
  48. package/dist/tool-system/mcp-workspace.js +56 -0
  49. package/dist/tool-system/permission.d.ts +6 -0
  50. package/dist/tool-system/permission.js +45 -9
  51. package/dist/tool-system/plan-mode-allowlist.js +5 -0
  52. package/dist/tool-system/sandbox/seatbelt.js +71 -2
  53. package/dist/tool-system/session-tool-host.js +9 -1
  54. package/dist/types.d.ts +4 -2
  55. package/package.json +1 -1
@@ -20,6 +20,8 @@ import { loadPluginHooks } from "../plugins/loadPluginHooks.js";
20
20
  import { pluginAgentDirs } from "../plugins/installer/loadPluginAgents.js";
21
21
  import { runShellHook, shellHookMatches } from "../hooks/shell-runner.js";
22
22
  import { ContextManager } from "../context/manager.js";
23
+ import { SessionContextNotes } from "../context/notes.js";
24
+ import { loadSection } from "../prompt/section-loader.js";
23
25
  import { CONTEXT_PACKAGE_MAX_OUTPUT_TOKENS, buildAnchoredSummaryMessage, buildContextPackagePromptFromSerialized, estimateTokens, groupMessagesByApiRound, serializeContextPackageMessages, clampContextRatios as clampContextRatiosImpl, } from "../context/compaction.js";
24
26
  import { PromptComposer } from "../prompt/composer.js";
25
27
  import { SessionManager, isEphemeralSessionState, sessionsRoot, } from "../session/session-manager.js";
@@ -699,6 +701,9 @@ export class Engine {
699
701
  setBrowserBridge(bridge) {
700
702
  this.config.browserBridge = bridge;
701
703
  }
704
+ setChildHostBindings(factory) {
705
+ this.config.createChildHostBindings = factory;
706
+ }
702
707
  /** Inject the host-backed workspace bridge after construction. */
703
708
  setWorkspaceBridge(bridge) {
704
709
  this.config.workspaceBridge = bridge;
@@ -994,6 +999,12 @@ export class Engine {
994
999
  }
995
1000
  return sessionProfile ?? explicitProfile;
996
1001
  }
1002
+ resolveContextStrategy(profile) {
1003
+ return (this.config.contextStrategy ??
1004
+ this.getSettingsManager().get().context?.strategy ??
1005
+ profile?.contextStrategy ??
1006
+ "summary");
1007
+ }
997
1008
  async runExclusive(task, options) {
998
1009
  // Freeze permission context once, before the first await. Per-turn protocol
999
1010
  // overrides live only for this run; persistent setPermissionMode/setPlanMode
@@ -1125,6 +1136,9 @@ export class Engine {
1125
1136
  const { messages: openedMessages, freshImageMessage, resumedFromDisk, claimClientMessageId, releaseClientMessageId, } = openedResult.opened;
1126
1137
  const session = openedResult.opened.session;
1127
1138
  let messages = openedMessages;
1139
+ toolCtx.contextStrategy = this.resolveContextStrategy(profile);
1140
+ const contextNotes = toolCtx.contextStrategy === "notes" ? new SessionContextNotes(session.transcript) : undefined;
1141
+ toolCtx.contextNotes = contextNotes;
1128
1142
  // A host-owned time/topic boundary must be applied only after the current
1129
1143
  // user message exists in the transcript (it is the exclusive end anchor),
1130
1144
  // yet before this turn assembles prompts or calls the model. Doing this as
@@ -1145,12 +1159,21 @@ export class Engine {
1145
1159
  : {}),
1146
1160
  };
1147
1161
  const before = estimateTokens(messages);
1162
+ let archiveStrategy = "range";
1148
1163
  if (options.archiveBeforeCurrentTurn.summary) {
1149
1164
  await this.appendArchiveMarker(session.state.sessionId, {
1150
1165
  ...anchors,
1151
1166
  summary: options.archiveBeforeCurrentTurn.summary,
1152
1167
  });
1153
1168
  }
1169
+ else if (!freshImageMessage &&
1170
+ contextNotes &&
1171
+ this.tryNotesRollover(contextNotes, messages)) {
1172
+ // The note covers its own cursor; any newer messages remain verbatim.
1173
+ this.compactedMessagesBySession.delete(session.state.sessionId);
1174
+ session.state.contextUsageAnchor = undefined;
1175
+ archiveStrategy = "notes";
1176
+ }
1154
1177
  else {
1155
1178
  await this.archiveTurnRange(session.state.sessionId, { start: 0, end: 0 }, anchors);
1156
1179
  }
@@ -1164,7 +1187,7 @@ export class Engine {
1164
1187
  if (archived.before > archived.after) {
1165
1188
  options.onStream?.({
1166
1189
  type: "context_compact",
1167
- strategy: "range",
1190
+ strategy: archiveStrategy,
1168
1191
  before: archived.before,
1169
1192
  after: archived.after,
1170
1193
  });
@@ -1219,7 +1242,7 @@ export class Engine {
1219
1242
  sessionProfileOverrides,
1220
1243
  runPlanMode,
1221
1244
  });
1222
- const { llmClient, fullSystemPrompt, dynamicContextMsg, userContextMsg } = await this.assembleRunPrompts({
1245
+ const { llmClient, fullSystemPrompt, dynamicContextMsg, userContextMsg, retainedContextMessages, } = await this.assembleRunPrompts({
1223
1246
  session,
1224
1247
  messages,
1225
1248
  hookMessages,
@@ -1269,6 +1292,7 @@ export class Engine {
1269
1292
  releaseClientMessageId,
1270
1293
  freshImageMessage,
1271
1294
  dynamicContextMsg,
1295
+ retainedContextMessages,
1272
1296
  });
1273
1297
  let result;
1274
1298
  let firstGoalTermination;
@@ -1524,17 +1548,21 @@ export class Engine {
1524
1548
  promptComposer.buildDynamicContextMessage(),
1525
1549
  ]);
1526
1550
  const fullSystemPrompt = composeRunSystemPrompt({
1527
- baseSystemPrompt,
1551
+ baseSystemPrompt: toolDefs.some((tool) => tool.name === "SaveContextNote")
1552
+ ? `${baseSystemPrompt}\n\n${loadSection("context-notes")}`
1553
+ : baseSystemPrompt,
1528
1554
  profile,
1529
1555
  profileParams,
1530
1556
  });
1531
1557
  const userContextMsg = promptComposer.buildUserContextMessage();
1558
+ const priorMessages = new Set(messages);
1532
1559
  assembleRunMessages({
1533
1560
  messages,
1534
1561
  userContextMsg,
1535
1562
  hookMessages,
1536
1563
  dynamicContextMsg,
1537
1564
  });
1565
+ const retainedContextMessages = messages.filter((message) => !priorMessages.has(message) && message !== dynamicContextMsg);
1538
1566
  this.lastSessionId = session.state.sessionId;
1539
1567
  this.lastMessages = messages;
1540
1568
  // Wire up LLM summarization for context compaction
@@ -1550,7 +1578,13 @@ export class Engine {
1550
1578
  contextManager.initReplacementStateFromMessages(messages);
1551
1579
  // Two summarizers with DIFFERENT quality needs (see the run loop wiring for
1552
1580
  // the aux-model summarizer set up alongside the ModelFacade).
1553
- return { llmClient, fullSystemPrompt, dynamicContextMsg, userContextMsg };
1581
+ return {
1582
+ llmClient,
1583
+ fullSystemPrompt,
1584
+ dynamicContextMsg,
1585
+ userContextMsg,
1586
+ retainedContextMessages,
1587
+ };
1554
1588
  }
1555
1589
  /**
1556
1590
  * Seed the run's {@link ContextManager}, emit the early `session_started`
@@ -1651,6 +1685,7 @@ export class Engine {
1651
1685
  // built-ins and product capabilities.
1652
1686
  const subAgentSpawner = createSubAgentSpawner({
1653
1687
  parentConfig: this.config,
1688
+ getParentSessionId: () => getSession().state.sessionId,
1654
1689
  parentSandbox: sandboxConfig,
1655
1690
  presetName: this.preset.name,
1656
1691
  cwd,
@@ -1667,7 +1702,12 @@ export class Engine {
1667
1702
  createChild: (config) => new Engine(config),
1668
1703
  runChild: async (config, childTask, childOptions) => {
1669
1704
  const child = new Engine(config);
1670
- return child.run(childTask, childOptions);
1705
+ try {
1706
+ return await child.run(childTask, childOptions);
1707
+ }
1708
+ finally {
1709
+ await child.dispose();
1710
+ }
1671
1711
  },
1672
1712
  },
1673
1713
  });
@@ -1723,7 +1763,7 @@ export class Engine {
1723
1763
  * phases consume cross back out.
1724
1764
  */
1725
1765
  async wireRunLoop(args) {
1726
- const { session, sid, task, cwd, options, profileMaxTurns, toolCtx, toolExecutor, contextManager, llmClient, auxSummaryClient, fullSystemPrompt, toolDefs, claimClientMessageId, releaseClientMessageId, freshImageMessage, dynamicContextMsg, } = args;
1766
+ const { session, sid, task, cwd, options, profileMaxTurns, toolCtx, toolExecutor, contextManager, llmClient, auxSummaryClient, fullSystemPrompt, toolDefs, claimClientMessageId, releaseClientMessageId, freshImageMessage, dynamicContextMsg, retainedContextMessages, } = args;
1727
1767
  // eslint-disable-next-line prefer-const
1728
1768
  let turnLoop;
1729
1769
  const accounting = createRunUsageAccounting({
@@ -1846,6 +1886,7 @@ export class Engine {
1846
1886
  normalizedGoal,
1847
1887
  freshImageMessage,
1848
1888
  dynamicContextMsg,
1889
+ retainedContextMessages,
1849
1890
  usageBaseline,
1850
1891
  getRunUsage,
1851
1892
  recordCumulativeUsage,
@@ -2031,6 +2072,7 @@ export class Engine {
2031
2072
  const mcpServers = this.config.mcpServers ?? {};
2032
2073
  const mcpDisabled = profile?.disableMcp === true;
2033
2074
  await connectRunMcp({
2075
+ toolContext: toolCtx,
2034
2076
  mcpServers,
2035
2077
  mcpDisabled,
2036
2078
  getManager: () => this.mcpManager,
@@ -2094,13 +2136,17 @@ export class Engine {
2094
2136
  * published through the `publishGoalJudgeContext` callback.
2095
2137
  */
2096
2138
  buildTurnLoop(args) {
2097
- const { modelFacade, toolExecutor, contextManager, session, fullSystemPrompt, toolDefs, sid, options, profileMaxTurns, cwd, claimClientMessageId, releaseClientMessageId, toolCtx, persistedRunGoal, goalHookHandler, normalizedGoal, freshImageMessage, dynamicContextMsg, usageBaseline, getRunUsage, recordCumulativeUsage, publishGoalJudgeContext, } = args;
2139
+ const { modelFacade, toolExecutor, contextManager, session, fullSystemPrompt, toolDefs, sid, options, profileMaxTurns, cwd, claimClientMessageId, releaseClientMessageId, toolCtx, persistedRunGoal, goalHookHandler, normalizedGoal, freshImageMessage, dynamicContextMsg, retainedContextMessages, usageBaseline, getRunUsage, recordCumulativeUsage, publishGoalJudgeContext, } = args;
2098
2140
  // Surface compaction events to the UI so the user knows when context was trimmed.
2099
2141
  // Buffer the most recent event so TurnLoop can drain it and emit the
2100
2142
  // post_compact hook on the next turn (ContextManager itself doesn't
2101
2143
  // know about HookRegistry — the buffer is the seam).
2102
2144
  let pendingCompactInfo = null;
2103
2145
  contextManager.setOnCompact((info) => {
2146
+ if (info.strategy === "notes") {
2147
+ session.state.contextUsageAnchor = undefined;
2148
+ this.compactedMessagesBySession.delete(sid);
2149
+ }
2104
2150
  pendingCompactInfo = info;
2105
2151
  options?.onStream?.({ type: "context_compact", ...info });
2106
2152
  });
@@ -2109,6 +2155,7 @@ export class Engine {
2109
2155
  model: modelFacade,
2110
2156
  toolExecutor,
2111
2157
  contextManager,
2158
+ contextNotes: toolCtx.contextNotes instanceof SessionContextNotes ? toolCtx.contextNotes : undefined,
2112
2159
  hooks: this.hooks,
2113
2160
  transcript: session.transcript,
2114
2161
  systemPrompt: fullSystemPrompt,
@@ -2228,6 +2275,7 @@ export class Engine {
2228
2275
  signal: options?.signal,
2229
2276
  freshImageMessages: freshImageMessage ? [freshImageMessage] : undefined,
2230
2277
  volatileContextMessages: dynamicContextMsg ? [dynamicContextMsg] : undefined,
2278
+ retainedContextMessages,
2231
2279
  // Goal mode: the active goal is surfaced to the on_stop handler via
2232
2280
  // ctx.data.goal; the GoalStopHook (registered above) judges it.
2233
2281
  goal: normalizedGoal,
@@ -2849,10 +2897,17 @@ export class Engine {
2849
2897
  cached.push({ role: "assistant", content });
2850
2898
  }
2851
2899
  }
2852
- /**
2853
- * Force context compaction on a session.
2854
- * Returns token stats before/after.
2855
- */
2900
+ /** Attempt the durable note path without issuing a separate model request. */
2901
+ tryNotesRollover(notes, messages) {
2902
+ try {
2903
+ notes.requestRollover();
2904
+ return notes.applyRollover(messages);
2905
+ }
2906
+ catch {
2907
+ return undefined;
2908
+ }
2909
+ }
2910
+ /** Force context compaction and return token stats before/after. */
2856
2911
  async forceCompact(sessionId) {
2857
2912
  const effectiveSessionId = sessionId ?? this.lastSessionId;
2858
2913
  if (!effectiveSessionId) {
@@ -2861,6 +2916,19 @@ export class Engine {
2861
2916
  const session = this.sessionManager.resume(effectiveSessionId);
2862
2917
  const sourceMessages = this.compactedMessagesBySession.get(effectiveSessionId) ?? session.transcript.toMessages();
2863
2918
  const before = estimateTokens(sourceMessages);
2919
+ const profile = [...this.behaviorProfiles.values()].find((candidate) => candidate.activateForSessionKinds?.includes(session.state.kind ?? "work"));
2920
+ if (this.resolveContextStrategy(profile) === "notes") {
2921
+ const compacted = this.tryNotesRollover(new SessionContextNotes(session.transcript), sourceMessages);
2922
+ if (compacted) {
2923
+ this.compactedMessagesBySession.set(effectiveSessionId, compacted);
2924
+ session.state.contextUsageAnchor = undefined;
2925
+ this.lastContextManager = undefined;
2926
+ this.persistRunProgress(session.state);
2927
+ this.lastSessionId = effectiveSessionId;
2928
+ this.lastMessages = compacted;
2929
+ return { before, after: estimateTokens(compacted), strategy: "notes" };
2930
+ }
2931
+ }
2864
2932
  const contextManager = await this.prepareContextManagerForSession(effectiveSessionId, session, sourceMessages, "force_compact");
2865
2933
  // Manual /compact emits its UI boundary at the protocol layer from the
2866
2934
  // final before/after result. Capture the tier here, but avoid reusing a
@@ -64,6 +64,7 @@ export declare function connectRunMcp(args: {
64
64
  runtimePool: MCPManager | undefined;
65
65
  toolRegistry: ToolRegistry;
66
66
  engineForConnect: Parameters<MCPManager["connectAll"]>[1];
67
+ toolContext?: Parameters<MCPManager["connectAll"]>[3];
67
68
  emitNotificationHook: (payload: Record<string, unknown>) => void;
68
69
  }): Promise<void>;
69
70
  /**
@@ -74,12 +75,14 @@ export declare function connectRunMcp(args: {
74
75
  */
75
76
  export interface ToolVisibilityInputs {
76
77
  cwd: string;
78
+ contextStrategy?: "summary" | "notes";
77
79
  workspace?: ToolContext["workspace"];
78
80
  hasGoal: boolean;
79
81
  sessionId?: string;
80
82
  settingsScope?: SettingsScope;
81
83
  /** Host identity a guard may require, e.g. Panel needs `"desktop"`. */
82
84
  host?: string;
85
+ hasBrowserAutomation?: boolean;
83
86
  isSubAgent?: boolean;
84
87
  behaviorProfile?: string;
85
88
  sessionMessageTargets?: readonly import("../session/session-message.js").SessionMessageTarget[];
@@ -126,30 +126,26 @@ export function buildRunPermissionPipeline(args) {
126
126
  }
127
127
  /** engine.ts L1986-2003 —— MCP 连接(Runtime 池优先,回退 per-Engine)。 */
128
128
  export async function connectRunMcp(args) {
129
- // Connect MCP servers (if configured and not already connected).
130
- // B1: prefer the Runtime-owned MCPManager so all sessions in a
131
- // worker share one set of connections. Falling back to a
132
- // per-Engine instance keeps the null-runtime path (tests, ad-hoc
133
- // scripts) working.
134
- const mcpServers = args.mcpServers;
135
- const mcpDisabled = args.mcpDisabled;
136
- if (!mcpDisabled && Object.keys(mcpServers).length > 0 && !args.getManager()) {
137
- if (args.runtimePool) {
138
- args.setManager(args.runtimePool);
139
- }
140
- else {
141
- args.setManager(new MCPManager(args.toolRegistry));
142
- }
143
- await args.getManager().connectAll(mcpServers, args.engineForConnect, (event) => {
144
- // Fire-and-forget onto the notification hook: hosts/plugins learn
145
- // about MCP availability changes without polling the pool.
146
- args.emitNotificationHook({
147
- kind: event.type,
148
- server: event.server,
149
- ...(event.error ? { error: event.error } : {}),
150
- });
151
- });
129
+ // A pool may outlive a run and the engine registry may predate discovery.
130
+ // Refresh scope and the engine's MCP-only view on every run (including cwd
131
+ // and project changes); identical scopes still share a single transport.
132
+ const enabledServers = args.mcpDisabled ? {} : args.mcpServers;
133
+ if (!args.getManager() && Object.keys(enabledServers).length > 0) {
134
+ args.setManager(args.runtimePool ?? new MCPManager(args.toolRegistry));
152
135
  }
136
+ const manager = args.getManager();
137
+ if (!manager)
138
+ return;
139
+ await manager.connectAll(enabledServers, args.engineForConnect, (event) => {
140
+ args.emitNotificationHook({
141
+ kind: event.type,
142
+ server: event.server,
143
+ ...(event.error ? { error: event.error } : {}),
144
+ });
145
+ }, args.toolContext);
146
+ manager.syncToolsToRegistry(args.toolRegistry, args.toolContext, new Set(Object.entries(enabledServers)
147
+ .filter(([, config]) => config.enabled !== false)
148
+ .map(([name]) => name)));
153
149
  }
154
150
  /**
155
151
  * Build the availability-guard context for a tool surface.
@@ -167,11 +163,15 @@ export async function connectRunMcp(args) {
167
163
  export function buildToolVisibility(inputs) {
168
164
  return {
169
165
  cwd: inputs.cwd,
166
+ ...(inputs.contextStrategy ? { contextStrategy: inputs.contextStrategy } : {}),
170
167
  ...(inputs.workspace ? { workspace: inputs.workspace } : {}),
171
168
  hasGoal: inputs.hasGoal,
172
169
  ...(inputs.sessionId ? { sessionId: inputs.sessionId } : {}),
173
170
  ...(inputs.settingsScope ? { settingsScope: inputs.settingsScope } : {}),
174
171
  ...(inputs.host !== undefined ? { host: inputs.host } : {}),
172
+ ...(inputs.hasBrowserAutomation !== undefined
173
+ ? { hasBrowserAutomation: inputs.hasBrowserAutomation }
174
+ : {}),
175
175
  ...(inputs.isSubAgent !== undefined ? { isSubAgent: inputs.isSubAgent } : {}),
176
176
  ...(inputs.behaviorProfile !== undefined ? { behaviorProfile: inputs.behaviorProfile } : {}),
177
177
  ...(inputs.sessionMessageTargets?.length
@@ -188,11 +188,13 @@ export function assembleRunToolDefs(args) {
188
188
  const guardCwd = args.guardCwd;
189
189
  const toolVisibility = buildToolVisibility({
190
190
  cwd: guardCwd,
191
+ contextStrategy: toolCtx.contextNotes ? toolCtx.contextStrategy : undefined,
191
192
  workspace: toolCtx.workspace,
192
193
  hasGoal: args.hasRunnableGoal,
193
194
  sessionId: toolCtx.sessionId,
194
195
  settingsScope: args.settingsScope,
195
196
  host: args.builtinToolHost,
197
+ hasBrowserAutomation: toolCtx.browser !== undefined,
196
198
  isSubAgent: args.isSubAgent,
197
199
  behaviorProfile: args.behaviorProfileId,
198
200
  sessionMessageTargets: toolCtx.sessionMessages?.targets,
@@ -22,6 +22,8 @@ export type RunBehaviorMode = string;
22
22
  */
23
23
  export interface RunBehaviorProfile {
24
24
  id: string;
25
+ /** Default context policy; explicit Engine/settings choices take precedence. */
26
+ contextStrategy?: "summary" | "notes";
25
27
  /** Appended to the system prompt after config.appendSystemPrompt. */
26
28
  systemPromptAppend?: string;
27
29
  /** Hard tool allowlist for the run (model visibility + execution gate). */
@@ -186,6 +188,8 @@ export type AgentRuntimeProgressEvent = {
186
188
  usage: TokenUsage;
187
189
  };
188
190
  export interface ChildEngineRuntime {
191
+ /** Release child-local resources after the entire supervised lifetime settles. */
192
+ dispose?(): void | Promise<void>;
189
193
  run(task: string, options?: EngineRunOptions): Promise<EngineResult>;
190
194
  setAgentControlStateListener(listener: ((state: LiveChildState) => void) | undefined): void;
191
195
  setAgentDirectionsDeliveredListener?(listener: ((envelopeIds: string[]) => void) | undefined): void;
@@ -17,6 +17,9 @@ export declare function resolveChildMcpServers(allowlist: string[] | undefined,
17
17
  export declare function wrapChildStream(destination: StreamCallback | undefined, agentId: string): StreamCallback | undefined;
18
18
  export interface CreateSubAgentSpawnerDeps {
19
19
  parentConfig: EngineConfig;
20
+ parentSessionId?: string;
21
+ /** Tool context is assembled before the run session is initialized. */
22
+ getParentSessionId?: () => string;
20
23
  /** Fully resolved sandbox for this parent run, including settings layers. */
21
24
  parentSandbox: SandboxConfig;
22
25
  presetName: AgentPresetName;
@@ -252,7 +252,11 @@ export function createSubAgentSpawner(deps) {
252
252
  maxContextTokens: deps.parentConfig.maxContextTokens ?? 200_000,
253
253
  sessionStorageDir: deps.parentConfig.sessionStorageDir,
254
254
  headless: deps.parentConfig.headless,
255
- readOnlySession: request.readOnlySession,
255
+ readOnlySession: deps.parentConfig.readOnlySession || request.readOnlySession,
256
+ projectTrusted: deps.parentConfig.projectTrusted,
257
+ allowBackgroundShells: deps.parentConfig.allowBackgroundShells,
258
+ approvalBackend: deps.parentConfig.approvalBackend,
259
+ approvalRouter: deps.parentConfig.approvalRouter,
256
260
  skillAllowlist: request.skillAllowlist,
257
261
  sandbox: resolveChildSandbox(request.sandboxMode, deps.parentSandbox),
258
262
  mcpServers: resolveChildMcpServers(request.mcpAllowlist, deps.parentConfig.mcpServers),
@@ -261,28 +265,55 @@ export function createSubAgentSpawner(deps) {
261
265
  };
262
266
  const destination = request.streamOverride ?? deps.parentStream;
263
267
  const childSessionId = request.resumeSessionId ?? request.agentId;
264
- if (deps.childRunner.createChild && request.runtimeGeneration !== undefined) {
265
- const runtime = deps.childRunner.createChild(childConfig);
266
- const supervisor = new ChildRunSupervisor(runtime, childSessionId, request.runtimeGeneration);
267
- const ownerToken = nanoid();
268
- const bound = request.bindLiveControl?.(supervisor, ownerToken);
269
- supervisor.setCloseIntake(() => request.closeLiveControl?.(supervisor, bound && bound !== true ? bound : undefined));
270
- if (bound === false) {
271
- throw new Error(`child session ${childSessionId} already has a live transcript writer`);
268
+ const parentSessionId = deps.getParentSessionId?.() ?? deps.parentSessionId;
269
+ const host = parentSessionId
270
+ ? deps.parentConfig.createChildHostBindings?.({
271
+ parentSessionId,
272
+ sessionId: childSessionId,
273
+ signal: request.signal,
274
+ })
275
+ : undefined;
276
+ if (host) {
277
+ childConfig.browserBridge = host.browserBridge;
278
+ childConfig.askUser = host.askUser;
279
+ childConfig.injectCredentialToBrowser = host.injectCredentialToBrowser;
280
+ childConfig.approvalBackend = host.approvalBackend ?? childConfig.approvalBackend;
281
+ }
282
+ let runtime;
283
+ try {
284
+ if (deps.childRunner.createChild && request.runtimeGeneration !== undefined) {
285
+ runtime = deps.childRunner.createChild(childConfig);
286
+ const supervisor = new ChildRunSupervisor(runtime, childSessionId, request.runtimeGeneration);
287
+ const ownerToken = nanoid();
288
+ const bound = request.bindLiveControl?.(supervisor, ownerToken);
289
+ supervisor.setCloseIntake(() => request.closeLiveControl?.(supervisor, bound && bound !== true ? bound : undefined));
290
+ if (bound === false) {
291
+ throw new Error(`child session ${childSessionId} already has a live transcript writer`);
292
+ }
293
+ host?.activate?.();
294
+ return await supervisor.run(request.prompt, {
295
+ signal: request.signal,
296
+ onStream: wrapChildRuntimeStream(destination, request.agentId, request.onProgressEvent),
297
+ sessionId: childSessionId,
298
+ onAgentProgress: request.onAgentProgress,
299
+ });
272
300
  }
273
- return await supervisor.run(request.prompt, {
301
+ host?.activate?.();
302
+ const result = await deps.childRunner.runChild(childConfig, request.prompt, {
274
303
  signal: request.signal,
275
304
  onStream: wrapChildRuntimeStream(destination, request.agentId, request.onProgressEvent),
276
305
  sessionId: childSessionId,
277
- onAgentProgress: request.onAgentProgress,
278
306
  });
307
+ return result;
308
+ }
309
+ finally {
310
+ try {
311
+ host?.dispose();
312
+ }
313
+ finally {
314
+ await runtime?.dispose?.();
315
+ }
279
316
  }
280
- const result = await deps.childRunner.runChild(childConfig, request.prompt, {
281
- signal: request.signal,
282
- onStream: wrapChildRuntimeStream(destination, request.agentId, request.onProgressEvent),
283
- sessionId: childSessionId,
284
- });
285
- return result;
286
317
  },
287
318
  };
288
319
  }
@@ -11,6 +11,7 @@ import type { LiveChildState } from "../tool-system/builtin/agent-registry.js";
11
11
  import { ModelFacade } from "./model-facade.js";
12
12
  import { ToolExecutor } from "../tool-system/executor.js";
13
13
  import { ContextManager } from "../context/manager.js";
14
+ import type { SessionContextNotes } from "../context/notes.js";
14
15
  import { HookRegistry } from "../hooks/registry.js";
15
16
  import type { GoalJudgeRuntimeContext } from "../hooks/goal-stop-hook.js";
16
17
  import type { PromptCacheDiagnosticSample } from "./prompt-cache-diagnostics.js";
@@ -34,6 +35,8 @@ export interface TurnLoopConfig {
34
35
  * summarized into durable history. Engine uses this for dynamicContext.
35
36
  */
36
37
  volatileContextMessages?: Iterable<Message>;
38
+ /** Current instructions and lifecycle hooks, reattached after a notes transition. */
39
+ retainedContextMessages?: Iterable<Message>;
37
40
  /**
38
41
  * Fired after each turn boundary is recorded. Lets the engine flush an
39
42
  * up-to-date snapshot to state.json mid-run, so a long run doesn't leave
@@ -66,6 +69,7 @@ export interface TurnLoopDeps {
66
69
  model: ModelFacade;
67
70
  toolExecutor: ToolExecutor;
68
71
  contextManager: ContextManager;
72
+ contextNotes?: SessionContextNotes;
69
73
  hooks: HookRegistry;
70
74
  transcript: Transcript;
71
75
  systemPrompt: string;
@@ -295,6 +299,12 @@ export declare class TurnLoop {
295
299
  * to the tail when that boundary was rewritten or compaction changed length.
296
300
  */
297
301
  private restoreVolatileAfterContextManagement;
302
+ private noteReminderIssued;
303
+ private readonly retainedHookMessages;
304
+ private readonly pendingContextReminders;
305
+ private appendContextReminder;
306
+ /** Called only before a model step or after every result in a tool batch is recorded. */
307
+ private applyNotesRollover;
298
308
  private manageContextMessages;
299
309
  private manageContextMessagesSync;
300
310
  private markPendingImagesConsumed;