@code-yeongyu/senpi 2026.9.6 → 2026.9.7-2

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 (194) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/core/agent-session.d.ts +51 -5
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +168 -48
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/compaction/compaction.d.ts.map +1 -1
  7. package/dist/core/compaction/compaction.js +2 -1
  8. package/dist/core/compaction/compaction.js.map +1 -1
  9. package/dist/core/compaction/stuck-overflow.d.ts +11 -0
  10. package/dist/core/compaction/stuck-overflow.d.ts.map +1 -0
  11. package/dist/core/compaction/stuck-overflow.js +17 -0
  12. package/dist/core/compaction/stuck-overflow.js.map +1 -0
  13. package/dist/core/extensions/builtin/cache-keepalive/index.d.ts.map +1 -1
  14. package/dist/core/extensions/builtin/cache-keepalive/index.js +3 -21
  15. package/dist/core/extensions/builtin/cache-keepalive/index.js.map +1 -1
  16. package/dist/core/extensions/builtin/claude-sdk-oauth/session-binding.d.ts.map +1 -1
  17. package/dist/core/extensions/builtin/claude-sdk-oauth/session-binding.js +36 -19
  18. package/dist/core/extensions/builtin/claude-sdk-oauth/session-binding.js.map +1 -1
  19. package/dist/core/extensions/builtin/claude-sdk-oauth/session-commit-boundary.d.ts.map +1 -1
  20. package/dist/core/extensions/builtin/claude-sdk-oauth/session-commit-boundary.js +21 -10
  21. package/dist/core/extensions/builtin/claude-sdk-oauth/session-commit-boundary.js.map +1 -1
  22. package/dist/core/extensions/builtin/claude-sdk-oauth/session-continuity.d.ts +4 -17
  23. package/dist/core/extensions/builtin/claude-sdk-oauth/session-continuity.d.ts.map +1 -1
  24. package/dist/core/extensions/builtin/claude-sdk-oauth/session-continuity.js +23 -33
  25. package/dist/core/extensions/builtin/claude-sdk-oauth/session-continuity.js.map +1 -1
  26. package/dist/core/extensions/builtin/claude-sdk-oauth/session-stream.d.ts.map +1 -1
  27. package/dist/core/extensions/builtin/claude-sdk-oauth/session-stream.js +1 -0
  28. package/dist/core/extensions/builtin/claude-sdk-oauth/session-stream.js.map +1 -1
  29. package/dist/core/extensions/builtin/claude-sdk-oauth/session-sync.d.ts.map +1 -1
  30. package/dist/core/extensions/builtin/claude-sdk-oauth/session-sync.js +5 -3
  31. package/dist/core/extensions/builtin/claude-sdk-oauth/session-sync.js.map +1 -1
  32. package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-attempt.d.ts.map +1 -1
  33. package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-attempt.js +8 -3
  34. package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-attempt.js.map +1 -1
  35. package/dist/core/extensions/builtin/claude-sdk-oauth/tools.d.ts +6 -5
  36. package/dist/core/extensions/builtin/claude-sdk-oauth/tools.d.ts.map +1 -1
  37. package/dist/core/extensions/builtin/claude-sdk-oauth/tools.js +6 -5
  38. package/dist/core/extensions/builtin/claude-sdk-oauth/tools.js.map +1 -1
  39. package/dist/core/extensions/builtin/compaction/circuit-breaker.d.ts +8 -0
  40. package/dist/core/extensions/builtin/compaction/circuit-breaker.d.ts.map +1 -1
  41. package/dist/core/extensions/builtin/compaction/circuit-breaker.js +8 -0
  42. package/dist/core/extensions/builtin/compaction/circuit-breaker.js.map +1 -1
  43. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts.map +1 -1
  44. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js +24 -1
  45. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js.map +1 -1
  46. package/dist/core/extensions/builtin/compaction/fallback-failed-turn-normalization.d.ts +18 -0
  47. package/dist/core/extensions/builtin/compaction/fallback-failed-turn-normalization.d.ts.map +1 -0
  48. package/dist/core/extensions/builtin/compaction/fallback-failed-turn-normalization.js +20 -0
  49. package/dist/core/extensions/builtin/compaction/fallback-failed-turn-normalization.js.map +1 -0
  50. package/dist/core/extensions/builtin/compaction/index.js +2 -2
  51. package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
  52. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +3 -0
  53. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts.map +1 -1
  54. package/dist/core/extensions/builtin/compaction/lane-policy.js +47 -23
  55. package/dist/core/extensions/builtin/compaction/lane-policy.js.map +1 -1
  56. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +6 -4
  57. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js.map +1 -1
  58. package/dist/core/extensions/builtin/goal/cache-warm.d.ts +19 -1
  59. package/dist/core/extensions/builtin/goal/cache-warm.d.ts.map +1 -1
  60. package/dist/core/extensions/builtin/goal/cache-warm.js +23 -10
  61. package/dist/core/extensions/builtin/goal/cache-warm.js.map +1 -1
  62. package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts +1 -0
  63. package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts.map +1 -1
  64. package/dist/core/extensions/builtin/goal/continuation-recovery.js +2 -0
  65. package/dist/core/extensions/builtin/goal/continuation-recovery.js.map +1 -1
  66. package/dist/core/extensions/builtin/goal/continuation.d.ts +3 -1
  67. package/dist/core/extensions/builtin/goal/continuation.d.ts.map +1 -1
  68. package/dist/core/extensions/builtin/goal/continuation.js +4 -0
  69. package/dist/core/extensions/builtin/goal/continuation.js.map +1 -1
  70. package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts +3 -0
  71. package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts.map +1 -1
  72. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js +14 -3
  73. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js.map +1 -1
  74. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts +1 -1
  75. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts.map +1 -1
  76. package/dist/core/extensions/builtin/goal/monitor-continuation.js +7 -3
  77. package/dist/core/extensions/builtin/goal/monitor-continuation.js.map +1 -1
  78. package/dist/core/extensions/builtin/hooks/plugin-manifest.js +2 -2
  79. package/dist/core/extensions/builtin/hooks/plugin-manifest.js.map +1 -1
  80. package/dist/core/extensions/builtin/hooks/safety.js +2 -2
  81. package/dist/core/extensions/builtin/hooks/safety.js.map +1 -1
  82. package/dist/core/extensions/builtin/permission-system/external-dir.d.ts.map +1 -1
  83. package/dist/core/extensions/builtin/permission-system/external-dir.js +5 -22
  84. package/dist/core/extensions/builtin/permission-system/external-dir.js.map +1 -1
  85. package/dist/core/extensions/builtin/permission-system/parsers.d.ts.map +1 -1
  86. package/dist/core/extensions/builtin/permission-system/parsers.js +5 -7
  87. package/dist/core/extensions/builtin/permission-system/parsers.js.map +1 -1
  88. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts.map +1 -1
  89. package/dist/core/extensions/builtin/terminal/monitor-registry.js +14 -7
  90. package/dist/core/extensions/builtin/terminal/monitor-registry.js.map +1 -1
  91. package/dist/core/package-identity.d.ts +12 -0
  92. package/dist/core/package-identity.d.ts.map +1 -0
  93. package/dist/core/package-identity.js +57 -0
  94. package/dist/core/package-identity.js.map +1 -0
  95. package/dist/core/resource-loader.d.ts +0 -2
  96. package/dist/core/resource-loader.d.ts.map +1 -1
  97. package/dist/core/resource-loader.js +8 -60
  98. package/dist/core/resource-loader.js.map +1 -1
  99. package/dist/core/tools/bounded-realpath.d.ts +28 -0
  100. package/dist/core/tools/bounded-realpath.d.ts.map +1 -0
  101. package/dist/core/tools/bounded-realpath.js +53 -0
  102. package/dist/core/tools/bounded-realpath.js.map +1 -0
  103. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  104. package/dist/core/tools/file-mutation-queue.js +15 -13
  105. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  106. package/dist/core/tools/filesystem-policy.d.ts +7 -0
  107. package/dist/core/tools/filesystem-policy.d.ts.map +1 -1
  108. package/dist/core/tools/filesystem-policy.js +25 -15
  109. package/dist/core/tools/filesystem-policy.js.map +1 -1
  110. package/dist/core/trust-manager.d.ts.map +1 -1
  111. package/dist/core/trust-manager.js +13 -2
  112. package/dist/core/trust-manager.js.map +1 -1
  113. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  114. package/dist/modes/interactive/interactive-mode.js +1 -0
  115. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  116. package/dist/modes/interactive/tips/catalog/memory-tips.d.ts.map +1 -1
  117. package/dist/modes/interactive/tips/catalog/memory-tips.js +6 -0
  118. package/dist/modes/interactive/tips/catalog/memory-tips.js.map +1 -1
  119. package/dist/modes/interactive/tips/catalog/subagent-tips.d.ts.map +1 -1
  120. package/dist/modes/interactive/tips/catalog/subagent-tips.js +6 -0
  121. package/dist/modes/interactive/tips/catalog/subagent-tips.js.map +1 -1
  122. package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
  123. package/dist/modes/rpc/host-lifecycle.js +31 -5
  124. package/dist/modes/rpc/host-lifecycle.js.map +1 -1
  125. package/dist/modes/rpc/host-watchdog.d.ts +11 -0
  126. package/dist/modes/rpc/host-watchdog.d.ts.map +1 -1
  127. package/dist/modes/rpc/host-watchdog.js +16 -2
  128. package/dist/modes/rpc/host-watchdog.js.map +1 -1
  129. package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
  130. package/dist/modes/rpc/multi-session-host.js +54 -16
  131. package/dist/modes/rpc/multi-session-host.js.map +1 -1
  132. package/dist/modes/rpc/socket-ownership.d.ts +46 -0
  133. package/dist/modes/rpc/socket-ownership.d.ts.map +1 -0
  134. package/dist/modes/rpc/socket-ownership.js +178 -0
  135. package/dist/modes/rpc/socket-ownership.js.map +1 -0
  136. package/dist/utils/fs-watch.d.ts +17 -0
  137. package/dist/utils/fs-watch.d.ts.map +1 -1
  138. package/dist/utils/fs-watch.js +38 -10
  139. package/dist/utils/fs-watch.js.map +1 -1
  140. package/dist/utils/paths.d.ts +33 -0
  141. package/dist/utils/paths.d.ts.map +1 -1
  142. package/dist/utils/paths.js +109 -3
  143. package/dist/utils/paths.js.map +1 -1
  144. package/docs/compaction-guide.md +2 -2
  145. package/docs/compaction.md +2 -2
  146. package/docs/development.md +3 -2
  147. package/docs/providers.md +1 -1
  148. package/docs/rpc.md +1 -1
  149. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +29 -0
  150. package/node_modules/@code-yeongyu/senpi-codemode/README.md +14 -1
  151. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  152. package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/protocol.ts +1 -0
  153. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/runtime-factory.ts +3 -0
  154. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/session-manager.ts +7 -0
  155. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/AGENTS.md +7 -0
  156. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/jl/kernel.ts +6 -2
  157. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/inline-worker.ts +2 -2
  158. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-contract.ts +3 -0
  159. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-core.js +29 -0
  160. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-indirect-eval.js +10 -6
  161. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-shell-capture.d.ts +16 -0
  162. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-shell-capture.js +44 -8
  163. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-startup.ts +3 -2
  164. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel-contract.ts +3 -0
  165. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/transport.ts +11 -3
  166. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/rb/kernel.ts +6 -2
  167. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/session-env.ts +56 -0
  168. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/runtime-asset.ts +40 -14
  169. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-contract.ts +3 -0
  170. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-kernel.ts +9 -1
  171. package/node_modules/@code-yeongyu/senpi-codemode/src/output/output-meta.ts +26 -2
  172. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/image.ts +12 -3
  173. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  174. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  175. package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
  176. package/node_modules/@earendil-works/pi-ai/dist/providers/data/azure-openai-responses.json +1 -1
  177. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  178. package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
  179. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai-codex.json +1 -1
  180. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai.json +1 -1
  181. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  182. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  183. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opengateway.json +1 -1
  184. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  185. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  186. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts +1 -1
  187. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts.map +1 -1
  188. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +14 -3
  189. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js.map +1 -1
  190. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  191. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  192. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  193. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  194. package/package.json +7 -7
@@ -35,6 +35,7 @@ import { executeBashWithOperations } from "./bash-executor.js";
35
35
  import { envValue } from "./brand.js";
36
36
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, resolveThresholdContextTokens, shouldCompact, } from "./compaction/index.js";
37
37
  import { CompactionLifecycleCoordinator } from "./compaction/lifecycle.js";
38
+ import { isTurnStuckOnContextOverflow } from "./compaction/stuck-overflow.js";
38
39
  import { isWarmSummaryAnchorValid } from "./compaction/warm-anchor.js";
39
40
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
40
41
  import { buildDynamicSystemPrompt } from "./dynamic-prompt/index.js";
@@ -42,7 +43,7 @@ import { areExperimentalFeaturesEnabled } from "./experimental.js";
42
43
  import { exportSessionToHtml } from "./export-html/index.js";
43
44
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
44
45
  import { ModelUsabilityBudgetError, projectModelUsabilityBudget, } from "./extensions/builtin/compaction/model-usability-budget.js";
45
- import { resolveReserveTokens } from "./extensions/builtin/compaction/policy.js";
46
+ import { resolveEffectiveReserveTokens, resolveReserveTokens, shouldTriggerCompaction, } from "./extensions/builtin/compaction/policy.js";
46
47
  import { WAKE_SOURCE_STATE_EVENT } from "./extensions/builtin/monitor-state-event.js";
47
48
  import { CODEX_RESPONSES_API } from "./extensions/builtin/service-tier.js";
48
49
  import { deriveExtensionRegistrationId } from "./extensions/builtin/tool-search/engine/marker.js";
@@ -766,7 +767,7 @@ export class AgentSession {
766
767
  * session model so compaction never silently breaks.
767
768
  */
768
769
  _resolveCompactionModel(sessionModel) {
769
- const override = this.settingsManager.getCompactionSettings().model;
770
+ const override = this._getCompactionSettings().model;
770
771
  if (!override)
771
772
  return sessionModel;
772
773
  const slash = override.indexOf("/");
@@ -1136,6 +1137,31 @@ export class AgentSession {
1136
1137
  }
1137
1138
  _incrementMessageRevision() {
1138
1139
  this._messageRevision++;
1140
+ // A revision bump alone is not evidence that the rejected context changed:
1141
+ // releasing here let a synthetic bump retry the unchanged oversized context
1142
+ // (#7921 case 6). Release only once the context actually shrank.
1143
+ this._releaseBlockedPostCompactionAdmissionIfReduced();
1144
+ }
1145
+ /** Byte-derived size of the context an admission decision would carry. */
1146
+ _blockedAdmissionContentTokens() {
1147
+ return estimateMessagesTokens(filterContextExcludedMessages(this.agent.state.messages));
1148
+ }
1149
+ /** A compaction that genuinely reduced the context clears the blocked state. */
1150
+ _releaseBlockedPostCompactionAdmissionIfReduced() {
1151
+ const blocked = this._blockedPostCompactionAssistant;
1152
+ if (blocked === undefined)
1153
+ return;
1154
+ if (this._blockedAdmissionContentTokens() < blocked.contentTokens) {
1155
+ this._blockedPostCompactionAssistant = undefined;
1156
+ }
1157
+ }
1158
+ /**
1159
+ * Unconditional release for manual `/compact`, the user's explicit remedy. An
1160
+ * ordinary user prompt is deliberately not a release: it only adds context, so
1161
+ * it cannot make the rejected context admissible, and the pre-prompt gate
1162
+ * already owns its own fail-closed rejection for that route.
1163
+ */
1164
+ _releaseBlockedPostCompactionAdmission() {
1139
1165
  this._blockedPostCompactionAssistant = undefined;
1140
1166
  }
1141
1167
  getMessageRevision() {
@@ -1184,7 +1210,7 @@ export class AgentSession {
1184
1210
  systemPrompt: this.agent.state.systemPrompt,
1185
1211
  tools: this.agent.state.tools,
1186
1212
  liveContextTokens: this._getDownswitchLiveContextTokens(model),
1187
- compaction: this.settingsManager.getCompactionSettings(),
1213
+ compaction: this._getCompactionSettings(),
1188
1214
  });
1189
1215
  return projection.usable ? undefined : projection;
1190
1216
  }
@@ -1374,13 +1400,22 @@ export class AgentSession {
1374
1400
  const messages = filterContextExcludedMessages(this.agent.state.messages);
1375
1401
  return resolveThresholdContextTokens(directContextTokens, estimateMessagesTokens(messages));
1376
1402
  }
1403
+ /**
1404
+ * `compaction.enabled=false` disables proactive (threshold) compaction only.
1405
+ * A provider-confirmed overflow is an error the session cannot progress past
1406
+ * by any other route, so overflow recovery stays armed regardless (#1422).
1407
+ */
1377
1408
  _getAutoCompactionReason(message) {
1378
- const settings = this.settingsManager.getCompactionSettings();
1379
- if (!settings.enabled || message.stopReason === "aborted") {
1409
+ const settings = this._getCompactionSettings();
1410
+ if (message.stopReason === "aborted") {
1380
1411
  return undefined;
1381
1412
  }
1382
1413
  const model = this.model;
1383
- if (!model || this._isAssistantFromBeforeLatestCompaction(message)) {
1414
+ // Narrowed to the stale usage number itself: pre-boundary provenance does
1415
+ // not exempt a context whose own content already exceeds the policy.
1416
+ if (!model)
1417
+ return undefined;
1418
+ if (this._isAssistantFromBeforeLatestCompaction(message) && !this._exceedsPolicyByContentEstimate()) {
1384
1419
  return undefined;
1385
1420
  }
1386
1421
  const sameModel = isSameOverflowSource(message, model, this._modelRuntime.getCompatibilityRequestConfig(model).upstreamModelId);
@@ -1388,11 +1423,15 @@ export class AgentSession {
1388
1423
  const currentContextNeedsCompaction = contextUsage !== undefined &&
1389
1424
  contextUsage.tokens !== null &&
1390
1425
  shouldCompact(contextUsage.tokens, contextUsage.contextWindow, settings);
1391
- if (isContextOverflow(message, model.contextWindow) && (sameModel || currentContextNeedsCompaction)) {
1392
- return "overflow";
1426
+ const isOverflow = (isContextOverflow(message, model.contextWindow) && (sameModel || currentContextNeedsCompaction)) ||
1427
+ this._isCursorPayloadOverflow(message);
1428
+ if (isOverflow) {
1429
+ if (settings.enabled || isTurnStuckOnContextOverflow(message, model.contextWindow))
1430
+ return "overflow";
1431
+ return undefined;
1393
1432
  }
1394
- if (this._isCursorPayloadOverflow(message)) {
1395
- return "overflow";
1433
+ if (!settings.enabled) {
1434
+ return undefined;
1396
1435
  }
1397
1436
  let contextTokens;
1398
1437
  const directContextTokens = message.usage ? calculateContextTokens(message.usage) : 0;
@@ -1412,13 +1451,31 @@ export class AgentSession {
1412
1451
  if (compactionEntry &&
1413
1452
  usageMessage?.role === "assistant" &&
1414
1453
  this._isAssistantFromBeforeLatestCompaction(usageMessage)) {
1415
- return undefined;
1454
+ // Drop only the stale usage number; the messages themselves still count.
1455
+ const contentTokens = estimateMessagesTokens(messages);
1456
+ return shouldCompact(contentTokens, model.contextWindow, settings) ? "threshold" : undefined;
1416
1457
  }
1417
1458
  }
1418
1459
  contextTokens = estimate.tokens;
1419
1460
  }
1420
1461
  return shouldCompact(contextTokens, model.contextWindow, settings) ? "threshold" : undefined;
1421
1462
  }
1463
+ /**
1464
+ * Content measured by bytes, ignoring every provider usage number. The
1465
+ * stale-usage exemptions below only claim that a usage figure measured before
1466
+ * the accepted compaction boundary is not evidence of current pressure; they
1467
+ * must not exempt the messages themselves, because fresh post-boundary content
1468
+ * (a large tool result, late steering) is measurable without any usage report
1469
+ * (#7921 case 5).
1470
+ */
1471
+ _exceedsPolicyByContentEstimate() {
1472
+ const model = this.model;
1473
+ if (!model)
1474
+ return false;
1475
+ const settings = this._getCompactionSettings();
1476
+ const contextTokens = estimateMessagesTokens(filterContextExcludedMessages(this.agent.state.messages));
1477
+ return shouldCompact(contextTokens, model.contextWindow, settings);
1478
+ }
1422
1479
  _hasPendingPostCompactionUsageExemption(message) {
1423
1480
  return (this._skipNextPostCompactionAssistantCheck &&
1424
1481
  !this._assistantsPendingAtCompaction.has(message) &&
@@ -3642,7 +3699,7 @@ export class AgentSession {
3642
3699
  systemPrompt: this.agent.state.systemPrompt,
3643
3700
  tools: this.agent.state.tools,
3644
3701
  liveContextTokens,
3645
- compaction: this.settingsManager.getCompactionSettings(),
3702
+ compaction: this._getCompactionSettings(),
3646
3703
  includeSpeculationLead: options?.includeSpeculationLead,
3647
3704
  admission: options?.admission,
3648
3705
  });
@@ -3653,7 +3710,7 @@ export class AgentSession {
3653
3710
  const currentModel = this.model;
3654
3711
  if (!currentModel)
3655
3712
  return 0;
3656
- const compaction = this.settingsManager.getCompactionSettings();
3713
+ const compaction = this._getCompactionSettings();
3657
3714
  const currentBudget = projectModelUsabilityBudget({
3658
3715
  model: currentModel,
3659
3716
  systemPrompt: this.agent.state.systemPrompt,
@@ -4275,8 +4332,11 @@ export class AgentSession {
4275
4332
  const model = this.model;
4276
4333
  if (!model)
4277
4334
  throw new Error(formatNoModelSelectedMessage());
4335
+ // Manual compaction is the user's explicit remedy for a blocked admission
4336
+ // (#7921 case 6).
4337
+ this._releaseBlockedPostCompactionAdmission();
4278
4338
  const pathEntries = this.sessionManager.getBranch();
4279
- const settings = cursorOverflowCompactionSettings(this.settingsManager.getCompactionSettings(), model.provider, "manual");
4339
+ const settings = cursorOverflowCompactionSettings(this._getCompactionSettings(), model.provider, "manual");
4280
4340
  if (!prepareCompaction(pathEntries, settings)) {
4281
4341
  const requestId = randomUUID();
4282
4342
  const lastEntry = pathEntries[pathEntries.length - 1];
@@ -4516,7 +4576,7 @@ export class AgentSession {
4516
4576
  throw new CompactionCancelledError();
4517
4577
  }
4518
4578
  const pathEntries = this.sessionManager.getBranch();
4519
- const settings = cursorOverflowCompactionSettings(this.settingsManager.getCompactionSettings(), this.model?.provider, request.reason);
4579
+ const settings = cursorOverflowCompactionSettings(this._getCompactionSettings(), this.model?.provider, request.reason);
4520
4580
  let compactionResult = request.precomputed;
4521
4581
  let fromExtension = request.precomputed !== undefined;
4522
4582
  if (!compactionResult) {
@@ -4719,7 +4779,7 @@ export class AgentSession {
4719
4779
  simulatedMessages = truncateToolResultBodies(simulatedMessages).messages ?? simulatedMessages;
4720
4780
  }
4721
4781
  const contextTokens = estimateMessagesTokens(filterContextExcludedMessages(simulatedMessages));
4722
- const settings = this.settingsManager.getCompactionSettings();
4782
+ const settings = this._getCompactionSettings();
4723
4783
  const reserveTokens = settings.reserveScalingEnabled === false
4724
4784
  ? settings.reserveTokens
4725
4785
  : resolveReserveTokens(model.contextWindow, settings.reserveTokens);
@@ -4832,13 +4892,12 @@ export class AgentSession {
4832
4892
  * @returns Whether the post-run loop should call `agent.continue()` for overflow recovery or queued messages
4833
4893
  */
4834
4894
  async _enforceCompactionBeforeProvider(assistantMessage, skipAbortedCheck, inlineReason, retryAfterCompaction = false) {
4895
+ this._releaseBlockedPostCompactionAdmissionIfReduced();
4835
4896
  const blockedAdmission = this._blockedPostCompactionAssistant;
4836
- if (blockedAdmission !== undefined &&
4837
- blockedAdmission.assistant === assistantMessage &&
4838
- blockedAdmission.revision === this._messageRevision) {
4897
+ if (blockedAdmission !== undefined && blockedAdmission.assistant === assistantMessage) {
4839
4898
  throw new RequiredCompactionError();
4840
4899
  }
4841
- const settings = this.settingsManager.getCompactionSettings();
4900
+ const settings = this._getCompactionSettings();
4842
4901
  const model = this.model;
4843
4902
  const contextTokens = estimateContextTokens(filterContextExcludedMessages(this.sessionManager.buildSessionContext().messages)).tokens;
4844
4903
  const compacted = assistantMessage
@@ -4867,12 +4926,28 @@ export class AgentSession {
4867
4926
  }
4868
4927
  throw new RequiredCompactionError();
4869
4928
  }
4929
+ /**
4930
+ * Pending queued input the provider will also carry on this turn. Steering and
4931
+ * follow-up text enqueued after the admission projection was assembled - by a
4932
+ * `before_agent_start` handler, an extension action, or the user racing the
4933
+ * gate - is drained into the same run, so the final gate must measure it
4934
+ * (#7921 case 5).
4935
+ */
4936
+ _pendingQueuedInputMessages() {
4937
+ const timestamp = Date.now();
4938
+ return [...this._steeringMessages, ...this._followUpMessages].map((text) => ({
4939
+ role: "user",
4940
+ content: [{ type: "text", text }],
4941
+ timestamp,
4942
+ }));
4943
+ }
4870
4944
  /**
4871
4945
  * The normal pre-prompt check only estimates persisted session context. This
4872
4946
  * final gate also includes turn-local messages which the provider will see:
4873
- * the current prompt, next-turn custom messages, and before_agent_start
4874
- * additions. Compaction rewrites only session context, so callers retain and
4875
- * reapply their already-assembled one-shot additions after it succeeds.
4947
+ * the current prompt, next-turn custom messages, before_agent_start additions,
4948
+ * and steering or follow-up input queued after that projection was assembled.
4949
+ * Compaction rewrites only session context, so callers retain and reapply
4950
+ * their already-assembled one-shot additions after it succeeds.
4876
4951
  */
4877
4952
  async _enforceFinalProviderAdmission(messages) {
4878
4953
  // User-only prompts are deliberately admitted without this gate: prompt
@@ -4880,18 +4955,24 @@ export class AgentSession {
4880
4955
  // (issues #531/#886), so oversized user prompts rely on threshold
4881
4956
  // compaction — which also samples the local transcript estimate — and on
4882
4957
  // provider-overflow recovery. This gate closes the separate gap opened by
4883
- // turn-local custom additions, which the pre-prompt check cannot observe.
4884
- if (!messages.some((message) => message.role === "custom"))
4958
+ // turn-local custom additions and by late queued input, neither of which the
4959
+ // pre-prompt check can observe.
4960
+ const lateQueuedMessages = this._pendingQueuedInputMessages();
4961
+ if (!messages.some((message) => message.role === "custom") && lateQueuedMessages.length === 0)
4885
4962
  return;
4886
4963
  const model = this.model;
4887
4964
  if (!model)
4888
4965
  return;
4889
- const settings = this.settingsManager.getCompactionSettings();
4890
- const reserveTokens = settings.reserveScalingEnabled === false
4891
- ? settings.reserveTokens
4892
- : resolveReserveTokens(model.contextWindow, settings.reserveTokens);
4966
+ const settings = this._getCompactionSettings();
4967
+ const reserveTokens = resolveEffectiveReserveTokens(model.contextWindow, settings);
4893
4968
  const isOversized = () => {
4894
- const providerMessages = filterContextExcludedMessages([...this.agent.state.messages, ...messages]);
4969
+ const providerMessages = filterContextExcludedMessages([
4970
+ ...this.agent.state.messages,
4971
+ ...messages,
4972
+ // Re-read the queues on every sample: this closure is evaluated again
4973
+ // after compaction, when more input may have arrived.
4974
+ ...this._pendingQueuedInputMessages(),
4975
+ ]);
4895
4976
  const estimate = estimateContextTokens(providerMessages);
4896
4977
  const usageMessage = estimate.lastUsageIndex === null ? undefined : providerMessages[estimate.lastUsageIndex];
4897
4978
  // Kept assistant usage can describe the pre-compaction request. Once a
@@ -4920,9 +5001,7 @@ export class AgentSession {
4920
5001
  }
4921
5002
  }
4922
5003
  async _checkCompaction(assistantMessage, skipAbortedCheck = true, inlineReason, retryAfterCompaction = false) {
4923
- const settings = this.settingsManager.getCompactionSettings();
4924
- if (!settings.enabled)
4925
- return false;
5004
+ const settings = this._getCompactionSettings();
4926
5005
  // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
4927
5006
  if (skipAbortedCheck && assistantMessage.stopReason === "aborted")
4928
5007
  return false;
@@ -4937,7 +5016,12 @@ export class AgentSession {
4937
5016
  // compaction boundary. This prevents a stale pre-compaction usage/error
4938
5017
  // from retriggering compaction on the first prompt after compaction.
4939
5018
  const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
4940
- if (this._isAssistantFromBeforeLatestCompaction(assistantMessage)) {
5019
+ // The inline (pre-prompt) caller re-samples the current context itself and
5020
+ // owns the fail-closed rejection, so only the automatic route needs the
5021
+ // narrowed exemption here: fresh post-boundary content still counts even
5022
+ // though this message's own usage predates the boundary (#7921 case 5).
5023
+ if (this._isAssistantFromBeforeLatestCompaction(assistantMessage) &&
5024
+ (inlineReason !== undefined || !this._exceedsPolicyByContentEstimate())) {
4941
5025
  return false;
4942
5026
  }
4943
5027
  // Case 1: Overflow - LLM returned context overflow error.
@@ -4951,6 +5035,9 @@ export class AgentSession {
4951
5035
  const isOverflow = (isContextOverflow(assistantMessage, contextWindow) && (sameModel || currentContextNeedsCompaction)) ||
4952
5036
  recoverableLength ||
4953
5037
  this._isCursorPayloadOverflow(assistantMessage);
5038
+ if (isOverflow && !settings.enabled && !isTurnStuckOnContextOverflow(assistantMessage, contextWindow)) {
5039
+ return false;
5040
+ }
4954
5041
  if (isOverflow &&
4955
5042
  assistantMessage.stopReason === "stop" &&
4956
5043
  this._consumePostCompactionUsageExemption(assistantMessage)) {
@@ -4969,7 +5056,7 @@ export class AgentSession {
4969
5056
  getLatestCompactionEntry(this.sessionManager.getBranch()) !== null) {
4970
5057
  this._blockedPostCompactionAssistant = {
4971
5058
  assistant: assistantMessage,
4972
- revision: this._messageRevision,
5059
+ contentTokens: this._blockedAdmissionContentTokens(),
4973
5060
  };
4974
5061
  }
4975
5062
  return compacted;
@@ -5021,6 +5108,10 @@ export class AgentSession {
5021
5108
  }
5022
5109
  return compacted;
5023
5110
  }
5111
+ // Stuck-overflow recovery above runs regardless of the flag; threshold
5112
+ // compaction below is the proactive path the user switched off (#1422).
5113
+ if (!settings.enabled)
5114
+ return false;
5024
5115
  // The first ordinary response can carry provider usage calculated before
5025
5116
  // compaction. Consume that exemption only after proving this is not an
5026
5117
  // overflow, then retain the decision for later admission re-sampling.
@@ -5031,6 +5122,7 @@ export class AgentSession {
5031
5122
  // This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
5032
5123
  // responses can still compact and do not reset context accounting.
5033
5124
  let contextTokens;
5125
+ let staleUsageContentTokens;
5034
5126
  if (inlineReason) {
5035
5127
  const messages = filterContextExcludedMessages(this.sessionManager.buildSessionContext().messages);
5036
5128
  contextTokens = estimateContextTokens(messages).tokens;
@@ -5051,10 +5143,13 @@ export class AgentSession {
5051
5143
  if (compactionEntry &&
5052
5144
  usageMsg.role === "assistant" &&
5053
5145
  this._isAssistantFromBeforeLatestCompaction(usageMsg)) {
5054
- return false;
5146
+ // Drop only the stale usage number; the messages themselves still count.
5147
+ staleUsageContentTokens = estimateMessagesTokens(messages);
5148
+ if (!shouldCompact(staleUsageContentTokens, contextWindow, settings))
5149
+ return false;
5055
5150
  }
5056
5151
  }
5057
- contextTokens = estimate.tokens;
5152
+ contextTokens = staleUsageContentTokens ?? estimate.tokens;
5058
5153
  }
5059
5154
  }
5060
5155
  if (shouldCompact(contextTokens, contextWindow, settings)) {
@@ -5069,7 +5164,7 @@ export class AgentSession {
5069
5164
  getLatestCompactionEntry(this.sessionManager.getBranch()) !== null) {
5070
5165
  this._blockedPostCompactionAssistant = {
5071
5166
  assistant: assistantMessage,
5072
- revision: this._messageRevision,
5167
+ contentTokens: this._blockedAdmissionContentTokens(),
5073
5168
  };
5074
5169
  }
5075
5170
  return compacted;
@@ -5153,7 +5248,7 @@ export class AgentSession {
5153
5248
  }
5154
5249
  async _revalidateScheduledContinuationAdmission() {
5155
5250
  const model = this.model;
5156
- const settings = this.settingsManager.getCompactionSettings();
5251
+ const settings = this._getCompactionSettings();
5157
5252
  if (!model || !settings.enabled)
5158
5253
  return;
5159
5254
  const canonicalMessages = filterContextExcludedMessages(this.sessionManager.buildSessionContext().messages);
@@ -5162,11 +5257,27 @@ export class AgentSession {
5162
5257
  const contextTokens = usageMessage?.role === "assistant" && this._isAssistantFromBeforeLatestCompaction(usageMessage)
5163
5258
  ? estimateMessagesTokens(canonicalMessages)
5164
5259
  : estimate.tokens;
5165
- if (!shouldCompact(contextTokens, model.contextWindow, settings))
5260
+ // An explicit user prompt passes the proactive policy in the compaction
5261
+ // extension's before_agent_start, which automatic continuations never emit.
5262
+ // Sampling the same predicate here makes both routes compact at the same
5263
+ // usage instead of letting a continuation ride past the threshold up to the
5264
+ // hard valve (#7921 case 4).
5265
+ const atHardLimit = shouldCompact(contextTokens, model.contextWindow, settings);
5266
+ const overProactiveThreshold = shouldTriggerCompaction({
5267
+ tokens: contextTokens,
5268
+ contextWindow: model.contextWindow,
5269
+ percent: model.contextWindow > 0 ? (contextTokens / model.contextWindow) * 100 : 0,
5270
+ }, model.contextWindow, settings);
5271
+ if (!atHardLimit && !overProactiveThreshold)
5166
5272
  return;
5167
5273
  const compacted = await this._runPrePromptCompaction(this._findLastAssistantMessage(), true, "pre_prompt");
5168
5274
  if (!compacted) {
5169
- if (this._isCompactionOnCooldown() || this._isCompactionDelegated() || this._hasSupersedingCompactionClaim()) {
5275
+ // Proactive pressure alone must never brick an automatic continuation:
5276
+ // only the hard reserve valve stays fail-closed (#531/#886).
5277
+ if (!atHardLimit ||
5278
+ this._isCompactionOnCooldown() ||
5279
+ this._isCompactionDelegated() ||
5280
+ this._hasSupersedingCompactionClaim()) {
5170
5281
  return;
5171
5282
  }
5172
5283
  throw new RequiredCompactionError();
@@ -5333,7 +5444,7 @@ export class AgentSession {
5333
5444
  }
5334
5445
  if (!this._ownsCompactionController(autoCompactionController, "auto"))
5335
5446
  return false;
5336
- const preparation = prepareCompaction(this.sessionManager.getBranch(), cursorOverflowCompactionSettings(this.settingsManager.getCompactionSettings(), this.model?.provider, reason), reason === "overflow");
5447
+ const preparation = prepareCompaction(this.sessionManager.getBranch(), cursorOverflowCompactionSettings(this._getCompactionSettings(), this.model?.provider, reason), reason === "overflow");
5337
5448
  if (!preparation) {
5338
5449
  return endBeforeExecution();
5339
5450
  }
@@ -5417,12 +5528,21 @@ export class AgentSession {
5417
5528
  }
5418
5529
  }
5419
5530
  /**
5420
- * Toggle auto-compaction setting.
5531
+ * Toggle auto-compaction for this session only. Persisting the flag is a
5532
+ * settings-editor concern; a session-level command (RPC `set_auto_compaction`,
5533
+ * one thread of a multi-session host) must never rewrite the global setting
5534
+ * for every other session on the machine (#1422).
5421
5535
  */
5422
5536
  setAutoCompactionEnabled(enabled) {
5423
- this.settingsManager.setCompactionEnabled(enabled);
5537
+ this._autoCompactionSessionOverride = enabled;
5424
5538
  this._emitSessionSettingsChanged();
5425
5539
  }
5540
+ _getCompactionSettings() {
5541
+ const settings = this.settingsManager.getCompactionSettings();
5542
+ if (this._autoCompactionSessionOverride === undefined)
5543
+ return settings;
5544
+ return { ...settings, enabled: this._autoCompactionSessionOverride };
5545
+ }
5426
5546
  _emitSessionSettingsChanged() {
5427
5547
  this._emit({
5428
5548
  type: "session_settings_changed",
@@ -5431,9 +5551,9 @@ export class AgentSession {
5431
5551
  autoCompactionEnabled: this.autoCompactionEnabled,
5432
5552
  });
5433
5553
  }
5434
- /** Whether auto-compaction is enabled */
5554
+ /** Whether auto-compaction is enabled for this session */
5435
5555
  get autoCompactionEnabled() {
5436
- return this.settingsManager.getCompactionEnabled();
5556
+ return this._autoCompactionSessionOverride ?? this.settingsManager.getCompactionEnabled();
5437
5557
  }
5438
5558
  async bindExtensions(bindings) {
5439
5559
  const finishBindingWork = this._sessionWorkBarrier.begin();
@@ -5670,7 +5790,7 @@ export class AgentSession {
5670
5790
  this._extensionShutdownHandler?.();
5671
5791
  },
5672
5792
  getContextUsage: () => this.getContextUsage(),
5673
- getCompactionSettings: () => this.settingsManager.getCompactionSettings(),
5793
+ getCompactionSettings: () => this._getCompactionSettings(),
5674
5794
  getPromptCacheSafeWaitSeconds: () => this.resolvePromptCacheSafeWaitSeconds(),
5675
5795
  getPromptCacheGoalBackstopMaxSeconds: () => this.settingsManager.getPromptCacheGoalBackstopMaxSeconds(),
5676
5796
  getPromptCacheKeepAliveSettings: () => this.settingsManager.getPromptCacheKeepAliveSettings(),
@@ -6659,7 +6779,7 @@ export class AgentSession {
6659
6779
  // the prior retry checks. Revalidate canonical session context immediately
6660
6780
  // before the continuation so a rejected compaction never admits that model.
6661
6781
  const model = this.model;
6662
- const compactionSettings = this.settingsManager.getCompactionSettings();
6782
+ const compactionSettings = this._getCompactionSettings();
6663
6783
  const contextTokens = estimateContextTokens(filterContextExcludedMessages(this.sessionManager.buildSessionContext().messages)).tokens;
6664
6784
  if (compactionSettings.enabled &&
6665
6785
  model &&