zigmini 0.9.0-beta.2 → 1.0.0-beta.1
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/README.ko.md +35 -5
- package/README.md +35 -5
- package/assets/agent/gateway-rules.md +1 -1
- package/assets/android/zigmini.apk +0 -0
- package/assets/android/zigmini.apk.json +3 -3
- package/assets/build.json +1 -1
- package/docs/manual.ko.md +51 -11
- package/docs/manual.md +52 -12
- package/host/omo-bridge/agent-control.ts +274 -0
- package/host/omo-bridge/bridge.ts +1 -1
- package/host/omo-bridge/continuity-control.ts +1 -1
- package/host/omo-bridge/model-control.ts +15 -3
- package/host/omo-bridge/task-control.ts +26 -16
- package/host/omo-check/patches/omo-5.1.48-compaction-recovery.patch +912 -0
- package/host/omo-check/patches/omo-5.1.48-gateway-port.manifest.json +12 -11
- package/host/omo-check/patches/omo-5.1.51-compaction-long-context.patch +92 -0
- package/host/omo-check/patches/omo-5.1.51-compaction-port.patch +2635 -0
- package/host/omo-check/patches/omo-5.1.51-compaction-recovery.patch +911 -0
- package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +363 -0
- package/host/omo-check/patches/omo-5.1.51-provider-failover.patch +50 -0
- package/host/omo-check/patches/omo-5.1.51-provider-port.patch +441 -0
- package/host/omo-check/patches/omo-5.1.51-rpc-port.patch +799 -0
- package/host/omo-check/patches/omo-5.1.51-runtime-compat.patch +31 -0
- package/package.json +3 -2
- package/src/agent/api.ts +215 -0
- package/src/agent/definition.ts +177 -0
- package/src/agent/fields.ts +42 -0
- package/src/agent/files.ts +192 -0
- package/src/agent/legacy-files.ts +46 -0
- package/src/agent/list.ts +21 -0
- package/src/agent-rules.ts +14 -5
- package/src/approvals.ts +6 -1
- package/src/cli/main.ts +33 -0
- package/src/cli/messages.ts +10 -2
- package/src/cli/rotate-main.ts +2 -1
- package/src/cli/scan.ts +414 -0
- package/src/cli/security-import.ts +155 -0
- package/src/compact.ts +22 -0
- package/src/decisions.ts +6 -4
- package/src/engine/compat.json +3 -3
- package/src/engine/index.ts +1 -0
- package/src/engine/schema-guard.ts +12 -4
- package/src/engine/sdk.ts +8 -0
- package/src/formal-verification.ts +366 -0
- package/src/gc.ts +37 -6
- package/src/hub.ts +525 -85
- package/src/main.ts +95 -9
- package/src/maintenance.ts +1 -1
- package/src/manual.ts +9 -2
- package/src/omo/client.ts +2 -0
- package/src/profile-prompt.ts +73 -0
- package/src/profile.ts +33 -10
- package/src/protocol.ts +11 -2
- package/src/question-notifications.ts +4 -1
- package/src/reply-notifications.ts +7 -1
- package/src/repo-security-setting.ts +33 -0
- package/src/runtime/paths.ts +16 -0
- package/src/security/baseline.ts +170 -0
- package/src/security/dex-strings.ts +242 -0
- package/src/security/repo-scan.ts +369 -0
- package/src/security/revision-probe.ts +144 -0
- package/src/security/terms.ts +97 -0
- package/src/security-allow.ts +1 -0
- package/src/security-baseline.ts +113 -0
- package/src/security-check.ts +91 -16
- package/src/server-text.ts +38 -0
- package/src/store.ts +142 -16
- package/src/transcript.ts +7 -1
- package/src/ui-language.ts +33 -0
- package/src/update/check.ts +7 -3
- package/src/update/handoff-system.ts +24 -16
- package/src/update/handoff.ts +23 -12
- package/src/update/helper.ts +72 -36
- package/src/update/request.ts +46 -30
- package/src/update/state.ts +21 -11
- package/src/update/strategy.ts +12 -5
- package/src/update/target.ts +32 -15
- package/src/usage.ts +43 -20
- package/src/version.ts +39 -16
- package/src/voice/agents.ts +8 -3
- package/src/voice/install.ts +29 -18
- package/src/work-watch.ts +40 -18
- package/web/agent-icon.js +15 -9
- package/web/agents.d.ts +201 -0
- package/web/agents.js +813 -0
- package/web/app-download.js +32 -19
- package/web/app.js +1407 -278
- package/web/autocopy.js +6 -1
- package/web/boot.js +27 -8
- package/web/commands.js +20 -11
- package/web/decisions.js +70 -49
- package/web/devlog.js +25 -19
- package/web/diagram.js +8 -3
- package/web/enroll.js +43 -22
- package/web/gc.js +32 -19
- package/web/i18n/agent-icon.js +24 -0
- package/web/i18n/agents.js +48 -0
- package/web/i18n/app-download.js +26 -0
- package/web/i18n/autocopy.js +5 -0
- package/web/i18n/boot.js +24 -0
- package/web/i18n/commands.js +34 -0
- package/web/i18n/decisions.js +24 -0
- package/web/i18n/devlog.js +60 -0
- package/web/i18n/diagram.js +12 -0
- package/web/i18n/enroll.js +22 -0
- package/web/i18n/gc.js +38 -0
- package/web/i18n/lastsent.js +10 -0
- package/web/i18n/main.d.ts +4 -0
- package/web/i18n/main.js +527 -0
- package/web/i18n/manual.d.ts +4 -0
- package/web/i18n/manual.js +14 -0
- package/web/i18n/memo.js +30 -0
- package/web/i18n/notifications.js +16 -0
- package/web/i18n/profile.js +13 -0
- package/web/i18n/reference.js +26 -0
- package/web/i18n/security-inbox.js +40 -0
- package/web/i18n/slash.js +38 -0
- package/web/i18n/turn-stats.js +5 -0
- package/web/i18n/update.js +42 -0
- package/web/i18n/voice.js +28 -0
- package/web/i18n/wake.js +16 -0
- package/web/i18n/workid.js +10 -0
- package/web/i18n/workstatus.js +20 -0
- package/web/i18n.d.ts +20 -0
- package/web/i18n.js +63 -0
- package/web/index.html +156 -96
- package/web/lastsent.js +9 -4
- package/web/manual.d.ts +1 -1
- package/web/manual.js +11 -5
- package/web/memo.js +18 -13
- package/web/notifications.js +12 -7
- package/web/profile.js +42 -17
- package/web/reference.js +20 -15
- package/web/security-inbox.d.ts +18 -1
- package/web/security-inbox.js +173 -4
- package/web/slash.js +23 -13
- package/web/style.css +132 -3
- package/web/tooltip.js +5 -2
- package/web/turn-stats.js +6 -2
- package/web/update.js +31 -23
- package/web/voice.js +18 -8
- package/web/wake.d.ts +4 -0
- package/web/wake.js +7 -3
- package/web/workid.js +12 -2
- package/web/workstatus.js +36 -32
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
--- a/@code-yeongyu/senpi/dist/core/agent-session.js
|
|
2
|
+
+++ b/@code-yeongyu/senpi/dist/core/agent-session.js
|
|
3
|
+
@@ -43,7 +43,7 @@
|
|
4
|
+
import { envValue } from "./brand.js";
|
|
5
|
+
import { clientMessageIdentity, readClientMessageIdentity, } from "./client-message-identity.js";
|
|
6
|
+
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, resolveThresholdContextTokens, shouldCompact, } from "./compaction/index.js";
|
|
7
|
+
-import { compactionFailure, validateSummaryResponse } from "./extensions/builtin/compaction/speculative.js";
|
|
8
|
+
+import { compactionFailure, validateSummaryResponse, summaryUnavailable, summaryFailureDetails, noteSummaryUnavailable } from "./extensions/builtin/compaction/speculative.js";
|
|
9
|
+
import { CompactionLifecycleCoordinator } from "./compaction/lifecycle.js";
|
|
10
|
+
import { isTurnStuckOnContextOverflow } from "./compaction/stuck-overflow.js";
|
|
11
|
+
import { isWarmSummaryAnchorValid } from "./compaction/warm-anchor.js";
|
|
12
|
+
@@ -1032,6 +1032,11 @@
|
|
13
|
+
explicit ? "Compaction model unavailable; check compaction.model or set it to auto."
|
|
14
|
+
: "No usable compaction model within the active provider."),
|
|
15
|
+
{ failure: { kind, provider: model.provider, model: model.id } });
|
|
16
|
+
+ };
|
|
17
|
+
+ const admit = (model) => {
|
|
18
|
+
+ const unavailable = summaryUnavailable(this.sessionManager, model);
|
|
19
|
+
+ if (unavailable) throw Object.assign(new Error("Compaction summary provider unavailable"), { failure: { ...unavailable, inputTokens, inputTokensSource: "estimate", requestSent: false } });
|
|
20
|
+
+ return model;
|
|
21
|
+
};
|
|
22
|
+
const chat = this._modelRuntime.getModels();
|
|
23
|
+
const available = this._modelRuntime.getAvailableSnapshot();
|
|
24
|
+
@@ -1056,7 +1061,7 @@
|
|
25
|
+
return fail("model_unavailable", configured);
|
|
26
|
+
if (!fits(configured))
|
|
27
|
+
return fail("context_budget", configured);
|
|
28
|
+
- return configured;
|
|
29
|
+
+ return admit(configured);
|
|
30
|
+
}
|
|
31
|
+
if (this._retryFallback.isProviderSpent(sessionModel.provider))
|
|
32
|
+
return fail("quota");
|
|
33
|
+
@@ -1069,7 +1074,7 @@
|
|
34
|
+
const candidates = [...(family?.test(sessionModel.id) ? [sessionModel] : []), ...preferred, sessionModel];
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
if (candidate.provider === sessionModel.provider && usable(candidate) && fits(candidate))
|
|
37
|
+
- return candidate;
|
|
38
|
+
+ return admit(candidate);
|
|
39
|
+
}
|
|
40
|
+
return fail(usable(sessionModel) ? "context_budget" : "model_unavailable");
|
|
41
|
+
}
|
|
42
|
+
@@ -1085,6 +1090,8 @@
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async _getCompactionRequestAuth(model, signal, callbacks) {
|
|
46
|
+
+ if (this._modelRuntime.couldRotateCredentials?.(model, { signal }))
|
|
47
|
+
+ return { model, thinkingLevel: this.thinkingLevel };
|
|
48
|
+
const prepared = await retryTransientCall(() => this._modelRuntime.prepareRequest(model, { signal }),
|
|
49
|
+
isOAuthRefreshUnavailableError, this.settingsManager.getRetrySettings(), signal,
|
|
50
|
+
callbacks ?? this._summarizationRetryCallbacks({ source: "compaction" }));
|
|
51
|
+
@@ -1702,6 +1709,15 @@
|
|
52
|
+
this._emitEntryAppended(entry.id);
|
|
53
|
+
}
|
|
54
|
+
_emit(event) {
|
|
55
|
+
+ if (event.type === "compaction_end" && !event.aborted && (event.failure || event.errorMessage || event.accepted === false)) {
|
|
56
|
+
+ const failure = compactionFailure({ failure: event.failure }, this._activeCompactionModel ?? this.model);
|
|
57
|
+
+ const catalog = this._modelRuntime.getModels();
|
|
58
|
+
+ if (!catalog.some((model) => model.provider === failure.provider && model.id === failure.model)) {
|
|
59
|
+
+ delete failure.provider;
|
|
60
|
+
+ delete failure.model;
|
|
61
|
+
+ }
|
|
62
|
+
+ event = { ...event, failure, errorMessage: "Compaction failed; previous context retained" };
|
|
63
|
+
+ }
|
|
64
|
+
if (event.type === "session_paused" ||
|
|
65
|
+
event.type === "session_resumed" ||
|
|
66
|
+
event.type === "compaction_start" ||
|
|
67
|
+
@@ -6871,18 +6887,34 @@
|
|
68
|
+
* fits is allowed to carry the switch. The deterministic slice is the floor when
|
|
69
|
+
* summarization did not get there, so a repairable switch cannot strand itself.
|
|
70
|
+
*/
|
|
71
|
+
+ _summaryUnavailableForSwitch() {
|
|
72
|
+
+ if (!this.model) return false;
|
|
73
|
+
+ try {
|
|
74
|
+
+ return summaryUnavailable(this.sessionManager, this._resolveCompactionModel(this.model)) !== undefined;
|
|
75
|
+
+ } catch (error) {
|
|
76
|
+
+ if (["quota", "account_blocked"].includes(error?.failure?.kind)) return true;
|
|
77
|
+
+ throw error;
|
|
78
|
+
+ }
|
|
79
|
+
+ }
|
|
80
|
+
async _applyPendingModelSwitch(assistantMessage, skipAbortedCheck, inlineReason) {
|
|
81
|
+
const pending = this._pendingModelSwitch;
|
|
82
|
+
if (pending === undefined)
|
|
83
|
+
return false;
|
|
84
|
+
if (!this.model)
|
|
85
|
+
throw new RequiredCompactionError();
|
|
86
|
+
- await this._runPrePromptCompaction(assistantMessage, skipAbortedCheck, inlineReason, false, false, pendingSwitchKeepRecentTokens(pending.projection));
|
|
87
|
+
+ if (skipAbortedCheck && assistantMessage?.stopReason === "aborted") return false;
|
|
88
|
+
+ const abortGeneration = this._userAbortGeneration;
|
|
89
|
+
let projection = this._projectSwitchFit(pending.model);
|
|
90
|
+
+ if (!projection.usable && !this._summaryUnavailableForSwitch()) {
|
|
91
|
+
+ await this._runPrePromptCompaction(assistantMessage, skipAbortedCheck, inlineReason, false, false, pendingSwitchKeepRecentTokens(pending.projection));
|
|
92
|
+
+ if (this._pendingModelSwitch !== pending || this._userAbortGeneration !== abortGeneration || this._lastPrePromptCompactionAborted) return false;
|
|
93
|
+
+ projection = this._projectSwitchFit(pending.model);
|
|
94
|
+
+ }
|
|
95
|
+
+ if (this._pendingModelSwitch !== pending) return false;
|
|
96
|
+
if (!projection.usable) {
|
|
97
|
+
const plan = planResumeSlice({ entries: this.sessionManager.getBranch(), projection });
|
|
98
|
+
if (plan) {
|
|
99
|
+
- this.applyResumeSlice(plan);
|
|
100
|
+
+ this.applyResumeSlice(plan, "explicit-model-switch");
|
|
101
|
+
projection = this._projectSwitchFit(pending.model);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
@@ -6892,6 +6924,10 @@
|
|
105
|
+
this._recordRejectedModelChange(pending.model, error);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
+ const hadActiveFallback = this._retryFallback.activeState !== undefined;
|
|
109
|
+
+ this._probeBackScheduler.cancel("manual-model-change");
|
|
110
|
+
+ this._retryFallback.clearForManualModelChange(pending.model);
|
|
111
|
+
+ if (hadActiveFallback && this._retryAbortController) this.abortRetry();
|
|
112
|
+
this._pendingModelSwitch = undefined;
|
|
113
|
+
try {
|
|
114
|
+
await this._switchActiveModel(pending.model, {
|
|
115
|
+
@@ -6943,10 +6979,11 @@
|
|
116
|
+
this._resumeCompactionRequirement = createResumeCompactionRequirement(projection);
|
|
117
|
+
}
|
|
118
|
+
/** Reduce an over-window restored context deterministically while the recorded transcript stays intact. */
|
|
119
|
+
- applyResumeSlice(plan) {
|
|
120
|
+
+ applyResumeSlice(plan, recoveryOrigin) {
|
|
121
|
+
this.sessionManager.appendCompaction(plan.summary, plan.firstKeptEntryId, plan.tokensBefore, {
|
|
122
|
+
schema: RESUME_SLICE_SCHEMA,
|
|
123
|
+
origin: RESUME_SLICE_ORIGIN,
|
|
124
|
+
+ ...(recoveryOrigin ? { recoveryOrigin, originalLeafId: this.sessionManager.getLeafId() } : {}),
|
|
125
|
+
});
|
|
126
|
+
this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
|
|
127
|
+
this._resumeSlice = plan;
|
|
128
|
+
@@ -7077,6 +7114,8 @@
|
|
129
|
+
// surface its failure a whole message later.
|
|
130
|
+
if (deferral !== undefined) {
|
|
131
|
+
this._admitSwitchCompactionRequired(model, deferral, updateGlobalDefaults, origin);
|
|
132
|
+
+ if (this.isIdle && !this.isCompacting && this._summaryUnavailableForSwitch())
|
|
133
|
+
+ await this._applyPendingModelSwitch(undefined, false, "pre_prompt");
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
// A manual model change abandons any active fallback window; if a fallback
|
|
137
|
+
@@ -7782,6 +7821,9 @@
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const checkedStream = async (model, context, options) => {
|
|
141
|
+
+ const unavailable = summaryUnavailable(this.sessionManager, model);
|
|
142
|
+
+ if (unavailable) throw Object.assign(new Error("Compaction summary provider unavailable"), { failure: { ...unavailable, requestSent: false } });
|
|
143
|
+
+ const inputTokens = estimateMessagesTokens(context.messages) + Math.ceil(JSON.stringify([context.systemPrompt, context.tools]).length / 4);
|
|
144
|
+
const stream = await this.agent.streamFunction(model, context, {
|
|
145
|
+
...options, refusalFallbacks: undefined, streamKind: "auxiliary",
|
|
146
|
+
});
|
|
147
|
+
@@ -7789,7 +7831,13 @@
|
|
148
|
+
[Symbol.asyncIterator]: () => stream[Symbol.asyncIterator](),
|
|
149
|
+
result: async () => {
|
|
150
|
+
const response = await stream.result();
|
|
151
|
+
- validateSummaryResponse(response);
|
|
152
|
+
+ if (response?.stopReason === "error") {
|
|
153
|
+
+ const failure = summaryFailureDetails(response, model, inputTokens);
|
|
154
|
+
+ noteSummaryUnavailable(this.sessionManager, model, failure);
|
|
155
|
+
+ throw Object.assign(new Error("Compaction summary failed"), { failure });
|
|
156
|
+
+ }
|
|
157
|
+
+ try { validateSummaryResponse(response); }
|
|
158
|
+
+ catch (error) { error.failure = { ...summaryFailureDetails(response, model, inputTokens), kind: error.failure?.kind ?? "provider" }; throw error; }
|
|
159
|
+
return response;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
@@ -7884,7 +7932,7 @@
|
|
163
|
+
}
|
|
164
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
165
|
+
const aborted = isCompactionExecutionAborted(error);
|
|
166
|
+
- const errorMessage = aborted ? undefined : `Compaction failed: ${message}`;
|
|
167
|
+
+ const errorMessage = aborted ? undefined : "Compaction failed; previous context retained";
|
|
168
|
+
this._emit({
|
|
169
|
+
type: "compaction_end",
|
|
170
|
+
reason: "manual",
|
|
171
|
+
@@ -7893,6 +7941,7 @@
|
|
172
|
+
willRetry: false,
|
|
173
|
+
requestId,
|
|
174
|
+
errorMessage,
|
|
175
|
+
+ failure: aborted ? undefined : compactionFailure(error, this._activeCompactionModel ?? this.model),
|
|
176
|
+
});
|
|
177
|
+
await this._emitSessionCompactFailed({
|
|
178
|
+
reason: "manual",
|
|
179
|
+
@@ -7974,7 +8023,8 @@
|
|
180
|
+
aborted,
|
|
181
|
+
willRetry: false,
|
|
182
|
+
requestId,
|
|
183
|
+
- errorMessage: aborted ? undefined : `Compaction failed: ${message}`,
|
|
184
|
+
+ errorMessage: aborted ? undefined : "Compaction failed; previous context retained",
|
|
185
|
+
+ failure: aborted ? undefined : compactionFailure(error, this._activeCompactionModel ?? this.model),
|
|
186
|
+
});
|
|
187
|
+
return { applied: false, reason: "rejected" };
|
|
188
|
+
}
|
|
189
|
+
@@ -8044,6 +8094,7 @@
|
|
190
|
+
}
|
|
191
|
+
async _executeCompaction(request) {
|
|
192
|
+
const model = this.model;
|
|
193
|
+
+ this._activeCompactionModel = model;
|
|
194
|
+
if (!model)
|
|
195
|
+
throw new Error(formatNoModelSelectedMessage());
|
|
196
|
+
const controller = request.controller;
|
|
197
|
+
@@ -8120,8 +8171,20 @@
|
|
198
|
+
Math.ceil(JSON.stringify([this.agent.state.systemPrompt, this.agent.state.tools,
|
|
199
|
+
preparation.previousSummary, request.customInstructions]).length / 4) + 8192;
|
|
200
|
+
const compactionModel = this._resolveCompactionModel(model, inputTokens);
|
|
201
|
+
- const { model: requestModel, apiKey, headers, extraBody, env, thinkingLevel, } = await this._getCompactionRequestAuth(compactionModel, signal, this._summarizationRetryCallbacks({ source: "compaction", reason: request.reason }));
|
|
202
|
+
- compactionResult = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, extraBody, request.customInstructions, signal, env, request.reason, thinkingLevel);
|
|
203
|
+
+ this._activeCompactionModel = compactionModel;
|
|
204
|
+
+ let authReady = false;
|
|
205
|
+
+ try {
|
|
206
|
+
+ const { model: requestModel, apiKey, headers, extraBody, env, thinkingLevel, } = await this._getCompactionRequestAuth(compactionModel, signal, this._summarizationRetryCallbacks({ source: "compaction", reason: request.reason }));
|
|
207
|
+
+ authReady = true;
|
|
208
|
+
+ compactionResult = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, extraBody, request.customInstructions, signal, env, request.reason, thinkingLevel);
|
|
209
|
+
+ } catch (error) {
|
|
210
|
+
+ if (signal.aborted) throw error;
|
|
211
|
+
+ const failure = compactionFailure(error, compactionModel);
|
|
212
|
+
+ if (!failure.inputTokens) failure.inputTokens = inputTokens;
|
|
213
|
+
+ if (!authReady) failure.requestSent = false;
|
|
214
|
+
+ noteSummaryUnavailable(this.sessionManager, compactionModel, failure);
|
|
215
|
+
+ throw Object.assign(error, { failure });
|
|
216
|
+
+ }
|
|
217
|
+
compactionResult.details = { ...compactionResult.details,
|
|
218
|
+
selectionMode: settings.model === undefined || settings.model === "auto" ? "auto" : "explicit",
|
|
219
|
+
requestedProvider: compactionModel.provider, requestedModel: compactionModel.id };
|
|
220
|
+
@@ -8259,7 +8322,7 @@
|
|
221
|
+
const aborted = signal.aborted;
|
|
222
|
+
if (ownsTerminalTransition && lifecycleState.status === "running")
|
|
223
|
+
return await this._rejectCompaction(request, requestId, operationId, "cancelled-by-extension", aborted,
|
|
224
|
+
- aborted ? undefined : "Compaction failed; previous context retained", aborted ? undefined : compactionFailure(error, model));
|
|
225
|
+
+ aborted ? undefined : "Compaction failed; previous context retained", aborted ? undefined : compactionFailure(error, this._activeCompactionModel ?? model));
|
|
226
|
+
throw new CompactionExecutionError(error, ownsTerminalTransition, aborted);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
@@ -8392,6 +8455,8 @@
|
|
230
|
+
* deeper in compaction or the provider request.
|
|
231
|
+
*/
|
|
232
|
+
async _assertModelReadyForTurn() {
|
|
233
|
+
+ if (this._pendingModelSwitch !== undefined && this._summaryUnavailableForSwitch())
|
|
234
|
+
+ await this._applyPendingModelSwitch(undefined, false, "pre_prompt");
|
|
235
|
+
if (!this.model) {
|
|
236
|
+
throw new ModelNotReadyError(formatNoModelSelectedMessage());
|
|
237
|
+
}
|
|
238
|
+
@@ -8429,6 +8494,11 @@
|
|
239
|
+
* @returns Whether the post-run loop should call `agent.continue()` for overflow recovery or queued messages
|
|
240
|
+
*/
|
|
241
|
+
async _enforceCompactionBeforeProvider(assistantMessage, skipAbortedCheck, inlineReason, retryAfterCompaction = false) {
|
|
242
|
+
+ if (this._pendingModelSwitch !== undefined) {
|
|
243
|
+
+ const applied = await this._applyPendingModelSwitch(assistantMessage, skipAbortedCheck, inlineReason);
|
|
244
|
+
+ this._releaseBlockedPostCompactionAdmissionIfReduced();
|
|
245
|
+
+ return applied;
|
|
246
|
+
+ }
|
|
247
|
+
this._releaseBlockedPostCompactionAdmissionIfReduced();
|
|
248
|
+
const blockedAdmission = this._blockedPostCompactionAssistant;
|
|
249
|
+
if (blockedAdmission !== undefined && blockedAdmission.assistant === assistantMessage) {
|
|
250
|
+
@@ -8436,9 +8506,6 @@
|
|
251
|
+
}
|
|
252
|
+
const settings = this._getCompactionSettings();
|
|
253
|
+
const model = this.model;
|
|
254
|
+
- if (this._pendingModelSwitch !== undefined) {
|
|
255
|
+
- return await this._applyPendingModelSwitch(assistantMessage, skipAbortedCheck, inlineReason);
|
|
256
|
+
- }
|
|
257
|
+
if (this._resumeCompactionRequirement !== undefined) {
|
|
258
|
+
if (!model)
|
|
259
|
+
throw new RequiredCompactionError();
|
|
260
|
+
@@ -8822,6 +8889,7 @@
|
|
261
|
+
// The flag reports THIS attempt's "nothing to compact" outcome only; a value left by an earlier attempt
|
|
262
|
+
// must not make a caller treat a cancelled or rejected compaction as too small (senpi#2925 review).
|
|
263
|
+
this._compactionSkippedTooSmall = false;
|
|
264
|
+
+ this._lastPrePromptCompactionAborted = false;
|
|
265
|
+
if (isAutomaticCompactionBlocked(this.sessionManager.getBranch(), this.model, this._getCompactionSettings())) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
@@ -8851,9 +8919,11 @@
|
|
269
|
+
isContextOverflow(lastAssistantMessage, this.model?.contextWindow ?? 0)) {
|
|
270
|
+
this._overflowRecoveryRungs = 0;
|
|
271
|
+
}
|
|
272
|
+
+ this._lastPrePromptCompactionAborted = controller.signal.aborted || execution.aborted || ["stale-revision", "external-owner"].includes(execution.rejectionCause);
|
|
273
|
+
return execution.accepted;
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
+ this._lastPrePromptCompactionAborted = isCompactionExecutionAborted(error) || !compactionExecutionOwnsTerminalTransition(error);
|
|
277
|
+
if (!compactionExecutionOwnsTerminalTransition(error)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
@@ -8870,7 +8940,8 @@
|
|
281
|
+
aborted,
|
|
282
|
+
willRetry: false,
|
|
283
|
+
requestId,
|
|
284
|
+
- errorMessage: aborted ? undefined : `Pre-prompt compaction failed: ${errorMessage}`,
|
|
285
|
+
+ errorMessage: aborted ? undefined : "Compaction failed; previous context retained",
|
|
286
|
+
+ failure: aborted ? undefined : compactionFailure(error, this._activeCompactionModel ?? this.model),
|
|
287
|
+
});
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
@@ -9520,6 +9591,7 @@
|
|
291
|
+
}, {
|
|
292
|
+
offload: (id, args, options) => offload(this._toolDispatcher, this._extensionRunner.getAllChildModules(), id, args, options),
|
|
293
|
+
getModel: () => this.model,
|
|
294
|
+
+ getModelSwitchState: () => ({ current: this.model, pending: this._pendingModelSwitch?.model }),
|
|
295
|
+
getServiceTier: () => this.serviceTier,
|
|
296
|
+
getEffectiveServiceTier: () => this.effectiveServiceTier,
|
|
297
|
+
getScopedModels: () => this._scopedModels,
|
|
298
|
+
@@ -9668,7 +9740,8 @@
|
|
299
|
+
aborted,
|
|
300
|
+
willRetry: false,
|
|
301
|
+
requestId,
|
|
302
|
+
- errorMessage: aborted ? undefined : `Compaction failed: ${message}`,
|
|
303
|
+
+ errorMessage: aborted ? undefined : "Compaction failed; previous context retained",
|
|
304
|
+
+ failure: aborted ? undefined : compactionFailure(error, this._activeCompactionModel ?? this.model),
|
|
305
|
+
});
|
|
306
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
307
|
+
options?.onError?.(err);
|
|
308
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/stream.js
|
|
309
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/anthropic-subscription/stream.js
|
|
310
|
+
@@ -1,6 +1,7 @@
|
|
311
|
+
import { createAssistantMessageDiagnostic, createAssistantMessageEventStream, } from "@earendil-works/pi-ai";
|
|
312
|
+
import { getSessionClaudeAccountPin } from "./account-command.js";
|
|
313
|
+
import { queryWithAuthLane } from "./auth-lane.js";
|
|
314
|
+
+import { AllAccountsBlockedError } from "./affinity.js";
|
|
315
|
+
import { markColdSeedOverflow } from "./cold-seed-budget.js";
|
|
316
|
+
import { buildCustomToolServers } from "./custom-tools.js";
|
|
317
|
+
import { sdkAssistantFailure, sdkResultFailure, sdkResultFailureUsage } from "./errors.js";
|
|
318
|
+
@@ -166,6 +167,7 @@
|
|
319
|
+
buildOptions,
|
|
320
|
+
});
|
|
321
|
+
for await (const message of messages) {
|
|
322
|
+
+ if (message.type === "assistant" || message.type === "result" || message.type === "stream_event") output.requestSent = true;
|
|
323
|
+
const refusal = refusalError(message);
|
|
324
|
+
if (refusal)
|
|
325
|
+
throw refusal;
|
|
326
|
+
@@ -265,6 +267,14 @@
|
|
327
|
+
if (billed)
|
|
328
|
+
updateUsage(model, output, billed, "query");
|
|
329
|
+
output.errorMessage = withAuthGuidance(error, errorMessage(error), claudeCodeRun);
|
|
330
|
+
+ if (error instanceof AllAccountsBlockedError) {
|
|
331
|
+
+ output.failure = { kind: "account_blocked",
|
|
332
|
+
+ ...(Number.isFinite(error.soonestUnblockAt) ? { retryAt: error.soonestUnblockAt } : {}),
|
|
333
|
+
+ ...(output.requestSent === true ? { requestSent: true } : sdkQuery === undefined ? { requestSent: false } : {}) };
|
|
334
|
+
+ }
|
|
335
|
+
+ if (error?.name === "AccountUsageLimitError" || error?.errorCause?.type === "usage_limit") {
|
|
336
|
+
+ output.failure = { kind: "quota", ...(Number.isFinite(error.retryAt) ? { retryAt: error.retryAt } : {}) };
|
|
337
|
+
+ }
|
|
338
|
+
markColdSeedOverflow(output, model, coldSeedAttempt, coldSeedEstimate, options?.sessionId);
|
|
339
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
340
|
+
}
|
|
341
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/index.js
|
|
342
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/index.js
|
|
343
|
+
@@ -14,7 +14,9 @@
|
|
344
|
+
import * as policy from "./policy.js";
|
|
345
|
+
import { compactionRecoveryStateKey, isAutomaticCompactionBlocked, REJECTED_RECOVERY_ENTRY, wouldCompactionOverflow, } from "./rejected-recovery.js";
|
|
346
|
+
import * as restoration from "./restoration-tracker.js";
|
|
347
|
+
-import { applyGeneratedCompaction, createEmergencyPruneLatch, createSpeculativeCompactionSnapshot, getPromptVariant, runExtensionCompaction, SummaryGenerationError, compactionFailure, } from "./speculative.js";
|
|
348
|
+
+import { applyGeneratedCompaction, createEmergencyPruneLatch, createSpeculativeCompactionSnapshot, getPromptVariant, runExtensionCompaction, SummaryGenerationError, compactionFailure, canStartSummary, summaryUnavailable, } from "./speculative.js";
|
|
349
|
+
+import { planResumeSlice } from "./resume-slice.js";
|
|
350
|
+
+import { projectModelUsabilityBudget } from "./model-usability-budget.js";
|
|
351
|
+
import { trackSpeculativeJob } from "./speculative-job.js";
|
|
352
|
+
import { createInitialState, resetTurnCounter } from "./state.js";
|
|
353
|
+
import { resolveInheritedTaskIntent } from "./task-intent.js";
|
|
354
|
+
@@ -121,6 +123,8 @@
|
|
355
|
+
return;
|
|
356
|
+
if (isContextRetired(ctx))
|
|
357
|
+
return;
|
|
358
|
+
+ if (ctx.getCompactionSettings().enabled === false)
|
|
359
|
+
+ return;
|
|
360
|
+
if (!ctx.isIdle())
|
|
361
|
+
return;
|
|
362
|
+
if (lanePolicy.disablesSenpiCompaction(ctx))
|
|
363
|
+
@@ -188,19 +192,22 @@
|
|
364
|
+
speculativeJob = undefined;
|
|
365
|
+
}
|
|
366
|
+
function startSpeculativeCompaction(ctx, customInstructions) {
|
|
367
|
+
+ if (ctx.getCompactionSettings().enabled === false)
|
|
368
|
+
+ return;
|
|
369
|
+
if (speculativeJob)
|
|
370
|
+
return;
|
|
371
|
+
if (isAutomaticCompactionBlocked(ctx.sessionManager.getBranch(), ctx.model, ctx.getCompactionSettings()))
|
|
372
|
+
return;
|
|
373
|
+
- const generation = ++speculativeGeneration;
|
|
374
|
+
+ const generation = speculativeGeneration + 1;
|
|
375
|
+
const snapshot = createSpeculativeCompactionSnapshot(ctx, {
|
|
376
|
+
generation,
|
|
377
|
+
customInstructions,
|
|
378
|
+
origin: "speculative",
|
|
379
|
+
tools: getSummarizationTools(),
|
|
380
|
+
});
|
|
381
|
+
- if (!snapshot)
|
|
382
|
+
+ if (!snapshot || !canStartSummary(ctx, snapshot))
|
|
383
|
+
return;
|
|
384
|
+
+ speculativeGeneration = generation;
|
|
385
|
+
getLogger(ctx).debug("speculative_started", { generation, origin: "speculative" });
|
|
386
|
+
const controller = new AbortController();
|
|
387
|
+
const settled = generateCompaction(ctx, snapshot, controller.signal).then((result) => ({ result, error: undefined }), (error) => ({ result: undefined, error: error instanceof Error ? error : new Error(String(error)) }));
|
|
388
|
+
@@ -236,9 +243,43 @@
|
|
389
|
+
async function generateCompaction(ctx, snapshot, signal, onProgress) {
|
|
390
|
+
return await runExtensionCompaction(ctx, snapshot, signal, onProgress);
|
|
391
|
+
}
|
|
392
|
+
- async function applyBlockingCompaction(ctx, customInstructions) {
|
|
393
|
+
+ function summaryIsUnavailable(ctx) {
|
|
394
|
+
+ try {
|
|
395
|
+
+ return summaryUnavailable(ctx.sessionManager, ctx.getCompactionModel?.() ?? ctx.model) !== undefined;
|
|
396
|
+
+ }
|
|
397
|
+
+ catch (error) {
|
|
398
|
+
+ if (error?.failure?.kind === "quota" || error?.failure?.kind === "account_blocked")
|
|
399
|
+
+ return true;
|
|
400
|
+
+ throw error;
|
|
401
|
+
+ }
|
|
402
|
+
+ }
|
|
403
|
+
+ function deterministicCompaction(ctx, pendingTokens = 0) {
|
|
404
|
+
+ const entries = ctx.sessionManager.getBranch();
|
|
405
|
+
+ const projection = projectModelUsabilityBudget({
|
|
406
|
+
+ model: ctx.model,
|
|
407
|
+
+ liveContextTokens: ctx.getContextUsage()?.tokens ?? 0,
|
|
408
|
+
+ systemPrompt: ctx.getSystemPrompt(), tools: getSummarizationTools(),
|
|
409
|
+
+ compaction: { ...ctx.getCompactionSettings(), enabled: true, speculativeEnabled: false },
|
|
410
|
+
+ includeSpeculationLead: false,
|
|
411
|
+
+ });
|
|
412
|
+
+ projection.safetyMarginTokens += pendingTokens;
|
|
413
|
+
+ const plan = planResumeSlice({ entries, projection });
|
|
414
|
+
+ if (!plan)
|
|
415
|
+
+ throw new SummaryGenerationError("context_budget", "No replay-safe context suffix fits the request budget");
|
|
416
|
+
+ return { ...plan, details: { schema: "senpi.compaction.resume-slice.v1", recoveryOrigin: "turn-start",
|
|
417
|
+
+ originalLeafId: ctx.sessionManager.getLeafId() } };
|
|
418
|
+
+ }
|
|
419
|
+
+ async function applyBlockingCompaction(ctx, customInstructions, pendingTokens = 0) {
|
|
420
|
+
const feedbackSignal = ctx.beginCompaction?.({ reason: "extension" });
|
|
421
|
+
try {
|
|
422
|
+
+ if (ctx.getCompactionSettings().enabled === false || summaryIsUnavailable(ctx)) {
|
|
423
|
+
+ invalidateSpeculativeCompaction(ctx);
|
|
424
|
+
+ const result = await ctx.applyCompaction(deterministicCompaction(ctx, pendingTokens), {
|
|
425
|
+
+ reason: "extension", blocking: true, expectedRevision: ctx.getMessageRevision(), signal: feedbackSignal,
|
|
426
|
+
+ });
|
|
427
|
+
+ endCompactionFeedback(ctx, feedbackSignal, result);
|
|
428
|
+
+ return result;
|
|
429
|
+
+ }
|
|
430
|
+
const provider = ctx.model?.provider;
|
|
431
|
+
if ((provider === "cursor" || provider === "cursor-cli-oauth") && !ctx.isIdle())
|
|
432
|
+
throw new SummaryGenerationError("provider", "Compaction unavailable while provider is busy");
|
|
433
|
+
@@ -306,6 +347,8 @@
|
|
434
|
+
if (breaker.isTripped(state, Date.now()) && !breaker.shouldBypass(state, { reason: event.reason }))
|
|
435
|
+
return { cancel: true, rejectionCause: "circuit-breaker", failure: { kind: "provider" } };
|
|
436
|
+
capturePendingMetadata(event.requestId, ctx);
|
|
437
|
+
+ if (event.reason !== "manual" && (ctx.getCompactionSettings().enabled === false || summaryIsUnavailable(ctx)))
|
|
438
|
+
+ return { compaction: deterministicCompaction(ctx) };
|
|
439
|
+
const model = ctx.model;
|
|
440
|
+
if (!model)
|
|
441
|
+
throw new SummaryGenerationError("model_unavailable", "Compaction model unavailable");
|
|
442
|
+
@@ -436,7 +479,7 @@
|
|
443
|
+
// The SDK owns this lane's context entirely, so even the hard-limit valve stands down;
|
|
444
|
+
// the circuit breaker never blocks that valve for senpi-owned lanes.
|
|
445
|
+
const laneOwnsCompaction = lanePolicy.disablesSenpiCompaction(ctx);
|
|
446
|
+
- const breakerCoolingDown = breaker.isTripped(state, Date.now()) || laneOwnsCompaction;
|
|
447
|
+
+ const breakerCoolingDown = breaker.isTripped(state, Date.now()) || laneOwnsCompaction || settings.enabled === false || summaryIsUnavailable(ctx);
|
|
448
|
+
const { reserveTokens, thresholdTokens, leadTokens } = resolveCompactionGeometry({
|
|
449
|
+
contextWindow,
|
|
450
|
+
settings,
|
|
451
|
+
@@ -450,7 +493,7 @@
|
|
452
|
+
tokens: usage.tokens ?? 0,
|
|
453
|
+
threshold: settings.reserveTokens,
|
|
454
|
+
});
|
|
455
|
+
- const result = await applyBlockingCompaction(ctx, EMERGENCY_COMPACTION_INSTRUCTIONS);
|
|
456
|
+
+ const result = await applyBlockingCompaction(ctx, EMERGENCY_COMPACTION_INSTRUCTIONS, pendingPromptTokens);
|
|
457
|
+
if (!result.applied) { ctx.abort("system"); return; }
|
|
458
|
+
}
|
|
459
|
+
else if (!breakerCoolingDown &&
|
|
460
|
+
@@ -512,6 +555,8 @@
|
|
461
|
+
pi.on("before_agent_start", onBeforeAgentStart, { previewSafe: true });
|
|
462
|
+
const contextReduction = registerContextReductionLifecycle(pi, {
|
|
463
|
+
emergencyPruneLatch,
|
|
464
|
+
+ isSummaryUnavailable: summaryIsUnavailable,
|
|
465
|
+
+ reduceContext: (ctx, pendingTokens) => applyBlockingCompaction(ctx, EMERGENCY_COMPACTION_INSTRUCTIONS, pendingTokens),
|
|
466
|
+
emergencyInstructions: EMERGENCY_COMPACTION_INSTRUCTIONS,
|
|
467
|
+
getTools: getSummarizationTools,
|
|
468
|
+
getPolicy: (ctx) => ({
|
|
469
|
+
@@ -545,7 +590,7 @@
|
|
470
|
+
});
|
|
471
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
472
|
+
try {
|
|
473
|
+
- if (lanePolicy.disablesSenpiCompaction(ctx))
|
|
474
|
+
+ if (lanePolicy.disablesSenpiCompaction(ctx) || ctx.getCompactionSettings().enabled === false)
|
|
475
|
+
return;
|
|
476
|
+
handleTurnEnd(degradationState);
|
|
477
|
+
if (degradationState.recoveryTriggeredThisCycle)
|
|
478
|
+
@@ -560,7 +605,7 @@
|
|
479
|
+
});
|
|
480
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
481
|
+
state = resetTurnCounter(state, "");
|
|
482
|
+
- if (lanePolicy.disablesSenpiCompaction(ctx))
|
|
483
|
+
+ if (lanePolicy.disablesSenpiCompaction(ctx) || ctx.getCompactionSettings().enabled === false)
|
|
484
|
+
return;
|
|
485
|
+
const usage = ctx.getContextUsage();
|
|
486
|
+
const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
487
|
+
@@ -615,7 +660,7 @@
|
|
488
|
+
if (isAbortedAssistantMessage(event)) {
|
|
489
|
+
invalidateSpeculativeCompaction(ctx);
|
|
490
|
+
}
|
|
491
|
+
- if (isMonitorableMessageEvent(event) && !lanePolicy.disablesSenpiCompaction(ctx)) {
|
|
492
|
+
+ if (ctx.getCompactionSettings().enabled !== false && isMonitorableMessageEvent(event) && !lanePolicy.disablesSenpiCompaction(ctx)) {
|
|
493
|
+
await handleMessageEnd(degradationState, event, {
|
|
494
|
+
applyCompaction: async (options) => {
|
|
495
|
+
return await applyBlockingCompaction(ctx, options.customInstructions);
|
|
496
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative-summary.js
|
|
497
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative-summary.js
|
|
498
|
+
@@ -83,6 +83,8 @@
|
|
499
|
+
// Request-local controller: the idle watchdog must be able to tear down a
|
|
500
|
+
// stalled summarization request without aborting the caller's own signal.
|
|
501
|
+
const requestController = new AbortController();
|
|
502
|
+
+ let requestSent;
|
|
503
|
+
+ let requestInputTokens;
|
|
504
|
+
const onCallerAbort = () => requestController.abort(options.signal?.reason);
|
|
505
|
+
if (options.signal) {
|
|
506
|
+
if (options.signal.aborted)
|
|
507
|
+
@@ -112,9 +114,10 @@
|
|
508
|
+
{ role: "user", content: requestContext.systemPrompt },
|
|
509
|
+
{ role: "user", content: JSON.stringify(requestContext.tools ?? []) },
|
|
510
|
+
]);
|
|
511
|
+
+ requestInputTokens = inputTokens;
|
|
512
|
+
if (inputTokens + summaryMaxTokens(options.snapshot.model, options.snapshot.contextWindow) > options.snapshot.contextWindow) {
|
|
513
|
+
throw Object.assign(new Error("Compaction request exceeds the selected model context budget"),
|
|
514
|
+
- { failure: { kind: "context_budget", provider: options.snapshot.model.provider, model: options.snapshot.model.id } });
|
|
515
|
+
+ { failure: { kind: "context_budget", provider: options.snapshot.model.provider, model: options.snapshot.model.id, error: "Compaction request exceeds the selected model context budget", inputTokens, inputTokensSource: "estimate", requestSent: false } });
|
|
516
|
+
}
|
|
517
|
+
const headers = providerRequest
|
|
518
|
+
? await providerRequest.transformHeaders(options.auth.headers ?? {})
|
|
519
|
+
@@ -126,6 +129,7 @@
|
|
520
|
+
env: options.auth.env,
|
|
521
|
+
cwd: options.context.cwd,
|
|
522
|
+
streamKind: "auxiliary",
|
|
523
|
+
+ onResponse: () => { requestSent = true; },
|
|
524
|
+
onPayload: async (payload, model) => {
|
|
525
|
+
const sanitized = model.api === "anthropic-messages" ? sanitizeAnthropicPayload(payload) : payload;
|
|
526
|
+
return providerRequest ? await providerRequest.transformPayload(sanitized) : sanitized;
|
|
527
|
+
@@ -137,7 +141,7 @@
|
|
528
|
+
});
|
|
529
|
+
// Settlement rides inside the watchdog: a provider whose iterator ends
|
|
530
|
+
// without a terminal event used to park here with every timer cleared.
|
|
531
|
+
- return await consumeStreamWithIdleTimeout(responseStream, {
|
|
532
|
+
+ const response = await consumeStreamWithIdleTimeout(responseStream, {
|
|
533
|
+
idleTimeoutMs: DEFAULT_SUMMARIZATION_IDLE_TIMEOUT_MS,
|
|
534
|
+
maxDurationMs,
|
|
535
|
+
abort: () => requestController.abort(),
|
|
536
|
+
@@ -149,7 +153,18 @@
|
|
537
|
+
},
|
|
538
|
+
settle: async () => await responseStream.result(),
|
|
539
|
+
});
|
|
540
|
+
+ if (response) {
|
|
541
|
+
+ response.failure = { ...response.failure, inputTokens, inputTokensSource: "estimate",
|
|
542
|
+
+ ...(requestSent === true ? { requestSent: true } : {}) };
|
|
543
|
+
+ }
|
|
544
|
+
+ return response;
|
|
545
|
+
}
|
|
546
|
+
+ catch (error) {
|
|
547
|
+
+ error.failure = { ...error.failure, provider: options.snapshot.model.provider, model: options.snapshot.model.id,
|
|
548
|
+
+ ...(requestInputTokens === undefined ? {} : { inputTokens: requestInputTokens, inputTokensSource: "estimate" }),
|
|
549
|
+
+ ...(requestSent === true ? { requestSent: true } : {}) };
|
|
550
|
+
+ throw error;
|
|
551
|
+
+ }
|
|
552
|
+
finally {
|
|
553
|
+
if (options.signal)
|
|
554
|
+
options.signal.removeEventListener("abort", onCallerAbort);
|
|
555
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative.js
|
|
556
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative.js
|
|
557
|
+
@@ -13,6 +13,76 @@
|
|
558
|
+
import { extractTaskIntent, resolveInheritedTaskIntent, taskIntentSummary, taskIntentMetadata } from "./task-intent.js";
|
|
559
|
+
export { createEmergencyPruneLatch, hardLimitEmergencyPrune, truncateContextMessages, } from "./emergency-prune.js";
|
|
560
|
+
import { computeStructuralYield } from "./yield.js";
|
|
561
|
+
+import { usageLimitResetMs } from "../../../credential-pool/reset-time.js";
|
|
562
|
+
+import { homedir } from "node:os";
|
|
563
|
+
+// Availability belongs to the session, not a speculative generation. Stable session files
|
|
564
|
+
+// retain it through extension reloads; fixture-only managers use their object identity.
|
|
565
|
+
+const summaryAvailability = new Map();
|
|
566
|
+
+function summaryBlocks(manager) {
|
|
567
|
+
+ const key = manager.getSessionFile?.() ?? manager.getSessionId?.() ?? manager;
|
|
568
|
+
+ let blocks = summaryAvailability.get(key);
|
|
569
|
+
+ if (!blocks) summaryAvailability.set(key, blocks = new Map());
|
|
570
|
+
+ return blocks;
|
|
571
|
+
+}
|
|
572
|
+
+export function summaryUnavailable(manager, model, now = Date.now()) {
|
|
573
|
+
+ if (!model) return undefined;
|
|
574
|
+
+ const key = model.provider + "/" + model.id;
|
|
575
|
+
+ const failure = summaryBlocks(manager).get(key);
|
|
576
|
+
+ if (!failure || now >= failure.retryAt) return undefined;
|
|
577
|
+
+ return failure;
|
|
578
|
+
+}
|
|
579
|
+
+export function noteSummaryUnavailable(manager, model, failure, now = Date.now()) {
|
|
580
|
+
+ if (failure.kind !== "quota" && failure.kind !== "account_blocked") return;
|
|
581
|
+
+ const retryAt = failure.retryAt ?? now + 30 * 60_000;
|
|
582
|
+
+ summaryBlocks(manager).set(model.provider + "/" + model.id, { ...failure, retryAt });
|
|
583
|
+
+}
|
|
584
|
+
+export function sanitizeSummaryDiagnostic(value) {
|
|
585
|
+
+ if (typeof value !== "string") return "Compaction provider failed";
|
|
586
|
+
+ // Read only the provider's message, never nested errors, payloads, or stacks.
|
|
587
|
+
+ const body = /(?:\r?\n\s*at\s+|\b(?:request[ _-]?body|response[ _-]?body|request[ _-]?payload|response[ _-]?payload|prompt|summary|conversation)\s*[:=])/i.exec(value);
|
|
588
|
+
+ let text = body ? value.slice(0, body.index) + "[redacted]" : value;
|
|
589
|
+
+ text = text.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-]*PRIVATE KEY-----|$)/g, "[redacted]")
|
|
590
|
+
+ .replace(/(\bauthorization\b\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\r\n,;]+)/gi, "$1[redacted]")
|
|
591
|
+
+ .replace(/\b(?:Bearer|Basic)\s+[^\s"'<>]+/gi, "[redacted]")
|
|
592
|
+
+ .replace(/(\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|password|secret)\b\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi, "$1[redacted]")
|
|
593
|
+
+ .replace(/\bsk-[A-Za-z0-9_-]+\b/g, "[redacted]")
|
|
594
|
+
+ .replace(/\b[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/g, "[redacted]")
|
|
595
|
+
+ .replace(/\b(?:eyJ[A-Za-z0-9_-]*|e30)\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*(?=$|[^A-Za-z0-9_-])/g, "[redacted]")
|
|
596
|
+
+ .replace(/\/(?:Users|home)\/[^/\s"']+(?=\/|\s|$)/g, "~")
|
|
597
|
+
+ .split(homedir()).join("~")
|
|
598
|
+
+ .replace(/(?<![A-Za-z0-9+/_-])[A-Za-z0-9+/_-]{24,}={0,2}/g, "[redacted]")
|
|
599
|
+
+ .replace(/[A-Z0-9.!#$%&'*+/=?^_\x60{|}~-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[redacted]")
|
|
600
|
+
+ .replace(/(https?:\/\/)[^/@\s]+:[^/@\s]+@/gi, "$1[redacted]@")
|
|
601
|
+
+ .replace(/(https?:\/\/[^\s?"'<>]+)\?[^\s"'<>]*/gi, "$1")
|
|
602
|
+
+ .replace(/(\baccount(?:[ _-]?(?:name|id))?\s*(?:[:=]\s*)?)(?:'[^']*'|"[^"]*")/gi, "$1[redacted]")
|
|
603
|
+
+ .replace(/(\baccount[ _-]?(?:name|id)\s*[:=]\s*)[^\s,;]+/gi, "$1[redacted]");
|
|
604
|
+
+ return text.slice(0, 512);
|
|
605
|
+
+}
|
|
606
|
+
+const WITHHELD = "Compaction provider failed";
|
|
607
|
+
+const CHATGPT_LIMIT = "You have hit your ChatGPT usage limit.";
|
|
608
|
+
+const POOL_REFUSAL = "No request was sent: the account pool refused it locally.";
|
|
609
|
+
+const LOCAL_BUDGET = "Compaction request exceeds the selected model context budget";
|
|
610
|
+
+export function summaryFailureDetails(value, model, inputTokens = 0) {
|
|
611
|
+
+ const message = value?.errorMessage ?? value?.message ?? "";
|
|
612
|
+
+ const pool = value?.name === "AllAccountsBlockedError" ||
|
|
613
|
+
+ (model?.provider === "anthropic-subscription" && message.includes(POOL_REFUSAL));
|
|
614
|
+
+ const chatgpt = model?.provider === "chatgpt-subscription" && message.startsWith("You have hit your ChatGPT usage limit");
|
|
615
|
+
+ const kind = pool ? "account_blocked" : chatgpt || value?.name === "AccountUsageLimitError" || value?.errorCause?.type === "usage_limit" ? "quota" : value?.failure?.kind ?? value?.kind;
|
|
616
|
+
+ const reset = (pool || chatgpt || kind === "quota") ? usageLimitResetMs(message, Date.now()) : undefined;
|
|
617
|
+
+ const retryAt = value?.failure?.retryAt ?? value?.retryAt ?? value?.soonestUnblockAt ?? (reset === undefined ? undefined : Date.now() + reset);
|
|
618
|
+
+ const usage = value?.usage?.contextUsage;
|
|
619
|
+
+ const measured = typeof usage?.input === "number" && Number.isFinite(usage.input) && usage.input >= 0;
|
|
620
|
+
+ return {
|
|
621
|
+
+ kind: kind ?? "provider",
|
|
622
|
+
+ ...(model ? { provider: model.provider, model: model.id } : {}),
|
|
623
|
+
+ error: sanitizeSummaryDiagnostic(message || value?.failure?.error || WITHHELD),
|
|
624
|
+
+ inputTokens: measured ? usage.input : value?.failure?.inputTokens ?? inputTokens,
|
|
625
|
+
+ inputTokensSource: measured ? "provider" : "estimate",
|
|
626
|
+
+ ...(typeof value?.failure?.requestSent === "boolean" ? { requestSent: value.failure.requestSent }
|
|
627
|
+
+ : typeof value?.requestSent === "boolean" ? { requestSent: value.requestSent } : {}),
|
|
628
|
+
+ ...(Number.isFinite(retryAt) ? { retryAt } : {}),
|
|
629
|
+
+ };
|
|
630
|
+
+}
|
|
631
|
+
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
632
|
+
// Hysteresis: the emergency prune engages at EMERGENCY_CONTEXT_TARGET_RATIO but only
|
|
633
|
+
// releases once the context falls below this lower ratio. A single threshold makes a
|
|
634
|
+
@@ -34,14 +104,22 @@
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
export function compactionFailure(error, model) {
|
|
638
|
+
- const kind = error?.refused ? "refused" : error?.failure?.kind ?? error?.kind;
|
|
639
|
+
- const known = ["invalid_model", "auth_missing", "auth_expired", "quota", "org_denied", "model_unavailable", "refused", "timeout", "incomplete", "empty", "context_budget", "stale", "provider"];
|
|
640
|
+
+ const details = error?.failure ?? summaryFailureDetails(error, model);
|
|
641
|
+
+ const kind = error?.refused ? "refused" : details.kind;
|
|
642
|
+
+ const known = ["invalid_model", "auth_missing", "auth_expired", "quota", "account_blocked", "org_denied", "model_unavailable", "refused", "timeout", "incomplete", "empty", "context_budget", "stale", "provider"];
|
|
643
|
+
+ const diagnostic = sanitizeSummaryDiagnostic(details.error ?? error?.message ?? WITHHELD);
|
|
644
|
+
return { kind: known.includes(kind) ? kind
|
|
645
|
+
: error instanceof StreamDurationBudgetError || error instanceof StreamIdleTimeoutError || error instanceof SummarizationTotalBudgetError || error?.name === "TimeoutError" ? "timeout"
|
|
646
|
+
: error instanceof SummarizationOverflowExhaustedError ? "context_budget" : "provider",
|
|
647
|
+
...(model ? { provider: model.provider, model: model.id } : {}),
|
|
648
|
+
- ...(error?.failure?.provider ? { provider: error.failure.provider } : {}),
|
|
649
|
+
- ...(error?.failure?.model ? { model: error.failure.model } : {}) };
|
|
650
|
+
+ ...(details.provider ? { provider: details.provider } : {}),
|
|
651
|
+
+ ...(details.model ? { model: details.model } : {}),
|
|
652
|
+
+ error: diagnostic,
|
|
653
|
+
+ inputTokens: Number.isFinite(details.inputTokens) && details.inputTokens >= 0 ? details.inputTokens : 0,
|
|
654
|
+
+ inputTokensSource: details.inputTokensSource === "provider" ? "provider" : "estimate",
|
|
655
|
+
+ ...(typeof details.requestSent === "boolean" ? { requestSent: details.requestSent } : {}),
|
|
656
|
+
+ ...(Number.isFinite(details.retryAt) ? { retryAt: details.retryAt } : {}),
|
|
657
|
+
+ };
|
|
658
|
+
}
|
|
659
|
+
export function validateSummaryResponse(response) {
|
|
660
|
+
if (response && isRefusalStop(response))
|
|
661
|
+
@@ -80,11 +158,11 @@
|
|
662
|
+
function isRefusalStop(response) {
|
|
663
|
+
return response.stopDetails?.type === "refusal" || response.stopDetails?.type === "sensitive";
|
|
664
|
+
}
|
|
665
|
+
-function summaryRequestFailureKind(response) {
|
|
666
|
+
+function summaryRequestFailureKind(response, model) {
|
|
667
|
+
if (isRefusalStop(response))
|
|
668
|
+
return undefined;
|
|
669
|
+
- const kind = response.failure?.kind;
|
|
670
|
+
- if (["auth_missing", "auth_expired", "quota", "org_denied", "model_unavailable", "timeout", "incomplete", "context_budget"].includes(kind))
|
|
671
|
+
+ const kind = summaryFailureDetails(response, model).kind;
|
|
672
|
+
+ if (["auth_missing", "auth_expired", "quota", "account_blocked", "org_denied", "model_unavailable", "timeout", "incomplete", "context_budget"].includes(kind))
|
|
673
|
+
return kind;
|
|
674
|
+
return UPSTREAM_STREAM_TRUNCATED_PATTERN.test(response.errorMessage ?? "") ? "upstream-stream-truncated" : undefined;
|
|
675
|
+
}
|
|
676
|
+
@@ -148,9 +226,7 @@
|
|
677
|
+
* before and after credential resolution so a cancelled request never
|
|
678
|
+
* misreports as an auth failure.
|
|
679
|
+
*/
|
|
680
|
+
-export async function runExtensionCompaction(context, snapshot, signal, onProgress) {
|
|
681
|
+
- if (signal?.aborted)
|
|
682
|
+
- return undefined;
|
|
683
|
+
+export function selectSummaryModel(context, snapshot) {
|
|
684
|
+
const prompt = buildPrompt({
|
|
685
|
+
variant: snapshot.promptVariant,
|
|
686
|
+
previousSummary: snapshot.preparation.previousSummary,
|
|
687
|
+
@@ -165,11 +241,33 @@
|
|
688
|
+
{ role: "user", content: JSON.stringify(snapshot.tools ?? []) },
|
|
689
|
+
]);
|
|
690
|
+
const selectedModel = context.getCompactionModel?.(inputTokens) ?? snapshot.model;
|
|
691
|
+
+ return { prompt, messages, inputTokens, selectedModel };
|
|
692
|
+
+}
|
|
693
|
+
+export function canStartSummary(context, snapshot) {
|
|
694
|
+
+ try {
|
|
695
|
+
+ const { selectedModel } = selectSummaryModel(context, snapshot);
|
|
696
|
+
+ return !summaryUnavailable(context.sessionManager, selectedModel);
|
|
697
|
+
+ } catch (error) {
|
|
698
|
+
+ if (["quota", "account_blocked"].includes(error?.failure?.kind)) return false;
|
|
699
|
+
+ return true;
|
|
700
|
+
+ }
|
|
701
|
+
+}
|
|
702
|
+
+export async function runExtensionCompaction(context, snapshot, signal, onProgress) {
|
|
703
|
+
+ if (signal?.aborted)
|
|
704
|
+
+ return undefined;
|
|
705
|
+
+ const { prompt, messages, inputTokens, selectedModel } = selectSummaryModel(context, snapshot);
|
|
706
|
+
+ const unavailable = summaryUnavailable(context.sessionManager, selectedModel);
|
|
707
|
+
+ if (unavailable) throw Object.assign(new SummaryGenerationError(unavailable.kind, "Compaction summary provider unavailable"), { failure: { ...unavailable, inputTokens, inputTokensSource: "estimate", requestSent: false } });
|
|
708
|
+
const selectedSnapshot = { ...snapshot, model: selectedModel, contextWindow: selectedModel.contextWindow, selectionMode: context.getCompactionSettings?.().model === undefined || context.getCompactionSettings?.().model === "auto" ? "auto" : "explicit" };
|
|
709
|
+
let auth;
|
|
710
|
+
try {
|
|
711
|
+
const runtime = context.modelRegistry?.modelRuntime;
|
|
712
|
+
- if (runtime) {
|
|
713
|
+
+ if (runtime?.couldRotateCredentials?.(selectedModel, { signal })) {
|
|
714
|
+
+ // Let the runtime select and authenticate a healthy slot. A resolved OAuth
|
|
715
|
+
+ // token here would become an explicit request key and disable rotation.
|
|
716
|
+
+ auth = { ok: true, ambient: true };
|
|
717
|
+
+ }
|
|
718
|
+
+ else if (runtime) {
|
|
719
|
+
const prepared = await runtime.prepareRequest(selectedModel, { signal });
|
|
720
|
+
auth = { ok: true, ...prepared.options, baseUrl: prepared.model.baseUrl, ambient: true };
|
|
721
|
+
}
|
|
722
|
+
@@ -180,16 +278,17 @@
|
|
723
|
+
catch (error) {
|
|
724
|
+
if (signal?.aborted)
|
|
725
|
+
return undefined;
|
|
726
|
+
- const kind = error?.failure?.kind ?? error?.kind;
|
|
727
|
+
- const known = ["auth_missing", "auth_expired", "quota", "org_denied", "model_unavailable"];
|
|
728
|
+
- const failure = { kind: kind === "auth" ? "auth_missing" : known.includes(kind) ? kind : "provider",
|
|
729
|
+
- provider: selectedModel.provider, model: selectedModel.id };
|
|
730
|
+
+ const details = summaryFailureDetails(error, selectedModel, inputTokens);
|
|
731
|
+
+ const kind = details.kind;
|
|
732
|
+
+ const known = ["auth_missing", "auth_expired", "quota", "account_blocked", "org_denied", "model_unavailable"];
|
|
733
|
+
+ const failure = { ...details, kind: kind === "auth" ? "auth_missing" : known.includes(kind) ? kind : "provider", requestSent: false };
|
|
734
|
+
+ noteSummaryUnavailable(context.sessionManager, selectedModel, failure);
|
|
735
|
+
throw Object.assign(new SummaryGenerationError(failure.kind, "Compaction request authentication failed"), { failure, cause: error });
|
|
736
|
+
}
|
|
737
|
+
if (signal?.aborted)
|
|
738
|
+
return undefined;
|
|
739
|
+
if (!auth?.ok || !(auth.apiKey || hasCredentialHeaders(auth.headers) || auth.ambient)) {
|
|
740
|
+
- const failure = { kind: "auth_missing", provider: selectedModel.provider, model: selectedModel.id };
|
|
741
|
+
+ const failure = { ...summaryFailureDetails(undefined, selectedModel, inputTokens), kind: "auth_missing", requestSent: false };
|
|
742
|
+
throw Object.assign(new SummaryGenerationError(failure.kind, "Compaction request authentication unavailable"), { failure });
|
|
743
|
+
}
|
|
744
|
+
const requestSnapshot = auth.baseUrl
|
|
745
|
+
@@ -222,6 +321,8 @@
|
|
746
|
+
// surrounding loop keeps overflow shrinking and abort handling, which
|
|
747
|
+
// must never be answered by replaying the same request.
|
|
748
|
+
response = await retryTransientCall(async () => {
|
|
749
|
+
+ const blocked = summaryUnavailable(context.sessionManager, selectedModel);
|
|
750
|
+
+ if (blocked) throw Object.assign(new SummaryGenerationError(blocked.kind, "Compaction summary provider unavailable"), { failure: { ...blocked, inputTokens, inputTokensSource: "estimate", requestSent: false } });
|
|
751
|
+
const attempt = await generateSummaryMessage({
|
|
752
|
+
context,
|
|
753
|
+
forbidToolCalls: true,
|
|
754
|
+
@@ -244,8 +345,10 @@
|
|
755
|
+
isAssistantMessage(attempt) &&
|
|
756
|
+
(attempt.stopReason === "error" || isRefusalStop(attempt)) &&
|
|
757
|
+
!isContextOverflow(attempt, requestSnapshot.contextWindow)) {
|
|
758
|
+
- const failureKind = summaryRequestFailureKind(attempt);
|
|
759
|
+
- throw new SummaryRequestError(attempt.errorMessage || "Compaction summary request failed", attempt.content.length === 0 && failureKind === undefined && isRetryableAssistantError(attempt), failureKind, isRefusalStop(attempt));
|
|
760
|
+
+ const failureKind = summaryRequestFailureKind(attempt, selectedModel);
|
|
761
|
+
+ const error = new SummaryRequestError(attempt.errorMessage || "Compaction summary request failed", attempt.content.length === 0 && failureKind === undefined && isRetryableAssistantError(attempt), failureKind, isRefusalStop(attempt));
|
|
762
|
+
+ error.failure = { ...summaryFailureDetails(attempt, selectedModel, inputTokens), kind: error.failure.kind };
|
|
763
|
+
+ throw error;
|
|
764
|
+
}
|
|
765
|
+
return attempt;
|
|
766
|
+
}, (error) => retryEligible &&
|
|
767
|
+
@@ -256,15 +359,23 @@
|
|
768
|
+
catch (error) {
|
|
769
|
+
if (signal?.aborted)
|
|
770
|
+
return undefined;
|
|
771
|
+
+ error.failure = compactionFailure(error, selectedModel);
|
|
772
|
+
+ if (!error.failure.inputTokens) error.failure.inputTokens = inputTokens;
|
|
773
|
+
+ noteSummaryUnavailable(context.sessionManager, selectedModel, error.failure);
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
if (!response && signal?.aborted)
|
|
777
|
+
return undefined;
|
|
778
|
+
if (response && isAssistantMessage(response) && isContextOverflow(response, requestSnapshot.contextWindow)) {
|
|
779
|
+
- const failure = { kind: "context_budget", provider: selectedModel.provider, model: selectedModel.id };
|
|
780
|
+
+ const failure = { ...summaryFailureDetails(response, selectedModel, inputTokens), kind: "context_budget" };
|
|
781
|
+
throw Object.assign(new SummaryGenerationError(failure.kind, "Compaction input exceeds the provider context budget"), { failure });
|
|
782
|
+
}
|
|
783
|
+
- const parsedSummary = validateSummaryResponse(response);
|
|
784
|
+
+ let parsedSummary;
|
|
785
|
+
+ try { parsedSummary = validateSummaryResponse(response); }
|
|
786
|
+
+ catch (error) {
|
|
787
|
+
+ error.failure = { ...summaryFailureDetails(response, selectedModel, inputTokens), kind: error.failure?.kind ?? "provider" };
|
|
788
|
+
+ throw error;
|
|
789
|
+
+ }
|
|
790
|
+
const inheritedIntent = resolveInheritedTaskIntent(snapshot.branchEntries ?? []);
|
|
791
|
+
if (inheritedIntent && parsedSummary.taskIntent && parsedSummary.taskIntent !== inheritedIntent)
|
|
792
|
+
throw new SummaryGenerationError("incomplete", "Compaction changed protected task intent");
|
|
793
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/runner.js
|
|
794
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/runner.js
|
|
795
|
+
@@ -396,6 +396,7 @@
|
|
796
|
+
this.offloadFn = contextActions.offload;
|
|
797
|
+
// Context actions (required)
|
|
798
|
+
this.getModel = contextActions.getModel;
|
|
799
|
+
+ this.getModelSwitchState = contextActions.getModelSwitchState;
|
|
800
|
+
this.getServiceTier = contextActions.getServiceTier;
|
|
801
|
+
this.getEffectiveServiceTier = contextActions.getEffectiveServiceTier ?? contextActions.getServiceTier;
|
|
802
|
+
this.getScopedModels = contextActions.getScopedModels;
|
|
803
|
+
@@ -1118,6 +1119,10 @@
|
|
804
|
+
runner.assertActive();
|
|
805
|
+
return getModel();
|
|
806
|
+
},
|
|
807
|
+
+ getModelSwitchState: () => {
|
|
808
|
+
+ runner.assertActive();
|
|
809
|
+
+ return runner.getModelSwitchState?.() ?? { current: getModel(), pending: undefined };
|
|
810
|
+
+ },
|
|
811
|
+
get serviceTier() {
|
|
812
|
+
runner.assertActive();
|
|
813
|
+
return getServiceTier();
|
|
814
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/types.d.ts
|
|
815
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/types.d.ts
|
|
816
|
+
@@ -361,9 +361,14 @@
|
|
817
|
+
text?: string;
|
|
818
|
+
}
|
|
819
|
+
export interface CompactionFailure {
|
|
820
|
+
- kind: "invalid_model" | "auth_missing" | "auth_expired" | "quota" | "org_denied" | "model_unavailable" | "refused" | "timeout" | "incomplete" | "empty" | "context_budget" | "stale" | "provider";
|
|
821
|
+
+ kind: "invalid_model" | "auth_missing" | "auth_expired" | "quota" | "account_blocked" | "org_denied" | "model_unavailable" | "refused" | "timeout" | "incomplete" | "empty" | "context_budget" | "stale" | "provider";
|
|
822
|
+
provider?: string;
|
|
823
|
+
model?: string;
|
|
824
|
+
+ error?: string;
|
|
825
|
+
+ inputTokens?: number;
|
|
826
|
+
+ inputTokensSource?: "estimate" | "provider";
|
|
827
|
+
+ requestSent?: boolean;
|
|
828
|
+
+ retryAt?: number;
|
|
829
|
+
}
|
|
830
|
+
export interface EndCompactionOptions {
|
|
831
|
+
failure?: CompactionFailure;
|
|
832
|
+
@@ -519,6 +524,8 @@
|
|
833
|
+
getCompactionSettings(): CompactionPreparation["settings"];
|
|
834
|
+
/** Select a budget-fitting summary model; auto stays within the active provider and explicit failures never fall back. */
|
|
835
|
+
getCompactionModel?(inputTokens?: number): Model<Api> | undefined;
|
|
836
|
+
+ /** Actual current and held model identities, resolved lazily from the session. */
|
|
837
|
+
+ getModelSwitchState?(): { current: Model<Api> | undefined; pending: Model<Api> | undefined };
|
|
838
|
+
/**
|
|
839
|
+
* Longest a tool may block in the foreground before the active model's prompt
|
|
840
|
+
* cache expires, or `undefined` when no cache-derived budget applies. Reads the
|
|
841
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/context-reduction-lifecycle.js
|
|
842
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/context-reduction-lifecycle.js
|
|
843
|
+
@@ -1,3 +1,4 @@
|
|
844
|
+
+import { buildSessionContext, getSessionContextEntryId } from "../../../session-manager.js";
|
|
845
|
+
import { buildCompactionContext } from "./context-pipeline.js";
|
|
846
|
+
import { createReductionHandoff } from "./context-reduction-handoff.js";
|
|
847
|
+
import { CONTEXT_REDUCTION_ENTRY_TYPE, clearReductionAnchor, createContextReductionState, readContextReductionState, recordReductionUsage, snapshotReductionState, } from "./context-reduction-state.js";
|
|
848
|
+
@@ -63,7 +64,7 @@
|
|
849
|
+
pending = undefined;
|
|
850
|
+
persist(before);
|
|
851
|
+
});
|
|
852
|
+
- pi.on("context", (event, ctx) => {
|
|
853
|
+
+ pi.on("context", async (event, ctx) => {
|
|
854
|
+
restore(ctx);
|
|
855
|
+
// The journal may not yet contain the current user/tool message.
|
|
856
|
+
// The dispatcher, not transcript equality, owns request provenance.
|
|
857
|
+
@@ -78,6 +79,7 @@
|
|
858
|
+
const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? 200_000;
|
|
859
|
+
const settings = ctx.getCompactionSettings();
|
|
860
|
+
const policy = options.getPolicy(ctx);
|
|
861
|
+
+ const automatic = settings.enabled !== false && !options.isSummaryUnavailable?.(ctx);
|
|
862
|
+
const overheadTokens = estimateTotalTokens([
|
|
863
|
+
{ role: "user", content: `${ctx.getSystemPrompt()}\n${JSON.stringify(options.getTools())}`, timestamp: 0 },
|
|
864
|
+
]);
|
|
865
|
+
@@ -94,21 +96,22 @@
|
|
866
|
+
usage.tokens >= contextWindow * computeEffectiveThreshold(contextWindow, policy.lastYield);
|
|
867
|
+
if (breakerFallback)
|
|
868
|
+
options.logBreakerFallback(ctx, usage.tokens ?? 0);
|
|
869
|
+
- const messages = buildCompactionContext({
|
|
870
|
+
+ const buildInput = {
|
|
871
|
+
event,
|
|
872
|
+
ctx,
|
|
873
|
+
contextWindow,
|
|
874
|
+
promptContextWindow,
|
|
875
|
+
contextReductionState: working,
|
|
876
|
+
contextOverheadTokens: overheadTokens,
|
|
877
|
+
- reductionCeilingTokens: Math.min(promptContextWindow, contextWindow - reserveTokens, thresholdTokens),
|
|
878
|
+
+ reductionCeilingTokens: Math.min(promptContextWindow, contextWindow - reserveTokens, automatic ? thresholdTokens : Infinity),
|
|
879
|
+
toolAdmissionEnabled: settings.toolAdmissionEnabled !== false,
|
|
880
|
+
breakerFallback,
|
|
881
|
+
laneOwnsCompaction: policy.laneOwnsCompaction,
|
|
882
|
+
appendOnlyTranscript: policy.appendOnlyTranscript,
|
|
883
|
+
emergencyPruneLatch: live ? options.emergencyPruneLatch : { ...options.emergencyPruneLatch },
|
|
884
|
+
logEmergencyPrune: (fields) => options.logEmergencyPrune(ctx, fields),
|
|
885
|
+
- });
|
|
886
|
+
+ };
|
|
887
|
+
+ let messages = buildCompactionContext(buildInput);
|
|
888
|
+
const managed = !policy.laneOwnsCompaction;
|
|
889
|
+
if (!live)
|
|
890
|
+
return { messages };
|
|
891
|
+
@@ -116,6 +119,21 @@
|
|
892
|
+
pending = undefined;
|
|
893
|
+
return { messages };
|
|
894
|
+
}
|
|
895
|
+
+ if (state.compactionRequired && !automatic && options.reduceContext) {
|
|
896
|
+
+ const ids = new Set(ctx.sessionManager.getBranch().map((entry) => entry.id));
|
|
897
|
+
+ const additions = event.messages.filter((message) => !ids.has(getSessionContextEntryId(message)));
|
|
898
|
+
+ const result = await options.reduceContext(ctx, estimateTotalTokens(additions));
|
|
899
|
+
+ if (!result.applied) {
|
|
900
|
+
+ ctx.abort("system");
|
|
901
|
+
+ return { messages };
|
|
902
|
+
+ }
|
|
903
|
+
+ messages = buildCompactionContext({ ...buildInput, contextReductionState: state,
|
|
904
|
+
+ event: { ...event, messages: [...buildSessionContext(ctx.sessionManager.getBranch()).messages, ...additions] } });
|
|
905
|
+
+ if (state.compactionRequired) {
|
|
906
|
+
+ ctx.abort("system");
|
|
907
|
+
+ return { messages };
|
|
908
|
+
+ }
|
|
909
|
+
+ }
|
|
910
|
+
if (state.compactionRequired) {
|
|
911
|
+
pending = undefined;
|
|
912
|
+
persist(before);
|