@code-yeongyu/senpi 2026.9.11 → 2026.9.12-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 (156) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  3. package/dist/core/agent-session-runtime.js +5 -1
  4. package/dist/core/agent-session-runtime.js.map +1 -1
  5. package/dist/core/agent-session.d.ts +13 -9
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +54 -121
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/cursor-history-admission.d.ts +50 -0
  10. package/dist/core/cursor-history-admission.d.ts.map +1 -0
  11. package/dist/core/cursor-history-admission.js +130 -0
  12. package/dist/core/cursor-history-admission.js.map +1 -0
  13. package/dist/core/extensions/builtin/cursor-cli-oauth/models.d.ts.map +1 -1
  14. package/dist/core/extensions/builtin/cursor-cli-oauth/models.js +2 -1
  15. package/dist/core/extensions/builtin/cursor-cli-oauth/models.js.map +1 -1
  16. package/dist/core/session-manager.d.ts +2 -0
  17. package/dist/core/session-manager.d.ts.map +1 -1
  18. package/dist/core/session-manager.js +22 -13
  19. package/dist/core/session-manager.js.map +1 -1
  20. package/dist/core/session-write-reservation.d.ts +9 -0
  21. package/dist/core/session-write-reservation.d.ts.map +1 -1
  22. package/dist/core/session-write-reservation.js +33 -0
  23. package/dist/core/session-write-reservation.js.map +1 -1
  24. package/dist/modes/interactive/components/ask-user-async-widget.d.ts +2 -0
  25. package/dist/modes/interactive/components/ask-user-async-widget.d.ts.map +1 -1
  26. package/dist/modes/interactive/components/ask-user-async-widget.js +17 -1
  27. package/dist/modes/interactive/components/ask-user-async-widget.js.map +1 -1
  28. package/dist/modes/interactive/interactive-mode.js +2 -2
  29. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  30. package/dist/modes/rpc/rpc-types.d.ts +2 -1
  31. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  32. package/dist/modes/rpc/rpc-types.js +1 -0
  33. package/dist/modes/rpc/rpc-types.js.map +1 -1
  34. package/dist/modes/rpc/session-path-reservations.d.ts +30 -0
  35. package/dist/modes/rpc/session-path-reservations.d.ts.map +1 -0
  36. package/dist/modes/rpc/session-path-reservations.js +58 -0
  37. package/dist/modes/rpc/session-path-reservations.js.map +1 -0
  38. package/dist/modes/rpc/session-registry.d.ts +3 -1
  39. package/dist/modes/rpc/session-registry.d.ts.map +1 -1
  40. package/dist/modes/rpc/session-registry.js.map +1 -1
  41. package/dist/modes/rpc/session-worker-client.d.ts +10 -8
  42. package/dist/modes/rpc/session-worker-client.d.ts.map +1 -1
  43. package/dist/modes/rpc/session-worker-client.js +14 -23
  44. package/dist/modes/rpc/session-worker-client.js.map +1 -1
  45. package/dist/modes/rpc/session-worker-credit.d.ts +17 -0
  46. package/dist/modes/rpc/session-worker-credit.d.ts.map +1 -0
  47. package/dist/modes/rpc/session-worker-credit.js +39 -0
  48. package/dist/modes/rpc/session-worker-credit.js.map +1 -0
  49. package/dist/modes/rpc/session-worker-protocol.d.ts +10 -0
  50. package/dist/modes/rpc/session-worker-protocol.d.ts.map +1 -1
  51. package/dist/modes/rpc/session-worker-protocol.js +2 -0
  52. package/dist/modes/rpc/session-worker-protocol.js.map +1 -1
  53. package/dist/modes/rpc/session-worker-signals.d.ts +7 -0
  54. package/dist/modes/rpc/session-worker-signals.d.ts.map +1 -0
  55. package/dist/modes/rpc/session-worker-signals.js +19 -0
  56. package/dist/modes/rpc/session-worker-signals.js.map +1 -0
  57. package/dist/modes/rpc/session-worker.js +12 -16
  58. package/dist/modes/rpc/session-worker.js.map +1 -1
  59. package/dist/modes/rpc/worker-session-registry.d.ts +12 -0
  60. package/dist/modes/rpc/worker-session-registry.d.ts.map +1 -1
  61. package/dist/modes/rpc/worker-session-registry.js +51 -24
  62. package/dist/modes/rpc/worker-session-registry.js.map +1 -1
  63. package/docs/rpc.md +14 -6
  64. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +24 -0
  65. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  66. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  67. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.d.ts +11 -0
  68. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.d.ts.map +1 -1
  69. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.js +18 -0
  70. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.js.map +1 -1
  71. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts +1 -1
  72. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts.map +1 -1
  73. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js +5 -1
  74. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js.map +1 -1
  75. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/discovery.d.ts +2 -0
  76. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/discovery.d.ts.map +1 -1
  77. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/discovery.js +96 -51
  78. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/discovery.js.map +1 -1
  79. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/gen/cascade_pb.d.ts +389 -1
  80. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/gen/cascade_pb.d.ts.map +1 -1
  81. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/gen/cascade_pb.js +132 -4
  82. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/gen/cascade_pb.js.map +1 -1
  83. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/metadata.d.ts +34 -4
  84. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/metadata.d.ts.map +1 -1
  85. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/metadata.js +41 -8
  86. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/metadata.js.map +1 -1
  87. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/paths.d.ts +20 -0
  88. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/paths.d.ts.map +1 -1
  89. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/paths.js +20 -0
  90. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/paths.js.map +1 -1
  91. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/request.d.ts +18 -6
  92. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/request.d.ts.map +1 -1
  93. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/request.js +77 -35
  94. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/request.js.map +1 -1
  95. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/stream-state.d.ts +9 -3
  96. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/stream-state.d.ts.map +1 -1
  97. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/stream-state.js +30 -38
  98. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/stream-state.js.map +1 -1
  99. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/trailer.d.ts +16 -0
  100. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/trailer.d.ts.map +1 -0
  101. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/trailer.js +69 -0
  102. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/trailer.js.map +1 -0
  103. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/unary.d.ts +26 -0
  104. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/unary.d.ts.map +1 -0
  105. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/unary.js +61 -0
  106. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/unary.js.map +1 -0
  107. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/wire.d.ts +6 -4
  108. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/wire.d.ts.map +1 -1
  109. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/wire.js +6 -4
  110. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent/wire.js.map +1 -1
  111. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent.d.ts +13 -5
  112. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent.d.ts.map +1 -1
  113. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent.js +76 -21
  114. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent.js.map +1 -1
  115. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/devin.d.ts +5 -0
  116. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/devin.d.ts.map +1 -1
  117. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/devin.js +7 -2
  118. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/devin.js.map +1 -1
  119. package/node_modules/@earendil-works/pi-ai/dist/cursor/context-limit-store.d.ts +32 -0
  120. package/node_modules/@earendil-works/pi-ai/dist/cursor/context-limit-store.d.ts.map +1 -0
  121. package/node_modules/@earendil-works/pi-ai/dist/cursor/context-limit-store.js +62 -0
  122. package/node_modules/@earendil-works/pi-ai/dist/cursor/context-limit-store.js.map +1 -0
  123. package/node_modules/@earendil-works/pi-ai/dist/cursor/store-migration.d.ts.map +1 -1
  124. package/node_modules/@earendil-works/pi-ai/dist/cursor/store-migration.js +2 -1
  125. package/node_modules/@earendil-works/pi-ai/dist/cursor/store-migration.js.map +1 -1
  126. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +1 -1
  127. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
  128. package/node_modules/@earendil-works/pi-ai/dist/index.js +1 -1
  129. package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
  130. package/node_modules/@earendil-works/pi-ai/dist/model.d.ts +11 -1
  131. package/node_modules/@earendil-works/pi-ai/dist/model.d.ts.map +1 -1
  132. package/node_modules/@earendil-works/pi-ai/dist/model.js.map +1 -1
  133. package/node_modules/@earendil-works/pi-ai/dist/providers/cursor.d.ts.map +1 -1
  134. package/node_modules/@earendil-works/pi-ai/dist/providers/cursor.js +2 -1
  135. package/node_modules/@earendil-works/pi-ai/dist/providers/cursor.js.map +1 -1
  136. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  137. package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
  138. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  139. package/node_modules/@earendil-works/pi-ai/dist/providers/data/venice.json +1 -1
  140. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  141. package/node_modules/@earendil-works/pi-ai/dist/providers/devin.d.ts.map +1 -1
  142. package/node_modules/@earendil-works/pi-ai/dist/providers/devin.js +3 -3
  143. package/node_modules/@earendil-works/pi-ai/dist/providers/devin.js.map +1 -1
  144. package/node_modules/@earendil-works/pi-ai/dist/providers/devin.models.d.ts +4 -2
  145. package/node_modules/@earendil-works/pi-ai/dist/providers/devin.models.d.ts.map +1 -1
  146. package/node_modules/@earendil-works/pi-ai/dist/providers/devin.models.js +18 -9
  147. package/node_modules/@earendil-works/pi-ai/dist/providers/devin.models.js.map +1 -1
  148. package/node_modules/@earendil-works/pi-ai/dist/utils/cursor-context-limit.d.ts +12 -0
  149. package/node_modules/@earendil-works/pi-ai/dist/utils/cursor-context-limit.d.ts.map +1 -0
  150. package/node_modules/@earendil-works/pi-ai/dist/utils/cursor-context-limit.js +93 -0
  151. package/node_modules/@earendil-works/pi-ai/dist/utils/cursor-context-limit.js.map +1 -0
  152. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  153. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  154. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  155. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  156. package/package.json +7 -7
@@ -18,8 +18,9 @@ import { rm } from "node:fs/promises";
18
18
  import { basename, dirname } from "node:path";
19
19
  import { isDeepStrictEqual } from "node:util";
20
20
  import { ProviderRetryWatchdogAbortError, prepareAgentToolCall } from "@earendil-works/pi-agent-core";
21
- import { contentText, measureCursorHistorySerializedBytes, providerNotConfiguredMessage, SERVER_FALLBACK_ABORTED_DIAGNOSTIC, } from "@earendil-works/pi-ai";
21
+ import { contentText, providerNotConfiguredMessage, SERVER_FALLBACK_ABORTED_DIAGNOSTIC, } from "@earendil-works/pi-ai";
22
22
  import { cleanupSessionResources, cursorOverflowCompactionSettings, isClassifierRefusal, isContextOverflow, isCursorPayloadResourceExhausted, isCursorQuotaResourceExhausted, isCursorZeroTokenResourceExhausted, isProviderStreamStallError, isProviderTimeoutError, isRecoverableLength, isRetryableAssistantError, modelsAreEqual, resetApiProviders, shouldRetryOverflowWithoutCompact, streamSimple, } from "@earendil-works/pi-ai/compat";
23
+ import { getCursorContextLimit } from "@earendil-works/pi-ai/utils/cursor-context-limit";
23
24
  import { extract429RetryAfterMs, parseRetryAfterMsMarker } from "@earendil-works/pi-ai/utils/retry-hint";
24
25
  import { retryBackoffDelayMs } from "@earendil-works/pi-ai/utils/retry-profile/backoff";
25
26
  import { getAgentDir } from "../config.js";
@@ -37,6 +38,7 @@ import { calculateContextTokens, collectEntriesForBranchSummary, compact, estima
37
38
  import { CompactionLifecycleCoordinator } from "./compaction/lifecycle.js";
38
39
  import { isTurnStuckOnContextOverflow } from "./compaction/stuck-overflow.js";
39
40
  import { isWarmSummaryAnchorValid } from "./compaction/warm-anchor.js";
41
+ import { admitCursorHistory, cursorAdmissionBudgetBytes } from "./cursor-history-admission.js";
40
42
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
41
43
  import { buildDynamicSystemPrompt } from "./dynamic-prompt/index.js";
42
44
  import { AssistantEditError, assertExpectedLeaf, assistantTextEquals, buildEditedAssistantMessage, SessionStreamingError, } from "./edited-assistant-message.js";
@@ -57,7 +59,7 @@ import { emitSessionShutdownEvent } from "./extensions/runner.js";
57
59
  import { normalizeToolExposure, RUNTIME_EXTENSION_PATH } from "./extensions/types.js";
58
60
  import { shouldWarnHighReasoning } from "./high-reasoning-warning.js";
59
61
  import { isManualContinueSubmission, MANUAL_CONTINUE_CUSTOM_TYPE, MANUAL_CONTINUE_DIRECTIVE, } from "./manual-continue.js";
60
- import { convertToLlm, filterContextExcludedMessages, } from "./messages.js";
62
+ import { filterContextExcludedMessages } from "./messages.js";
61
63
  import { ModelRegistry } from "./model-registry.js";
62
64
  import { getModelNarrowingPatterns, resolveModelScope } from "./model-resolver.js";
63
65
  import { PROMPT_CACHE_SAFE_WAIT_ENV, resolvePromptCacheSafeWaitSeconds } from "./prompt-cache-budget.js";
@@ -335,121 +337,11 @@ function isSameOverflowSource(message, model, upstreamModelId) {
335
337
  const THINKING_LEVELS_WITH_MAX = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
336
338
  /** Caps explicit skill expansion so one prompt cannot consume unbounded context. */
337
339
  export const MAX_SKILL_EXPANSIONS_PER_PROMPT = 5;
338
- /** Cursor ingest rejects large verbatim tool payloads. The bound is UTF-8 bytes. */
339
- export const CURSOR_TOOL_RESULT_MAX_CHARS = 2000;
340
- export const CURSOR_TOOL_RESULT_MAX_BYTES = 50_000;
341
- const CURSOR_TRUNCATION_MARKER = "\n...[truncated]";
342
- export function truncateToolResultBodies(messages, maxChars = CURSOR_TOOL_RESULT_MAX_CHARS, maxBytes = CURSOR_TOOL_RESULT_MAX_BYTES, convert = (candidate) => convertToLlm(candidate)) {
343
- if (!Array.isArray(messages) || messages.length === 0)
344
- return { messages, changed: false };
345
- const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
346
- const markerChars = [...segmenter.segment(CURSOR_TRUNCATION_MARKER)].length;
347
- const result = messages.slice();
348
- let changed = false;
349
- // Apply the per-result character cap first, independent of the aggregate wire cap.
350
- for (let messageIndex = result.length - 1; messageIndex >= 0; messageIndex--) {
351
- const message = result[messageIndex];
352
- if (message.role !== "toolResult" || !Array.isArray(message.content))
353
- continue;
354
- let content = message.content;
355
- for (let partIndex = content.length - 1; partIndex >= 0; partIndex--) {
356
- const part = content[partIndex];
357
- if (part.type === "image" && typeof part.data === "string")
358
- continue;
359
- if (part.type !== "text" || typeof part.text !== "string")
360
- continue;
361
- const graphemes = [...segmenter.segment(part.text)].map((item) => item.segment);
362
- if (graphemes.length <= maxChars)
363
- continue;
364
- const kept = graphemes.slice(0, Math.max(0, maxChars - markerChars)).join("");
365
- const nextText = kept + CURSOR_TRUNCATION_MARKER;
366
- content = content === message.content ? content.slice() : content;
367
- content[partIndex] = { ...part, text: nextText };
368
- result[messageIndex] = { ...message, content };
369
- changed = true;
370
- }
371
- }
372
- const measure = (candidate) => {
373
- const converted = convert(candidate);
374
- const activeUserMessageIndex = converted.at(-1)?.role === "user" ? converted.length - 1 : -1;
375
- return measureCursorHistorySerializedBytes(converted, activeUserMessageIndex);
376
- };
377
- const fits = (candidate = result) => measure(candidate) <= maxBytes;
378
- if (fits())
379
- return { messages: changed ? result : messages, changed };
380
- // Empty the oldest result bodies. Search the monotonic prefix of candidates
381
- // rather than serializing once for every result (admission must stay bounded).
382
- const emptyToolResult = (message) => {
383
- if (message.role !== "toolResult" || !Array.isArray(message.content))
384
- return message;
385
- const emptiedContent = message.content.map((part) => part.type === "text" ? { ...part, text: "" } : part.type === "image" ? { ...part, data: "" } : part);
386
- const content = emptiedContent.filter((part, index) => {
387
- if (index === 0)
388
- return true;
389
- const previous = emptiedContent[index - 1];
390
- const empty = part.type === "text" ? part.text === "" : part.type === "image" && part.data === "";
391
- const previousEmpty = previous.type === "text" ? previous.text === "" : previous.type === "image" && previous.data === "";
392
- return !empty || !previousEmpty;
393
- });
394
- return { ...message, content };
395
- };
396
- const toolResultIndexes = result.flatMap((message, index) => message.role === "toolResult" && Array.isArray(message.content) ? [index] : []);
397
- const withEmptyPrefix = (count) => {
398
- const candidate = result.slice();
399
- for (let i = 0; i < count; i++)
400
- candidate[toolResultIndexes[i]] = emptyToolResult(candidate[toolResultIndexes[i]]);
401
- return candidate;
402
- };
403
- let low = 0;
404
- let high = toolResultIndexes.length;
405
- while (low < high) {
406
- const middle = Math.floor((low + high) / 2);
407
- if (fits(withEmptyPrefix(middle + 1)))
408
- high = middle;
409
- else
410
- low = middle + 1;
411
- }
412
- const emptyCount = Math.min(low + 1, toolResultIndexes.length);
413
- if (emptyCount > 0) {
414
- result.splice(0, result.length, ...withEmptyPrefix(emptyCount));
415
- changed = true;
416
- }
417
- if (fits())
418
- return { messages: changed ? result : messages, changed };
419
- // If metadata alone exceeds the cap, discard the oldest complete turns. This
420
- // search is also monotonic and avoids quadratic whole-history reserialization.
421
- const turnRanges = [];
422
- const isConvertedUser = (message) => convert([message])[0]?.role === "user";
423
- for (let index = 0; index < result.length; index++) {
424
- if (!isConvertedUser(result[index]))
425
- continue;
426
- const nextUser = result.findIndex((message, nextIndex) => nextIndex > index && isConvertedUser(message));
427
- turnRanges.push([index, nextUser < 0 ? result.length : nextUser]);
428
- }
429
- const withoutTurns = (count) => {
430
- if (count === 0)
431
- return result;
432
- const start = turnRanges[0]?.[0] ?? 0;
433
- const end = turnRanges[count - 1]?.[1] ?? start;
434
- return [...result.slice(0, start), ...result.slice(end)];
435
- };
436
- low = 0;
437
- high = turnRanges.length;
438
- while (low < high) {
439
- const middle = Math.floor((low + high) / 2);
440
- if (fits(withoutTurns(middle + 1)))
441
- high = middle;
442
- else
443
- low = middle + 1;
444
- }
445
- const turnCount = Math.min(low + 1, turnRanges.length);
446
- if (turnCount > 0) {
447
- const next = withoutTurns(turnCount);
448
- result.splice(0, result.length, ...next);
449
- changed = true;
450
- }
451
- return { messages: changed ? result : messages, changed };
452
- }
340
+ /**
341
+ * Cursor admission lives in its own module; the names stay exported here so
342
+ * existing importers keep resolving them.
343
+ */
344
+ export { CURSOR_TOOL_RESULT_MAX_CHARS, truncateToolResultBodies } from "./cursor-history-admission.js";
453
345
  // ============================================================================
454
346
  // AgentSession Class
455
347
  // ============================================================================
@@ -905,6 +797,20 @@ export class AgentSession {
905
797
  usage: hookResult?.usage,
906
798
  };
907
799
  }
800
+ /**
801
+ * Cursor states a model's real context ceiling on every conversation
802
+ * checkpoint. Once observed, it replaces the catalog guess on the live model
803
+ * so context usage, compaction thresholds and admission all size against the
804
+ * window the server will actually enforce.
805
+ */
806
+ _applyObservedCursorContextWindow(model) {
807
+ const observed = getCursorContextLimit(model.id);
808
+ if (observed === undefined || observed <= 0 || observed === model.contextWindow)
809
+ return;
810
+ const previous = model.contextWindow;
811
+ model.contextWindow = observed;
812
+ this._sessionLogger.info("cursor_context_window_observed", { modelId: model.id, previous, observed });
813
+ }
908
814
  _installAgentNextTurnRefresh() {
909
815
  const previousPrepareNextTurnWithContext = this.agent.prepareNextTurnWithContext ??
910
816
  (this.agent.prepareNextTurn
@@ -913,10 +819,33 @@ export class AgentSession {
913
819
  const previousTransformContext = this.agent.transformContext;
914
820
  this.agent.transformContext = async (messages, signal) => {
915
821
  const transformed = previousTransformContext ? await previousTransformContext(messages, signal) : messages;
916
- if (this.model?.provider === "cursor" || this.model?.provider === "cursor-cli-oauth") {
917
- return ((await truncateToolResultBodies(transformed, CURSOR_TOOL_RESULT_MAX_CHARS, CURSOR_TOOL_RESULT_MAX_BYTES, (candidate) => this.agent.convertToLlm(candidate))).messages ?? transformed);
822
+ const model = this.model;
823
+ if (model?.provider !== "cursor" && model?.provider !== "cursor-cli-oauth")
824
+ return transformed;
825
+ this._applyObservedCursorContextWindow(model);
826
+ const budgetBytes = cursorAdmissionBudgetBytes(model.contextWindow);
827
+ const admission = admitCursorHistory({
828
+ messages: transformed,
829
+ budgetBytes,
830
+ convert: (candidate) => this.agent.convertToLlm(candidate),
831
+ });
832
+ if (admission.blankedToolResults > 0) {
833
+ this._sessionLogger.info("cursor_admission_truncated", {
834
+ blankedToolResults: admission.blankedToolResults,
835
+ bytesBefore: admission.bytesBefore,
836
+ bytesAfter: admission.bytesAfter,
837
+ budgetBytes,
838
+ });
839
+ }
840
+ if (admission.overBudget) {
841
+ // The request is still admitted: Cursor answers an oversized history
842
+ // with a 0-token resource_exhausted, which the session layer compacts.
843
+ this._sessionLogger.warn("cursor_admission_over_budget", {
844
+ bytes: admission.bytesAfter,
845
+ budgetBytes,
846
+ });
918
847
  }
919
- return transformed;
848
+ return admission.messages ?? transformed;
920
849
  };
921
850
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
922
851
  // Enforce compaction only when this prepare precedes an actual provider
@@ -4897,7 +4826,11 @@ export class AgentSession {
4897
4826
  // Size the same retained context that will be admitted to Cursor. Persisted JSONL
4898
4827
  // remains verbatim, but the in-memory request representation is bounded first.
4899
4828
  if (model.provider === "cursor" || model.provider === "cursor-cli-oauth") {
4900
- simulatedMessages = truncateToolResultBodies(simulatedMessages).messages ?? simulatedMessages;
4829
+ simulatedMessages =
4830
+ admitCursorHistory({
4831
+ messages: simulatedMessages,
4832
+ budgetBytes: cursorAdmissionBudgetBytes(model.contextWindow),
4833
+ }).messages ?? simulatedMessages;
4901
4834
  }
4902
4835
  const contextTokens = estimateMessagesTokens(filterContextExcludedMessages(simulatedMessages));
4903
4836
  const settings = this._getCompactionSettings();