@code-yeongyu/senpi 2026.9.19-2 → 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 +53 -0
- package/dist/bundle/chunks/{app-server-command-QBJ4GPN7.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-EHSDTQ6I.js → chunk-E5R67F7G.js} +37 -37
- package/dist/bundle/chunks/{chunk-5E5TLB76.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-MLJ2H637.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-7C75IXNF.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-2YCJKQOL.js → multi-session-host-QNEBJW2P.js} +3 -3
- package/dist/bundle/chunks/{package-manager-cli-4ANSELYT.js → package-manager-cli-4MVJ3JSE.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-OPFQIWFP.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 +66 -66
- 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 +45 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +233 -11
- 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 +12 -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) {
|
|
@@ -3702,6 +3718,120 @@ export class AgentSession {
|
|
|
3702
3718
|
throw new ModelUsabilityBudgetError(projection);
|
|
3703
3719
|
}
|
|
3704
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
|
+
}
|
|
3705
3835
|
admitResumeCompactionRequired(projection) {
|
|
3706
3836
|
this._resumeCompactionRequirement = createResumeCompactionRequirement(projection);
|
|
3707
3837
|
}
|
|
@@ -3807,8 +3937,17 @@ export class AgentSession {
|
|
|
3807
3937
|
* observable on one path and invisible on its sibling.
|
|
3808
3938
|
*/
|
|
3809
3939
|
_assertModelUsableForSwitch(model, liveContextTokens) {
|
|
3940
|
+
const admission = this._modelSwitchAdmission();
|
|
3810
3941
|
try {
|
|
3811
|
-
|
|
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
|
+
});
|
|
3812
3951
|
}
|
|
3813
3952
|
catch (error) {
|
|
3814
3953
|
this._recordRejectedModelChange(model, error);
|
|
@@ -3816,12 +3955,23 @@ export class AgentSession {
|
|
|
3816
3955
|
}
|
|
3817
3956
|
}
|
|
3818
3957
|
async _setModel(model, updateGlobalDefaults) {
|
|
3819
|
-
|
|
3958
|
+
const liveContextTokens = this._getDownswitchLiveContextTokens(model);
|
|
3959
|
+
const deferral = this._projectSwitchDeferral(model, liveContextTokens);
|
|
3960
|
+
if (deferral === undefined) {
|
|
3961
|
+
this._assertModelUsableForSwitch(model, liveContextTokens);
|
|
3962
|
+
}
|
|
3820
3963
|
if (!(await this._modelRuntime.checkAuth(model.provider))) {
|
|
3821
3964
|
const error = new Error(`No API key for ${model.provider}/${model.id}`);
|
|
3822
3965
|
this._recordRejectedModelChange(model, error);
|
|
3823
3966
|
throw error;
|
|
3824
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
|
+
}
|
|
3825
3975
|
// A manual model change abandons any active fallback window; if a fallback
|
|
3826
3976
|
// retry sleep is still pending, cancel it so no surprise continuation fires.
|
|
3827
3977
|
const hadActiveFallback = this._retryFallback.activeState !== undefined;
|
|
@@ -3868,6 +4018,11 @@ export class AgentSession {
|
|
|
3868
4018
|
}
|
|
3869
4019
|
const thinking = this._getThinkingForModelSwitch(model, opts.ephemeralThinkingLevel);
|
|
3870
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;
|
|
3871
4026
|
this.agent.state.model = model;
|
|
3872
4027
|
if (!(model.id === "gpt-6-astra" && (model.provider === "openai" || model.provider === "openai-codex"))) {
|
|
3873
4028
|
this.agent.state.reasoningBaseline = undefined;
|
|
@@ -3877,8 +4032,6 @@ export class AgentSession {
|
|
|
3877
4032
|
const previousFastMode = this.isFastModeActive();
|
|
3878
4033
|
const previousThinkingLevel = this.agent.state.thinkingLevel;
|
|
3879
4034
|
const previousThinkingSelection = this.agent.state.thinkingSelection;
|
|
3880
|
-
const previousReasoningBaseline = this.agent.state.reasoningBaseline;
|
|
3881
|
-
const previousAbortServerSideFallback = this.agent.abortServerSideFallback;
|
|
3882
4035
|
this.agent.abortServerSideFallback =
|
|
3883
4036
|
this.settingsManager.getAbortServerSideFallback() && this._retryFallback.hasConfiguredChain();
|
|
3884
4037
|
this._currentServiceTier = this._resolveServiceTier(model, scopedMatch?.serviceTier);
|
|
@@ -3894,12 +4047,35 @@ export class AgentSession {
|
|
|
3894
4047
|
const systemPromptChange = opts.emitModelSelect
|
|
3895
4048
|
? await this._emitModelSelect(model, previousModel, opts.modelSelectSource)
|
|
3896
4049
|
: undefined;
|
|
3897
|
-
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);
|
|
3898
4069
|
if (opts.appendSessionEntry) {
|
|
3899
4070
|
this.sessionManager.appendModelChange(model.provider, model.id, opts.entryReason, previousModel?.provider, previousModel?.id);
|
|
3900
4071
|
}
|
|
3901
4072
|
if (opts.persistDefault)
|
|
3902
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;
|
|
3903
4079
|
// Emit only after all admission hooks have accepted the candidate.
|
|
3904
4080
|
this._emit({
|
|
3905
4081
|
type: "model_changed",
|
|
@@ -3994,7 +4170,14 @@ export class AgentSession {
|
|
|
3994
4170
|
const alternatives = favoriteModels.filter((entry) => !modelsAreEqual(entry.model, currentModel));
|
|
3995
4171
|
const onlyAlternative = alternatives.length === 1 ? alternatives[0] : undefined;
|
|
3996
4172
|
if (onlyAlternative) {
|
|
3997
|
-
|
|
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
|
+
}
|
|
3998
4181
|
}
|
|
3999
4182
|
return {
|
|
4000
4183
|
model: currentModel,
|
|
@@ -4005,6 +4188,18 @@ export class AgentSession {
|
|
|
4005
4188
|
}
|
|
4006
4189
|
const next = favoriteModels[selectedIndex];
|
|
4007
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
|
+
}
|
|
4008
4203
|
this._assertModelUsableForSwitch(next.model, liveContextTokens);
|
|
4009
4204
|
const invalidatesCompaction = this._modelSelectionChangesContext(currentModel, next.model) ||
|
|
4010
4205
|
currentModel?.provider !== next.model.provider ||
|
|
@@ -4029,9 +4224,29 @@ export class AgentSession {
|
|
|
4029
4224
|
// default - may be written before this guard accepts, or a refused cycle
|
|
4030
4225
|
// would resume on a model that never ran (the ordering `_switchActiveModel`
|
|
4031
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
|
+
}
|
|
4032
4243
|
this._assertModelUsableForSwitch(next.model, liveContextTokens);
|
|
4033
4244
|
this.sessionManager.appendModelChange(next.model.provider, next.model.id);
|
|
4034
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;
|
|
4035
4250
|
// Post-switch, same contract as _switchActiveModel: the level in force AFTER the cycle.
|
|
4036
4251
|
this._emit({
|
|
4037
4252
|
type: "model_changed",
|
|
@@ -4662,7 +4877,10 @@ export class AgentSession {
|
|
|
4662
4877
|
throw new CompactionCancelledError();
|
|
4663
4878
|
}
|
|
4664
4879
|
const pathEntries = this.sessionManager.getBranch();
|
|
4665
|
-
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);
|
|
4666
4884
|
let compactionResult = request.precomputed;
|
|
4667
4885
|
let fromExtension = request.precomputed !== undefined;
|
|
4668
4886
|
if (!compactionResult) {
|
|
@@ -4989,6 +5207,9 @@ export class AgentSession {
|
|
|
4989
5207
|
}
|
|
4990
5208
|
const settings = this._getCompactionSettings();
|
|
4991
5209
|
const model = this.model;
|
|
5210
|
+
if (this._pendingModelSwitch !== undefined) {
|
|
5211
|
+
return await this._applyPendingModelSwitch(assistantMessage, skipAbortedCheck, inlineReason);
|
|
5212
|
+
}
|
|
4992
5213
|
if (this._resumeCompactionRequirement !== undefined) {
|
|
4993
5214
|
if (!model)
|
|
4994
5215
|
throw new RequiredCompactionError();
|
|
@@ -5305,7 +5526,7 @@ export class AgentSession {
|
|
|
5305
5526
|
this._delegatedCompactionKey.provider === model.provider &&
|
|
5306
5527
|
this._delegatedCompactionKey.id === model.id);
|
|
5307
5528
|
}
|
|
5308
|
-
async _runPrePromptCompaction(lastAssistantMessage, skipAbortedCheck, reason = "pre_prompt", willRetry = false, allowSummaryOnly = false) {
|
|
5529
|
+
async _runPrePromptCompaction(lastAssistantMessage, skipAbortedCheck, reason = "pre_prompt", willRetry = false, allowSummaryOnly = false, keepRecentTokensOverride) {
|
|
5309
5530
|
if (this._isCompactionDelegated())
|
|
5310
5531
|
return false;
|
|
5311
5532
|
const controller = new AbortController();
|
|
@@ -5322,6 +5543,7 @@ export class AgentSession {
|
|
|
5322
5543
|
lastAssistantMessage,
|
|
5323
5544
|
skipAbortedCheck,
|
|
5324
5545
|
allowSummaryOnly,
|
|
5546
|
+
keepRecentTokensOverride,
|
|
5325
5547
|
});
|
|
5326
5548
|
if (!execution.accepted &&
|
|
5327
5549
|
lastAssistantMessage &&
|