@cjhyy/code-shell-core 0.9.5 → 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 (89) 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/credentials/access.d.ts +9 -1
  8. package/dist/credentials/access.js +15 -4
  9. package/dist/credentials/store.d.ts +11 -0
  10. package/dist/credentials/store.js +44 -9
  11. package/dist/credentials/types.d.ts +4 -0
  12. package/dist/credentials/types.js +4 -0
  13. package/dist/credentials/use-credential-tool.js +12 -2
  14. package/dist/engine/engine-workspace-authority.js +10 -3
  15. package/dist/engine/engine.d.ts +8 -4
  16. package/dist/engine/engine.js +115 -15
  17. package/dist/engine/prompt-cache-diagnostics.js +10 -1
  18. package/dist/engine/run-goal.js +7 -3
  19. package/dist/engine/run-tooling.d.ts +3 -0
  20. package/dist/engine/run-tooling.js +25 -23
  21. package/dist/engine/run-types.d.ts +20 -0
  22. package/dist/engine/run-workspace.js +5 -21
  23. package/dist/engine/subagent-spawner.d.ts +3 -0
  24. package/dist/engine/subagent-spawner.js +48 -17
  25. package/dist/engine/turn-loop.d.ts +14 -4
  26. package/dist/engine/turn-loop.js +120 -28
  27. package/dist/engine/types.d.ts +19 -0
  28. package/dist/index.d.ts +4 -4
  29. package/dist/index.js +2 -2
  30. package/dist/links/index.d.ts +1 -0
  31. package/dist/links/index.js +1 -0
  32. package/dist/links/link-action-tool.d.ts +2 -1
  33. package/dist/links/link-action-tool.js +80 -44
  34. package/dist/links/status.d.ts +53 -0
  35. package/dist/links/status.js +175 -0
  36. package/dist/llm/prompt-cache.d.ts +35 -3
  37. package/dist/llm/prompt-cache.js +63 -3
  38. package/dist/llm/providers/openai.d.ts +3 -0
  39. package/dist/llm/providers/openai.js +57 -14
  40. package/dist/prompt/section-loader.js +1 -0
  41. package/dist/prompt/sections/browser.md +4 -2
  42. package/dist/prompt/sections/context-notes.md +9 -0
  43. package/dist/protocol/background-result-wakeup.d.ts +8 -1
  44. package/dist/protocol/background-result-wakeup.js +76 -38
  45. package/dist/protocol/chat-session-manager.d.ts +7 -1
  46. package/dist/protocol/chat-session-manager.js +44 -8
  47. package/dist/protocol/chat-session.d.ts +2 -0
  48. package/dist/protocol/chat-session.js +4 -1
  49. package/dist/protocol/server.d.ts +4 -0
  50. package/dist/protocol/server.js +355 -62
  51. package/dist/protocol/session-message-result.d.ts +12 -0
  52. package/dist/protocol/session-message-result.js +42 -0
  53. package/dist/protocol/session-message-workspace.d.ts +21 -0
  54. package/dist/protocol/session-message-workspace.js +57 -0
  55. package/dist/protocol/types.d.ts +4 -0
  56. package/dist/session/session-manager.js +8 -7
  57. package/dist/session/session-message.d.ts +17 -2
  58. package/dist/session/transcript.d.ts +17 -0
  59. package/dist/session/transcript.js +271 -16
  60. package/dist/settings/schema.d.ts +9 -0
  61. package/dist/settings/schema.js +4 -0
  62. package/dist/themes/paths.js +20 -1
  63. package/dist/tool-system/browser-bridge.d.ts +21 -1
  64. package/dist/tool-system/browser-discovery.d.ts +6 -0
  65. package/dist/tool-system/browser-discovery.js +17 -0
  66. package/dist/tool-system/builtin/agent.js +23 -9
  67. package/dist/tool-system/builtin/browser-tools.js +30 -9
  68. package/dist/tool-system/builtin/context-notes.d.ts +12 -0
  69. package/dist/tool-system/builtin/context-notes.js +188 -0
  70. package/dist/tool-system/builtin/index.js +50 -3
  71. package/dist/tool-system/builtin/mcp-tools.d.ts +5 -3
  72. package/dist/tool-system/builtin/mcp-tools.js +10 -10
  73. package/dist/tool-system/builtin/send-message-to-session.js +19 -3
  74. package/dist/tool-system/builtin/tool-search.js +15 -3
  75. package/dist/tool-system/context.d.ts +9 -0
  76. package/dist/tool-system/executor.js +5 -3
  77. package/dist/tool-system/mcp-compat.d.ts +3 -0
  78. package/dist/tool-system/mcp-compat.js +51 -0
  79. package/dist/tool-system/mcp-manager.d.ts +27 -26
  80. package/dist/tool-system/mcp-manager.js +273 -111
  81. package/dist/tool-system/mcp-workspace.d.ts +18 -0
  82. package/dist/tool-system/mcp-workspace.js +56 -0
  83. package/dist/tool-system/permission.d.ts +6 -0
  84. package/dist/tool-system/permission.js +45 -9
  85. package/dist/tool-system/plan-mode-allowlist.js +5 -0
  86. package/dist/tool-system/sandbox/seatbelt.js +71 -2
  87. package/dist/tool-system/session-tool-host.js +9 -1
  88. package/dist/types.d.ts +4 -2
  89. package/package.json +1 -1
@@ -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,10 +22,19 @@ 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). */
28
30
  allowedToolNames?: ReadonlySet<string>;
31
+ /**
32
+ * Positive safe-integer ceiling on model/tool turns. Clamps the existing
33
+ * resolved config/Goal limit, including during live Goal edits/extensions.
34
+ * Invalid values are ignored. The existing no-tools final summary may run
35
+ * once after this ceiling is reached.
36
+ */
37
+ maxTurns?: number;
29
38
  /** When set, the run's permission mode is locked to this value. */
30
39
  forcePermissionMode?: NonNullable<EngineConfig["permissionMode"]>;
31
40
  /** When true, per-run planMode requests are ignored. */
@@ -70,6 +79,13 @@ export interface RunBehaviorProfile {
70
79
  profileParams: Readonly<Record<string, unknown>>;
71
80
  reportResult: (key: string, value: unknown) => void;
72
81
  }) => Record<string, unknown>;
82
+ /**
83
+ * Called with this run's services after each new steered user message is
84
+ * persisted, before its injection event and the next model step. Profiles
85
+ * may invalidate deferred drafts that answered an earlier input revision.
86
+ * Initial input, queued/revoked steers and duplicate messages do not call it.
87
+ */
88
+ onUserInputChanged?: (services: Record<string, unknown>) => void;
73
89
  /**
74
90
  * Per-run metadata exposed to builtin availability guards / definition
75
91
  * rewriters via ToolVisibilityContext.profileMeta.
@@ -98,6 +114,8 @@ export interface EngineRunOptions {
98
114
  planMode?: boolean;
99
115
  approvalRouter?: ApprovalRouter;
100
116
  goal?: string | GoalConfig;
117
+ /** Run this standalone turn without explicit, persisted, or configured Goal mode. */
118
+ disableGoal?: boolean;
101
119
  injected?: boolean;
102
120
  clientMessageId?: string;
103
121
  /**
@@ -170,6 +188,8 @@ export type AgentRuntimeProgressEvent = {
170
188
  usage: TokenUsage;
171
189
  };
172
190
  export interface ChildEngineRuntime {
191
+ /** Release child-local resources after the entire supervised lifetime settles. */
192
+ dispose?(): void | Promise<void>;
173
193
  run(task: string, options?: EngineRunOptions): Promise<EngineResult>;
174
194
  setAgentControlStateListener(listener: ((state: LiveChildState) => void) | undefined): void;
175
195
  setAgentDirectionsDeliveredListener?(listener: ((envelopeIds: string[]) => void) | undefined): void;
@@ -54,30 +54,12 @@ export async function resolveRunWorkspace(args) {
54
54
  ? await args.sessionManager.resolveSessionWorkspaceForResume(options.sessionId)
55
55
  : undefined;
56
56
  if (workspaceResume && !workspaceResume.ok) {
57
- return {
58
- ok: false,
59
- result: {
60
- text: `ERROR: ${workspaceResume.message}`,
61
- reason: "completed",
62
- sessionId: options.sessionId,
63
- turnCount: 0,
64
- usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
65
- },
66
- };
57
+ return workspaceError(options?.sessionId, workspaceResume.message);
67
58
  }
68
59
  if (workspaceResume?.ok &&
69
60
  workspaceResume.reason === "worktree_missing_branch_gone" &&
70
61
  workspaceResume.message) {
71
- return {
72
- ok: false,
73
- result: {
74
- text: workspaceResume.message,
75
- reason: "completed",
76
- sessionId: options.sessionId,
77
- turnCount: 0,
78
- usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
79
- },
80
- };
62
+ return workspaceError(options?.sessionId, workspaceResume.message);
81
63
  }
82
64
  // Existing P1 sessions resolve cwd from SessionWorkspace, even if the host
83
65
  // passes a stale cwd. Legacy sessions without workspace keep the historical
@@ -158,7 +140,9 @@ function workspaceError(sessionId, message) {
158
140
  ok: false,
159
141
  result: {
160
142
  text: `ERROR: ${message}`,
161
- reason: "completed",
143
+ // Like other initialization failures, this run never reached a model
144
+ // turn. Hosts must see a failure rather than report a completed delivery.
145
+ reason: "model_error",
162
146
  sessionId: sessionId ?? "workspace-invalid",
163
147
  turnCount: 0,
164
148
  usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
@@ -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;
@@ -289,12 +293,18 @@ export declare class TurnLoop {
289
293
  private stripVolatileContextMessages;
290
294
  /**
291
295
  * Keep volatile context out of compaction/summarization without moving it on
292
- * every model round. If context management is a no-op, return the original
293
- * array so the provider sees a strictly append-only prompt. A real rewrite
294
- * (dedupe/compaction/truncation) already invalidates the old prefix, so start
295
- * a fresh append-only segment with the volatile snapshot at the new tail.
296
+ * every model round. Normalizing a newly appended tool result must not move
297
+ * an earlier volatile snapshot and invalidate an otherwise unchanged prefix.
298
+ * Keep its position when the preceding stable messages remain intact; reset
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;
@@ -342,25 +342,114 @@ export class TurnLoop {
342
342
  }
343
343
  /**
344
344
  * Keep volatile context out of compaction/summarization without moving it on
345
- * every model round. If context management is a no-op, return the original
346
- * array so the provider sees a strictly append-only prompt. A real rewrite
347
- * (dedupe/compaction/truncation) already invalidates the old prefix, so start
348
- * a fresh append-only segment with the volatile snapshot at the new tail.
345
+ * every model round. Normalizing a newly appended tool result must not move
346
+ * an earlier volatile snapshot and invalidate an otherwise unchanged prefix.
347
+ * Keep its position when the preceding stable messages remain intact; reset
348
+ * to the tail when that boundary was rewritten or compaction changed length.
349
349
  */
350
350
  restoreVolatileAfterContextManagement(original, stableInput, managedStable) {
351
- const unchanged = stableInput.length === managedStable.length &&
352
- stableInput.every((message, index) => managedStable[index] === message);
353
- if (unchanged)
354
- return original;
351
+ if (stableInput.length === managedStable.length) {
352
+ // Some cleanup passes copy messages even when their final content is
353
+ // unchanged. Object identity is only a fast path, not a cache boundary.
354
+ const firstChangedIndex = stableInput.findIndex((message, index) => managedStable[index] !== message &&
355
+ JSON.stringify(managedStable[index]) !== JSON.stringify(message));
356
+ if (firstChangedIndex === -1)
357
+ return original;
358
+ let stableIndex = 0;
359
+ let lastVolatileBoundary = 0;
360
+ for (const message of original) {
361
+ if (this.volatileContextMessages.has(message)) {
362
+ lastVolatileBoundary = stableIndex;
363
+ }
364
+ else {
365
+ stableIndex++;
366
+ }
367
+ }
368
+ if (firstChangedIndex >= lastVolatileBoundary) {
369
+ // Apply the managed tail in place, including its persisted/truncated
370
+ // results. Never restore old payloads just to preserve cache hits.
371
+ stableIndex = 0;
372
+ return original.map((message) => this.volatileContextMessages.has(message) ? message : managedStable[stableIndex++]);
373
+ }
374
+ }
355
375
  const volatile = original.filter((message) => this.volatileContextMessages.has(message));
356
376
  return [...managedStable, ...volatile];
357
377
  }
378
+ noteReminderIssued = false;
379
+ retainedHookMessages = new Map();
380
+ pendingContextReminders = new Map();
381
+ appendContextReminder(messages, message, retainForRun = false) {
382
+ messages.push(message);
383
+ if (!this.deps.contextNotes)
384
+ return;
385
+ const target = retainForRun ? this.retainedHookMessages : this.pendingContextReminders;
386
+ target.set(JSON.stringify(message), message);
387
+ }
388
+ /** Called only before a model step or after every result in a tool batch is recorded. */
389
+ applyNotesRollover(messages) {
390
+ const notes = this.deps.contextNotes;
391
+ if (!notes ||
392
+ !notes.hasPendingRollover() ||
393
+ this.pendingImageMessages.size > 0 ||
394
+ this.sensitiveToolResultRedactions.size > 0) {
395
+ return messages;
396
+ }
397
+ if (this.config.signal?.aborted || this.goalControlStopRequested)
398
+ return messages;
399
+ const stable = this.stripVolatileContextMessages(messages);
400
+ const liveContent = new Set(stable.map((message) => JSON.stringify(message)));
401
+ for (const key of this.retainedHookMessages.keys()) {
402
+ if (!liveContent.has(key))
403
+ this.retainedHookMessages.delete(key);
404
+ }
405
+ const retainedByContent = new Map([
406
+ ...(this.config.retainedContextMessages ?? []),
407
+ ...this.retainedHookMessages.values(),
408
+ ...this.pendingContextReminders.values(),
409
+ ].map((message) => [JSON.stringify(message), message]));
410
+ const retained = [...retainedByContent.values()];
411
+ const checkpoint = notes.applyRollover(stable, retained);
412
+ if (!checkpoint)
413
+ return messages;
414
+ const managed = [...retained, ...checkpoint];
415
+ this.deps.contextManager.recordNotesCompaction(stable, managed);
416
+ this.noteReminderIssued = false;
417
+ return this.restoreVolatileAfterContextManagement(messages, stable, managed);
418
+ }
358
419
  async manageContextMessages(messages) {
359
- if (this.volatileContextMessages.size === 0) {
360
- return this.deps.contextManager.manageAsync(messages, this.config.signal);
420
+ messages = this.applyNotesRollover(messages);
421
+ const notes = this.deps.contextNotes;
422
+ if (notes) {
423
+ const stable = this.stripVolatileContextMessages(messages);
424
+ const limits = this.deps.contextManager.checkLimits(stable);
425
+ if (limits.needsCompact) {
426
+ try {
427
+ notes.requestRollover();
428
+ messages = this.applyNotesRollover(messages);
429
+ }
430
+ catch {
431
+ // No usable note (or it was already consumed): existing summary is the fallback.
432
+ }
433
+ }
434
+ const nearBudget = this.deps.contextManager.shouldPrepareNote(this.stripVolatileContextMessages(messages));
435
+ if (!nearBudget)
436
+ this.noteReminderIssued = false;
437
+ const canSaveNotes = this.deps.tools.some((tool) => tool.name === "SaveContextNote");
438
+ if (nearBudget && !this.noteReminderIssued && canSaveNotes) {
439
+ messages = [
440
+ ...messages,
441
+ {
442
+ role: "user",
443
+ content: "<system-reminder>Context budget is running low. Use SaveContextNote to preserve the current goal, latest corrections, decisions, verified work, pending questions and next steps. Then call NewContext in a separate tool batch and continue the same task. Original history remains available through SearchHistory.</system-reminder>",
444
+ },
445
+ ];
446
+ this.noteReminderIssued = true;
447
+ }
361
448
  }
362
449
  const stable = this.stripVolatileContextMessages(messages);
363
- const managed = await this.deps.contextManager.manageAsync(stable, this.config.signal);
450
+ const managed = await this.deps.contextManager.manageAsync(stable, this.config.signal, {
451
+ preferNotes: notes !== undefined,
452
+ });
364
453
  return this.restoreVolatileAfterContextManagement(messages, stable, managed);
365
454
  }
366
455
  manageContextMessagesSync(messages) {
@@ -647,7 +736,7 @@ export class TurnLoop {
647
736
  });
648
737
  const turnStartInjection = wrapHookMessages(turnStartHook.messages);
649
738
  if (turnStartInjection) {
650
- messages.push(turnStartInjection);
739
+ this.appendContextReminder(messages, turnStartInjection, true);
651
740
  }
652
741
  if (this.goalControlStopRequested) {
653
742
  messages = this.redactConsumedSensitiveToolResults(messages);
@@ -659,21 +748,21 @@ export class TurnLoop {
659
748
  // cap, the limit a re-blocked goal actually hits first.)
660
749
  const turnsRemaining = this.config.maxTurns - this.turnCount;
661
750
  if (turnsRemaining === 2) {
662
- messages.push({
751
+ this.appendContextReminder(messages, {
663
752
  role: "user",
664
753
  content: "<system-reminder>Warning: you have only 2 turns remaining before the turn limit is reached. " +
665
754
  "Start wrapping up your work and prepare a summary of what you've accomplished and what remains to be done.</system-reminder>",
666
755
  });
667
756
  }
668
757
  else if (turnsRemaining === 1) {
669
- messages.push({
758
+ this.appendContextReminder(messages, {
670
759
  role: "user",
671
760
  content: "<system-reminder>Warning: you have only 1 turn remaining before the turn limit is reached. " +
672
761
  "Wrap up your work now — your next turn will be your last.</system-reminder>",
673
762
  });
674
763
  }
675
764
  else if (turnsRemaining === 0) {
676
- messages.push({
765
+ this.appendContextReminder(messages, {
677
766
  role: "user",
678
767
  content: "<system-reminder>This is your LAST turn. You MUST respond with a final text summary now. " +
679
768
  "Do NOT call any tools. Summarize what you have accomplished and list any remaining work.</system-reminder>",
@@ -732,7 +821,7 @@ export class TurnLoop {
732
821
  });
733
822
  const compactInjection = wrapHookMessages(compactHook.messages);
734
823
  if (compactInjection) {
735
- messages.push(compactInjection);
824
+ this.appendContextReminder(messages, compactInjection, true);
736
825
  }
737
826
  }
738
827
  if (this.goalControlStopRequested) {
@@ -760,6 +849,7 @@ export class TurnLoop {
760
849
  });
761
850
  let response;
762
851
  try {
852
+ this.deps.contextNotes?.markModelBoundary();
763
853
  response = await this.callModelWithFallback(messages, assistantMessageId);
764
854
  }
765
855
  catch (err) {
@@ -846,6 +936,7 @@ export class TurnLoop {
846
936
  if (anchor)
847
937
  this.deps.recordContextUsageAnchor?.(anchor);
848
938
  }
939
+ this.pendingContextReminders.clear();
849
940
  messages = this.markPendingImagesConsumed(messages);
850
941
  // stopBlockCount guards only the no-tool final-answer loop evaluated by
851
942
  // on_stop (text -> block -> text -> block). A response that structurally
@@ -889,7 +980,7 @@ export class TurnLoop {
889
980
  },
890
981
  });
891
982
  }
892
- messages.push({
983
+ this.appendContextReminder(messages, {
893
984
  role: "user",
894
985
  content: "<system-reminder>Your previous response was truncated by the max output token limit before the tool call finished, so its arguments are incomplete. Do not assume it ran. Either retry with a smaller/more focused tool call (e.g. write the file in sections via Edit), or raise this model's maxOutputTokens.</system-reminder>",
895
986
  });
@@ -1133,13 +1224,13 @@ export class TurnLoop {
1133
1224
  this.maybeAnnounceApproachingLimit();
1134
1225
  const injection = wrapHookMessages(stopHook.messages);
1135
1226
  if (injection) {
1136
- messages.push(injection);
1227
+ this.appendContextReminder(messages, injection, true);
1137
1228
  }
1138
1229
  else {
1139
1230
  // No guidance from the handler — inject a generic nudge so the
1140
1231
  // model knows it must keep going rather than re-emitting the
1141
1232
  // same final answer.
1142
- messages.push({
1233
+ this.appendContextReminder(messages, {
1143
1234
  role: "user",
1144
1235
  content: "<system-reminder>The goal is not yet complete. Continue working toward it.</system-reminder>",
1145
1236
  });
@@ -1284,6 +1375,7 @@ export class TurnLoop {
1284
1375
  const toolResultMessage = { role: "user", content: resultBlocks };
1285
1376
  messages.push(toolResultMessage);
1286
1377
  this.trackFreshImageMessage(toolResultMessage);
1378
+ messages = this.applyNotesRollover(messages);
1287
1379
  // B-3: tell the model which of its requested tool calls were dropped by
1288
1380
  // the per-turn cap so it can re-issue them, instead of silently assuming
1289
1381
  // they ran. Appended to the same user message that carries the results.
@@ -1298,7 +1390,7 @@ export class TurnLoop {
1298
1390
  // Separate user message (not folded into the tool_result blocks) so the
1299
1391
  // OpenAI converter — which lifts tool_results into standalone role:tool
1300
1392
  // messages — keeps the reminder as a plain user turn after them.
1301
- messages.push({
1393
+ this.appendContextReminder(messages, {
1302
1394
  role: "user",
1303
1395
  content: `<system-reminder>Only the first ${toolCalls.length} of your ${response.toolCalls.length} ` +
1304
1396
  `tool calls ran this turn (per-turn limit is ${this.config.maxToolCallsPerTurn}). ` +
@@ -1383,7 +1475,7 @@ export class TurnLoop {
1383
1475
  return { text: finalText, reason: "completed", messages };
1384
1476
  }
1385
1477
  tlog.warn("turn.goal_self_reported_complete_persist_failed", { cat: "goal" });
1386
- messages.push({
1478
+ this.appendContextReminder(messages, {
1387
1479
  role: "user",
1388
1480
  content: "<system-reminder>complete_goal 未能持久化完成状态;目标仍处于活动状态。请不要宣称已经结束,并在会话状态恢复可写后重试。</system-reminder>",
1389
1481
  });
@@ -1419,7 +1511,7 @@ export class TurnLoop {
1419
1511
  };
1420
1512
  }
1421
1513
  tlog.warn("turn.goal_user_cancelled_persist_failed", { cat: "goal" });
1422
- messages.push({
1514
+ this.appendContextReminder(messages, {
1423
1515
  role: "user",
1424
1516
  content: "<system-reminder>cancel_goal 未能持久化删除状态;目标仍处于活动状态。请在会话状态恢复可写后重试。</system-reminder>",
1425
1517
  });
@@ -1452,7 +1544,7 @@ export class TurnLoop {
1452
1544
  };
1453
1545
  }
1454
1546
  if (budgetDecision === "nudge") {
1455
- messages.push({
1547
+ this.appendContextReminder(messages, {
1456
1548
  role: "user",
1457
1549
  content: "<system-reminder>You are approaching the token budget limit. Please start wrapping up your work and provide a summary.</system-reminder>",
1458
1550
  });
@@ -1465,7 +1557,7 @@ export class TurnLoop {
1465
1557
  guard.noteText(response.text);
1466
1558
  const turnReminder = guard.turnEnded(this.turnCount);
1467
1559
  if (turnReminder) {
1468
- messages.push({ role: "user", content: turnReminder });
1560
+ this.appendContextReminder(messages, { role: "user", content: turnReminder });
1469
1561
  tlog.info("guard.silent_turn", { cat: "guard", turn: this.turnCount });
1470
1562
  }
1471
1563
  }
@@ -1477,7 +1569,7 @@ export class TurnLoop {
1477
1569
  if (taskGuard) {
1478
1570
  const taskReminder = taskGuard.turnEnded(this.turnCount);
1479
1571
  if (taskReminder) {
1480
- messages.push({ role: "user", content: taskReminder });
1572
+ this.appendContextReminder(messages, { role: "user", content: taskReminder });
1481
1573
  tlog.info("guard.stale_task", { cat: "guard", turn: this.turnCount });
1482
1574
  }
1483
1575
  }
@@ -1556,7 +1648,7 @@ export class TurnLoop {
1556
1648
  messages = this.redactConsumedSensitiveToolResults(messages);
1557
1649
  return { text: finalText, reason: "completed", messages };
1558
1650
  }
1559
- messages.push({
1651
+ this.appendContextReminder(messages, {
1560
1652
  role: "user",
1561
1653
  content: "<system-reminder>Turn limit reached. Provide a final summary of what you accomplished and what remains to be done. Do NOT call any tools.</system-reminder>",
1562
1654
  });
@@ -1754,11 +1846,11 @@ export class TurnLoop {
1754
1846
  break;
1755
1847
  }
1756
1848
  consumed = true;
1757
- this.deps.setOriginClientMessageId?.(clientMessageId);
1758
1849
  const message = { role: "user", content };
1759
1850
  messages.push(message);
1760
1851
  this.trackFreshImageMessage(message);
1761
1852
  this.deps.transcript.appendMessage("user", content, { steerId: id, clientMessageId });
1853
+ this.deps.setOriginClientMessageId?.(clientMessageId);
1762
1854
  this.config.onStream?.({ type: "steer_injected", text, id });
1763
1855
  }
1764
1856
  return consumed;
@@ -1796,7 +1888,7 @@ export class TurnLoop {
1796
1888
  this.deps.onAgentDirectionsDelivered?.(envelopeIds);
1797
1889
  const hookInjection = wrapHookMessages(hook.messages);
1798
1890
  if (hookInjection)
1799
- messages.push(hookInjection);
1891
+ this.appendContextReminder(messages, hookInjection, true);
1800
1892
  return true;
1801
1893
  }
1802
1894
  /**