@gajae-code/coding-agent 0.9.1 → 0.9.3

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 (123) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/types/capability/mcp.d.ts +3 -1
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/notify-cli.d.ts +5 -0
  5. package/dist/types/cli.d.ts +3 -0
  6. package/dist/types/commands/launch.d.ts +3 -0
  7. package/dist/types/commands/mcp.d.ts +1 -1
  8. package/dist/types/config/model-profile-activation.d.ts +7 -7
  9. package/dist/types/config/model-registry.d.ts +7 -3
  10. package/dist/types/config/model-resolver.d.ts +2 -2
  11. package/dist/types/config/settings-schema.d.ts +1 -1
  12. package/dist/types/config/settings.d.ts +7 -0
  13. package/dist/types/extensibility/runtime-skill-discovery.d.ts +20 -0
  14. package/dist/types/gjc-runtime/team-runtime.d.ts +9 -0
  15. package/dist/types/modes/components/settings-selector.d.ts +2 -2
  16. package/dist/types/modes/components/status-line/types.d.ts +6 -0
  17. package/dist/types/modes/components/status-line.d.ts +1 -0
  18. package/dist/types/modes/components/thinking-selector.d.ts +6 -1
  19. package/dist/types/modes/types.d.ts +1 -0
  20. package/dist/types/modes/utils/ui-helpers.d.ts +12 -0
  21. package/dist/types/notifications/config-commands.d.ts +29 -0
  22. package/dist/types/notifications/index.d.ts +12 -1
  23. package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
  24. package/dist/types/notifications/threaded-render.d.ts +2 -0
  25. package/dist/types/notifications/topic-registry.d.ts +4 -5
  26. package/dist/types/runtime-credential-selector.d.ts +7 -0
  27. package/dist/types/runtime-mcp/config-writer.d.ts +4 -3
  28. package/dist/types/runtime-mcp/types.d.ts +5 -4
  29. package/dist/types/sdk.d.ts +7 -1
  30. package/dist/types/session/agent-session.d.ts +22 -3
  31. package/dist/types/session/session-manager.d.ts +1 -0
  32. package/dist/types/tools/index.d.ts +1 -0
  33. package/dist/types/tools/skill-discovery.d.ts +40 -0
  34. package/dist/types/tools/skill.d.ts +9 -8
  35. package/dist/types/utils/linkedom.d.ts +4 -0
  36. package/dist/types/utils/pasted-image-path.d.ts +5 -3
  37. package/package.json +11 -7
  38. package/scripts/build-binary.ts +2 -40
  39. package/scripts/compile-args.ts +91 -0
  40. package/scripts/generate-docs-index.ts +1 -0
  41. package/src/capability/mcp.ts +3 -1
  42. package/src/cli/args.ts +8 -0
  43. package/src/cli/mcp-cli.ts +108 -26
  44. package/src/cli/notify-cli.ts +27 -1
  45. package/src/cli.ts +26 -0
  46. package/src/commands/launch.ts +5 -0
  47. package/src/commands/mcp.ts +8 -8
  48. package/src/commands/notify.ts +2 -2
  49. package/src/commands/stats.ts +6 -2
  50. package/src/config/mcp-schema.json +21 -17
  51. package/src/config/model-profile-activation.ts +23 -7
  52. package/src/config/model-registry.ts +21 -4
  53. package/src/config/model-resolver.ts +5 -16
  54. package/src/config/settings-schema.ts +3 -2
  55. package/src/config/settings.ts +24 -1
  56. package/src/coordinator-mcp/server.ts +7 -4
  57. package/src/discovery/builtin.ts +14 -0
  58. package/src/discovery/mcp-json.ts +14 -0
  59. package/src/extensibility/runtime-skill-discovery.ts +229 -0
  60. package/src/gjc-runtime/launch-tmux.ts +72 -12
  61. package/src/gjc-runtime/psmux-detect.ts +17 -2
  62. package/src/gjc-runtime/state-runtime.ts +105 -12
  63. package/src/gjc-runtime/team-runtime.ts +35 -3
  64. package/src/gjc-runtime/workflow-manifest.generated.json +4 -28
  65. package/src/gjc-runtime/workflow-manifest.ts +1 -7
  66. package/src/internal-urls/docs-index.generated.ts +3 -2
  67. package/src/internal-urls/gjc-protocol.ts +14 -1
  68. package/src/main.ts +26 -3
  69. package/src/modes/components/assistant-message.ts +1 -1
  70. package/src/modes/components/settings-selector.ts +3 -3
  71. package/src/modes/components/status-line/presets.ts +5 -7
  72. package/src/modes/components/status-line/segments.ts +14 -0
  73. package/src/modes/components/status-line/types.ts +6 -0
  74. package/src/modes/components/status-line.ts +7 -1
  75. package/src/modes/components/thinking-selector.ts +79 -18
  76. package/src/modes/components/tool-execution.ts +1 -1
  77. package/src/modes/controllers/event-controller.ts +9 -9
  78. package/src/modes/controllers/input-controller.ts +52 -11
  79. package/src/modes/controllers/selector-controller.ts +7 -5
  80. package/src/modes/interactive-mode.ts +12 -4
  81. package/src/modes/types.ts +1 -0
  82. package/src/modes/utils/ui-helpers.ts +202 -47
  83. package/src/notifications/config-commands.ts +70 -0
  84. package/src/notifications/index.ts +156 -9
  85. package/src/notifications/rich-render.ts +1 -0
  86. package/src/notifications/telegram-daemon-cli.ts +13 -5
  87. package/src/notifications/telegram-daemon.ts +87 -4
  88. package/src/notifications/threaded-render.ts +14 -0
  89. package/src/notifications/topic-registry.ts +9 -8
  90. package/src/prompts/tools/skill-discovery.md +13 -0
  91. package/src/prompts/tools/skill.md +2 -2
  92. package/src/runtime-credential-selector.ts +35 -0
  93. package/src/runtime-mcp/config-writer.ts +4 -3
  94. package/src/runtime-mcp/config.ts +1 -0
  95. package/src/runtime-mcp/types.ts +5 -4
  96. package/src/sdk.ts +68 -5
  97. package/src/session/agent-session.ts +159 -21
  98. package/src/session/session-manager.ts +68 -40
  99. package/src/system-prompt.ts +4 -6
  100. package/src/tools/bash-interactive.ts +9 -4
  101. package/src/tools/browser/readable.ts +2 -2
  102. package/src/tools/fetch.ts +5 -5
  103. package/src/tools/index.ts +4 -0
  104. package/src/tools/skill-discovery.ts +73 -0
  105. package/src/tools/skill.ts +29 -13
  106. package/src/utils/linkedom.ts +14 -0
  107. package/src/utils/pasted-image-path.ts +23 -9
  108. package/src/web/scrapers/arxiv.ts +2 -2
  109. package/src/web/scrapers/go-pkg.ts +2 -2
  110. package/src/web/scrapers/iacr.ts +2 -2
  111. package/src/web/scrapers/readthedocs.ts +2 -2
  112. package/src/web/scrapers/twitter.ts +3 -2
  113. package/src/web/scrapers/wikipedia.ts +2 -2
  114. package/src/export/html/template.css +0 -1060
  115. package/src/export/html/template.html +0 -47
  116. package/src/export/html/template.js +0 -2314
  117. package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
  118. package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
  119. package/vendor/insane-search/engine/tests/test_u1.py +0 -200
  120. package/vendor/insane-search/engine/tests/test_u4.py +0 -131
  121. package/vendor/insane-search/engine/tests/test_u5.py +0 -163
  122. package/vendor/insane-search/engine/tests/test_u7.py +0 -124
  123. package/vendor/insane-search/engine/tests/test_u8.py +0 -216
package/src/sdk.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  type ThinkingLevel,
10
10
  } from "@gajae-code/agent-core";
11
11
  import {
12
+ type AuthCredentialSelector,
12
13
  type CredentialDisabledEvent,
13
14
  type Message,
14
15
  type Model,
@@ -39,7 +40,7 @@ import {
39
40
  import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled, jobElapsedMs } from "./async";
40
41
  import { loadCapability } from "./capability";
41
42
  import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
42
- import { ModelRegistry } from "./config/model-registry";
43
+ import { kNoAuth, ModelRegistry } from "./config/model-registry";
43
44
  import {
44
45
  formatModelString,
45
46
  parseModelPattern,
@@ -272,6 +273,8 @@ export interface CreateAgentSessionOptions {
272
273
  /** Optional provider-facing session identifier for prompt caches and sticky auth selection.
273
274
  * Keeps persisted session files isolated while reusing provider-side caches. */
274
275
  providerSessionId?: string;
276
+ /** Runtime credential selector for multi-account auth pools. */
277
+ credentialSelector?: { provider?: string; selector: AuthCredentialSelector; raw: string };
275
278
 
276
279
  /** Custom tools to register (in addition to built-in tools). Accepts both CustomTool and ToolDefinition. */
277
280
  customTools?: (CustomTool | ToolDefinition)[];
@@ -903,10 +906,22 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
903
906
  startupCredentialDisabledEvents.push(event);
904
907
  }
905
908
  });
909
+ let runtimeCredentialSelectorInstalled = false;
910
+ const installRuntimeCredentialSelector = (provider: string): void => {
911
+ if (!options.credentialSelector || runtimeCredentialSelectorInstalled) return;
912
+ authStorage.setRuntimeCredentialSelector(provider, options.credentialSelector.selector);
913
+ runtimeCredentialSelectorInstalled = true;
914
+ };
915
+ const earlyCredentialSelectorProvider = options.credentialSelector?.provider ?? options.model?.provider;
916
+ if (earlyCredentialSelectorProvider) {
917
+ installRuntimeCredentialSelector(earlyCredentialSelectorProvider);
918
+ }
906
919
  const settings = options.settings ?? (await logger.time("settings", Settings.init, { cwd, agentDir }));
907
920
  modelRegistry.applyConfiguredModelBindings(settings);
908
921
  logger.time("initializeWithSettings", initializeWithSettings, settings);
909
- if (!options.modelRegistry) {
922
+ const canRefreshModelsBeforeCredentialSelector =
923
+ !options.credentialSelector || runtimeCredentialSelectorInstalled || options.modelRegistry !== undefined;
924
+ if (!options.modelRegistry && canRefreshModelsBeforeCredentialSelector) {
910
925
  modelRegistry.refreshInBackground();
911
926
  }
912
927
  // Kick off workspace tree discovery early. The native workspace scan returns
@@ -972,7 +987,29 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
972
987
  return cached;
973
988
  }
974
989
 
975
- const hasKey = !!(await modelRegistry.getApiKey(candidate, providerSessionId));
990
+ const credentialSelector =
991
+ options.credentialSelector && !runtimeCredentialSelectorInstalled
992
+ ? options.credentialSelector.provider === undefined ||
993
+ options.credentialSelector.provider === candidate.provider
994
+ ? options.credentialSelector.selector
995
+ : undefined
996
+ : undefined;
997
+ if (options.credentialSelector?.provider && options.credentialSelector.provider !== candidate.provider) {
998
+ modelApiKeyAvailability.set(availabilityKey, false);
999
+ return false;
1000
+ }
1001
+ const key = await modelRegistry.getApiKey(candidate, providerSessionId, { credentialSelector }).catch(error => {
1002
+ if (credentialSelector) {
1003
+ logger.debug("Credential selector did not match model availability candidate", {
1004
+ provider: candidate.provider,
1005
+ model: candidate.id,
1006
+ error: error instanceof Error ? error.message : String(error),
1007
+ });
1008
+ return undefined;
1009
+ }
1010
+ throw error;
1011
+ });
1012
+ const hasKey = Boolean(key) && (!credentialSelector || key !== kNoAuth);
976
1013
  modelApiKeyAvailability.set(availabilityKey, hasKey);
977
1014
  return hasKey;
978
1015
  };
@@ -1047,6 +1084,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1047
1084
  const taskDepth = options.taskDepth ?? 0;
1048
1085
 
1049
1086
  let thinkingLevel = options.thinkingLevel;
1087
+ const hasExplicitDefaultThinkingLevel = settings.has("defaultThinkingLevel");
1088
+ let thinkingLevelFromSchemaDefault = false;
1050
1089
 
1051
1090
  // If session has data and includes a thinking entry, restore it
1052
1091
  if (thinkingLevel === undefined && hasExistingSession && hasThinkingEntry) {
@@ -1057,13 +1096,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1057
1096
  thinkingLevel = defaultRoleSpec.thinkingLevel;
1058
1097
  }
1059
1098
 
1060
- // Prefer the selected model's configured defaultLevel, otherwise fall back
1061
- // to the global settings default.
1099
+ // An explicit user/project default should win over the model's bundled
1100
+ // defaultLevel. The schema default is only a final fallback so model metadata
1101
+ // can keep driving first-run behavior until the user chooses "Set as default".
1102
+ if (thinkingLevel === undefined && hasExplicitDefaultThinkingLevel) {
1103
+ thinkingLevel = settings.get("defaultThinkingLevel");
1104
+ }
1105
+
1062
1106
  if (thinkingLevel === undefined && model?.thinking?.defaultLevel !== undefined) {
1063
1107
  thinkingLevel = model.thinking.defaultLevel;
1064
1108
  }
1109
+
1065
1110
  if (thinkingLevel === undefined) {
1066
1111
  thinkingLevel = settings.get("defaultThinkingLevel");
1112
+ thinkingLevelFromSchemaDefault = true;
1067
1113
  }
1068
1114
  if (model) {
1069
1115
  const resolvedModel = model;
@@ -1608,6 +1654,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1608
1654
  if (resolved) {
1609
1655
  model = resolved;
1610
1656
  modelFallbackMessage = undefined;
1657
+ if (thinkingLevelFromSchemaDefault && resolved.thinking?.defaultLevel !== undefined) {
1658
+ thinkingLevel = resolved.thinking.defaultLevel;
1659
+ thinkingLevelFromSchemaDefault = false;
1660
+ }
1661
+ thinkingLevel = resolveThinkingLevelForModel(resolved, thinkingLevel);
1611
1662
  } else {
1612
1663
  modelFallbackMessage = `Model "${options.modelPattern}" not found`;
1613
1664
  }
@@ -1639,6 +1690,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1639
1690
  }
1640
1691
  }
1641
1692
 
1693
+ if (options.credentialSelector && !runtimeCredentialSelectorInstalled) {
1694
+ const credentialProvider = options.credentialSelector.provider ?? model?.provider;
1695
+ if (!credentialProvider) {
1696
+ throw new Error(
1697
+ `--credential ${options.credentialSelector.raw} requires a resolved model or an explicit provider prefix`,
1698
+ );
1699
+ }
1700
+ installRuntimeCredentialSelector(credentialProvider);
1701
+ if (!options.modelRegistry && !canRefreshModelsBeforeCredentialSelector) {
1702
+ modelRegistry.refreshInBackground();
1703
+ }
1704
+ }
1642
1705
  const customCommandsResult: CustomCommandsLoadResult = { commands: [], errors: [] };
1643
1706
 
1644
1707
  let extensionRunner: ExtensionRunner | undefined;
@@ -26,6 +26,7 @@ import {
26
26
  type AgentMessage,
27
27
  type AgentState,
28
28
  type AgentTool,
29
+ assertImagePlaceholdersHavePayload,
29
30
  resolveTelemetry,
30
31
  type StablePrefixSnapshot,
31
32
  ThinkingLevel,
@@ -151,6 +152,7 @@ import { onAppendOnlyModeChanged } from "../config/settings";
151
152
  import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
152
153
  import { loadCapability } from "../discovery";
153
154
  import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
155
+ import { MAX_EDIT_FILE_BYTES } from "../edit/read-file";
154
156
  import { disposeVmContextsByOwner } from "../eval/js/context-manager";
155
157
  import {
156
158
  disposeKernelSessionsByOwner,
@@ -360,6 +362,11 @@ export interface AsyncJobSnapshot {
360
362
  // Types
361
363
  // ============================================================================
362
364
 
365
+ export interface RetainedMemorySample {
366
+ tuiChatChildren?: number;
367
+ tuiCachedRenderBytes?: number;
368
+ }
369
+
363
370
  export interface AgentSessionConfig {
364
371
  agent: Agent;
365
372
  sessionManager: SessionManager;
@@ -439,6 +446,8 @@ export interface AgentSessionConfig {
439
446
  * **MUST NOT** dispose it on their own teardown.
440
447
  */
441
448
  ownedAsyncJobManager?: AsyncJobManager;
449
+ /** Cheap TUI retained-memory counters; absent for headless sessions. */
450
+ retainedMemorySampler?: () => RetainedMemorySample;
442
451
  /**
443
452
  * MCPManager whose lifecycle this session owns (top-level sessions that
444
453
  * connected plugin-bundle MCP servers). Only the owned manager is
@@ -473,6 +482,8 @@ export interface PromptOptions {
473
482
  images?: ImageContent[];
474
483
  /** When streaming, how to queue the message: "steer" (interrupt) or "followUp" (wait). */
475
484
  streamingBehavior?: "steer" | "followUp";
485
+ /** When set to "sequential", this follow-up is delivered one prompt at a time even if followUpMode is "all". */
486
+ followUpQueuePolicy?: "respect-mode" | "sequential";
476
487
  /** Optional tool choice override for the next LLM call. */
477
488
  toolChoice?: ToolChoice;
478
489
  /** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
@@ -1082,6 +1093,68 @@ export function buildContextInjectionSignature(kind: string, parts: readonly str
1082
1093
  }
1083
1094
  return hash.digest("base64url");
1084
1095
  }
1096
+
1097
+ const STREAMING_EDIT_FILE_CACHE_MAX_ENTRIES = 16;
1098
+ const STREAMING_EDIT_FILE_CACHE_MAX_TOTAL_BYTES = 32 * 1024 * 1024;
1099
+
1100
+ type StreamingEditFileCacheEntry = {
1101
+ content: string;
1102
+ bytes: number;
1103
+ };
1104
+
1105
+ export class StreamingEditFileCache {
1106
+ #entries = new Map<string, StreamingEditFileCacheEntry>();
1107
+ #totalBytes = 0;
1108
+
1109
+ get(path: string): string | undefined {
1110
+ const entry = this.#entries.get(path);
1111
+ if (entry === undefined) return undefined;
1112
+ this.#entries.delete(path);
1113
+ this.#entries.set(path, entry);
1114
+ return entry.content;
1115
+ }
1116
+
1117
+ set(path: string, content: string): void {
1118
+ const bytes = Buffer.byteLength(content, "utf8");
1119
+ if (bytes > MAX_EDIT_FILE_BYTES || bytes > STREAMING_EDIT_FILE_CACHE_MAX_TOTAL_BYTES) {
1120
+ this.delete(path);
1121
+ return;
1122
+ }
1123
+
1124
+ this.delete(path);
1125
+ while (
1126
+ this.#entries.size >= STREAMING_EDIT_FILE_CACHE_MAX_ENTRIES ||
1127
+ this.#totalBytes + bytes > STREAMING_EDIT_FILE_CACHE_MAX_TOTAL_BYTES
1128
+ ) {
1129
+ const oldestPath = this.#entries.keys().next().value;
1130
+ if (oldestPath === undefined) break;
1131
+ this.delete(oldestPath);
1132
+ }
1133
+
1134
+ this.#entries.set(path, { content, bytes });
1135
+ this.#totalBytes += bytes;
1136
+ }
1137
+
1138
+ delete(path: string): void {
1139
+ const entry = this.#entries.get(path);
1140
+ if (entry === undefined) return;
1141
+ this.#entries.delete(path);
1142
+ this.#totalBytes -= entry.bytes;
1143
+ }
1144
+
1145
+ clear(): void {
1146
+ this.#entries.clear();
1147
+ this.#totalBytes = 0;
1148
+ }
1149
+
1150
+ has(path: string): boolean {
1151
+ return this.#entries.has(path);
1152
+ }
1153
+
1154
+ get totalBytes(): number {
1155
+ return this.#totalBytes;
1156
+ }
1157
+ }
1085
1158
  export class AgentSession {
1086
1159
  readonly agent: Agent;
1087
1160
  readonly sessionManager: SessionManager;
@@ -1146,6 +1219,7 @@ export class AgentSession {
1146
1219
  #compactionAbortController: AbortController | undefined = undefined;
1147
1220
  #autoCompactionAbortController: AbortController | undefined = undefined;
1148
1221
  #resourceSampler: () => EmergencyCompactionSample = () => this.#defaultResourceSample();
1222
+ #retainedMemorySampler: (() => RetainedMemorySample) | undefined;
1149
1223
  #prePromptContextCheckPromise: Promise<void> | undefined = undefined;
1150
1224
 
1151
1225
  // Branch summarization state
@@ -1319,7 +1393,7 @@ export class AgentSession {
1319
1393
 
1320
1394
  #streamingEditPrecheckedToolCallIds = new Set<string>();
1321
1395
 
1322
- #streamingEditFileCache = new Map<string, string>();
1396
+ #streamingEditFileCache = new StreamingEditFileCache();
1323
1397
  readonly streamingEditDebugCounters = {
1324
1398
  guardRuns: 0,
1325
1399
  processedChars: 0,
@@ -1445,6 +1519,7 @@ export class AgentSession {
1445
1519
  // Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
1446
1520
  this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
1447
1521
  this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
1522
+ this.#retainedMemorySampler = config.retainedMemorySampler;
1448
1523
  this.#ownedMcpManager = config.ownedMcpManager;
1449
1524
  this.#scopedModels = config.scopedModels ?? [];
1450
1525
  this.#thinkingLevel = config.thinkingLevel;
@@ -3168,7 +3243,31 @@ export class AgentSession {
3168
3243
  // which is the optional patch-preview verification gated by
3169
3244
  // edit.streamingAbort. Skip the read when the setting is off.
3170
3245
  if (this.settings.get("edit.streamingAbort")) {
3171
- this.#ensureFileCache(streamingEdit.resolvedPath);
3246
+ // Fire-and-forget async priming: toolcall deltas arrive on the hot
3247
+ // stream path, so never block it on filesystem I/O. The abort-check
3248
+ // path (#maybeAbortStreamingEdit) still falls back to the sync read
3249
+ // if priming hasn't completed by the time removed lines appear.
3250
+ void this.#preCacheFileAsync(streamingEdit.resolvedPath);
3251
+ }
3252
+ }
3253
+
3254
+ #streamingEditPrecachePending = new Set<string>();
3255
+ async #preCacheFileAsync(resolvedPath: string): Promise<void> {
3256
+ if (this.#streamingEditFileCache.has(resolvedPath)) return;
3257
+ if (this.#streamingEditPrecachePending.has(resolvedPath)) return;
3258
+ this.#streamingEditPrecachePending.add(resolvedPath);
3259
+ try {
3260
+ const stat = await fs.promises.stat(resolvedPath);
3261
+ if (stat.size > MAX_EDIT_FILE_BYTES) return;
3262
+
3263
+ const rawText = await fs.promises.readFile(resolvedPath, "utf-8");
3264
+ if (this.#streamingEditFileCache.has(resolvedPath)) return;
3265
+ const { text } = stripBom(rawText);
3266
+ this.#streamingEditFileCache.set(resolvedPath, normalizeToLF(text));
3267
+ } catch {
3268
+ // Don't cache on read errors (including ENOENT) - let the edit tool handle them
3269
+ } finally {
3270
+ this.#streamingEditPrecachePending.delete(resolvedPath);
3172
3271
  }
3173
3272
  }
3174
3273
 
@@ -3176,6 +3275,9 @@ export class AgentSession {
3176
3275
  if (this.#streamingEditFileCache.has(resolvedPath)) return;
3177
3276
 
3178
3277
  try {
3278
+ const stat = fs.statSync(resolvedPath);
3279
+ if (stat.size > MAX_EDIT_FILE_BYTES) return;
3280
+
3179
3281
  const rawText = fs.readFileSync(resolvedPath, "utf-8");
3180
3282
  const { text } = stripBom(rawText);
3181
3283
  this.#streamingEditFileCache.set(resolvedPath, normalizeToLF(text));
@@ -5296,6 +5398,7 @@ export class AgentSession {
5296
5398
 
5297
5399
  // Expand file-based prompt templates if requested
5298
5400
  const expandedText = expandPromptTemplates ? expandPromptTemplate(text, [...this.#promptTemplates]) : text;
5401
+ assertImagePlaceholdersHavePayload(expandedText, options?.images);
5299
5402
  const workflowIntentDiff = options?.synthetic ? null : buildWorkflowIntentDiff(expandedText);
5300
5403
 
5301
5404
  // If streaming, queue via steer() or followUp() based on option
@@ -5304,7 +5407,9 @@ export class AgentSession {
5304
5407
  throw new AgentBusyError();
5305
5408
  }
5306
5409
  if (options.streamingBehavior === "followUp") {
5307
- await this.#queueFollowUp(expandedText, options?.images);
5410
+ await this.#queueFollowUp(expandedText, options?.images, {
5411
+ forceOneAtATime: options.followUpQueuePolicy === "sequential",
5412
+ });
5308
5413
  } else {
5309
5414
  await this.#queueSteer(expandedText, options?.images);
5310
5415
  }
@@ -5422,7 +5527,7 @@ export class AgentSession {
5422
5527
 
5423
5528
  async promptCustomMessage<T = unknown>(
5424
5529
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
5425
- options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice">,
5530
+ options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy">,
5426
5531
  ): Promise<void> {
5427
5532
  const textContent =
5428
5533
  typeof message.content === "string"
@@ -5436,7 +5541,10 @@ export class AgentSession {
5436
5541
  if (!options?.streamingBehavior) {
5437
5542
  throw new AgentBusyError();
5438
5543
  }
5439
- await this.sendCustomMessage(message, { deliverAs: options.streamingBehavior });
5544
+ await this.sendCustomMessage(message, {
5545
+ deliverAs: options.streamingBehavior,
5546
+ followUpQueuePolicy: options.followUpQueuePolicy,
5547
+ });
5440
5548
  return;
5441
5549
  }
5442
5550
 
@@ -5777,25 +5885,34 @@ export class AgentSession {
5777
5885
  }
5778
5886
 
5779
5887
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
5888
+ assertImagePlaceholdersHavePayload(expandedText, images);
5780
5889
  await this.#queueSteer(expandedText, images);
5781
5890
  }
5782
5891
 
5783
5892
  /**
5784
5893
  * Queue a follow-up message to process after the agent would otherwise stop.
5785
5894
  */
5786
- async followUp(text: string, images?: ImageContent[]): Promise<void> {
5895
+ async followUp(
5896
+ text: string,
5897
+ images?: ImageContent[],
5898
+ options?: Pick<PromptOptions, "followUpQueuePolicy">,
5899
+ ): Promise<void> {
5787
5900
  if (text.startsWith("/")) {
5788
5901
  this.#throwIfExtensionCommand(text);
5789
5902
  }
5790
5903
 
5791
5904
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
5792
- await this.#queueFollowUp(expandedText, images);
5905
+ assertImagePlaceholdersHavePayload(expandedText, images);
5906
+ await this.#queueFollowUp(expandedText, images, {
5907
+ forceOneAtATime: options?.followUpQueuePolicy === "sequential",
5908
+ });
5793
5909
  }
5794
5910
 
5795
5911
  /**
5796
5912
  * Internal: Queue a steering message (already expanded, no extension command check).
5797
5913
  */
5798
5914
  async #queueSteer(text: string, images?: ImageContent[]): Promise<void> {
5915
+ assertImagePlaceholdersHavePayload(text, images);
5799
5916
  const displayText = text || (images && images.length > 0 ? "[Image]" : "");
5800
5917
  this.#steeringMessages.push(this.#createQueuedDisplayEntry(displayText));
5801
5918
  const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
@@ -5826,19 +5943,23 @@ export class AgentSession {
5826
5943
  /**
5827
5944
  * Internal: Queue a follow-up message (already expanded, no extension command check).
5828
5945
  */
5829
- async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
5946
+ async #queueFollowUp(text: string, images?: ImageContent[], options?: { forceOneAtATime?: boolean }): Promise<void> {
5947
+ assertImagePlaceholdersHavePayload(text, images);
5830
5948
  const displayText = text || (images && images.length > 0 ? "[Image]" : "");
5831
5949
  this.#followUpMessages.push(this.#createQueuedDisplayEntry(displayText));
5832
5950
  const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
5833
5951
  if (images && images.length > 0) {
5834
5952
  content.push(...images);
5835
5953
  }
5836
- this.agent.followUp({
5837
- role: "user",
5838
- content,
5839
- attribution: "user",
5840
- timestamp: Date.now(),
5841
- });
5954
+ this.agent.followUp(
5955
+ {
5956
+ role: "user",
5957
+ content,
5958
+ attribution: "user",
5959
+ timestamp: Date.now(),
5960
+ },
5961
+ options?.forceOneAtATime ? { forceOneAtATime: true } : undefined,
5962
+ );
5842
5963
  // When fully idle AND the session is in a resumable assistant-ended state,
5843
5964
  // schedule an immediate continue so the queued follow-up is delivered
5844
5965
  // without waiting for the next user turn. We gate on isStreaming (model
@@ -5986,7 +6107,11 @@ export class AgentSession {
5986
6107
  */
5987
6108
  async sendCustomMessage<T = unknown>(
5988
6109
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
5989
- options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
6110
+ options?: {
6111
+ triggerTurn?: boolean;
6112
+ deliverAs?: "steer" | "followUp" | "nextTurn";
6113
+ followUpQueuePolicy?: "respect-mode" | "sequential";
6114
+ },
5990
6115
  ): Promise<void> {
5991
6116
  const appMessage: CustomMessage<T> = {
5992
6117
  role: "custom",
@@ -6004,7 +6129,10 @@ export class AgentSession {
6004
6129
  }
6005
6130
 
6006
6131
  if (options?.deliverAs === "followUp") {
6007
- this.agent.followUp(appMessage);
6132
+ this.agent.followUp(
6133
+ appMessage,
6134
+ options.followUpQueuePolicy === "sequential" ? { forceOneAtATime: true } : undefined,
6135
+ );
6008
6136
  } else {
6009
6137
  this.agent.steer(appMessage);
6010
6138
  }
@@ -6930,7 +7058,7 @@ export class AgentSession {
6930
7058
 
6931
7059
  /**
6932
7060
  * Set thinking level.
6933
- * Saves the effective metadata-clamped level to session and settings only if it changes.
7061
+ * Saves the effective metadata-clamped level to the session, and to settings when requested.
6934
7062
  */
6935
7063
  setThinkingLevel(level: ThinkingLevel | undefined, persist: boolean = false): void {
6936
7064
  const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
@@ -6939,11 +7067,12 @@ export class AgentSession {
6939
7067
  this.#thinkingLevel = effectiveLevel;
6940
7068
  this.agent.setThinkingLevel(toReasoningEffort(effectiveLevel));
6941
7069
 
7070
+ if (persist && effectiveLevel !== undefined) {
7071
+ this.settings.set("defaultThinkingLevel", effectiveLevel);
7072
+ }
7073
+
6942
7074
  if (isChanging) {
6943
7075
  this.sessionManager.appendThinkingLevelChange(effectiveLevel);
6944
- if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
6945
- this.settings.set("defaultThinkingLevel", effectiveLevel);
6946
- }
6947
7076
  this.#emit({ type: "thinking_level_changed", thinkingLevel: effectiveLevel });
6948
7077
  }
6949
7078
  }
@@ -7708,9 +7837,14 @@ export class AgentSession {
7708
7837
  this.#resourceSampler = sampler;
7709
7838
  }
7710
7839
 
7840
+ setRetainedMemorySampler(sampler: (() => RetainedMemorySample) | undefined): void {
7841
+ this.#retainedMemorySampler = sampler;
7842
+ }
7843
+
7711
7844
  #defaultResourceSample(): EmergencyCompactionSample {
7712
7845
  let providerBytes = 0;
7713
7846
  let imageBytes = 0;
7847
+ const retainedMemory = this.#retainedMemorySampler?.() ?? {};
7714
7848
  for (const message of this.state.messages) {
7715
7849
  const content = (message as { content?: unknown }).content;
7716
7850
  if (typeof content === "string") {
@@ -7732,6 +7866,10 @@ export class AgentSession {
7732
7866
  providerBytes,
7733
7867
  messageCount: this.state.messages.length,
7734
7868
  imageBytes,
7869
+ sessionResidentImageBytes: this.sessionManager.getResidentImageBytes(),
7870
+ materializedResidentBytes: this.#streamingEditFileCache.totalBytes,
7871
+ tuiChatChildren: retainedMemory.tuiChatChildren ?? 0,
7872
+ tuiCachedRenderBytes: retainedMemory.tuiCachedRenderBytes ?? 0,
7735
7873
  };
7736
7874
  }
7737
7875
 
@@ -8047,7 +8185,7 @@ export class AgentSession {
8047
8185
  attribution: "agent",
8048
8186
  timestamp: Date.now(),
8049
8187
  });
8050
- this.#scheduleAgentContinue({ generation: this.#promptGeneration });
8188
+ this.#scheduleAgentContinue({ skipCompactionCheck: true });
8051
8189
  }
8052
8190
 
8053
8191
  /**