@code-yeongyu/senpi 2026.9.19 → 2026.9.20
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.
- package/CHANGELOG.md +67 -0
- package/dist/bundle/chunks/{app-server-command-QK7Q6XK5.js → app-server-command-B35FQUTE.js} +1 -1
- package/dist/bundle/chunks/{bun-extension-importer-JQZRPNJH.js → bun-extension-importer-HHH4TV4B.js} +3 -3
- package/dist/bundle/chunks/{chunk-YRN3PCTM.js → chunk-DQZB4GVV.js} +4 -4
- package/dist/bundle/chunks/{chunk-KKDSBABY.js → chunk-E5R67F7G.js} +38 -38
- package/dist/bundle/chunks/{chunk-NARL7XFO.js → chunk-TWGC3TBR.js} +2 -2
- package/dist/bundle/chunks/{chunk-RLRLQRI2.js → chunk-V2ZB62DG.js} +1 -1
- package/dist/bundle/chunks/{chunk-3FRTRCR6.js → chunk-YXQG2SUX.js} +2 -2
- package/dist/bundle/chunks/{chunk-PLZK7ABD.js → chunk-Z4LHCTJ6.js} +1 -1
- package/dist/bundle/chunks/{chunk-3Q4WGKFA.js → chunk-ZMFHNH6U.js} +1 -1
- package/dist/bundle/chunks/{cli-main-7SG3K4K3.js → cli-main-USVT7JMN.js} +1 -1
- package/dist/bundle/chunks/{interactive-host-runtime-AITXUDCT.js → interactive-host-runtime-ELBAZ4LN.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-7MN6GGCG.js → interactive-mode-R6225BKI.js} +1 -1
- package/dist/bundle/chunks/{list-tips-KTDNGQUK.js → list-tips-Z7T2ACVU.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-WKX34SOP.js → multi-session-host-QNEBJW2P.js} +3 -3
- package/dist/bundle/chunks/{package-manager-cli-CB2X57TD.js → package-manager-cli-4MVJ3JSE.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-CI6V2SBW.js → rpc-mode-CXVDRIBU.js} +1 -1
- package/dist/bundle/chunks/{session-picker-DXCITGLS.js → session-picker-6N666KQZ.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +67 -67
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +14 -1
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +49 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +290 -44
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/builtin/ask-user/extension.d.ts.map +1 -1
- package/dist/core/extensions/builtin/ask-user/extension.js +16 -5
- package/dist/core/extensions/builtin/ask-user/extension.js.map +1 -1
- package/dist/core/extensions/builtin/ask-user/format.d.ts +5 -0
- package/dist/core/extensions/builtin/ask-user/format.d.ts.map +1 -1
- package/dist/core/extensions/builtin/ask-user/format.js +4 -0
- package/dist/core/extensions/builtin/ask-user/format.js.map +1 -1
- package/dist/core/extensions/builtin/ask-user/notify.d.ts +2 -0
- package/dist/core/extensions/builtin/ask-user/notify.d.ts.map +1 -1
- package/dist/core/extensions/builtin/ask-user/notify.js +2 -0
- package/dist/core/extensions/builtin/ask-user/notify.js.map +1 -1
- package/dist/core/extensions/builtin/ask-user/registry.d.ts +15 -2
- package/dist/core/extensions/builtin/ask-user/registry.d.ts.map +1 -1
- package/dist/core/extensions/builtin/ask-user/registry.js +13 -0
- package/dist/core/extensions/builtin/ask-user/registry.js.map +1 -1
- package/dist/core/extensions/builtin/ask-user/render.d.ts +6 -0
- package/dist/core/extensions/builtin/ask-user/render.d.ts.map +1 -1
- package/dist/core/extensions/builtin/ask-user/render.js +9 -0
- package/dist/core/extensions/builtin/ask-user/render.js.map +1 -1
- package/dist/core/extensions/builtin/ask-user/resume.d.ts.map +1 -1
- package/dist/core/extensions/builtin/ask-user/resume.js +59 -24
- package/dist/core/extensions/builtin/ask-user/resume.js.map +1 -1
- package/dist/core/extensions/builtin/ask-user/tool.d.ts +1 -1
- package/dist/core/extensions/builtin/ask-user/tool.d.ts.map +1 -1
- package/dist/core/extensions/builtin/ask-user/tool.js +98 -17
- package/dist/core/extensions/builtin/ask-user/tool.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/model-usability-budget.d.ts +18 -0
- package/dist/core/extensions/builtin/compaction/model-usability-budget.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/model-usability-budget.js +16 -8
- package/dist/core/extensions/builtin/compaction/model-usability-budget.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/switch-admission.d.ts +23 -0
- package/dist/core/extensions/builtin/compaction/switch-admission.d.ts.map +1 -0
- package/dist/core/extensions/builtin/compaction/switch-admission.js +19 -0
- package/dist/core/extensions/builtin/compaction/switch-admission.js.map +1 -0
- package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/index.js +10 -0
- package/dist/core/extensions/builtin/goal/index.js.map +1 -1
- package/dist/core/extensions/bun-extension-commonjs.d.ts +1 -1
- package/dist/core/extensions/bun-extension-commonjs.d.ts.map +1 -1
- package/dist/core/extensions/bun-extension-commonjs.js +3 -3
- package/dist/core/extensions/bun-extension-commonjs.js.map +1 -1
- package/dist/core/extensions/bun-extension-importer.d.ts.map +1 -1
- package/dist/core/extensions/bun-extension-importer.js +4 -1
- package/dist/core/extensions/bun-extension-importer.js.map +1 -1
- package/dist/core/extensions/bun-extension-registry.d.ts.map +1 -1
- package/dist/core/extensions/bun-extension-registry.js +5 -9
- package/dist/core/extensions/bun-extension-registry.js.map +1 -1
- package/dist/core/resolved-paths-memo.d.ts +31 -0
- package/dist/core/resolved-paths-memo.d.ts.map +1 -0
- package/dist/core/resolved-paths-memo.js +57 -0
- package/dist/core/resolved-paths-memo.js.map +1 -0
- package/dist/core/resource-loader.d.ts +4 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +25 -12
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/retry-fallback/settings.d.ts +10 -2
- package/dist/core/retry-fallback/settings.d.ts.map +1 -1
- package/dist/core/retry-fallback/settings.js +28 -6
- package/dist/core/retry-fallback/settings.js.map +1 -1
- package/dist/core/skill-discovery.d.ts.map +1 -1
- package/dist/core/skill-discovery.js +11 -1
- package/dist/core/skill-discovery.js.map +1 -1
- package/dist/main.d.ts +10 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +19 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/ask-user-answer-chip.d.ts +2 -5
- package/dist/modes/interactive/components/ask-user-answer-chip.d.ts.map +1 -1
- package/dist/modes/interactive/components/ask-user-answer-chip.js +1 -4
- package/dist/modes/interactive/components/ask-user-answer-chip.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +2 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +8 -0
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/assistant-render-descriptors.d.ts +1 -0
- package/dist/modes/interactive/components/assistant-render-descriptors.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-render-descriptors.js +4 -0
- package/dist/modes/interactive/components/assistant-render-descriptors.js.map +1 -1
- package/dist/modes/interactive/components/status-indicator.d.ts +3 -1
- package/dist/modes/interactive/components/status-indicator.d.ts.map +1 -1
- package/dist/modes/interactive/components/status-indicator.js +15 -2
- package/dist/modes/interactive/components/status-indicator.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +81 -6
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/provider-error-presentation.d.ts +18 -0
- package/dist/modes/interactive/provider-error-presentation.d.ts.map +1 -0
- package/dist/modes/interactive/provider-error-presentation.js +100 -0
- package/dist/modes/interactive/provider-error-presentation.js.map +1 -0
- package/dist/modes/rpc/rpc-types.d.ts +18 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
- package/dist/modes/rpc/session-command-router.js +14 -0
- package/dist/modes/rpc/session-command-router.js.map +1 -1
- package/dist/modes/rpc/session-event-writer.d.ts +9 -1
- package/dist/modes/rpc/session-event-writer.d.ts.map +1 -1
- package/dist/modes/rpc/session-event-writer.js +21 -0
- package/dist/modes/rpc/session-event-writer.js.map +1 -1
- package/dist/modes/rpc/session-registry.d.ts.map +1 -1
- package/dist/modes/rpc/session-registry.js +9 -2
- package/dist/modes/rpc/session-registry.js.map +1 -1
- package/dist/modes/rpc/session-worker-requests.d.ts +10 -1
- package/dist/modes/rpc/session-worker-requests.d.ts.map +1 -1
- package/dist/modes/rpc/session-worker-requests.js +8 -2
- package/dist/modes/rpc/session-worker-requests.js.map +1 -1
- package/dist/modes/rpc/session-worker-requests.test.d.ts +2 -0
- package/dist/modes/rpc/session-worker-requests.test.d.ts.map +1 -0
- package/dist/modes/rpc/session-worker-requests.test.js +44 -0
- package/dist/modes/rpc/session-worker-requests.test.js.map +1 -0
- package/docs/rpc.md +7 -0
- package/docs/settings.md +1 -1
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +24 -0
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opengateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai-coding-cn.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/native-module-path.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/native-module-path.js +26 -3
- package/node_modules/@earendil-works/pi-tui/dist/native-module-path.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js +13 -6
- package/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui-main-screen.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui-main-screen.js +11 -3
- package/node_modules/@earendil-works/pi-tui/dist/tui-main-screen.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts +15 -2
- package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui.js +45 -2
- package/node_modules/@earendil-works/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +8 -8
|
@@ -50,7 +50,8 @@ import { CLAUDE_SDK_OAUTH_PROVIDER_ID } from "./extensions/builtin/claude-sdk-oa
|
|
|
50
50
|
import { ModelUsabilityBudgetError, projectModelUsabilityBudget, } from "./extensions/builtin/compaction/model-usability-budget.js";
|
|
51
51
|
import { resolveEffectiveReserveTokens, resolveReserveTokens, shouldTriggerCompaction, } from "./extensions/builtin/compaction/policy.js";
|
|
52
52
|
import { createResumeCompactionRequirement, } from "./extensions/builtin/compaction/resume-admission.js";
|
|
53
|
-
import { RESUME_SLICE_ORIGIN, RESUME_SLICE_SCHEMA, resumeSliceNotice, } from "./extensions/builtin/compaction/resume-slice.js";
|
|
53
|
+
import { planResumeSlice, RESUME_SLICE_ORIGIN, RESUME_SLICE_SCHEMA, resumeSliceNotice, } from "./extensions/builtin/compaction/resume-slice.js";
|
|
54
|
+
import { createPendingModelSwitch, pendingSwitchKeepRecentTokens, } from "./extensions/builtin/compaction/switch-admission.js";
|
|
54
55
|
import { WAKE_SOURCE_STATE_EVENT } from "./extensions/builtin/monitor-state-event.js";
|
|
55
56
|
import { CODEX_RESPONSES_API } from "./extensions/builtin/service-tier.js";
|
|
56
57
|
import { deriveExtensionRegistrationId } from "./extensions/builtin/tool-search/engine/marker.js";
|
|
@@ -440,6 +441,8 @@ export class AgentSession {
|
|
|
440
441
|
modelSelectSource: reason,
|
|
441
442
|
invalidateCompaction: true,
|
|
442
443
|
ephemeralThinkingLevel: thinking,
|
|
444
|
+
allowDeferral: false,
|
|
445
|
+
repairWithSlice: true,
|
|
443
446
|
});
|
|
444
447
|
},
|
|
445
448
|
emit: (event) => this._emit(event),
|
|
@@ -1081,17 +1084,30 @@ export class AgentSession {
|
|
|
1081
1084
|
this._delegatedCompactionKey = undefined;
|
|
1082
1085
|
this._incrementMessageRevision();
|
|
1083
1086
|
}
|
|
1087
|
+
/**
|
|
1088
|
+
* #1873 narrows #1378: a cycle silently passes over a candidate only when no
|
|
1089
|
+
* reduction could ever make it serve. A candidate that one compaction would fix
|
|
1090
|
+
* is landed on and held as a pending switch instead, because skipping it throws
|
|
1091
|
+
* away the model the user was cycling towards.
|
|
1092
|
+
*/
|
|
1084
1093
|
_modelChangeWouldExhaustContext(model) {
|
|
1085
1094
|
if (model.contextWindow <= 0)
|
|
1086
1095
|
return undefined;
|
|
1096
|
+
const compaction = this._getCompactionSettings();
|
|
1087
1097
|
const projection = projectModelUsabilityBudget({
|
|
1088
1098
|
model,
|
|
1089
1099
|
systemPrompt: this.agent.state.systemPrompt,
|
|
1090
1100
|
tools: this.agent.state.tools,
|
|
1091
1101
|
liveContextTokens: this._getDownswitchLiveContextTokens(model),
|
|
1092
|
-
compaction
|
|
1102
|
+
compaction,
|
|
1103
|
+
includeSpeculationLead: false,
|
|
1104
|
+
admission: this._modelSwitchAdmission(),
|
|
1093
1105
|
});
|
|
1094
|
-
|
|
1106
|
+
if (projection.usable)
|
|
1107
|
+
return undefined;
|
|
1108
|
+
if (compaction.enabled && projection.verdict === "fits-after-compaction")
|
|
1109
|
+
return undefined;
|
|
1110
|
+
return projection;
|
|
1095
1111
|
}
|
|
1096
1112
|
_getIdleWaitPromise() {
|
|
1097
1113
|
if (!this._idleWaitPromise) {
|
|
@@ -2936,30 +2952,7 @@ export class AgentSession {
|
|
|
2936
2952
|
}
|
|
2937
2953
|
// Emit before_agent_start extension event
|
|
2938
2954
|
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPrompt, this._baseSystemPromptOptions);
|
|
2939
|
-
|
|
2940
|
-
if (result?.messages) {
|
|
2941
|
-
for (const msg of result.messages) {
|
|
2942
|
-
messages.push({
|
|
2943
|
-
role: "custom",
|
|
2944
|
-
customType: msg.customType,
|
|
2945
|
-
// Untyped extensions can pass null/missing content; normalize at ingestion.
|
|
2946
|
-
content: msg.content ?? [],
|
|
2947
|
-
display: msg.display,
|
|
2948
|
-
details: msg.details,
|
|
2949
|
-
timestamp: Date.now(),
|
|
2950
|
-
});
|
|
2951
|
-
}
|
|
2952
|
-
}
|
|
2953
|
-
// Apply extension-modified system prompt, or reset to base
|
|
2954
|
-
if (result?.systemPrompt !== undefined) {
|
|
2955
|
-
this._systemPromptOverride = result.systemPrompt;
|
|
2956
|
-
this.agent.state.systemPrompt = result.systemPrompt;
|
|
2957
|
-
}
|
|
2958
|
-
else {
|
|
2959
|
-
// Ensure we're using the base prompt (in case previous turn had modifications)
|
|
2960
|
-
this._systemPromptOverride = undefined;
|
|
2961
|
-
this.agent.state.systemPrompt = this._baseSystemPrompt;
|
|
2962
|
-
}
|
|
2955
|
+
this._applyBeforeAgentStartResult(messages, result);
|
|
2963
2956
|
// The preflight above only sees persisted session context. These prompt,
|
|
2964
2957
|
// next-turn, and before_agent_start additions are also provider-visible,
|
|
2965
2958
|
// so make one final admission decision against the complete request.
|
|
@@ -3008,6 +3001,31 @@ export class AgentSession {
|
|
|
3008
3001
|
this._startSessionTitleGeneration(titlePrompt);
|
|
3009
3002
|
}
|
|
3010
3003
|
}
|
|
3004
|
+
/** Apply before_agent_start additions and reset any prior turn's system-prompt override. */
|
|
3005
|
+
_applyBeforeAgentStartResult(messages, result) {
|
|
3006
|
+
if (result?.messages) {
|
|
3007
|
+
for (const msg of result.messages) {
|
|
3008
|
+
messages.push({
|
|
3009
|
+
role: "custom",
|
|
3010
|
+
customType: msg.customType,
|
|
3011
|
+
// Untyped extensions can pass null/missing content; normalize at ingestion.
|
|
3012
|
+
content: msg.content ?? [],
|
|
3013
|
+
display: msg.display,
|
|
3014
|
+
details: msg.details,
|
|
3015
|
+
timestamp: Date.now(),
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
if (result?.systemPrompt !== undefined) {
|
|
3020
|
+
this._systemPromptOverride = result.systemPrompt;
|
|
3021
|
+
this.agent.state.systemPrompt = result.systemPrompt;
|
|
3022
|
+
}
|
|
3023
|
+
else {
|
|
3024
|
+
// Ensure we're using the base prompt (in case a previous turn had modifications).
|
|
3025
|
+
this._systemPromptOverride = undefined;
|
|
3026
|
+
this.agent.state.systemPrompt = this._baseSystemPrompt;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3011
3029
|
/**
|
|
3012
3030
|
* Try to execute an extension command. Returns true if command was found and executed.
|
|
3013
3031
|
*/
|
|
@@ -3384,24 +3402,43 @@ export class AgentSession {
|
|
|
3384
3402
|
}
|
|
3385
3403
|
}
|
|
3386
3404
|
else if (options?.triggerTurn) {
|
|
3405
|
+
finishSessionWork ??= this._sessionWorkBarrier.begin();
|
|
3406
|
+
const messages = [appMessage];
|
|
3407
|
+
const queueTriggerForLater = () => {
|
|
3408
|
+
if (options.deliverAs === "followUp") {
|
|
3409
|
+
this.agent.followUp(appMessage);
|
|
3410
|
+
}
|
|
3411
|
+
else {
|
|
3412
|
+
this.agent.steer(appMessage);
|
|
3413
|
+
}
|
|
3414
|
+
};
|
|
3415
|
+
this._triggerTurnAdmissionAbortGeneration = userAbortGeneration;
|
|
3387
3416
|
try {
|
|
3388
3417
|
await this._enforceCompactionBeforeProvider(this._findLastAssistantMessage(), false, "pre_prompt");
|
|
3389
|
-
await this.
|
|
3418
|
+
const result = await this._extensionRunner.emitBeforeAgentStart(contentText(appMessage.content, ""), undefined, this._baseSystemPrompt, this._baseSystemPromptOptions);
|
|
3419
|
+
if (userAbortGeneration !== this._userAbortGeneration) {
|
|
3420
|
+
queueTriggerForLater();
|
|
3421
|
+
return;
|
|
3422
|
+
}
|
|
3423
|
+
this._applyBeforeAgentStartResult(messages, result);
|
|
3424
|
+
await this._enforceFinalProviderAdmission(messages);
|
|
3425
|
+
if (userAbortGeneration !== this._userAbortGeneration) {
|
|
3426
|
+
queueTriggerForLater();
|
|
3427
|
+
return;
|
|
3428
|
+
}
|
|
3390
3429
|
}
|
|
3391
3430
|
catch (error) {
|
|
3392
3431
|
// Mirror sendUserMessage's retention contract: an admission
|
|
3393
3432
|
// rejection must retain the message for later delivery instead
|
|
3394
3433
|
// of silently dropping it (the fire-and-forget extension action
|
|
3395
3434
|
// swallows this rejection).
|
|
3396
|
-
|
|
3397
|
-
this.agent.followUp(appMessage);
|
|
3398
|
-
}
|
|
3399
|
-
else {
|
|
3400
|
-
this.agent.steer(appMessage);
|
|
3401
|
-
}
|
|
3435
|
+
queueTriggerForLater();
|
|
3402
3436
|
throw error;
|
|
3403
3437
|
}
|
|
3404
|
-
|
|
3438
|
+
finally {
|
|
3439
|
+
this._triggerTurnAdmissionAbortGeneration = undefined;
|
|
3440
|
+
}
|
|
3441
|
+
await this._promptAgent(messages, deferredTurnClaim);
|
|
3405
3442
|
}
|
|
3406
3443
|
else if (this.isStreaming) {
|
|
3407
3444
|
this._pendingCustomMessages.push(appMessage);
|
|
@@ -3593,12 +3630,15 @@ export class AgentSession {
|
|
|
3593
3630
|
// Streaming aborts are carried by agent_end provenance; only gaps need session_abort.
|
|
3594
3631
|
const wasMidRun = this.isStreaming && this._retryAbortController === undefined;
|
|
3595
3632
|
const hadRetryBackoff = this._retryAbortController !== undefined;
|
|
3596
|
-
const
|
|
3633
|
+
const hasPendingTriggerTurnAdmission = this._triggerTurnAdmissionAbortGeneration === this._userAbortGeneration;
|
|
3634
|
+
const hadCompactionOrPending = !this.isStreaming && (this.isCompacting || this.pendingMessageCount > 0 || hasPendingTriggerTurnAdmission);
|
|
3597
3635
|
const hadClearedQueues = this._hadClearedQueuedMessages;
|
|
3598
3636
|
const joinedAgentEndBoundary = this._abortProvenance.hasOpenAgentEndBoundary;
|
|
3599
3637
|
this._hadClearedQueuedMessages = false;
|
|
3600
3638
|
const shouldEmitAbort = !joinedAgentEndBoundary && !wasMidRun && (hadRetryBackoff || hadCompactionOrPending || hadClearedQueues);
|
|
3601
3639
|
this.abortCompaction();
|
|
3640
|
+
if (hasPendingTriggerTurnAdmission)
|
|
3641
|
+
this._recordUserAbort();
|
|
3602
3642
|
await this._abortActiveAgentAndRetry("user");
|
|
3603
3643
|
if (!shouldEmitAbort)
|
|
3604
3644
|
return;
|
|
@@ -3678,6 +3718,120 @@ export class AgentSession {
|
|
|
3678
3718
|
throw new ModelUsabilityBudgetError(projection);
|
|
3679
3719
|
}
|
|
3680
3720
|
/** Admit an oversized restored transcript so the normal pre-provider compaction can run. */
|
|
3721
|
+
/** The switch waiting for the next send to compact for it, if any (#1873). */
|
|
3722
|
+
get pendingModelSwitch() {
|
|
3723
|
+
return this._pendingModelSwitch;
|
|
3724
|
+
}
|
|
3725
|
+
/**
|
|
3726
|
+
* A switch the target cannot hold yet but one compaction would fix. Refusing it
|
|
3727
|
+
* discards what the user asked for; committing it strands the session on a model
|
|
3728
|
+
* that cannot answer. Holding it does neither: nothing durable is written until
|
|
3729
|
+
* the next send has reduced the transcript and the switch actually applies.
|
|
3730
|
+
*/
|
|
3731
|
+
_projectSwitchDeferral(model, liveContextTokens) {
|
|
3732
|
+
if (model.contextWindow <= 0)
|
|
3733
|
+
return undefined;
|
|
3734
|
+
const compaction = this._getCompactionSettings();
|
|
3735
|
+
if (!compaction.enabled)
|
|
3736
|
+
return undefined;
|
|
3737
|
+
if (this._modelSwitchAdmission() !== "switch")
|
|
3738
|
+
return undefined;
|
|
3739
|
+
const projection = projectModelUsabilityBudget({
|
|
3740
|
+
model,
|
|
3741
|
+
systemPrompt: this.agent.state.systemPrompt,
|
|
3742
|
+
tools: this.agent.state.tools,
|
|
3743
|
+
liveContextTokens,
|
|
3744
|
+
compaction,
|
|
3745
|
+
includeSpeculationLead: false,
|
|
3746
|
+
admission: "switch",
|
|
3747
|
+
});
|
|
3748
|
+
return projection.verdict === "fits-after-compaction" ? projection : undefined;
|
|
3749
|
+
}
|
|
3750
|
+
_admitSwitchCompactionRequired(model, projection, persistDefault) {
|
|
3751
|
+
const pending = createPendingModelSwitch({ model, projection, persistDefault });
|
|
3752
|
+
this._pendingModelSwitch = pending;
|
|
3753
|
+
this._emit({
|
|
3754
|
+
type: "model_change_pending",
|
|
3755
|
+
model,
|
|
3756
|
+
contextWindow: projection.contextWindow,
|
|
3757
|
+
liveContextTokens: projection.liveContextTokens,
|
|
3758
|
+
requiredTokens: projection.requiredTokens,
|
|
3759
|
+
shortfallTokens: projection.shortfallTokens,
|
|
3760
|
+
notice: pending.notice,
|
|
3761
|
+
});
|
|
3762
|
+
}
|
|
3763
|
+
/**
|
|
3764
|
+
* Fit a fallback rung by reducing the transcript without a provider request
|
|
3765
|
+
* (#1873). Summarizing is not available here: the model being fallen back from
|
|
3766
|
+
* has usually just failed, and the rung itself cannot hold the transcript, so
|
|
3767
|
+
* the deterministic slice is the only reduction that can run. The recorded
|
|
3768
|
+
* transcript stays intact in the session file.
|
|
3769
|
+
*/
|
|
3770
|
+
_reduceForSwitchTarget(model, liveContextTokens) {
|
|
3771
|
+
if (!this._getCompactionSettings().enabled)
|
|
3772
|
+
return liveContextTokens;
|
|
3773
|
+
const projection = this._projectSwitchFit(model);
|
|
3774
|
+
if (projection.usable || projection.verdict !== "fits-after-compaction")
|
|
3775
|
+
return liveContextTokens;
|
|
3776
|
+
const plan = planResumeSlice({ entries: this.sessionManager.getBranch(), projection });
|
|
3777
|
+
if (!plan)
|
|
3778
|
+
return liveContextTokens;
|
|
3779
|
+
this.applyResumeSlice(plan);
|
|
3780
|
+
return this._projectSwitchFit(model).liveContextTokens;
|
|
3781
|
+
}
|
|
3782
|
+
_projectSwitchFit(model) {
|
|
3783
|
+
// Measured per message rather than through the context estimate: a retained
|
|
3784
|
+
// turn keeps the usage it reported before the reduction, so a usage-derived
|
|
3785
|
+
// total cannot observe the compaction that just ran (the sdk.ts convention).
|
|
3786
|
+
const liveContextTokens = filterContextExcludedMessages(this.sessionManager.buildSessionContext().messages).reduce((total, message) => total + estimateTokens(message), 0);
|
|
3787
|
+
return projectModelUsabilityBudget({
|
|
3788
|
+
model,
|
|
3789
|
+
systemPrompt: this.agent.state.systemPrompt,
|
|
3790
|
+
tools: this.agent.state.tools,
|
|
3791
|
+
liveContextTokens,
|
|
3792
|
+
compaction: this._getCompactionSettings(),
|
|
3793
|
+
includeSpeculationLead: false,
|
|
3794
|
+
admission: "switch",
|
|
3795
|
+
});
|
|
3796
|
+
}
|
|
3797
|
+
/**
|
|
3798
|
+
* Repair and apply a held switch, in the one order that works: the model that
|
|
3799
|
+
* can still hold the transcript writes the summary, the reduction aims at the
|
|
3800
|
+
* window the transcript must end up inside, and only a transcript that actually
|
|
3801
|
+
* fits is allowed to carry the switch. The deterministic slice is the floor when
|
|
3802
|
+
* summarization did not get there, so a repairable switch cannot strand itself.
|
|
3803
|
+
*/
|
|
3804
|
+
async _applyPendingModelSwitch(assistantMessage, skipAbortedCheck, inlineReason) {
|
|
3805
|
+
const pending = this._pendingModelSwitch;
|
|
3806
|
+
if (pending === undefined)
|
|
3807
|
+
return false;
|
|
3808
|
+
if (!this.model)
|
|
3809
|
+
throw new RequiredCompactionError();
|
|
3810
|
+
await this._runPrePromptCompaction(assistantMessage, skipAbortedCheck, inlineReason, false, false, pendingSwitchKeepRecentTokens(pending.projection));
|
|
3811
|
+
let projection = this._projectSwitchFit(pending.model);
|
|
3812
|
+
if (!projection.usable) {
|
|
3813
|
+
const plan = planResumeSlice({ entries: this.sessionManager.getBranch(), projection });
|
|
3814
|
+
if (plan) {
|
|
3815
|
+
this.applyResumeSlice(plan);
|
|
3816
|
+
projection = this._projectSwitchFit(pending.model);
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
this._pendingModelSwitch = undefined;
|
|
3820
|
+
if (!projection.usable) {
|
|
3821
|
+
const error = new ModelUsabilityBudgetError(projection);
|
|
3822
|
+
this._recordRejectedModelChange(pending.model, error);
|
|
3823
|
+
throw error;
|
|
3824
|
+
}
|
|
3825
|
+
await this._switchActiveModel(pending.model, {
|
|
3826
|
+
persistDefault: pending.persistDefault,
|
|
3827
|
+
appendSessionEntry: true,
|
|
3828
|
+
emitModelSelect: true,
|
|
3829
|
+
modelSelectSource: "set",
|
|
3830
|
+
invalidateCompaction: true,
|
|
3831
|
+
allowDeferral: false,
|
|
3832
|
+
});
|
|
3833
|
+
return true;
|
|
3834
|
+
}
|
|
3681
3835
|
admitResumeCompactionRequired(projection) {
|
|
3682
3836
|
this._resumeCompactionRequirement = createResumeCompactionRequirement(projection);
|
|
3683
3837
|
}
|
|
@@ -3783,8 +3937,17 @@ export class AgentSession {
|
|
|
3783
3937
|
* observable on one path and invisible on its sibling.
|
|
3784
3938
|
*/
|
|
3785
3939
|
_assertModelUsableForSwitch(model, liveContextTokens) {
|
|
3940
|
+
const admission = this._modelSwitchAdmission();
|
|
3786
3941
|
try {
|
|
3787
|
-
|
|
3942
|
+
// #1873: a live switch only has to fit the next single request. Speculation
|
|
3943
|
+
// runs after the switch is admitted and cannot shrink a transcript it has not
|
|
3944
|
+
// been admitted to yet - the reasoning #1339 applied to resume, which this
|
|
3945
|
+
// extends to the switch path. The cold-start floor (`start`) keeps charging
|
|
3946
|
+
// the lead, because there it is a property of the model, not of a transcript.
|
|
3947
|
+
this.assertModelUsable(model, liveContextTokens, {
|
|
3948
|
+
admission,
|
|
3949
|
+
includeSpeculationLead: admission === "start",
|
|
3950
|
+
});
|
|
3788
3951
|
}
|
|
3789
3952
|
catch (error) {
|
|
3790
3953
|
this._recordRejectedModelChange(model, error);
|
|
@@ -3792,12 +3955,23 @@ export class AgentSession {
|
|
|
3792
3955
|
}
|
|
3793
3956
|
}
|
|
3794
3957
|
async _setModel(model, updateGlobalDefaults) {
|
|
3795
|
-
|
|
3958
|
+
const liveContextTokens = this._getDownswitchLiveContextTokens(model);
|
|
3959
|
+
const deferral = this._projectSwitchDeferral(model, liveContextTokens);
|
|
3960
|
+
if (deferral === undefined) {
|
|
3961
|
+
this._assertModelUsableForSwitch(model, liveContextTokens);
|
|
3962
|
+
}
|
|
3796
3963
|
if (!(await this._modelRuntime.checkAuth(model.provider))) {
|
|
3797
3964
|
const error = new Error(`No API key for ${model.provider}/${model.id}`);
|
|
3798
3965
|
this._recordRejectedModelChange(model, error);
|
|
3799
3966
|
throw error;
|
|
3800
3967
|
}
|
|
3968
|
+
// Auth is the last check that can refuse outright, so the switch is only held
|
|
3969
|
+
// once it is otherwise good: a pending switch to a model with no key would
|
|
3970
|
+
// surface its failure a whole message later.
|
|
3971
|
+
if (deferral !== undefined) {
|
|
3972
|
+
this._admitSwitchCompactionRequired(model, deferral, updateGlobalDefaults);
|
|
3973
|
+
return undefined;
|
|
3974
|
+
}
|
|
3801
3975
|
// A manual model change abandons any active fallback window; if a fallback
|
|
3802
3976
|
// retry sleep is still pending, cancel it so no surprise continuation fires.
|
|
3803
3977
|
const hadActiveFallback = this._retryFallback.activeState !== undefined;
|
|
@@ -3844,6 +4018,11 @@ export class AgentSession {
|
|
|
3844
4018
|
}
|
|
3845
4019
|
const thinking = this._getThinkingForModelSwitch(model, opts.ephemeralThinkingLevel);
|
|
3846
4020
|
const liveContextTokens = this._getDownswitchLiveContextTokens(model);
|
|
4021
|
+
// Snapshot before any mutation: the baseline reset below used to run first, so the
|
|
4022
|
+
// "previous" value captured the already-cleared one and a refused switch restored
|
|
4023
|
+
// `undefined` instead of the baseline the session came in with.
|
|
4024
|
+
const previousReasoningBaseline = this.agent.state.reasoningBaseline;
|
|
4025
|
+
const previousAbortServerSideFallback = this.agent.abortServerSideFallback;
|
|
3847
4026
|
this.agent.state.model = model;
|
|
3848
4027
|
if (!(model.id === "gpt-6-astra" && (model.provider === "openai" || model.provider === "openai-codex"))) {
|
|
3849
4028
|
this.agent.state.reasoningBaseline = undefined;
|
|
@@ -3853,8 +4032,6 @@ export class AgentSession {
|
|
|
3853
4032
|
const previousFastMode = this.isFastModeActive();
|
|
3854
4033
|
const previousThinkingLevel = this.agent.state.thinkingLevel;
|
|
3855
4034
|
const previousThinkingSelection = this.agent.state.thinkingSelection;
|
|
3856
|
-
const previousReasoningBaseline = this.agent.state.reasoningBaseline;
|
|
3857
|
-
const previousAbortServerSideFallback = this.agent.abortServerSideFallback;
|
|
3858
4035
|
this.agent.abortServerSideFallback =
|
|
3859
4036
|
this.settingsManager.getAbortServerSideFallback() && this._retryFallback.hasConfiguredChain();
|
|
3860
4037
|
this._currentServiceTier = this._resolveServiceTier(model, scopedMatch?.serviceTier);
|
|
@@ -3870,12 +4047,35 @@ export class AgentSession {
|
|
|
3870
4047
|
const systemPromptChange = opts.emitModelSelect
|
|
3871
4048
|
? await this._emitModelSelect(model, previousModel, opts.modelSelectSource)
|
|
3872
4049
|
: undefined;
|
|
3873
|
-
this.
|
|
4050
|
+
const deferral = opts.allowDeferral === false ? undefined : this._projectSwitchDeferral(model, liveContextTokens);
|
|
4051
|
+
if (deferral !== undefined) {
|
|
4052
|
+
// Roll the in-memory selection back: the switch is held, not applied, so
|
|
4053
|
+
// nothing may observe the target as active until the next send repairs it.
|
|
4054
|
+
if (previousModel)
|
|
4055
|
+
this.agent.state.model = previousModel;
|
|
4056
|
+
this.agent.state.systemPrompt = previousSystemPrompt;
|
|
4057
|
+
this.agent.state.thinkingLevel = previousThinkingLevel;
|
|
4058
|
+
this.agent.state.thinkingSelection = previousThinkingSelection;
|
|
4059
|
+
this.agent.state.reasoningBaseline = previousReasoningBaseline;
|
|
4060
|
+
this.agent.abortServerSideFallback = previousAbortServerSideFallback;
|
|
4061
|
+
this._currentServiceTier = previousTier;
|
|
4062
|
+
this._admitSwitchCompactionRequired(model, deferral, opts.persistDefault);
|
|
4063
|
+
return undefined;
|
|
4064
|
+
}
|
|
4065
|
+
const admittedLiveContextTokens = opts.repairWithSlice
|
|
4066
|
+
? this._reduceForSwitchTarget(model, liveContextTokens)
|
|
4067
|
+
: liveContextTokens;
|
|
4068
|
+
this._assertModelUsableForSwitch(model, admittedLiveContextTokens);
|
|
3874
4069
|
if (opts.appendSessionEntry) {
|
|
3875
4070
|
this.sessionManager.appendModelChange(model.provider, model.id, opts.entryReason, previousModel?.provider, previousModel?.id);
|
|
3876
4071
|
}
|
|
3877
4072
|
if (opts.persistDefault)
|
|
3878
4073
|
this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
|
|
4074
|
+
// A switch that actually landed supersedes anything still being held (#1873).
|
|
4075
|
+
// Without this a held switch survives its own resolution - an explicit
|
|
4076
|
+
// /compact followed by a manual retry applies the switch here, and the stale
|
|
4077
|
+
// hold would compact and re-switch again on the next message.
|
|
4078
|
+
this._pendingModelSwitch = undefined;
|
|
3879
4079
|
// Emit only after all admission hooks have accepted the candidate.
|
|
3880
4080
|
this._emit({
|
|
3881
4081
|
type: "model_changed",
|
|
@@ -3970,7 +4170,14 @@ export class AgentSession {
|
|
|
3970
4170
|
const alternatives = favoriteModels.filter((entry) => !modelsAreEqual(entry.model, currentModel));
|
|
3971
4171
|
const onlyAlternative = alternatives.length === 1 ? alternatives[0] : undefined;
|
|
3972
4172
|
if (onlyAlternative) {
|
|
3973
|
-
|
|
4173
|
+
const onlyLiveContextTokens = this._getDownswitchLiveContextTokens(onlyAlternative.model);
|
|
4174
|
+
const onlyDeferral = this._projectSwitchDeferral(onlyAlternative.model, onlyLiveContextTokens);
|
|
4175
|
+
if (onlyDeferral !== undefined) {
|
|
4176
|
+
this._admitSwitchCompactionRequired(onlyAlternative.model, onlyDeferral, true);
|
|
4177
|
+
}
|
|
4178
|
+
else {
|
|
4179
|
+
this._assertModelUsableForSwitch(onlyAlternative.model, onlyLiveContextTokens);
|
|
4180
|
+
}
|
|
3974
4181
|
}
|
|
3975
4182
|
return {
|
|
3976
4183
|
model: currentModel,
|
|
@@ -3981,6 +4188,18 @@ export class AgentSession {
|
|
|
3981
4188
|
}
|
|
3982
4189
|
const next = favoriteModels[selectedIndex];
|
|
3983
4190
|
const liveContextTokens = this._getDownswitchLiveContextTokens(next.model);
|
|
4191
|
+
// #1873: the cycle lands on a candidate one compaction would admit instead of
|
|
4192
|
+
// refusing it. Nothing is mutated yet, so holding here needs no rollback.
|
|
4193
|
+
const cycleDeferral = this._projectSwitchDeferral(next.model, liveContextTokens);
|
|
4194
|
+
if (cycleDeferral !== undefined) {
|
|
4195
|
+
this._admitSwitchCompactionRequired(next.model, cycleDeferral, true);
|
|
4196
|
+
return {
|
|
4197
|
+
model: currentModel,
|
|
4198
|
+
thinkingLevel: this.thinkingLevel,
|
|
4199
|
+
isScoped: true,
|
|
4200
|
+
skippedModels,
|
|
4201
|
+
};
|
|
4202
|
+
}
|
|
3984
4203
|
this._assertModelUsableForSwitch(next.model, liveContextTokens);
|
|
3985
4204
|
const invalidatesCompaction = this._modelSelectionChangesContext(currentModel, next.model) ||
|
|
3986
4205
|
currentModel?.provider !== next.model.provider ||
|
|
@@ -4005,9 +4224,29 @@ export class AgentSession {
|
|
|
4005
4224
|
// default - may be written before this guard accepts, or a refused cycle
|
|
4006
4225
|
// would resume on a model that never ran (the ordering `_switchActiveModel`
|
|
4007
4226
|
// already uses).
|
|
4227
|
+
// #1873: a prompt that only grew past the budget is held, not refused; the
|
|
4228
|
+
// in-memory selection rolls back exactly as the catch below would.
|
|
4229
|
+
const postHookDeferral = this._projectSwitchDeferral(next.model, liveContextTokens);
|
|
4230
|
+
if (postHookDeferral !== undefined) {
|
|
4231
|
+
if (currentModel)
|
|
4232
|
+
this.agent.state.model = currentModel;
|
|
4233
|
+
this.agent.state.systemPrompt = previousSystemPrompt;
|
|
4234
|
+
this._currentServiceTier = previousTier;
|
|
4235
|
+
this._admitSwitchCompactionRequired(next.model, postHookDeferral, true);
|
|
4236
|
+
return {
|
|
4237
|
+
model: currentModel,
|
|
4238
|
+
thinkingLevel: this.thinkingLevel,
|
|
4239
|
+
isScoped: true,
|
|
4240
|
+
skippedModels,
|
|
4241
|
+
};
|
|
4242
|
+
}
|
|
4008
4243
|
this._assertModelUsableForSwitch(next.model, liveContextTokens);
|
|
4009
4244
|
this.sessionManager.appendModelChange(next.model.provider, next.model.id);
|
|
4010
4245
|
this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
|
|
4246
|
+
// #1873: the cycle commits here rather than through `_switchActiveModel`, so it
|
|
4247
|
+
// has to supersede a held switch itself - otherwise cycling onto a model that
|
|
4248
|
+
// fits leaves an older hold to reclaim the session on the next message.
|
|
4249
|
+
this._pendingModelSwitch = undefined;
|
|
4011
4250
|
// Post-switch, same contract as _switchActiveModel: the level in force AFTER the cycle.
|
|
4012
4251
|
this._emit({
|
|
4013
4252
|
type: "model_changed",
|
|
@@ -4638,7 +4877,10 @@ export class AgentSession {
|
|
|
4638
4877
|
throw new CompactionCancelledError();
|
|
4639
4878
|
}
|
|
4640
4879
|
const pathEntries = this.sessionManager.getBranch();
|
|
4641
|
-
const
|
|
4880
|
+
const resolvedSettings = this._getCompactionSettings();
|
|
4881
|
+
const settings = cursorOverflowCompactionSettings(request.keepRecentTokensOverride === undefined
|
|
4882
|
+
? resolvedSettings
|
|
4883
|
+
: { ...resolvedSettings, keepRecentTokens: request.keepRecentTokensOverride }, this.model?.provider, request.reason);
|
|
4642
4884
|
let compactionResult = request.precomputed;
|
|
4643
4885
|
let fromExtension = request.precomputed !== undefined;
|
|
4644
4886
|
if (!compactionResult) {
|
|
@@ -4965,6 +5207,9 @@ export class AgentSession {
|
|
|
4965
5207
|
}
|
|
4966
5208
|
const settings = this._getCompactionSettings();
|
|
4967
5209
|
const model = this.model;
|
|
5210
|
+
if (this._pendingModelSwitch !== undefined) {
|
|
5211
|
+
return await this._applyPendingModelSwitch(assistantMessage, skipAbortedCheck, inlineReason);
|
|
5212
|
+
}
|
|
4968
5213
|
if (this._resumeCompactionRequirement !== undefined) {
|
|
4969
5214
|
if (!model)
|
|
4970
5215
|
throw new RequiredCompactionError();
|
|
@@ -5281,7 +5526,7 @@ export class AgentSession {
|
|
|
5281
5526
|
this._delegatedCompactionKey.provider === model.provider &&
|
|
5282
5527
|
this._delegatedCompactionKey.id === model.id);
|
|
5283
5528
|
}
|
|
5284
|
-
async _runPrePromptCompaction(lastAssistantMessage, skipAbortedCheck, reason = "pre_prompt", willRetry = false, allowSummaryOnly = false) {
|
|
5529
|
+
async _runPrePromptCompaction(lastAssistantMessage, skipAbortedCheck, reason = "pre_prompt", willRetry = false, allowSummaryOnly = false, keepRecentTokensOverride) {
|
|
5285
5530
|
if (this._isCompactionDelegated())
|
|
5286
5531
|
return false;
|
|
5287
5532
|
const controller = new AbortController();
|
|
@@ -5298,6 +5543,7 @@ export class AgentSession {
|
|
|
5298
5543
|
lastAssistantMessage,
|
|
5299
5544
|
skipAbortedCheck,
|
|
5300
5545
|
allowSummaryOnly,
|
|
5546
|
+
keepRecentTokensOverride,
|
|
5301
5547
|
});
|
|
5302
5548
|
if (!execution.accepted &&
|
|
5303
5549
|
lastAssistantMessage &&
|