@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
@@ -1,7 +1,7 @@
1
1
  import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { homedir } from "node:os";
4
- import { credentialAllowsEnvExposure, credentialSecretHint, } from "./types.js";
4
+ import { credentialAllowsEnvExposure, credentialSecretHint, isCredentialSecretAvailable, } from "./types.js";
5
5
  import { getDefaultCredentialCipher } from "./cipher.js";
6
6
  import { logger } from "../logging/logger.js";
7
7
  import { summarizeOAuthCredentialSecret } from "./oauth.js";
@@ -329,6 +329,34 @@ export class CredentialStore {
329
329
  return true;
330
330
  });
331
331
  }
332
+ /**
333
+ * Update an existing record while holding the store lock. The callback sees
334
+ * the latest decrypted record and may return undefined to decline the write.
335
+ * Unlike save(), this never recreates a deleted credential.
336
+ */
337
+ updateExisting(scope, id, update) {
338
+ if (typeof id !== "string" || !id || id.length > MAX_CREDENTIAL_ID_CHARS || id.includes("\0")) {
339
+ throw new Error("invalid credential id");
340
+ }
341
+ let changed = false;
342
+ this.mutate(scope, (file) => {
343
+ const idx = file.credentials.findIndex((credential) => credential.id === id);
344
+ if (idx < 0)
345
+ return false;
346
+ const next = update(normalizeCredential(file.credentials[idx], true));
347
+ if (!next)
348
+ return false;
349
+ const normalized = normalizeCredential(next, true);
350
+ if (normalized.id !== id)
351
+ throw new Error("credential update cannot change id");
352
+ file.credentials[idx] = credentialAllowsEnvExposure(normalized.type)
353
+ ? normalized
354
+ : { ...normalized, exposeAsEnv: undefined };
355
+ changed = true;
356
+ return true;
357
+ });
358
+ return changed;
359
+ }
332
360
  remove(scope, id) {
333
361
  if (typeof id !== "string" || !id || id.length > MAX_CREDENTIAL_ID_CHARS || id.includes("\0")) {
334
362
  throw new Error("invalid credential id");
@@ -350,14 +378,20 @@ export class CredentialStore {
350
378
  * same host-isolation contract as {@link envExposures} and top-level env.
351
379
  */
352
380
  list(scope = "full") {
381
+ return this.listWithStatus(scope).credentials;
382
+ }
383
+ /** Preserve read failures so diagnostics can distinguish unknown state from an empty store. */
384
+ listWithStatus(scope = "full") {
353
385
  const byId = new Map();
354
- if (scope === "full") {
355
- for (const c of this.read("user").credentials)
356
- byId.set(c.id, c);
386
+ let readable = true;
387
+ const scopes = scope === "full" ? ["user", "project"] : ["project"];
388
+ for (const layer of scopes) {
389
+ const result = this.readGuarded(layer);
390
+ readable = readable && result.readable;
391
+ for (const credential of result.file.credentials)
392
+ byId.set(credential.id, credential);
357
393
  }
358
- for (const c of this.read("project").credentials)
359
- byId.set(c.id, c); // project wins
360
- return [...byId.values()];
394
+ return { credentials: [...byId.values()], readable };
361
395
  }
362
396
  resolve(id, scope = "full") {
363
397
  return this.list(scope).find((c) => c.id === id);
@@ -397,11 +431,12 @@ export class CredentialStore {
397
431
  listMasked(scope = "full") {
398
432
  return this.list(scope).map((c) => {
399
433
  const { secret, ...rest } = c;
434
+ const available = isCredentialSecretAvailable(secret);
400
435
  return {
401
436
  ...rest,
402
437
  ...(credentialAllowsEnvExposure(c.type) ? {} : { exposeAsEnv: undefined }),
403
- hasSecret: typeof secret === "string" && secret.length > 0,
404
- secretHint: credentialSecretHint(c.type, secret),
438
+ hasSecret: available,
439
+ secretHint: available ? credentialSecretHint(c.type, secret) : undefined,
405
440
  ...(c.type === "oauth" || isBrowserOAuthLinkCredential(c)
406
441
  ? { oauthStatus: summarizeOAuthCredentialSecret(secret) }
407
442
  : {}),
@@ -10,6 +10,8 @@ export type CredentialType = "token" | "link" | "cookie" | "oauth";
10
10
  * values and must remain behind the credential resolver boundary.
11
11
  */
12
12
  export declare function credentialAllowsEnvExposure(type: CredentialType): boolean;
13
+ /** An unreadable encrypted value is preserved on disk, but is not a usable secret. */
14
+ export declare function isCredentialSecretAvailable(secret: string | undefined): secret is string;
13
15
  /** Build a renderer-safe hint without deriving bytes from structured JSON. */
14
16
  export declare function credentialSecretHint(type: CredentialType, secret: string | undefined): string | undefined;
15
17
  export interface OAuthCredentialSecret {
@@ -112,6 +114,8 @@ export interface Credential {
112
114
  domain?: string;
113
115
  scope?: "domain" | "all";
114
116
  switchMode?: "clear" | "merge";
117
+ /** Opt in to writing browser-side cookie rotations back to this saved credential. */
118
+ autoRefreshFromBrowser?: boolean;
115
119
  /** Link provider id for credentials owned by a local/server app connection. */
116
120
  linkProvider?: string;
117
121
  /** Provider-specific connection method, e.g. fine-grained-pat or github-app. */
@@ -6,6 +6,10 @@
6
6
  export function credentialAllowsEnvExposure(type) {
7
7
  return type === "token" || type === "link";
8
8
  }
9
+ /** An unreadable encrypted value is preserved on disk, but is not a usable secret. */
10
+ export function isCredentialSecretAvailable(secret) {
11
+ return typeof secret === "string" && secret.length > 0 && !secret.startsWith("enc:");
12
+ }
9
13
  /** Build a renderer-safe hint without deriving bytes from structured JSON. */
10
14
  export function credentialSecretHint(type, secret) {
11
15
  if (!secret)
@@ -23,7 +23,10 @@ const BASE_DESCRIPTION = "Use a stored credential (token / API key / login cooki
23
23
  "or manually exporting credentials. Before `--cookies-from-browser` or retrying an " +
24
24
  "authentication failure, check stored credentials here. If `Currently available` names a " +
25
25
  "matching id, fetch it directly; otherwise call with NO arguments first to list available " +
26
- "credentials (id + label + type), then call again with `id`. Token/link credentials return their secret " +
26
+ "credentials (id + label + type), then call again with `id`. Provider-owned Link connections " +
27
+ "and existing CLI sessions are not listed here. Call LinkAction with provider only to check " +
28
+ "those before concluding " +
29
+ "a service is disconnected or asking the user to log in. Exposable token credentials return their secret " +
27
30
  "value; cookie credentials are materialized to a temporary Netscape cookies.txt file " +
28
31
  "(use it as `yt-dlp --cookies <cookiesFile>` / `curl -b <cookiesFile>`). " +
29
32
  "Each use is gated by a quick user approval unless auto-approve is on.";
@@ -125,7 +128,14 @@ export async function useCredentialTool(args, ctx) {
125
128
  .listMasked(cwd, scope)
126
129
  .filter(isAgentExposableCredential)
127
130
  .map((c) => ({ id: c.id, label: c.label, type: c.type }));
128
- return json({ kind: "list", credentials });
131
+ return json({
132
+ kind: "list",
133
+ credentials,
134
+ notice: "This list excludes provider-owned Link credentials and CLI login sessions. " +
135
+ 'Call LinkAction with provider only, for example LinkAction({provider: "github"}), ' +
136
+ "to check saved connections and current CLI authentication; " +
137
+ "an absent credential here does not mean the service is signed out.",
138
+ });
129
139
  }
130
140
  const cred = access.resolveMeta(cwd, id, scope);
131
141
  if (!cred) {
@@ -104,7 +104,8 @@ export function createAuthorizedSessionMessageService(options) {
104
104
  const targets = catalog.filter((target) => target.sessionId !== sourceSessionId);
105
105
  return {
106
106
  targets,
107
- send: async ({ targetSessionId, message }) => {
107
+ send: async ({ targetSessionId, message, signal }) => {
108
+ signal?.throwIfAborted();
108
109
  const target = targets.find((candidate) => candidate.sessionId === targetSessionId);
109
110
  if (!target)
110
111
  throw new Error("target Session is not in the host-authorized project list");
@@ -112,8 +113,14 @@ export function createAuthorizedSessionMessageService(options) {
112
113
  throw new Error("message is required");
113
114
  if (message.length > 48_000)
114
115
  throw new Error("message exceeds 48000 characters");
115
- await router({ sourceSessionId, target, message, catalog });
116
- return target;
116
+ const receipt = await router({
117
+ sourceSessionId,
118
+ target,
119
+ message,
120
+ catalog,
121
+ ...(signal ? { signal } : {}),
122
+ });
123
+ return receipt ? { ...target, receipt } : target;
117
124
  },
118
125
  };
119
126
  }
@@ -158,6 +158,8 @@ export declare class Engine {
158
158
  * running goal's turn/budget ceilings mid-run (TODO 3.1). Null when idle.
159
159
  */
160
160
  private activeTurnLoop;
161
+ /** Frozen behavior-profile ceiling for the live run's Goal controls. */
162
+ private activeProfileMaxTurns;
161
163
  /**
162
164
  * The goal-stop hook of the in-flight goal run, exposed so clearGoal() can
163
165
  * unregister it mid-run (the closure holds the now-cleared goal and would
@@ -279,6 +281,7 @@ export declare class Engine {
279
281
  * clear "no browser panel" error.
280
282
  */
281
283
  setBrowserBridge(bridge: import("../tool-system/browser-bridge.js").BrowserBridge | undefined): void;
284
+ setChildHostBindings(factory: EngineConfig["createChildHostBindings"]): void;
282
285
  /** Inject the host-backed workspace bridge after construction. */
283
286
  setWorkspaceBridge(bridge: import("../tool-system/workspace-bridge.js").WorkspaceBridge | undefined): void;
284
287
  /** Inject the host-backed panel discovery/focus bridge after construction. */
@@ -364,6 +367,7 @@ export declare class Engine {
364
367
  * permission-boundary bypass.
365
368
  */
366
369
  private resolveBehaviorProfile;
370
+ private resolveContextStrategy;
367
371
  private runExclusive;
368
372
  /** A receipt write failure must not turn an already finalized model result
369
373
  * into a second synthetic failure (or reject the Engine.run contract). */
@@ -459,6 +463,7 @@ export declare class Engine {
459
463
  * published through the `publishGoalJudgeContext` callback.
460
464
  */
461
465
  private buildTurnLoop;
466
+ private resolveRunMaxTurns;
462
467
  private buildSummarizeFn;
463
468
  private resolveAuxClient;
464
469
  private runMemoryPipeline;
@@ -611,10 +616,9 @@ export declare class Engine {
611
616
  migrateSessionMainRoot(sessionId: string, project: import("../types.js").SessionProjectBinding, mainRoot: string): SessionWorkspace;
612
617
  releaseSessionWorkspace(sessionId: string): SessionWorkspace | null;
613
618
  injectContext(sessionId: string, content: string): void;
614
- /**
615
- * Force context compaction on a session.
616
- * Returns token stats before/after.
617
- */
619
+ /** Attempt the durable note path without issuing a separate model request. */
620
+ private tryNotesRollover;
621
+ /** Force context compaction and return token stats before/after. */
618
622
  forceCompact(sessionId?: string): Promise<{
619
623
  before: number;
620
624
  after: number;
@@ -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";
@@ -232,6 +234,8 @@ export class Engine {
232
234
  * running goal's turn/budget ceilings mid-run (TODO 3.1). Null when idle.
233
235
  */
234
236
  activeTurnLoop = null;
237
+ /** Frozen behavior-profile ceiling for the live run's Goal controls. */
238
+ activeProfileMaxTurns;
235
239
  /**
236
240
  * The goal-stop hook of the in-flight goal run, exposed so clearGoal() can
237
241
  * unregister it mid-run (the closure holds the now-cleared goal and would
@@ -697,6 +701,9 @@ export class Engine {
697
701
  setBrowserBridge(bridge) {
698
702
  this.config.browserBridge = bridge;
699
703
  }
704
+ setChildHostBindings(factory) {
705
+ this.config.createChildHostBindings = factory;
706
+ }
700
707
  /** Inject the host-backed workspace bridge after construction. */
701
708
  setWorkspaceBridge(bridge) {
702
709
  this.config.workspaceBridge = bridge;
@@ -992,6 +999,12 @@ export class Engine {
992
999
  }
993
1000
  return sessionProfile ?? explicitProfile;
994
1001
  }
1002
+ resolveContextStrategy(profile) {
1003
+ return (this.config.contextStrategy ??
1004
+ this.getSettingsManager().get().context?.strategy ??
1005
+ profile?.contextStrategy ??
1006
+ "summary");
1007
+ }
995
1008
  async runExclusive(task, options) {
996
1009
  // Freeze permission context once, before the first await. Per-turn protocol
997
1010
  // overrides live only for this run; persistent setPermissionMode/setPlanMode
@@ -1123,6 +1136,9 @@ export class Engine {
1123
1136
  const { messages: openedMessages, freshImageMessage, resumedFromDisk, claimClientMessageId, releaseClientMessageId, } = openedResult.opened;
1124
1137
  const session = openedResult.opened.session;
1125
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;
1126
1142
  // A host-owned time/topic boundary must be applied only after the current
1127
1143
  // user message exists in the transcript (it is the exclusive end anchor),
1128
1144
  // yet before this turn assembles prompts or calls the model. Doing this as
@@ -1143,12 +1159,21 @@ export class Engine {
1143
1159
  : {}),
1144
1160
  };
1145
1161
  const before = estimateTokens(messages);
1162
+ let archiveStrategy = "range";
1146
1163
  if (options.archiveBeforeCurrentTurn.summary) {
1147
1164
  await this.appendArchiveMarker(session.state.sessionId, {
1148
1165
  ...anchors,
1149
1166
  summary: options.archiveBeforeCurrentTurn.summary,
1150
1167
  });
1151
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
+ }
1152
1177
  else {
1153
1178
  await this.archiveTurnRange(session.state.sessionId, { start: 0, end: 0 }, anchors);
1154
1179
  }
@@ -1162,7 +1187,7 @@ export class Engine {
1162
1187
  if (archived.before > archived.after) {
1163
1188
  options.onStream?.({
1164
1189
  type: "context_compact",
1165
- strategy: "range",
1190
+ strategy: archiveStrategy,
1166
1191
  before: archived.before,
1167
1192
  after: archived.after,
1168
1193
  });
@@ -1217,7 +1242,7 @@ export class Engine {
1217
1242
  sessionProfileOverrides,
1218
1243
  runPlanMode,
1219
1244
  });
1220
- const { llmClient, fullSystemPrompt, dynamicContextMsg, userContextMsg } = await this.assembleRunPrompts({
1245
+ const { llmClient, fullSystemPrompt, dynamicContextMsg, userContextMsg, retainedContextMessages, } = await this.assembleRunPrompts({
1221
1246
  session,
1222
1247
  messages,
1223
1248
  hookMessages,
@@ -1251,6 +1276,11 @@ export class Engine {
1251
1276
  task,
1252
1277
  cwd,
1253
1278
  options,
1279
+ profileMaxTurns: typeof profile?.maxTurns === "number" &&
1280
+ Number.isSafeInteger(profile.maxTurns) &&
1281
+ profile.maxTurns > 0
1282
+ ? profile.maxTurns
1283
+ : undefined,
1254
1284
  toolCtx,
1255
1285
  toolExecutor,
1256
1286
  contextManager,
@@ -1262,6 +1292,7 @@ export class Engine {
1262
1292
  releaseClientMessageId,
1263
1293
  freshImageMessage,
1264
1294
  dynamicContextMsg,
1295
+ retainedContextMessages,
1265
1296
  });
1266
1297
  let result;
1267
1298
  let firstGoalTermination;
@@ -1285,8 +1316,10 @@ export class Engine {
1285
1316
  this.activeRuntimeGoal = null;
1286
1317
  this.activePersistedRunGoal = null;
1287
1318
  }
1288
- if (this.activeTurnLoop === turnLoop)
1319
+ if (this.activeTurnLoop === turnLoop) {
1289
1320
  this.activeTurnLoop = null;
1321
+ this.activeProfileMaxTurns = undefined;
1322
+ }
1290
1323
  if (this.activeRunSession === session)
1291
1324
  this.activeRunSession = null;
1292
1325
  // Run-scoped too: this handler is re-registered every run(), so it must be
@@ -1515,17 +1548,21 @@ export class Engine {
1515
1548
  promptComposer.buildDynamicContextMessage(),
1516
1549
  ]);
1517
1550
  const fullSystemPrompt = composeRunSystemPrompt({
1518
- baseSystemPrompt,
1551
+ baseSystemPrompt: toolDefs.some((tool) => tool.name === "SaveContextNote")
1552
+ ? `${baseSystemPrompt}\n\n${loadSection("context-notes")}`
1553
+ : baseSystemPrompt,
1519
1554
  profile,
1520
1555
  profileParams,
1521
1556
  });
1522
1557
  const userContextMsg = promptComposer.buildUserContextMessage();
1558
+ const priorMessages = new Set(messages);
1523
1559
  assembleRunMessages({
1524
1560
  messages,
1525
1561
  userContextMsg,
1526
1562
  hookMessages,
1527
1563
  dynamicContextMsg,
1528
1564
  });
1565
+ const retainedContextMessages = messages.filter((message) => !priorMessages.has(message) && message !== dynamicContextMsg);
1529
1566
  this.lastSessionId = session.state.sessionId;
1530
1567
  this.lastMessages = messages;
1531
1568
  // Wire up LLM summarization for context compaction
@@ -1541,7 +1578,13 @@ export class Engine {
1541
1578
  contextManager.initReplacementStateFromMessages(messages);
1542
1579
  // Two summarizers with DIFFERENT quality needs (see the run loop wiring for
1543
1580
  // the aux-model summarizer set up alongside the ModelFacade).
1544
- return { llmClient, fullSystemPrompt, dynamicContextMsg, userContextMsg };
1581
+ return {
1582
+ llmClient,
1583
+ fullSystemPrompt,
1584
+ dynamicContextMsg,
1585
+ userContextMsg,
1586
+ retainedContextMessages,
1587
+ };
1545
1588
  }
1546
1589
  /**
1547
1590
  * Seed the run's {@link ContextManager}, emit the early `session_started`
@@ -1642,6 +1685,7 @@ export class Engine {
1642
1685
  // built-ins and product capabilities.
1643
1686
  const subAgentSpawner = createSubAgentSpawner({
1644
1687
  parentConfig: this.config,
1688
+ getParentSessionId: () => getSession().state.sessionId,
1645
1689
  parentSandbox: sandboxConfig,
1646
1690
  presetName: this.preset.name,
1647
1691
  cwd,
@@ -1658,7 +1702,12 @@ export class Engine {
1658
1702
  createChild: (config) => new Engine(config),
1659
1703
  runChild: async (config, childTask, childOptions) => {
1660
1704
  const child = new Engine(config);
1661
- return child.run(childTask, childOptions);
1705
+ try {
1706
+ return await child.run(childTask, childOptions);
1707
+ }
1708
+ finally {
1709
+ await child.dispose();
1710
+ }
1662
1711
  },
1663
1712
  },
1664
1713
  });
@@ -1714,7 +1763,7 @@ export class Engine {
1714
1763
  * phases consume cross back out.
1715
1764
  */
1716
1765
  async wireRunLoop(args) {
1717
- const { session, sid, task, cwd, options, 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;
1718
1767
  // eslint-disable-next-line prefer-const
1719
1768
  let turnLoop;
1720
1769
  const accounting = createRunUsageAccounting({
@@ -1827,6 +1876,7 @@ export class Engine {
1827
1876
  toolDefs,
1828
1877
  sid,
1829
1878
  options,
1879
+ profileMaxTurns,
1830
1880
  cwd,
1831
1881
  claimClientMessageId,
1832
1882
  releaseClientMessageId,
@@ -1836,6 +1886,7 @@ export class Engine {
1836
1886
  normalizedGoal,
1837
1887
  freshImageMessage,
1838
1888
  dynamicContextMsg,
1889
+ retainedContextMessages,
1839
1890
  usageBaseline,
1840
1891
  getRunUsage,
1841
1892
  recordCumulativeUsage,
@@ -1846,8 +1897,10 @@ export class Engine {
1846
1897
  toolCtx.recordBilledUsage = recordExternalBilledUsage;
1847
1898
  // Expose this run's loop for mid-run extension (TODO 3.1). Top-level only —
1848
1899
  // a sub-agent's loop is its own concern and isn't user-extendable.
1849
- if (this.config.isSubAgent !== true)
1900
+ if (this.config.isSubAgent !== true) {
1850
1901
  this.activeTurnLoop = turnLoop;
1902
+ this.activeProfileMaxTurns = profileMaxTurns;
1903
+ }
1851
1904
  // Expose this run's session bundle so a mid-run clearGoal() wipes the goal
1852
1905
  // on the very instance this loop keeps saving (see field doc). Top-level
1853
1906
  // only — sub-agents don't carry user-clearable persistent goals.
@@ -2019,6 +2072,7 @@ export class Engine {
2019
2072
  const mcpServers = this.config.mcpServers ?? {};
2020
2073
  const mcpDisabled = profile?.disableMcp === true;
2021
2074
  await connectRunMcp({
2075
+ toolContext: toolCtx,
2022
2076
  mcpServers,
2023
2077
  mcpDisabled,
2024
2078
  getManager: () => this.mcpManager,
@@ -2082,13 +2136,17 @@ export class Engine {
2082
2136
  * published through the `publishGoalJudgeContext` callback.
2083
2137
  */
2084
2138
  buildTurnLoop(args) {
2085
- const { modelFacade, toolExecutor, contextManager, session, fullSystemPrompt, toolDefs, sid, options, 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;
2086
2140
  // Surface compaction events to the UI so the user knows when context was trimmed.
2087
2141
  // Buffer the most recent event so TurnLoop can drain it and emit the
2088
2142
  // post_compact hook on the next turn (ContextManager itself doesn't
2089
2143
  // know about HookRegistry — the buffer is the seam).
2090
2144
  let pendingCompactInfo = null;
2091
2145
  contextManager.setOnCompact((info) => {
2146
+ if (info.strategy === "notes") {
2147
+ session.state.contextUsageAnchor = undefined;
2148
+ this.compactedMessagesBySession.delete(sid);
2149
+ }
2092
2150
  pendingCompactInfo = info;
2093
2151
  options?.onStream?.({ type: "context_compact", ...info });
2094
2152
  });
@@ -2097,6 +2155,7 @@ export class Engine {
2097
2155
  model: modelFacade,
2098
2156
  toolExecutor,
2099
2157
  contextManager,
2158
+ contextNotes: toolCtx.contextNotes instanceof SessionContextNotes ? toolCtx.contextNotes : undefined,
2100
2159
  hooks: this.hooks,
2101
2160
  transcript: session.transcript,
2102
2161
  systemPrompt: fullSystemPrompt,
@@ -2146,6 +2205,9 @@ export class Engine {
2146
2205
  releaseClientMessageId,
2147
2206
  setOriginClientMessageId: (clientMessageId) => {
2148
2207
  toolCtx.originClientMessageId = clientMessageId;
2208
+ const profileId = toolCtx.toolVisibility?.behaviorProfile;
2209
+ const profile = profileId ? this.behaviorProfiles.get(profileId) : undefined;
2210
+ profile?.onUserInputChanged?.(toolCtx.runScopedServices ?? {});
2149
2211
  },
2150
2212
  recordCumulativeUsage,
2151
2213
  onAgentUsage: (usage) => options?.onAgentProgress?.({ type: "usage", usage }),
@@ -2199,7 +2261,7 @@ export class Engine {
2199
2261
  // getting re-blocked by the stop-hook until it's done, and the 100
2200
2262
  // interactive default would silently truncate a long objective. The
2201
2263
  // real backstops are the goal token/time budgets + maxStopBlocks.
2202
- maxTurns: resolveMaxTurns(this.config.maxTurns, normalizedGoal),
2264
+ maxTurns: this.resolveRunMaxTurns(normalizedGoal, profileMaxTurns),
2203
2265
  // Consecutive stop-block cap: config override > goal.maxStopBlocks >
2204
2266
  // GOAL_DEFAULT_MAX_STOP_BLOCKS(25). The old hardcoded 8 was too tight
2205
2267
  // for complex goals that legitimately get re-blocked while advancing.
@@ -2213,6 +2275,7 @@ export class Engine {
2213
2275
  signal: options?.signal,
2214
2276
  freshImageMessages: freshImageMessage ? [freshImageMessage] : undefined,
2215
2277
  volatileContextMessages: dynamicContextMsg ? [dynamicContextMsg] : undefined,
2278
+ retainedContextMessages,
2216
2279
  // Goal mode: the active goal is surfaced to the on_stop handler via
2217
2280
  // ctx.data.goal; the GoalStopHook (registered above) judges it.
2218
2281
  goal: normalizedGoal,
@@ -2253,6 +2316,10 @@ export class Engine {
2253
2316
  });
2254
2317
  return turnLoop;
2255
2318
  }
2319
+ resolveRunMaxTurns(goal, profileMaxTurns) {
2320
+ const resolved = resolveMaxTurns(this.config.maxTurns, goal);
2321
+ return profileMaxTurns === undefined ? resolved : Math.min(resolved, profileMaxTurns);
2322
+ }
2256
2323
  buildSummarizeFn(auxSummaryClient, recordCumulativeUsage) {
2257
2324
  return this.auxiliaryPipeline.buildSummarizeFn(auxSummaryClient, recordCumulativeUsage);
2258
2325
  }
@@ -2717,7 +2784,7 @@ export class Engine {
2717
2784
  : resumed
2718
2785
  ? `目标已恢复:${next.objective}`
2719
2786
  : undefined, {
2720
- maxTurns: resolveMaxTurns(this.config.maxTurns, next),
2787
+ maxTurns: this.resolveRunMaxTurns(next, this.activeProfileMaxTurns),
2721
2788
  maxStopBlocks: resolveMaxStopBlocks(this.config.maxStopBlocks, next),
2722
2789
  });
2723
2790
  if (this.activeGoalHook && !this.activeGoalHookAttached) {
@@ -2830,10 +2897,17 @@ export class Engine {
2830
2897
  cached.push({ role: "assistant", content });
2831
2898
  }
2832
2899
  }
2833
- /**
2834
- * Force context compaction on a session.
2835
- * Returns token stats before/after.
2836
- */
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. */
2837
2911
  async forceCompact(sessionId) {
2838
2912
  const effectiveSessionId = sessionId ?? this.lastSessionId;
2839
2913
  if (!effectiveSessionId) {
@@ -2842,6 +2916,19 @@ export class Engine {
2842
2916
  const session = this.sessionManager.resume(effectiveSessionId);
2843
2917
  const sourceMessages = this.compactedMessagesBySession.get(effectiveSessionId) ?? session.transcript.toMessages();
2844
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
+ }
2845
2932
  const contextManager = await this.prepareContextManagerForSession(effectiveSessionId, session, sourceMessages, "force_compact");
2846
2933
  // Manual /compact emits its UI boundary at the protocol layer from the
2847
2934
  // final before/after result. Capture the tier here, but avoid reusing a
@@ -3134,6 +3221,19 @@ export class Engine {
3134
3221
  extendGoalRun(opts) {
3135
3222
  if (!this.activeTurnLoop)
3136
3223
  return null;
3224
+ if (this.activeProfileMaxTurns !== undefined &&
3225
+ typeof opts.addTurns === "number" &&
3226
+ Number.isFinite(opts.addTurns) &&
3227
+ opts.addTurns > 0 &&
3228
+ opts.addTurns <= Number.MAX_SAFE_INTEGER) {
3229
+ // An empty extension is a no-op that returns the live ceilings. Goal
3230
+ // budgets may still grow, but a behavior-profile ceiling cannot.
3231
+ const currentMaxTurns = this.activeTurnLoop.extend({}).maxTurns;
3232
+ return this.activeTurnLoop.extend({
3233
+ ...opts,
3234
+ addTurns: Math.min(opts.addTurns, this.activeProfileMaxTurns - currentMaxTurns),
3235
+ });
3236
+ }
3137
3237
  return this.activeTurnLoop.extend(opts);
3138
3238
  }
3139
3239
  /**
@@ -4,6 +4,12 @@ const DEFAULT_MAX_SESSIONS = 256;
4
4
  const DROP_MIN_PREVIOUS_TOKENS = 100;
5
5
  const DROP_MAX_CURRENT_TOKENS = 64;
6
6
  const DROP_RATIO = 0.1;
7
+ // Partial cache invalidation can leave thousands of tokens cached. Require
8
+ // both a halving and a meaningful absolute loss to avoid reporting small
9
+ // fluctuations. Compare cache reads, not hit rates: appending uncached input
10
+ // can lower the hit rate while the reusable prefix remains fully cached.
11
+ const PARTIAL_DROP_MAX_RETAINED_RATIO = 0.5;
12
+ const PARTIAL_DROP_MIN_LOST_TOKENS = 4096;
7
13
  /**
8
14
  * Session stickiness audit. Semantic, capability, and security switches must
9
15
  * take effect on the next request even when they invalidate a cache prefix.
@@ -150,7 +156,10 @@ export class PromptCacheDiagnosticRecorder {
150
156
  if (previous.cacheReadTokens < DROP_MIN_PREVIOUS_TOKENS)
151
157
  return { kind: "updated" };
152
158
  const dropRatio = previous.cacheReadTokens > 0 ? cacheReadTokens / previous.cacheReadTokens : 1;
153
- if (cacheReadTokens > DROP_MAX_CURRENT_TOKENS || dropRatio > DROP_RATIO) {
159
+ const nearColdDrop = cacheReadTokens <= DROP_MAX_CURRENT_TOKENS && dropRatio <= DROP_RATIO;
160
+ const partialDrop = dropRatio <= PARTIAL_DROP_MAX_RETAINED_RATIO &&
161
+ previous.cacheReadTokens - cacheReadTokens >= PARTIAL_DROP_MIN_LOST_TOKENS;
162
+ if (!nearColdDrop && !partialDrop) {
154
163
  return { kind: "updated" };
155
164
  }
156
165
  return {
@@ -10,9 +10,13 @@ import { logger } from "../logging/logger.js";
10
10
  /** engine.ts:explicit/stored/fallback 归一化 + 持久化 + goal_set 事件。 */
11
11
  export function resolveRunGoal(args) {
12
12
  const { options, session, sessionManager, configGoal, isSubAgent, sid, onStream } = args;
13
- const explicitGoal = normalizeGoal(options?.goal);
13
+ const goalDisabled = options?.disableGoal === true;
14
+ const explicitGoal = goalDisabled ? undefined : normalizeGoal(options?.goal);
14
15
  const storedLifecycle = session.state.goalLifecycle;
15
- const storedGoal = isSubAgent !== true && storedLifecycle && isGoalLifecycleCurrent(storedLifecycle)
16
+ const storedGoal = !goalDisabled &&
17
+ isSubAgent !== true &&
18
+ storedLifecycle &&
19
+ isGoalLifecycleCurrent(storedLifecycle)
16
20
  ? goalConfigFromLifecycle(storedLifecycle)
17
21
  : undefined;
18
22
  if (storedGoal &&
@@ -48,7 +52,7 @@ export function resolveRunGoal(args) {
48
52
  replaced,
49
53
  });
50
54
  }
51
- const fallbackGoal = normalizeGoal(configGoal);
55
+ const fallbackGoal = goalDisabled ? undefined : normalizeGoal(configGoal);
52
56
  if (fallbackGoal && !fallbackGoal.goalId)
53
57
  fallbackGoal.goalId = randomUUID();
54
58
  if (fallbackGoal && !fallbackGoal.revision)
@@ -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[];