zigmini 0.9.0-beta.3 → 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 +24 -4
- package/README.md +24 -4
- 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/task-control.ts +26 -16
- 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 +339 -79
- package/src/main.ts +95 -9
- package/src/maintenance.ts +1 -1
- package/src/manual.ts +9 -2
- 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,2635 @@
|
|
|
1
|
+
--- a/@code-yeongyu/senpi/dist/core/agent-session.d.ts
|
|
2
|
+
+++ b/@code-yeongyu/senpi/dist/core/agent-session.d.ts
|
|
3
|
+
@@ -32,7 +32,7 @@ import { type ResumeSlicePlan } from "./extensions/builtin/compaction/resume-sli
|
|
4
|
+
import { type PendingModelSwitch } from "./extensions/builtin/compaction/switch-admission.ts";
|
|
5
|
+
import { type ServiceTier } from "./extensions/builtin/service-tier.ts";
|
|
6
|
+
import { type ContextUsage, type ExecuteToolOptions, type ExtensionCommandContextActions, type ExtensionErrorListener, type ExtensionMode, ExtensionRunner, type ExtensionToolHookLifecycleEvent, type ExtensionUIContext, type InputSource, type ReplacedSessionContext, type SessionStartEvent, type ShutdownHandler, type SystemPromptChangeEvent, type ToolDefinition, type ToolInfo } from "./extensions/index.ts";
|
|
7
|
+
-import type { ApplyCompactionOptions, ApplyCompactionResult, CompactionReason, CompactionRejectionCause, ModelSelectSource } from "./extensions/types.ts";
|
|
8
|
+
+import type { ApplyCompactionOptions, ApplyCompactionResult, CompactionReason, CompactionRejectionCause, CompactionFailure, ModelSelectSource } from "./extensions/types.ts";
|
|
9
|
+
import { type UserActivityKind } from "./extensions/user-activity.ts";
|
|
10
|
+
import { ExternalAdmission } from "./external-admission.ts";
|
|
11
|
+
import { type HoldInputResult } from "./held-inputs.ts";
|
|
12
|
+
@@ -136,6 +136,10 @@ export type AgentSessionEvent = WithParentToolCallId<Exclude<AgentEvent, {
|
|
13
|
+
id: string;
|
|
14
|
+
deadlineMs: number;
|
|
15
|
+
} | SessionHoldEvent | SessionLifecycleEvent | {
|
|
16
|
+
+ type: "queued_input_resume";
|
|
17
|
+
+ clientInputId: string;
|
|
18
|
+
+ disposition: "stale_epoch" | "busy" | "not_head" | "resumed";
|
|
19
|
+
+} | {
|
|
20
|
+
type: "resume_compaction_required";
|
|
21
|
+
projection: ModelUsabilityBudgetProjection;
|
|
22
|
+
notice: string;
|
|
23
|
+
@@ -186,12 +190,14 @@ export type AgentSessionEvent = WithParentToolCallId<Exclude<AgentEvent, {
|
|
24
|
+
type: "pending_input_update";
|
|
25
|
+
} & PendingInputUpdate) | {
|
|
26
|
+
type: "queue_update";
|
|
27
|
+
+ queueEpoch: string;
|
|
28
|
+
steering: readonly string[];
|
|
29
|
+
followUp: readonly string[];
|
|
30
|
+
ordered: readonly QueuedInput[];
|
|
31
|
+
} | {
|
|
32
|
+
type: "compaction_start";
|
|
33
|
+
reason: CompactionReason;
|
|
34
|
+
+ blocking?: boolean;
|
|
35
|
+
requestId?: string;
|
|
36
|
+
} | {
|
|
37
|
+
type: "compaction_progress";
|
|
38
|
+
@@ -280,7 +286,9 @@ export type AgentSessionEvent = WithParentToolCallId<Exclude<AgentEvent, {
|
|
39
|
+
autoCompactionEnabled: boolean;
|
|
40
|
+
} | {
|
|
41
|
+
type: "compaction_end";
|
|
42
|
+
+ failure?: CompactionFailure;
|
|
43
|
+
reason: CompactionReason;
|
|
44
|
+
+ blocking?: boolean;
|
|
45
|
+
result: CompactionResult | undefined;
|
|
46
|
+
aborted: boolean;
|
|
47
|
+
willRetry: boolean;
|
|
48
|
+
@@ -642,6 +650,10 @@ export interface SessionStats {
|
|
49
|
+
export { CURSOR_TOOL_RESULT_MAX_CHARS, truncateToolResultBodies } from "./cursor-history-admission.ts";
|
|
50
|
+
export declare function providerRetryWatchdogAbortMessage(retryTimeoutMs: number | undefined, streamStartTimeoutMs: number | undefined): string;
|
|
51
|
+
export declare class AgentSession {
|
|
52
|
+
+ queueEpoch: string;
|
|
53
|
+
+ resumeQueuedInput(clientInputId: string, queueEpoch: string): {
|
|
54
|
+
+ disposition: "stale_epoch" | "not_queued" | "busy" | "scheduled";
|
|
55
|
+
+ };
|
|
56
|
+
readonly agent: Agent;
|
|
57
|
+
readonly sessionManager: SessionManager;
|
|
58
|
+
readonly settingsManager: SettingsManager;
|
|
59
|
+
@@ -953,9 +965,8 @@ export declare class AgentSession {
|
|
60
|
+
* `compaction.model` override ("provider/model"), that model is used for the
|
|
61
|
+
* summarization call instead of the session model — this is what lets an
|
|
62
|
+
* SDK-owned lane (anthropic-subscription) compact on a cheaper/different model.
|
|
63
|
+
- * Unset, malformed, unknown, spent, or credential-less overrides keep a usable session model;
|
|
64
|
+
- * a set override that cannot serve logs why the session model was used.
|
|
65
|
+
- * A spent account instead uses the held target or another authenticated model.
|
|
66
|
+
+ * Auto selection stays within the active provider; explicit values never fall back.
|
|
67
|
+
+ * Selection uses the availability snapshot. Authentication remains request-time.
|
|
68
|
+
*/
|
|
69
|
+
private _resolveCompactionModel;
|
|
70
|
+
private _logCompactionModelFallback;
|
|
71
|
+
@@ -2447,4 +2458,4 @@ export declare class AgentSession {
|
|
72
|
+
*/
|
|
73
|
+
get extensionRunner(): ExtensionRunner;
|
|
74
|
+
}
|
|
75
|
+
-//# sourceMappingURL=agent-session.d.ts.map
|
|
76
|
+
|
|
77
|
+
+//# sourceMappingURL=agent-session.d.ts.map
|
|
78
|
+
--- a/@code-yeongyu/senpi/dist/core/agent-session.js
|
|
79
|
+
+++ b/@code-yeongyu/senpi/dist/core/agent-session.js
|
|
80
|
+
@@ -43,6 +43,7 @@ import { executeBashWithOperations } from "./bash-executor.js";
|
|
81
|
+
import { envValue } from "./brand.js";
|
|
82
|
+
import { clientMessageIdentity, readClientMessageIdentity, } from "./client-message-identity.js";
|
|
83
|
+
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, resolveThresholdContextTokens, shouldCompact, } from "./compaction/index.js";
|
|
84
|
+
+import { compactionFailure, validateSummaryResponse } from "./extensions/builtin/compaction/speculative.js";
|
|
85
|
+
import { CompactionLifecycleCoordinator } from "./compaction/lifecycle.js";
|
|
86
|
+
import { isTurnStuckOnContextOverflow } from "./compaction/stuck-overflow.js";
|
|
87
|
+
import { isWarmSummaryAnchorValid } from "./compaction/warm-anchor.js";
|
|
88
|
+
@@ -234,12 +235,13 @@ function describeCompactionRejection(cause) {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
class CompactionRejectedError extends Error {
|
|
92
|
+
- constructor(rejectionCause) {
|
|
93
|
+
- super(rejectionCause === "cancelled-by-extension"
|
|
94
|
+
+ constructor(rejectionCause, failure) {
|
|
95
|
+
+ super(failure ? "Compaction failed; previous context retained" : rejectionCause === "cancelled-by-extension"
|
|
96
|
+
? "Compaction cancelled"
|
|
97
|
+
: describeCompactionRejection(rejectionCause));
|
|
98
|
+
this.name = "CompactionRejectedError";
|
|
99
|
+
this.rejectionCause = rejectionCause;
|
|
100
|
+
+ this.failure = failure;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
class CompactionCancelledError extends Error {
|
|
104
|
+
@@ -259,6 +261,7 @@ class CompactionExecutionError extends Error {
|
|
105
|
+
this.name = "CompactionExecutionError";
|
|
106
|
+
this.ownsTerminalTransition = ownsTerminalTransition;
|
|
107
|
+
this.aborted = aborted;
|
|
108
|
+
+ this.failure = aborted ? undefined : compactionFailure(error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function compactionExecutionOwnsTerminalTransition(error) {
|
|
112
|
+
@@ -475,6 +478,7 @@ export class AgentSession {
|
|
113
|
+
this._queuedInputAges = new QueuedInputAges();
|
|
114
|
+
/** Recently delivered `queuedId`s (bounded), so a late dequeue answers `already_delivered`, not `not_found`. */
|
|
115
|
+
this._deliveredQueuedIds = new Set();
|
|
116
|
+
+ this.queueEpoch = randomUUID();
|
|
117
|
+
this._supersededCompactionLogAttemptIds = new Set();
|
|
118
|
+
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
119
|
+
this._pendingNextTurnMessages = [];
|
|
120
|
+
@@ -1025,58 +1029,58 @@ export class AgentSession {
|
|
121
|
+
* `compaction.model` override ("provider/model"), that model is used for the
|
|
122
|
+
* summarization call instead of the session model — this is what lets an
|
|
123
|
+
* SDK-owned lane (anthropic-subscription) compact on a cheaper/different model.
|
|
124
|
+
- * Unset, malformed, unknown, spent, or credential-less overrides keep a usable session model;
|
|
125
|
+
- * a set override that cannot serve logs why the session model was used.
|
|
126
|
+
- * A spent account instead uses the held target or another authenticated model.
|
|
127
|
+
- */
|
|
128
|
+
- _resolveCompactionModel(sessionModel) {
|
|
129
|
+
- const override = this._getCompactionSettings().model;
|
|
130
|
+
- const slash = override?.indexOf("/") ?? -1;
|
|
131
|
+
- const configured = override && slash > 0
|
|
132
|
+
- ? this._modelRuntime.getModel(override.slice(0, slash), override.slice(slash + 1))
|
|
133
|
+
- : undefined;
|
|
134
|
+
- const usable = (model) => !this._retryFallback.isProviderSpent(model.provider);
|
|
135
|
+
- // Preserve the explicit override and original-model geometry on healthy sessions.
|
|
136
|
+
- // A held downswitch uses its target only when the original account cannot serve.
|
|
137
|
+
- if (usable(sessionModel)) {
|
|
138
|
+
- if (!override)
|
|
139
|
+
- return sessionModel;
|
|
140
|
+
- const fallbackReason = !configured
|
|
141
|
+
- ? slash > 0
|
|
142
|
+
- ? "unknown_model"
|
|
143
|
+
- : "malformed"
|
|
144
|
+
- : !usable(configured)
|
|
145
|
+
- ? "provider_spent"
|
|
146
|
+
- : this._modelRuntime.hasFreshAvailabilitySnapshot() &&
|
|
147
|
+
- !this._modelRuntime.hasConfiguredAuth(configured.provider)
|
|
148
|
+
- ? // Only a published availability pass can say "no credentials": before it (or
|
|
149
|
+
- // after a busy store read) the answer is unknown, the override is kept, and
|
|
150
|
+
- // request-time auth decides.
|
|
151
|
+
- "no_credentials"
|
|
152
|
+
- : undefined;
|
|
153
|
+
- if (configured && fallbackReason === undefined) {
|
|
154
|
+
- this._compactionModelFallbackNotice = undefined;
|
|
155
|
+
- return configured;
|
|
156
|
+
- }
|
|
157
|
+
- if (fallbackReason)
|
|
158
|
+
- this._logCompactionModelFallback(override, fallbackReason, sessionModel);
|
|
159
|
+
- return sessionModel;
|
|
160
|
+
- }
|
|
161
|
+
- const pending = this._pendingModelSwitch?.model;
|
|
162
|
+
- if (pending && usable(pending))
|
|
163
|
+
- return pending;
|
|
164
|
+
- if (configured && usable(configured))
|
|
165
|
+
+ * Auto selection stays within the active provider; explicit values never fall back.
|
|
166
|
+
+ * Selection uses the availability snapshot. Authentication remains request-time.
|
|
167
|
+
+ */
|
|
168
|
+
+ _resolveCompactionModel(sessionModel, inputTokens = 0) {
|
|
169
|
+
+ const settings = this._getCompactionSettings();
|
|
170
|
+
+ const override = settings.model;
|
|
171
|
+
+ const explicit = override !== undefined && override !== "auto";
|
|
172
|
+
+ const fail = (kind, model = sessionModel) => {
|
|
173
|
+
+ throw Object.assign(new Error(
|
|
174
|
+
+ explicit ? "Compaction model unavailable; check compaction.model or set it to auto."
|
|
175
|
+
+ : "No usable compaction model within the active provider."),
|
|
176
|
+
+ { failure: { kind, provider: model.provider, model: model.id } });
|
|
177
|
+
+ };
|
|
178
|
+
+ const chat = this._modelRuntime.getModels();
|
|
179
|
+
+ const available = this._modelRuntime.getAvailableSnapshot();
|
|
180
|
+
+ const same = (left, right) => left.provider === right.provider && left.id === right.id;
|
|
181
|
+
+ const usable = (model) => chat.some((entry) => same(entry, model)) && (!available.some((entry) => entry.provider === model.provider) || available.some((entry) => same(entry, model))) &&
|
|
182
|
+
+ !this._retryFallback.isProviderSpent(model.provider) &&
|
|
183
|
+
+ this._modelRuntime.isFallbackEligible(model.provider);
|
|
184
|
+
+ const fits = (model) => {
|
|
185
|
+
+ const output = Math.min(32768, model.maxTokens > 0 ? model.maxTokens : 32768, Math.floor(model.contextWindow / 2));
|
|
186
|
+
+ return model.contextWindow > 0 && inputTokens + Math.max(settings.reserveTokens, output) <= model.contextWindow;
|
|
187
|
+
+ };
|
|
188
|
+
+ if (explicit) {
|
|
189
|
+
+ if (typeof override !== "string" || !/^[^/\s]+\/\S+$/.test(override))
|
|
190
|
+
+ return fail("invalid_model");
|
|
191
|
+
+ const slash = override.indexOf("/");
|
|
192
|
+
+ const configured = this._modelRuntime.getModel(override.slice(0, slash), override.slice(slash + 1));
|
|
193
|
+
+ if (!configured || !chat.some((model) => same(model, configured)))
|
|
194
|
+
+ return fail("invalid_model");
|
|
195
|
+
+ if (this._retryFallback.isProviderSpent(configured.provider))
|
|
196
|
+
+ return fail("quota", configured);
|
|
197
|
+
+ if (!usable(configured))
|
|
198
|
+
+ return fail("model_unavailable", configured);
|
|
199
|
+
+ if (!fits(configured))
|
|
200
|
+
+ return fail("context_budget", configured);
|
|
201
|
+
return configured;
|
|
202
|
+
- const requiredContextWindow = estimateMessagesTokens(filterContextExcludedMessages(this._runtimeMessages())) +
|
|
203
|
+
- this._getCompactionSettings().reserveTokens;
|
|
204
|
+
- const available = this._modelRuntime
|
|
205
|
+
- .getModels()
|
|
206
|
+
- .find((model) => usable(model) &&
|
|
207
|
+
- model.contextWindow >= requiredContextWindow &&
|
|
208
|
+
- this._modelRuntime.hasConfiguredAuth(model.provider));
|
|
209
|
+
- if (available)
|
|
210
|
+
- return available;
|
|
211
|
+
- throw new AccountUsageLimitError(sessionModel.provider, this._retryFallback.accountError(sessionModel.provider));
|
|
212
|
+
+ }
|
|
213
|
+
+ if (this._retryFallback.isProviderSpent(sessionModel.provider))
|
|
214
|
+
+ return fail("quota");
|
|
215
|
+
+ const family = ["anthropic", "anthropic-subscription"].includes(sessionModel.provider) && /^claude-/.test(sessionModel.id) ? /^claude-sonnet-/
|
|
216
|
+
+ : ["openai", "chatgpt-subscription"].includes(sessionModel.provider) && /^gpt-/.test(sessionModel.id) ? /^gpt-.*-luna(?:-fast)?$/ : undefined;
|
|
217
|
+
+ const preferred = family ? available.filter((model) =>
|
|
218
|
+
+ model.provider === sessionModel.provider && family.test(model.id)) : [];
|
|
219
|
+
+ // Stable sort preserves registry order within the regular and fast Luna groups.
|
|
220
|
+
+ preferred.sort((left, right) => Number(/-luna-fast$/.test(left.id)) - Number(/-luna-fast$/.test(right.id)));
|
|
221
|
+
+ const candidates = [...(family?.test(sessionModel.id) ? [sessionModel] : []), ...preferred, sessionModel];
|
|
222
|
+
+ for (const candidate of candidates) {
|
|
223
|
+
+ if (candidate.provider === sessionModel.provider && usable(candidate) && fits(candidate))
|
|
224
|
+
+ return candidate;
|
|
225
|
+
+ }
|
|
226
|
+
+ return fail(usable(sessionModel) ? "context_budget" : "model_unavailable");
|
|
227
|
+
}
|
|
228
|
+
_logCompactionModelFallback(override, reason, sessionModel) {
|
|
229
|
+
const notice = `${override}\u0000${reason}`;
|
|
230
|
+
@@ -1090,11 +1094,10 @@ export class AgentSession {
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
async _getCompactionRequestAuth(model, signal, callbacks) {
|
|
234
|
+
- const auth = await this._getSummarizationRequestAuth(model, signal, callbacks);
|
|
235
|
+
- return {
|
|
236
|
+
- ...auth,
|
|
237
|
+
- extraBody: this._modelRuntime.getCompatibilityRequestConfig(model).extraBody,
|
|
238
|
+
- };
|
|
239
|
+
+ const prepared = await retryTransientCall(() => this._modelRuntime.prepareRequest(model, { signal }),
|
|
240
|
+
+ isOAuthRefreshUnavailableError, this.settingsManager.getRetrySettings(), signal,
|
|
241
|
+
+ callbacks ?? this._summarizationRetryCallbacks({ source: "compaction" }));
|
|
242
|
+
+ return { ...prepared.options, model: prepared.model, thinkingLevel: this.thinkingLevel };
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The model whose limits apply to `message`, or undefined when the message came from another
|
|
246
|
+
@@ -1893,6 +1896,7 @@ export class AgentSession {
|
|
247
|
+
steering: [...this._steeringMessages],
|
|
248
|
+
followUp: [...this._followUpMessages],
|
|
249
|
+
ordered: [...this._queuedInputOrder].sort((a, b) => a.enqueueOrder - b.enqueueOrder),
|
|
250
|
+
+ queueEpoch: this.queueEpoch,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
_incrementMessageRevision() {
|
|
254
|
+
@@ -6463,6 +6467,16 @@ export class AgentSession {
|
|
255
|
+
this._nextQueuedId += 1;
|
|
256
|
+
return `q_${this._nextQueuedId}`;
|
|
257
|
+
}
|
|
258
|
+
+ resumeQueuedInput(clientInputId, queueEpoch) {
|
|
259
|
+
+ if (queueEpoch !== this.queueEpoch)
|
|
260
|
+
+ return { disposition: "stale_epoch" };
|
|
261
|
+
+ if (!this._queuedInputOrder.some((input) => input.clientInputId === clientInputId))
|
|
262
|
+
+ return { disposition: "not_queued" };
|
|
263
|
+
+ if (this.isStreaming || this.isCompacting)
|
|
264
|
+
+ return { disposition: "busy" };
|
|
265
|
+
+ this._scheduleContinuationAfterCurrentEvent({ retainedInput: { clientInputId, queueEpoch } });
|
|
266
|
+
+ return { disposition: "scheduled" };
|
|
267
|
+
+ }
|
|
268
|
+
/** A queued input reached a turn: forget its record and remember its id as delivered. */
|
|
269
|
+
_removeQueuedInput(text, mode, clientMessageId) {
|
|
270
|
+
const index = this._queuedInputOrder.findIndex((message) => clientMessageId === undefined
|
|
271
|
+
@@ -6601,6 +6615,7 @@ export class AgentSession {
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
this._queueClearRecorded = recorded;
|
|
275
|
+
+ this.queueEpoch = randomUUID();
|
|
276
|
+
this._postCompactionDeferredSteeringMessages = [];
|
|
277
|
+
this._postCompactionDeferredFollowUpMessages = [];
|
|
278
|
+
const clearedDeliveries = this._queuedDeliveryTickets();
|
|
279
|
+
@@ -7798,9 +7813,22 @@ export class AgentSession {
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
+ const checkedStream = async (model, context, options) => {
|
|
284
|
+
+ const stream = await this.agent.streamFunction(model, context, {
|
|
285
|
+
+ ...options, refusalFallbacks: undefined, streamKind: "auxiliary",
|
|
286
|
+
+ });
|
|
287
|
+
+ return {
|
|
288
|
+
+ [Symbol.asyncIterator]: () => stream[Symbol.asyncIterator](),
|
|
289
|
+
+ result: async () => {
|
|
290
|
+
+ const response = await stream.result();
|
|
291
|
+
+ validateSummaryResponse(response);
|
|
292
|
+
+ return response;
|
|
293
|
+
+ },
|
|
294
|
+
+ };
|
|
295
|
+
+ };
|
|
296
|
+
return compact(preparation, requestModel, apiKey, headers, customInstructions, signal, extraBody,
|
|
297
|
+
// The routed thinking level under a virtual selection, the session's otherwise.
|
|
298
|
+
- thinkingLevel, this.agent.streamFunction, env, this.agent.transformContext, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }), this.sessionManager.getSessionId(), cacheFriendly);
|
|
299
|
+
+ thinkingLevel, checkedStream, env, this.agent.transformContext, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }), this.sessionManager.getSessionId(), cacheFriendly);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Manually compact the session context.
|
|
303
|
+
@@ -7872,7 +7900,7 @@ export class AgentSession {
|
|
304
|
+
willRetry: false,
|
|
305
|
+
});
|
|
306
|
+
if (!execution.accepted) {
|
|
307
|
+
- throw new CompactionRejectedError(execution.rejectionCause);
|
|
308
|
+
+ throw new CompactionRejectedError(execution.rejectionCause, execution.failure);
|
|
309
|
+
}
|
|
310
|
+
outcome = "completed";
|
|
311
|
+
this._clearResumeCompactionRequirementIfFits();
|
|
312
|
+
@@ -7881,7 +7909,7 @@ export class AgentSession {
|
|
313
|
+
catch (error) {
|
|
314
|
+
outcome = isCompactionExecutionAborted(error) ? "aborted" : "failed";
|
|
315
|
+
if (error instanceof CompactionRejectedError) {
|
|
316
|
+
- throw new Error(error.message);
|
|
317
|
+
+ throw Object.assign(new Error(error.message), { failure: error.failure });
|
|
318
|
+
}
|
|
319
|
+
if (!compactionExecutionOwnsTerminalTransition(error)) {
|
|
320
|
+
throw error;
|
|
321
|
+
@@ -7939,6 +7967,7 @@ export class AgentSession {
|
|
322
|
+
this._emit({
|
|
323
|
+
type: "compaction_start",
|
|
324
|
+
reason: options.reason,
|
|
325
|
+
+ blocking: options.blocking,
|
|
326
|
+
requestId,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
@@ -7951,6 +7980,7 @@ export class AgentSession {
|
|
330
|
+
controller,
|
|
331
|
+
owner: "compaction",
|
|
332
|
+
reason: options.reason,
|
|
333
|
+
+ blocking: options.blocking,
|
|
334
|
+
requestId,
|
|
335
|
+
willRetry: false,
|
|
336
|
+
precomputed,
|
|
337
|
+
@@ -7971,6 +8001,7 @@ export class AgentSession {
|
|
338
|
+
this._emit({
|
|
339
|
+
type: "compaction_end",
|
|
340
|
+
reason: options.reason,
|
|
341
|
+
+ blocking: options.blocking,
|
|
342
|
+
result: undefined,
|
|
343
|
+
aborted,
|
|
344
|
+
willRetry: false,
|
|
345
|
+
@@ -8036,11 +8067,12 @@ export class AgentSession {
|
|
346
|
+
aborted,
|
|
347
|
+
willRetry: false,
|
|
348
|
+
requestId: operation.operationId,
|
|
349
|
+
+ accepted: false,
|
|
350
|
+
+ failure: aborted ? undefined : options.failure ?? { kind: "provider" },
|
|
351
|
+
errorMessage: aborted ? undefined : (options.errorMessage ?? "Compaction did not apply"),
|
|
352
|
+
});
|
|
353
|
+
this._releaseCompactionController(options.signal);
|
|
354
|
+
- if (!aborted)
|
|
355
|
+
- this._resumeQueuedMessagesAfterCompaction();
|
|
356
|
+
+
|
|
357
|
+
}
|
|
358
|
+
async _executeCompaction(request) {
|
|
359
|
+
const model = this.model;
|
|
360
|
+
@@ -8084,7 +8116,6 @@ export class AgentSession {
|
|
361
|
+
}
|
|
362
|
+
throw new Error("Nothing to compact (session too small)");
|
|
363
|
+
}
|
|
364
|
+
- const compactionModel = this._resolveCompactionModel(model);
|
|
365
|
+
if (this._extensionRunner.hasHandlers("session_before_compact")) {
|
|
366
|
+
const messagesBeforeExtension = new Set(this.agent.state.messages);
|
|
367
|
+
const extensionResult = (await this._extensionRunner.emit({
|
|
368
|
+
@@ -8107,7 +8138,7 @@ export class AgentSession {
|
|
369
|
+
throw new CompactionCancelledError();
|
|
370
|
+
}
|
|
371
|
+
if (extensionResult?.cancel) {
|
|
372
|
+
- return await this._rejectCompaction(request, requestId, operationId, extensionResult.rejectionCause ?? "cancelled-by-extension", true, extensionResult.reason);
|
|
373
|
+
+ return await this._rejectCompaction(request, requestId, operationId, extensionResult.rejectionCause ?? "cancelled-by-extension", signal.aborted, extensionResult.reason, extensionResult.failure);
|
|
374
|
+
}
|
|
375
|
+
if (extensionResult?.compaction) {
|
|
376
|
+
compactionResult = extensionResult.compaction;
|
|
377
|
+
@@ -8115,13 +8146,17 @@ export class AgentSession {
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (!compactionResult) {
|
|
381
|
+
- // A configured compaction.model override redirects only the
|
|
382
|
+
- // summarization call to that model; every other part of the session
|
|
383
|
+
- // (lifecycle record, preparation, branch) still tracks the session
|
|
384
|
+
- // model. Falls back to the session model when the override is unset
|
|
385
|
+
- // or cannot be resolved.
|
|
386
|
+
+ // Size the complete source, including control text, before selecting a model.
|
|
387
|
+
+ // The final serialized request is checked again at the dispatch boundary.
|
|
388
|
+
+ const inputTokens = estimateMessagesTokens([...preparation.messagesToSummarize, ...preparation.turnPrefixMessages]) +
|
|
389
|
+
+ Math.ceil(JSON.stringify([this.agent.state.systemPrompt, this.agent.state.tools,
|
|
390
|
+
+ preparation.previousSummary, request.customInstructions]).length / 4) + 8192;
|
|
391
|
+
+ const compactionModel = this._resolveCompactionModel(model, inputTokens);
|
|
392
|
+
const { model: requestModel, apiKey, headers, extraBody, env, thinkingLevel, } = await this._getCompactionRequestAuth(compactionModel, signal, this._summarizationRetryCallbacks({ source: "compaction", reason: request.reason }));
|
|
393
|
+
compactionResult = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, extraBody, request.customInstructions, signal, env, request.reason, thinkingLevel);
|
|
394
|
+
+ compactionResult.details = { ...compactionResult.details,
|
|
395
|
+
+ selectionMode: settings.model === undefined || settings.model === "auto" ? "auto" : "explicit",
|
|
396
|
+
+ requestedProvider: compactionModel.provider, requestedModel: compactionModel.id };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (signal.aborted) {
|
|
400
|
+
@@ -8148,24 +8183,20 @@ export class AgentSession {
|
|
401
|
+
if (sourceChanged) {
|
|
402
|
+
return await this._rejectCompaction(request, requestId, operationId, "stale-revision", false);
|
|
403
|
+
}
|
|
404
|
+
- if (await this._wouldCompactionOverflow(pathEntries, compactionResult, fromExtension, model)) {
|
|
405
|
+
+ if (typeof compactionResult.summary !== "string" || !compactionResult.summary.trim())
|
|
406
|
+
+ return await this._rejectCompaction(request, requestId, operationId, "cancelled-by-extension", false,
|
|
407
|
+
+ "Compaction summary body is empty", { kind: "empty" });
|
|
408
|
+
+ const fitModel = this._limitsModel() ?? model;
|
|
409
|
+
+ if (await this._wouldCompactionOverflow(pathEntries, compactionResult, fromExtension, fitModel)) {
|
|
410
|
+
return await this._rejectCompaction(request, requestId, operationId, "would-overflow", false);
|
|
411
|
+
}
|
|
412
|
+
- const latestConfigurationEffort = this.sessionManager
|
|
413
|
+
- .getBranch()
|
|
414
|
+
- .findLast((entry) => entry.type === "configuration_update")?.reasoning.effort;
|
|
415
|
+
- const compactionEntryId = this.sessionManager.appendCompaction(compactionResult.summary, compactionResult.firstKeptEntryId, compactionResult.tokensBefore, compactionResult.details, fromExtension, compactionResult.usage);
|
|
416
|
+
- const savedEntry = this.sessionManager.getEntry(compactionEntryId);
|
|
417
|
+
- if (savedEntry?.type !== "compaction") {
|
|
418
|
+
- throw new Error("Compaction entry was not saved");
|
|
419
|
+
- }
|
|
420
|
+
- const modelAfterCompaction = this.model;
|
|
421
|
+
- if (latestConfigurationEffort !== undefined &&
|
|
422
|
+
- modelAfterCompaction !== undefined &&
|
|
423
|
+
- supportsConfigurationUpdate(modelAfterCompaction)) {
|
|
424
|
+
- this.sessionManager.appendConfigurationUpdate(latestConfigurationEffort);
|
|
425
|
+
- }
|
|
426
|
+
- const sessionContext = this.sessionManager.buildSessionContext();
|
|
427
|
+
+ if (signal.aborted)
|
|
428
|
+
+ throw new CompactionCancelledError();
|
|
429
|
+
+ if (this._limitsModel() !== fitModel || this._messageRevision !== lifecycleState.startedRevision + compactionOwnedDiagnosticMessages.size ||
|
|
430
|
+
+ !agentMessagesAtStart.every((message, index) => this.agent.state.messages[index] === message) ||
|
|
431
|
+
+ !this.agent.state.messages.slice(agentMessagesAtStart.length).every((message) =>
|
|
432
|
+
+ this._messageEndsAwaitingPersistence.has(message) || compactionOwnedDiagnosticMessages.has(message)))
|
|
433
|
+
+ return await this._rejectCompaction(request, requestId, operationId, "stale-revision", false);
|
|
434
|
+
const currentAgentMessages = this.agent.state.messages;
|
|
435
|
+
const hasUnchangedPrefix = agentMessagesAtStart.every((message, index) => currentAgentMessages[index] === message);
|
|
436
|
+
const messagesAppendedDuringCompaction = hasUnchangedPrefix
|
|
437
|
+
@@ -8182,6 +8213,24 @@ export class AgentSession {
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const preservedPendingMessages = currentAgentMessages.filter((message) => preservedIdentities.delete(message));
|
|
441
|
+
+ if (preservedPendingMessages.length > 0 &&
|
|
442
|
+
+ wouldCompactionOverflow(pathEntries, compactionResult, fromExtension, fitModel, this._getCompactionSettings(), preservedPendingMessages))
|
|
443
|
+
+ return await this._rejectCompaction(request, requestId, operationId, "would-overflow", false);
|
|
444
|
+
+ const latestConfigurationEffort = this.sessionManager
|
|
445
|
+
+ .getBranch()
|
|
446
|
+
+ .findLast((entry) => entry.type === "configuration_update")?.reasoning.effort;
|
|
447
|
+
+ const compactionEntryId = this.sessionManager.appendCompaction(compactionResult.summary, compactionResult.firstKeptEntryId, compactionResult.tokensBefore, compactionResult.details, fromExtension, compactionResult.usage);
|
|
448
|
+
+ const savedEntry = this.sessionManager.getEntry(compactionEntryId);
|
|
449
|
+
+ if (savedEntry?.type !== "compaction") {
|
|
450
|
+
+ throw new Error("Compaction entry was not saved");
|
|
451
|
+
+ }
|
|
452
|
+
+ const modelAfterCompaction = this.model;
|
|
453
|
+
+ if (latestConfigurationEffort !== undefined &&
|
|
454
|
+
+ modelAfterCompaction !== undefined &&
|
|
455
|
+
+ supportsConfigurationUpdate(modelAfterCompaction)) {
|
|
456
|
+
+ this.sessionManager.appendConfigurationUpdate(latestConfigurationEffort);
|
|
457
|
+
+ }
|
|
458
|
+
+ const sessionContext = this.sessionManager.buildSessionContext();
|
|
459
|
+
this._skipNextPostCompactionAssistantCheck = true;
|
|
460
|
+
for (const message of preservedPendingMessages) {
|
|
461
|
+
if (message.role === "assistant") {
|
|
462
|
+
@@ -8189,7 +8238,7 @@ export class AgentSession {
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
this.agent.state.messages = [...sessionContext.messages, ...preservedPendingMessages];
|
|
466
|
+
- compactionResult.estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
|
|
467
|
+
+ compactionResult.estimatedTokensAfter = estimateMessagesTokens(this.agent.state.messages);
|
|
468
|
+
this._incrementMessageRevision();
|
|
469
|
+
if (!this._compactionLifecycle.finish({
|
|
470
|
+
operationId,
|
|
471
|
+
@@ -8205,6 +8254,7 @@ export class AgentSession {
|
|
472
|
+
this._emit({
|
|
473
|
+
type: "compaction_end",
|
|
474
|
+
reason: request.reason,
|
|
475
|
+
+ blocking: request.blocking,
|
|
476
|
+
result: compactionResult,
|
|
477
|
+
aborted: false,
|
|
478
|
+
willRetry: request.willRetry,
|
|
479
|
+
@@ -8238,15 +8288,10 @@ export class AgentSession {
|
|
480
|
+
}
|
|
481
|
+
const lifecycleState = this._compactionLifecycle.state;
|
|
482
|
+
const ownsTerminalTransition = lifecycleState.status !== "idle" && lifecycleState.operationId === operationId;
|
|
483
|
+
- const aborted = signal.aborted || isCompactionExecutionAborted(error);
|
|
484
|
+
- if (ownsTerminalTransition && lifecycleState.status === "running") {
|
|
485
|
+
- this._compactionLifecycle.finish({
|
|
486
|
+
- operationId,
|
|
487
|
+
- status: aborted ? "aborted" : "failed",
|
|
488
|
+
- endedRevision: this._messageRevision,
|
|
489
|
+
- errorMessage: error instanceof Error ? error.message : String(error),
|
|
490
|
+
- });
|
|
491
|
+
- }
|
|
492
|
+
+ const aborted = signal.aborted;
|
|
493
|
+
+ if (ownsTerminalTransition && lifecycleState.status === "running")
|
|
494
|
+
+ return await this._rejectCompaction(request, requestId, operationId, "cancelled-by-extension", aborted,
|
|
495
|
+
+ aborted ? undefined : "Compaction failed; previous context retained", aborted ? undefined : compactionFailure(error, model));
|
|
496
|
+
throw new CompactionExecutionError(error, ownsTerminalTransition, aborted);
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
@@ -8294,7 +8339,8 @@ export class AgentSession {
|
|
500
|
+
}
|
|
501
|
+
this.agent.state.messages = [...sessionMessages, ...pendingMessages];
|
|
502
|
+
}
|
|
503
|
+
- async _rejectCompaction(request, requestId, operationId, rejectionCause, aborted, extensionReason) {
|
|
504
|
+
+ async _rejectCompaction(request, requestId, operationId, rejectionCause, aborted, extensionReason, failure) {
|
|
505
|
+
+ failure = aborted || rejectionCause === "external-owner" ? undefined : failure ?? { kind: rejectionCause === "stale-revision" ? "stale" : rejectionCause === "would-overflow" ? "context_budget" : "provider" };
|
|
506
|
+
// Per plan Section 1: rejection must never be silent. The compaction_end event
|
|
507
|
+
// carries a non-empty human-readable errorMessage (unless the user aborted, where
|
|
508
|
+
// the aborted branch already renders "Compaction cancelled"). session_compact is
|
|
509
|
+
@@ -8311,7 +8357,7 @@ export class AgentSession {
|
|
510
|
+
const errorMessage = aborted && !trimmedExtensionReason ? undefined : detailedMessage;
|
|
511
|
+
if (!this._compactionLifecycle.finish({
|
|
512
|
+
operationId,
|
|
513
|
+
- status: "failed",
|
|
514
|
+
+ status: aborted ? "aborted" : "failed",
|
|
515
|
+
endedRevision: this._messageRevision,
|
|
516
|
+
rejectionCause,
|
|
517
|
+
...(trimmedExtensionReason !== undefined ? { errorMessage: trimmedExtensionReason } : {}),
|
|
518
|
+
@@ -8321,12 +8367,14 @@ export class AgentSession {
|
|
519
|
+
this._emit({
|
|
520
|
+
type: "compaction_end",
|
|
521
|
+
reason: request.reason,
|
|
522
|
+
+ blocking: request.blocking,
|
|
523
|
+
result: undefined,
|
|
524
|
+
aborted,
|
|
525
|
+
willRetry: false,
|
|
526
|
+
requestId,
|
|
527
|
+
accepted: false,
|
|
528
|
+
rejectionCause,
|
|
529
|
+
+ failure,
|
|
530
|
+
errorMessage,
|
|
531
|
+
});
|
|
532
|
+
await this._extensionRunner.emit({
|
|
533
|
+
@@ -8335,10 +8383,11 @@ export class AgentSession {
|
|
534
|
+
requestId,
|
|
535
|
+
accepted: false,
|
|
536
|
+
rejectionCause,
|
|
537
|
+
+ failure,
|
|
538
|
+
fromExtension: false,
|
|
539
|
+
willRetry: false,
|
|
540
|
+
});
|
|
541
|
+
- return { accepted: false, requestId, rejectionCause };
|
|
542
|
+
+ return { accepted: false, requestId, rejectionCause, aborted, failure };
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Cancel in-progress compaction (manual or auto).
|
|
546
|
+
@@ -8923,6 +8972,23 @@ export class AgentSession {
|
|
547
|
+
this._transcriptWriteFailures.startRun();
|
|
548
|
+
await runBoundedRetryContinuation({
|
|
549
|
+
continueRun: async () => {
|
|
550
|
+
+ if (options.retainedInput) {
|
|
551
|
+
+ const { retainedInput, ...continuationOptions } = options;
|
|
552
|
+
+ const disposition = retainedInput.queueEpoch !== this.queueEpoch
|
|
553
|
+
+ ? "stale_epoch"
|
|
554
|
+
+ : await this.agent.continueQueuedInput(retainedInput.clientInputId, {
|
|
555
|
+
+ ...continuationOptions,
|
|
556
|
+
+ onStart: () => {
|
|
557
|
+
+ this._isAgentRunActive = true;
|
|
558
|
+
+ },
|
|
559
|
+
+ });
|
|
560
|
+
+ this._emit({
|
|
561
|
+
+ type: "queued_input_resume",
|
|
562
|
+
+ clientInputId: retainedInput.clientInputId,
|
|
563
|
+
+ disposition,
|
|
564
|
+
+ });
|
|
565
|
+
+ return;
|
|
566
|
+
+ }
|
|
567
|
+
if (this._continueWithQueuedInput) {
|
|
568
|
+
// Joined input of a start that never ran: the transcript tail may be anything, even empty.
|
|
569
|
+
this._continueWithQueuedInput = false;
|
|
570
|
+
@@ -9180,6 +9246,8 @@ export class AgentSession {
|
|
571
|
+
willRetry: false,
|
|
572
|
+
requestId,
|
|
573
|
+
errorMessage: formattedErrorMessage,
|
|
574
|
+
+ accepted: false,
|
|
575
|
+
+ failure: aborted ? undefined : compactionFailure(error, model),
|
|
576
|
+
});
|
|
577
|
+
await this._emitSessionCompactFailed({
|
|
578
|
+
reason,
|
|
579
|
+
@@ -9530,9 +9598,10 @@ export class AgentSession {
|
|
580
|
+
},
|
|
581
|
+
getContextUsage: () => this.getContextUsage(),
|
|
582
|
+
getCompactionSettings: () => this._getCompactionSettings(),
|
|
583
|
+
- // One resolver for core and builtin compaction: a configured `compaction.model`
|
|
584
|
+
- // summarizes on a healthy provider too, not only after the account is spent (#830).
|
|
585
|
+
- getCompactionModel: () => (this.model ? this._resolveCompactionModel(this.model) : undefined),
|
|
586
|
+
+ getCompactionModel: (inputTokens) => {
|
|
587
|
+
+ const model = this._limitsModel();
|
|
588
|
+
+ return model && this._resolveCompactionModel(model, inputTokens);
|
|
589
|
+
+ },
|
|
590
|
+
getPromptCacheSafeWaitSeconds: () => this.resolvePromptCacheSafeWaitSeconds(),
|
|
591
|
+
getPromptCacheGoalBackstopMaxSeconds: () => this.settingsManager.getPromptCacheGoalBackstopMaxSeconds(),
|
|
592
|
+
getPromptCacheKeepAliveSettings: () => this.settingsManager.getPromptCacheKeepAliveSettings(),
|
|
593
|
+
@@ -11682,4 +11751,4 @@ function messageFingerprint(message) {
|
|
594
|
+
const requestContext = usage?.contextUsage;
|
|
595
|
+
return `${size}:${usage?.input ?? ""}:${usage?.output ?? ""}:${usage?.totalTokens ?? ""}:${requestContext?.input}:${requestContext?.output}:${requestContext === null}`;
|
|
596
|
+
}
|
|
597
|
+
-//# sourceMappingURL=agent-session.js.map
|
|
598
|
+
|
|
599
|
+
+//# sourceMappingURL=agent-session.js.map
|
|
600
|
+
--- a/@code-yeongyu/senpi/dist/core/compaction/compaction.js
|
|
601
|
+
+++ b/@code-yeongyu/senpi/dist/core/compaction/compaction.js
|
|
602
|
+
@@ -637,8 +637,13 @@ export async function completeSummarization(model, context, options, streamFn, r
|
|
603
|
+
onPayload: withoutServerSearch(options.onPayload, options.cacheRetention),
|
|
604
|
+
cacheRetention: options.cacheRetention ?? "none",
|
|
605
|
+
affinitySessionId: options.affinitySessionId ?? options.sessionId,
|
|
606
|
+
+ streamKind: "auxiliary",
|
|
607
|
+
sessionId: uuidv7(),
|
|
608
|
+
};
|
|
609
|
+
+ if (model.contextWindow <= 0 || estimateProviderContextTokens(context).tokens + options.maxTokens > model.contextWindow) {
|
|
610
|
+
+ throw Object.assign(new Error("Compaction request exceeds the selected model context budget"),
|
|
611
|
+
+ { failure: { kind: "context_budget", provider: model.provider, model: model.id } });
|
|
612
|
+
+ }
|
|
613
|
+
const callerSignal = options.signal;
|
|
614
|
+
// The 120s floor was tuned for healthy summaries; a large session feeds the
|
|
615
|
+
// summarizer hundreds of thousands of tokens, which legitimately takes longer
|
|
616
|
+
@@ -980,4 +985,4 @@ export function prepareCompaction(pathEntries, settings, forceProgress = false,
|
|
617
|
+
// Main compaction function
|
|
618
|
+
// ============================================================================
|
|
619
|
+
export { compact } from "./compaction-execution.js";
|
|
620
|
+
-//# sourceMappingURL=compaction.js.map
|
|
621
|
+
|
|
622
|
+
+//# sourceMappingURL=compaction.js.map
|
|
623
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/context-pipeline.js
|
|
624
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/context-pipeline.js
|
|
625
|
+
@@ -1,64 +1,21 @@
|
|
626
|
+
import { convertToLlm } from "../../../messages.js";
|
|
627
|
+
-import { BUILTIN_CONTEXT_REDUCTION_OPTIONS, reduceContextMessages } from "./context-reduction.js";
|
|
628
|
+
-import { reduceContextWithFrontier } from "./context-reduction-frontier.js";
|
|
629
|
+
import { createContextReductionState } from "./context-reduction-state.js";
|
|
630
|
+
import { markOpenAiRemoteReplayBoundary } from "./openai-remote.js";
|
|
631
|
+
-import { isOpenAiRemoteCompactionModel } from "./openai-remote-model.js";
|
|
632
|
+
-import { admitContextToolResults, injectTokenBudgetReminder } from "./orchestration.js";
|
|
633
|
+
+import { injectTokenBudgetReminder } from "./orchestration.js";
|
|
634
|
+
import { estimateTotalTokens } from "./overflow-retry.js";
|
|
635
|
+
import { repairOrphanedToolResults } from "./repair-tool-pairs.js";
|
|
636
|
+
-import { hardLimitEmergencyPrune } from "./speculative.js";
|
|
637
|
+
export { createContextReductionState, readContextReductionState } from "./context-reduction-state.js";
|
|
638
|
+
export function buildCompactionContext(input) {
|
|
639
|
+
if (input.laneOwnsCompaction) {
|
|
640
|
+
return repairOrphanedToolResults(convertToLlm(input.event.messages));
|
|
641
|
+
}
|
|
642
|
+
- const admittedMessages = admitContextToolResults(input.event.messages, input.contextWindow, input.toolAdmissionEnabled);
|
|
643
|
+
const state = input.contextReductionState ?? createContextReductionState();
|
|
644
|
+
- const native = isOpenAiRemoteCompactionModel(input.ctx.model);
|
|
645
|
+
- const managed = input.appendOnlyTranscript !== true && !native;
|
|
646
|
+
- const sourceMessages = managed
|
|
647
|
+
- ? reduceContextWithFrontier(admittedMessages, state, {
|
|
648
|
+
- unreducedMessages: input.event.messages,
|
|
649
|
+
- contextWindow: input.contextWindow,
|
|
650
|
+
- ceilingTokens: input.reductionCeilingTokens ?? input.promptContextWindow,
|
|
651
|
+
- overheadTokens: input.contextOverheadTokens ?? 0,
|
|
652
|
+
- blockBudgetRatio: input.contextReductionBlockBudgetRatio,
|
|
653
|
+
- force: input.breakerFallback,
|
|
654
|
+
- })
|
|
655
|
+
- : native && input.breakerFallback && input.appendOnlyTranscript !== true
|
|
656
|
+
- ? reduceContextMessages(admittedMessages, BUILTIN_CONTEXT_REDUCTION_OPTIONS).messages
|
|
657
|
+
- : admittedMessages;
|
|
658
|
+
- const emergency = input.laneOwnsCompaction
|
|
659
|
+
- ? { messages: sourceMessages, needsAggressiveCompaction: false }
|
|
660
|
+
- : hardLimitEmergencyPrune(sourceMessages, managed && state.engaged
|
|
661
|
+
- ? Math.max(0, input.promptContextWindow - (input.contextOverheadTokens ?? 0))
|
|
662
|
+
- : input.promptContextWindow, input.emergencyPruneLatch);
|
|
663
|
+
- // This is the ONE site that actually prunes, so the emergency_prune counter must be
|
|
664
|
+
- // emitted here. Engagement shows up either as a rewritten message array or as
|
|
665
|
+
- // needsAggressiveCompaction when nothing prunable remained.
|
|
666
|
+
- if (!input.laneOwnsCompaction && (emergency.needsAggressiveCompaction || emergency.messages !== sourceMessages)) {
|
|
667
|
+
- input.logEmergencyPrune?.({
|
|
668
|
+
- route: "context-event",
|
|
669
|
+
- tokensBefore: estimateTotalTokens(sourceMessages),
|
|
670
|
+
- tokens: estimateTotalTokens(emergency.messages),
|
|
671
|
+
- });
|
|
672
|
+
- }
|
|
673
|
+
- const marked = markOpenAiRemoteReplayBoundary(emergency.messages, {
|
|
674
|
+
- model: input.ctx.model,
|
|
675
|
+
- branchEntries: input.ctx.sessionManager.getBranch(),
|
|
676
|
+
+ const marked = markOpenAiRemoteReplayBoundary(input.event.messages, {
|
|
677
|
+
+ model: input.ctx.model, branchEntries: input.ctx.sessionManager.getBranch(),
|
|
678
|
+
});
|
|
679
|
+
- const reminded = injectTokenBudgetReminder(marked, input.reminder);
|
|
680
|
+
- const outgoing = repairOrphanedToolResults(convertToLlm(reminded));
|
|
681
|
+
- const overhead = input.contextOverheadTokens ?? 0;
|
|
682
|
+
- const beforePrune = estimateTotalTokens(sourceMessages) + overhead;
|
|
683
|
+
- const afterPrune = estimateTotalTokens(outgoing) + overhead;
|
|
684
|
+
- if (managed &&
|
|
685
|
+
- (state.engaged || emergency.messages !== sourceMessages) &&
|
|
686
|
+
- (emergency.needsAggressiveCompaction ||
|
|
687
|
+
- afterPrune > input.promptContextWindow ||
|
|
688
|
+
- (afterPrune < beforePrune && input.emergencyPruneLatch.engaged)))
|
|
689
|
+
- state.compactionRequired = true;
|
|
690
|
+
+ const outgoing = repairOrphanedToolResults(convertToLlm(injectTokenBudgetReminder(marked, input.reminder)));
|
|
691
|
+
+ const ceiling = input.reductionCeilingTokens ?? input.promptContextWindow;
|
|
692
|
+
+ state.compactionRequired = estimateTotalTokens(outgoing) + (input.contextOverheadTokens ?? 0) > ceiling;
|
|
693
|
+
return outgoing;
|
|
694
|
+
}
|
|
695
|
+
-//# sourceMappingURL=context-pipeline.js.map
|
|
696
|
+
|
|
697
|
+
+//# sourceMappingURL=context-pipeline.js.map
|
|
698
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/context-reduction-handoff.js
|
|
699
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/context-reduction-handoff.js
|
|
700
|
+
@@ -17,7 +17,7 @@ export function createReductionHandoff(pi, customInstructions) {
|
|
701
|
+
customInstructions,
|
|
702
|
+
onError: () => {
|
|
703
|
+
if (active === request)
|
|
704
|
+
- active = undefined;
|
|
705
|
+
+ ctx.abort("system");
|
|
706
|
+
},
|
|
707
|
+
onComplete: () => {
|
|
708
|
+
if (active !== request)
|
|
709
|
+
@@ -37,4 +37,4 @@ export function createReductionHandoff(pi, customInstructions) {
|
|
710
|
+
},
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
-//# sourceMappingURL=context-reduction-handoff.js.map
|
|
714
|
+
|
|
715
|
+
+//# sourceMappingURL=context-reduction-handoff.js.map
|
|
716
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/context-reduction-lifecycle.js
|
|
717
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/context-reduction-lifecycle.js
|
|
718
|
+
@@ -46,6 +46,7 @@ export function registerContextReductionLifecycle(pi, options) {
|
|
719
|
+
pi.on("session_start", resetForNavigation);
|
|
720
|
+
pi.on("session_tree", resetForNavigation);
|
|
721
|
+
pi.on("session_shutdown", resetForNavigation);
|
|
722
|
+
+ pi.on("before_agent_start", () => handoff.reset());
|
|
723
|
+
pi.on("message_end", (event, ctx) => {
|
|
724
|
+
const request = pending;
|
|
725
|
+
if (!request || sessionId !== ctx.sessionManager.getSessionId() || event.message.role !== "assistant")
|
|
726
|
+
@@ -108,7 +109,7 @@ export function registerContextReductionLifecycle(pi, options) {
|
|
727
|
+
emergencyPruneLatch: live ? options.emergencyPruneLatch : { ...options.emergencyPruneLatch },
|
|
728
|
+
logEmergencyPrune: (fields) => options.logEmergencyPrune(ctx, fields),
|
|
729
|
+
});
|
|
730
|
+
- const managed = !policy.laneOwnsCompaction && !policy.appendOnlyTranscript && !isOpenAiRemoteCompactionModel(ctx.model);
|
|
731
|
+
+ const managed = !policy.laneOwnsCompaction;
|
|
732
|
+
if (!live)
|
|
733
|
+
return { messages };
|
|
734
|
+
if (!managed) {
|
|
735
|
+
@@ -120,7 +121,10 @@ export function registerContextReductionLifecycle(pi, options) {
|
|
736
|
+
persist(before);
|
|
737
|
+
// Never dispatch a prompt that is legal only because pruning changed
|
|
738
|
+
// its lineage. Only an accepted, current handoff can resume work.
|
|
739
|
+
- handoff.request(ctx);
|
|
740
|
+
+ if (policy.breakerTripped)
|
|
741
|
+
+ ctx.abort("system");
|
|
742
|
+
+ else
|
|
743
|
+
+ handoff.request(ctx);
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
persist(before);
|
|
747
|
+
@@ -135,4 +139,4 @@ export function registerContextReductionLifecycle(pi, options) {
|
|
748
|
+
}, { mutatesMessages: false });
|
|
749
|
+
return { reset };
|
|
750
|
+
}
|
|
751
|
+
-//# sourceMappingURL=context-reduction-lifecycle.js.map
|
|
752
|
+
|
|
753
|
+
+//# sourceMappingURL=context-reduction-lifecycle.js.map
|
|
754
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/deterministic-fallback.js
|
|
755
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/deterministic-fallback.js
|
|
756
|
+
@@ -118,6 +118,8 @@ function isTerminalSummarizationProviderFailure(error) {
|
|
757
|
+
return error instanceof SummaryRequestError && !error.transient && !error.refused && error.failureKind === undefined;
|
|
758
|
+
}
|
|
759
|
+
export function classifyRequiredCompactionFallbackFailure(error) {
|
|
760
|
+
+ if (error instanceof SummaryRequestError && error.refused)
|
|
761
|
+
+ return undefined;
|
|
762
|
+
if (error instanceof UnsafeRetainedSuffixError)
|
|
763
|
+
return "unsafe-retained-content";
|
|
764
|
+
if (error instanceof StreamDurationBudgetError ||
|
|
765
|
+
@@ -477,4 +479,4 @@ export function createRequiredCompactionFallback(preparation, contextWindow, fai
|
|
766
|
+
diagnostics.candidatesChecked = candidateCount;
|
|
767
|
+
return undefined;
|
|
768
|
+
}
|
|
769
|
+
-//# sourceMappingURL=deterministic-fallback.js.map
|
|
770
|
+
|
|
771
|
+
+//# sourceMappingURL=deterministic-fallback.js.map
|
|
772
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/emergency-prune.js
|
|
773
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/emergency-prune.js
|
|
774
|
+
@@ -53,13 +53,6 @@ export function hardLimitEmergencyPrune(messages, contextWindow, latch) {
|
|
775
|
+
if (!engaged) {
|
|
776
|
+
return { messages, needsAggressiveCompaction: false };
|
|
777
|
+
}
|
|
778
|
+
- const noLlmPruned = truncateContextMessages(pruneToolResults(messages, contextWindow, SUMMARIZATION_INPUT_BUDGET_RATIO));
|
|
779
|
+
- if (estimateTotalTokens(noLlmPruned) <= targetTokens) {
|
|
780
|
+
- return { messages: noLlmPruned, needsAggressiveCompaction: false };
|
|
781
|
+
- }
|
|
782
|
+
- return {
|
|
783
|
+
- messages: pruneOldMessagesToBudget(noLlmPruned, targetTokens),
|
|
784
|
+
- needsAggressiveCompaction: true,
|
|
785
|
+
- };
|
|
786
|
+
+ return { messages, needsAggressiveCompaction: true };
|
|
787
|
+
}
|
|
788
|
+
-//# sourceMappingURL=emergency-prune.js.map
|
|
789
|
+
|
|
790
|
+
+//# sourceMappingURL=emergency-prune.js.map
|
|
791
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/extension-wiring.js
|
|
792
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/extension-wiring.js
|
|
793
|
+
@@ -63,7 +63,8 @@ export function endCompactionFeedback(ctx, signal, result, remoteFallbackReason)
|
|
794
|
+
const errorMessage = signal?.aborted || parts.length === 0
|
|
795
|
+
? undefined
|
|
796
|
+
: `Compaction did not apply: ${parts.join("; local fallback ")}`;
|
|
797
|
+
- ctx.endCompaction?.({ reason: "extension", signal, aborted: signal?.aborted, errorMessage });
|
|
798
|
+
+ ctx.endCompaction?.({ reason: "extension", signal, aborted: signal?.aborted, errorMessage,
|
|
799
|
+
+ failure: signal?.aborted ? undefined : { kind: result.reason === "stale" ? "stale" : "provider" } });
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
@@ -101,4 +102,4 @@ export function createBlockingRemoteCompactionEvent(ctx, snapshot, customInstruc
|
|
804
|
+
signal,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
-//# sourceMappingURL=extension-wiring.js.map
|
|
808
|
+
|
|
809
|
+
+//# sourceMappingURL=extension-wiring.js.map
|
|
810
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/index.js
|
|
811
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/index.js
|
|
812
|
+
@@ -1,12 +1,9 @@
|
|
813
|
+
import { createWarmAnchorSnapshot, isWarmSummaryAnchorValid } from "../../../compaction/warm-anchor.js";
|
|
814
|
+
-import { AccountUsageLimitError } from "../../../retry-fallback/usage-limit.js";
|
|
815
|
+
import * as checkpointState from "./checkpoint-state.js";
|
|
816
|
+
import * as breaker from "./circuit-breaker.js";
|
|
817
|
+
import { registerContextReductionLifecycle } from "./context-reduction-lifecycle.js";
|
|
818
|
+
import { createDegradationMonitorState, handleMessageEnd, handleTurnEnd, RECOVERY_INSTRUCTIONS, resetOnSessionCompact, } from "./degradation-monitor.js";
|
|
819
|
+
-import { classifyRequiredCompactionFallbackFailure, createRequiredCompactionFallback, formatRequiredCompactionFallbackNotice, formatRequiredCompactionFallbackRejection, stripTurnRetrySuppressionPrefix, UnsafeRetainedSuffixError, } from "./deterministic-fallback.js";
|
|
820
|
+
import * as idle from "./idle.js";
|
|
821
|
+
-import * as idleRetry from "./idle-retry.js";
|
|
822
|
+
import { ANTHROPIC_SUBSCRIPTION_COMPACT_ENTRY_TYPE, collectCompactBoundaryEntries, createCompactionLanePolicy, SDK_NATIVE_LANE_REJECTION_REASON, } from "./lane-policy.js";
|
|
823
|
+
import { createCompactionLogger } from "./log.js";
|
|
824
|
+
import { handleCompactionModelSelect } from "./model-selection.js";
|
|
825
|
+
@@ -17,7 +14,7 @@ import * as cap from "./per-turn-cap.js";
|
|
826
|
+
import * as policy from "./policy.js";
|
|
827
|
+
import { compactionRecoveryStateKey, isAutomaticCompactionBlocked, REJECTED_RECOVERY_ENTRY, wouldCompactionOverflow, } from "./rejected-recovery.js";
|
|
828
|
+
import * as restoration from "./restoration-tracker.js";
|
|
829
|
+
-import { applyGeneratedCompaction, createEmergencyPruneLatch, createSpeculativeCompactionSnapshot, getPromptVariant, runExtensionCompaction, SummaryGenerationError, } from "./speculative.js";
|
|
830
|
+
+import { applyGeneratedCompaction, createEmergencyPruneLatch, createSpeculativeCompactionSnapshot, getPromptVariant, runExtensionCompaction, SummaryGenerationError, compactionFailure, } from "./speculative.js";
|
|
831
|
+
import { trackSpeculativeJob } from "./speculative-job.js";
|
|
832
|
+
import { createInitialState, resetTurnCounter } from "./state.js";
|
|
833
|
+
import { resolveInheritedTaskIntent } from "./task-intent.js";
|
|
834
|
+
@@ -60,7 +57,6 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
let idleWarmupTimer;
|
|
838
|
+
- let idleWarmupAttempt = 0;
|
|
839
|
+
// True from the `agent_end` idle trigger until the next turn starts (or the
|
|
840
|
+
// session shuts down). The idle-apply watcher below is fenced on it so a
|
|
841
|
+
// summary that lands after the user has already prompted is never applied
|
|
842
|
+
@@ -95,68 +91,6 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
843
|
+
// Fenced on the observed job: a prompt, invalidation, a reload, or a newer
|
|
844
|
+
// warm-up stands this watcher down before it can start a duplicate
|
|
845
|
+
// summarization.
|
|
846
|
+
- function armIdleWarmupRetry(ctx) {
|
|
847
|
+
- const job = speculativeJob;
|
|
848
|
+
- if (!job)
|
|
849
|
+
- return;
|
|
850
|
+
- void job.failure.then((failure) => {
|
|
851
|
+
- if (failure === undefined) {
|
|
852
|
+
- idleWarmupAttempt = 0;
|
|
853
|
+
- return;
|
|
854
|
+
- }
|
|
855
|
+
- if (speculativeJob !== job)
|
|
856
|
+
- return;
|
|
857
|
+
- // A reload between arming and this continuation retires the context.
|
|
858
|
+
- if (isContextRetired(ctx)) {
|
|
859
|
+
- cancelIdleWarmupRetry();
|
|
860
|
+
- return;
|
|
861
|
+
- }
|
|
862
|
+
- const usage = ctx.getContextUsage();
|
|
863
|
+
- const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
864
|
+
- const settings = ctx.getCompactionSettings();
|
|
865
|
+
- const breakerTripped = breaker.isTripped(state, Date.now());
|
|
866
|
+
- const retryDecision = {
|
|
867
|
+
- attempt: idleWarmupAttempt,
|
|
868
|
+
- transient: isTransientSummarizationFailure(failure, failure.message),
|
|
869
|
+
- isIdle: ctx.isIdle(),
|
|
870
|
+
- breakerTripped,
|
|
871
|
+
- stillWarmEligible: idle.shouldWarmAtIdle({
|
|
872
|
+
- willRetry: false,
|
|
873
|
+
- aborted: false,
|
|
874
|
+
- settings,
|
|
875
|
+
- usage,
|
|
876
|
+
- contextWindow,
|
|
877
|
+
- breakerTripped,
|
|
878
|
+
- lastYield: state.lastYield ?? undefined,
|
|
879
|
+
- mode: ctx.mode,
|
|
880
|
+
- }),
|
|
881
|
+
- };
|
|
882
|
+
- if (!idleRetry.shouldRetryIdleWarmup(retryDecision))
|
|
883
|
+
- return;
|
|
884
|
+
- cancelIdleWarmupRetry();
|
|
885
|
+
- idleWarmupTimer = setTimeout(() => {
|
|
886
|
+
- idleWarmupTimer = undefined;
|
|
887
|
+
- if (speculativeJob !== job)
|
|
888
|
+
- return;
|
|
889
|
+
- // The reload may land after the timer was armed; a throw here would
|
|
890
|
+
- // escape into the timer queue as an uncaughtException.
|
|
891
|
+
- if (isContextRetired(ctx))
|
|
892
|
+
- return;
|
|
893
|
+
- if (!ctx.isIdle())
|
|
894
|
+
- return;
|
|
895
|
+
- idleWarmupAttempt += 1;
|
|
896
|
+
- getLogger(ctx).debug("idle_trigger", {
|
|
897
|
+
- contextWindow,
|
|
898
|
+
- tokens: usage?.tokens ?? 0,
|
|
899
|
+
- count: idleWarmupAttempt,
|
|
900
|
+
- });
|
|
901
|
+
- invalidateSpeculativeCompaction(ctx);
|
|
902
|
+
- startSpeculativeCompaction(ctx, idle.IDLE_COMPACTION_INSTRUCTIONS);
|
|
903
|
+
- armIdleWarmupRetry(ctx);
|
|
904
|
+
- armIdleApply(ctx);
|
|
905
|
+
- }, idleRetry.IDLE_WARMUP_RETRY_DELAY_MS);
|
|
906
|
+
- });
|
|
907
|
+
- }
|
|
908
|
+
/**
|
|
909
|
+
* Apply the idle warm summary as soon as it finishes generating, while the
|
|
910
|
+
* session is still idle. Holding it warm until the next `before_agent_start`
|
|
911
|
+
@@ -201,7 +135,7 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
912
|
+
!policy.shouldTriggerCompaction(usage, contextWindow, ctx.getCompactionSettings(), state.lastYield ?? undefined)) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
- const result = await applyGeneratedCompaction(ctx, job.snapshot, () => speculativeGeneration, compaction);
|
|
916
|
+
+ const result = await applyGeneratedCompaction(ctx, job.snapshot, () => speculativeGeneration, compaction, undefined, false);
|
|
917
|
+
if (!result.applied)
|
|
918
|
+
return;
|
|
919
|
+
// The job is consumed: clearing it here is also what stands the idle
|
|
920
|
+
@@ -300,394 +234,122 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
921
|
+
todoBridge.persistTodoSnapshot(pi, metadata.todoSnapshot);
|
|
922
|
+
}
|
|
923
|
+
async function generateCompaction(ctx, snapshot, signal, onProgress) {
|
|
924
|
+
- const compaction = await runExtensionCompaction(ctx, snapshot, signal, onProgress);
|
|
925
|
+
- if (!compaction || signal?.aborted)
|
|
926
|
+
- return compaction;
|
|
927
|
+
- if (!wouldCompactionOverflow(snapshot.branchEntries ?? [], compaction, true, snapshot.model, snapshot.preparation.settings))
|
|
928
|
+
- return compaction;
|
|
929
|
+
- if (!wouldCompactionOverflow(snapshot.branchEntries ?? [], { ...compaction, summary: "" }, true, snapshot.model, snapshot.preparation.settings))
|
|
930
|
+
- return compaction;
|
|
931
|
+
- // #3061: only a SUCCESSFUL summary can establish a terminal context fault.
|
|
932
|
+
- // A failed summary plus a rejected emergency checkpoint says nothing about
|
|
933
|
+
- // whether a recovered provider could produce a smaller, usable summary.
|
|
934
|
+
- // Probe with zero summary text so marker/previous-summary overhead alone
|
|
935
|
+
- // cannot turn a provider failure or an oversized summary into a durable latch.
|
|
936
|
+
- const diagnostics = {};
|
|
937
|
+
- const viable = createRequiredCompactionFallback(snapshot.preparation, snapshot.contextWindow, "unsafe-retained-content", { summary: "" }, snapshot.branchEntries, diagnostics);
|
|
938
|
+
- if (!viable &&
|
|
939
|
+
- (diagnostics.rejectionReason === "unsafe-retained-content" ||
|
|
940
|
+
- diagnostics.rejectionReason === "atomic-tool-chain-cut" ||
|
|
941
|
+
- diagnostics.rejectionReason === "retained-token-budget-exceeded")) {
|
|
942
|
+
- throw new UnsafeRetainedSuffixError(diagnostics);
|
|
943
|
+
- }
|
|
944
|
+
- return compaction;
|
|
945
|
+
- }
|
|
946
|
+
- function recoverRequiredCompaction(ctx, snapshot, failureKind, cause) {
|
|
947
|
+
- const terminal = cause instanceof UnsafeRetainedSuffixError;
|
|
948
|
+
- const diagnostics = terminal ? cause.diagnostics : {};
|
|
949
|
+
- const compaction = terminal
|
|
950
|
+
- ? undefined
|
|
951
|
+
- : createRequiredCompactionFallback(snapshot.preparation, snapshot.contextWindow, failureKind, { taskIntent: resolveInheritedTaskIntent(snapshot.branchEntries ?? []) }, snapshot.branchEntries, diagnostics);
|
|
952
|
+
- if (!compaction) {
|
|
953
|
+
- const branch = snapshot.branchEntries ?? [];
|
|
954
|
+
- // Provider/timeout/abort/empty-summary failures never create or replace
|
|
955
|
+
- // this record, even when their opportunistic deterministic fallback fails.
|
|
956
|
+
- if (terminal && !isAutomaticCompactionBlocked(branch, snapshot.model, ctx.getCompactionSettings())) {
|
|
957
|
+
- pi.appendEntry(REJECTED_RECOVERY_ENTRY, {
|
|
958
|
+
- failureKind: "unsafe-retained-content",
|
|
959
|
+
- stateKey: compactionRecoveryStateKey(branch, snapshot.model, ctx.getCompactionSettings()),
|
|
960
|
+
- reason: diagnostics.rejectionReason,
|
|
961
|
+
- entryId: diagnostics.candidateRejections?.at(-1)?.unsafeEntryId ??
|
|
962
|
+
- diagnostics.candidateRejections?.at(-1)?.firstKeptEntryId,
|
|
963
|
+
- });
|
|
964
|
+
- }
|
|
965
|
+
- return { rejectionReason: formatRequiredCompactionFallbackRejection(diagnostics, terminal) };
|
|
966
|
+
- }
|
|
967
|
+
- // The compaction itself succeeds, so nothing else tells the user their
|
|
968
|
+
- // transcript was reduced without a provider summary. Say it plainly here,
|
|
969
|
+
- // and never with the internal retry-suppression marker (#1741).
|
|
970
|
+
- ctx.ui.notify(formatRequiredCompactionFallbackNotice(failureKind, cause), "warning");
|
|
971
|
+
- return { compaction };
|
|
972
|
+
+ return await runExtensionCompaction(ctx, snapshot, signal, onProgress);
|
|
973
|
+
}
|
|
974
|
+
async function applyBlockingCompaction(ctx, customInstructions) {
|
|
975
|
+
- if (isAutomaticCompactionBlocked(ctx.sessionManager.getBranch(), ctx.model, ctx.getCompactionSettings())) {
|
|
976
|
+
- return { applied: false, reason: "rejected" };
|
|
977
|
+
- }
|
|
978
|
+
- const provider = ctx.model?.provider;
|
|
979
|
+
- if ((provider === "cursor" || provider === "cursor-cli-oauth") && !ctx.isIdle()) {
|
|
980
|
+
- getLogger(ctx).debug("skip_cursor_mid_turn", { route: "blocking" });
|
|
981
|
+
- return { applied: false, reason: "rejected" };
|
|
982
|
+
- }
|
|
983
|
+
- if (breaker.isTripped(state, Date.now())) {
|
|
984
|
+
- getLogger(ctx).debug("skip_breaker", { route: "blocking" });
|
|
985
|
+
- return { applied: false, reason: "rejected" };
|
|
986
|
+
- }
|
|
987
|
+
- if (cap.shouldRejectByCap(state).cancel) {
|
|
988
|
+
- getLogger(ctx).debug("skip_cap", { route: "blocking" });
|
|
989
|
+
- return { applied: false, reason: "rejected" };
|
|
990
|
+
- }
|
|
991
|
+
- let feedbackSignal = ctx.beginCompaction?.({ reason: "extension" });
|
|
992
|
+
+ const feedbackSignal = ctx.beginCompaction?.({ reason: "extension" });
|
|
993
|
+
try {
|
|
994
|
+
- let remoteFallbackReason;
|
|
995
|
+
- if (isOpenAiRemoteCompactionModel(ctx.model) && (ctx.getCompactionModel?.() ?? ctx.model) === ctx.model) {
|
|
996
|
+
- const remoteGeneration = speculativeGeneration + 1;
|
|
997
|
+
- const remoteSnapshot = createSpeculativeCompactionSnapshot(ctx, {
|
|
998
|
+
- generation: remoteGeneration,
|
|
999
|
+
- customInstructions,
|
|
1000
|
+
- origin: "blocking",
|
|
1001
|
+
- });
|
|
1002
|
+
- getLogger(ctx).debug("blocking_started", { generation: remoteGeneration, origin: "blocking" });
|
|
1003
|
+
- if (remoteSnapshot) {
|
|
1004
|
+
- const remoteSignal = feedbackSignal ?? new AbortController().signal;
|
|
1005
|
+
- const remoteCompaction = await runOpenAiRemoteCompaction(ctx, createBlockingRemoteCompactionEvent(ctx, remoteSnapshot, customInstructions, remoteSignal), (data) => {
|
|
1006
|
+
- if (data?.action === "remote_fallback" && typeof data.reason === "string") {
|
|
1007
|
+
- remoteFallbackReason = data.reason;
|
|
1008
|
+
- }
|
|
1009
|
+
- reportRemoteCompactionTimeout(ctx, "extension", feedbackSignal, data);
|
|
1010
|
+
- pi.events.emit(SENPI_COMPACTION_EVENT, data);
|
|
1011
|
+
- }, remoteCompactionDependencies);
|
|
1012
|
+
- if (remoteCompaction) {
|
|
1013
|
+
- if (speculativeGeneration !== remoteGeneration - 1) {
|
|
1014
|
+
- const result = { applied: false, reason: "stale" };
|
|
1015
|
+
- getLogger(ctx).debug("speculative_stale", { generation: remoteGeneration });
|
|
1016
|
+
- endCompactionFeedback(ctx, feedbackSignal, result);
|
|
1017
|
+
- return result;
|
|
1018
|
+
- }
|
|
1019
|
+
- speculativeGeneration = remoteGeneration;
|
|
1020
|
+
- speculativeJob?.controller.abort();
|
|
1021
|
+
- speculativeJob = undefined;
|
|
1022
|
+
- const result = await applyGeneratedCompaction(ctx, remoteSnapshot, () => speculativeGeneration, remoteCompaction, feedbackSignal);
|
|
1023
|
+
- getLogger(ctx).debug("speculative_applied", { generation: remoteGeneration, origin: "blocking" });
|
|
1024
|
+
- endCompactionFeedback(ctx, feedbackSignal, result);
|
|
1025
|
+
- return result;
|
|
1026
|
+
- }
|
|
1027
|
+
- }
|
|
1028
|
+
- }
|
|
1029
|
+
+ const provider = ctx.model?.provider;
|
|
1030
|
+
+ if ((provider === "cursor" || provider === "cursor-cli-oauth") && !ctx.isIdle())
|
|
1031
|
+
+ throw new SummaryGenerationError("provider", "Compaction unavailable while provider is busy");
|
|
1032
|
+
+ if (isAutomaticCompactionBlocked(ctx.sessionManager.getBranch(), ctx.model, ctx.getCompactionSettings()) ||
|
|
1033
|
+
+ breaker.isTripped(state, Date.now()) || cap.shouldRejectByCap(state).cancel)
|
|
1034
|
+
+ throw new SummaryGenerationError("provider", "Compaction is blocked after an earlier failure");
|
|
1035
|
+
const pendingJob = speculativeJob;
|
|
1036
|
+
+ speculativeJob = undefined;
|
|
1037
|
+
+ let snapshot;
|
|
1038
|
+
+ let compaction;
|
|
1039
|
+
if (pendingJob) {
|
|
1040
|
+
- const unlinkAbort = linkAbortSignal(feedbackSignal, pendingJob.controller);
|
|
1041
|
+
- let compaction;
|
|
1042
|
+
- let inheritedFailure;
|
|
1043
|
+
+ snapshot = pendingJob.snapshot;
|
|
1044
|
+
+ const unlink = linkAbortSignal(feedbackSignal, pendingJob.controller);
|
|
1045
|
+
try {
|
|
1046
|
+
compaction = await pendingJob.promise;
|
|
1047
|
+
- inheritedFailure = await pendingJob.failure;
|
|
1048
|
+
- if (compaction)
|
|
1049
|
+
- getLogger(ctx).debug("warm_consumed", { generation: pendingJob.generation, route: "speculative" });
|
|
1050
|
+
- }
|
|
1051
|
+
- finally {
|
|
1052
|
+
- unlinkAbort();
|
|
1053
|
+
- }
|
|
1054
|
+
- if (inheritedFailure !== undefined) {
|
|
1055
|
+
- speculativeJob = undefined;
|
|
1056
|
+
- const failureKind = classifyRequiredCompactionFallbackFailure(inheritedFailure);
|
|
1057
|
+
- if (failureKind !== undefined &&
|
|
1058
|
+
- !feedbackSignal?.aborted &&
|
|
1059
|
+
- pendingJob.snapshot.generation === speculativeGeneration &&
|
|
1060
|
+
- pendingJob.snapshot.expectedRevision === ctx.getMessageRevision()) {
|
|
1061
|
+
- const recovery = recoverRequiredCompaction(ctx, pendingJob.snapshot, failureKind, inheritedFailure);
|
|
1062
|
+
- compaction = recovery.compaction;
|
|
1063
|
+
- if (!compaction) {
|
|
1064
|
+
- const result = { applied: false, reason: "failed" };
|
|
1065
|
+
- endCompactionFeedback(ctx, feedbackSignal, result, recovery.rejectionReason);
|
|
1066
|
+
- return result;
|
|
1067
|
+
- }
|
|
1068
|
+
- }
|
|
1069
|
+
- else if (failureKind === undefined &&
|
|
1070
|
+
- isTransientSummarizationFailure(inheritedFailure, inheritedFailure.message)) {
|
|
1071
|
+
- ctx.endCompaction?.({
|
|
1072
|
+
- reason: "extension",
|
|
1073
|
+
- signal: feedbackSignal,
|
|
1074
|
+
- aborted: feedbackSignal?.aborted,
|
|
1075
|
+
- errorMessage: `Compaction failed: ${stripTurnRetrySuppressionPrefix(inheritedFailure.message)}`,
|
|
1076
|
+
- });
|
|
1077
|
+
- state = breaker.recordFailure(state, Date.now(), { route: "extension" });
|
|
1078
|
+
- return { applied: false, reason: "failed" };
|
|
1079
|
+
- }
|
|
1080
|
+
- }
|
|
1081
|
+
- const result = await applyGeneratedCompaction(ctx, pendingJob.snapshot, () => speculativeGeneration, compaction, feedbackSignal);
|
|
1082
|
+
- if (result.applied) {
|
|
1083
|
+
- speculativeJob = undefined;
|
|
1084
|
+
- getLogger(ctx).debug("speculative_applied", {
|
|
1085
|
+
- generation: pendingJob.generation,
|
|
1086
|
+
- origin: "speculative",
|
|
1087
|
+
- });
|
|
1088
|
+
- endCompactionFeedback(ctx, feedbackSignal, result);
|
|
1089
|
+
- return result;
|
|
1090
|
+
- }
|
|
1091
|
+
- if (result.reason === "stale") {
|
|
1092
|
+
- getLogger(ctx).debug("speculative_stale", { generation: pendingJob.generation });
|
|
1093
|
+
+ const failure = await pendingJob.failure;
|
|
1094
|
+
+ if (failure !== undefined)
|
|
1095
|
+
+ throw failure;
|
|
1096
|
+
}
|
|
1097
|
+
- else if (result.reason === "rejected") {
|
|
1098
|
+
- feedbackSignal = ctx.beginCompaction?.({ reason: "extension" });
|
|
1099
|
+
- }
|
|
1100
|
+
- speculativeJob = undefined;
|
|
1101
|
+
- }
|
|
1102
|
+
- const generation = ++speculativeGeneration;
|
|
1103
|
+
- const snapshot = createSpeculativeCompactionSnapshot(ctx, {
|
|
1104
|
+
- generation,
|
|
1105
|
+
- customInstructions,
|
|
1106
|
+
- origin: "core-route",
|
|
1107
|
+
- tools: getSummarizationTools(),
|
|
1108
|
+
- });
|
|
1109
|
+
- if (!snapshot) {
|
|
1110
|
+
- const result = { applied: false, reason: "unavailable" };
|
|
1111
|
+
- getLogger(ctx).debug("summary_failed", { reason: "unavailable" });
|
|
1112
|
+
- endCompactionFeedback(ctx, feedbackSignal, result, remoteFallbackReason);
|
|
1113
|
+
- return result;
|
|
1114
|
+
- }
|
|
1115
|
+
- let compaction;
|
|
1116
|
+
- try {
|
|
1117
|
+
- compaction = await generateCompaction(ctx, snapshot, feedbackSignal, (delta) => ctx.updateCompaction?.({
|
|
1118
|
+
- reason: "extension",
|
|
1119
|
+
- signal: feedbackSignal,
|
|
1120
|
+
- delta,
|
|
1121
|
+
- }));
|
|
1122
|
+
+ finally { unlink(); }
|
|
1123
|
+
}
|
|
1124
|
+
- catch (error) {
|
|
1125
|
+
- const failureKind = classifyRequiredCompactionFallbackFailure(error);
|
|
1126
|
+
- if (failureKind !== undefined && !feedbackSignal?.aborted) {
|
|
1127
|
+
- const recovery = recoverRequiredCompaction(ctx, snapshot, failureKind, error);
|
|
1128
|
+
- compaction = recovery.compaction;
|
|
1129
|
+
- if (!compaction) {
|
|
1130
|
+
- const result = { applied: false, reason: "failed" };
|
|
1131
|
+
- endCompactionFeedback(ctx, feedbackSignal, result, recovery.rejectionReason);
|
|
1132
|
+
- return result;
|
|
1133
|
+
- }
|
|
1134
|
+
- }
|
|
1135
|
+
- else {
|
|
1136
|
+
- if (!(error instanceof SummaryGenerationError))
|
|
1137
|
+
- throw error;
|
|
1138
|
+
- getLogger(ctx).debug("summary_failed", { reason: error.kind });
|
|
1139
|
+
+ else {
|
|
1140
|
+
+ snapshot = createSpeculativeCompactionSnapshot(ctx, {
|
|
1141
|
+
+ generation: ++speculativeGeneration, customInstructions, origin: "blocking", tools: getSummarizationTools(),
|
|
1142
|
+
+ });
|
|
1143
|
+
+ if (!snapshot)
|
|
1144
|
+
+ throw new SummaryGenerationError("empty", "No compaction source available");
|
|
1145
|
+
+ if (isOpenAiRemoteCompactionModel(ctx.model) && (ctx.getCompactionModel?.() ?? ctx.model) === ctx.model) {
|
|
1146
|
+
+ const remote = await runOpenAiRemoteCompaction(ctx,
|
|
1147
|
+
+ createBlockingRemoteCompactionEvent(ctx, snapshot, customInstructions, feedbackSignal ?? new AbortController().signal),
|
|
1148
|
+
+ (data) => pi.events.emit(SENPI_COMPACTION_EVENT, data), remoteCompactionDependencies);
|
|
1149
|
+
+ if (remote.status === "failed")
|
|
1150
|
+
+ throw Object.assign(new Error("Remote compaction failed"), { failure: remote.failure });
|
|
1151
|
+
+ if (remote.status === "accepted")
|
|
1152
|
+
+ compaction = remote.compaction;
|
|
1153
|
+
}
|
|
1154
|
+
+ if (!compaction)
|
|
1155
|
+
+ compaction = await generateCompaction(ctx, snapshot, feedbackSignal,
|
|
1156
|
+
+ (delta) => ctx.updateCompaction?.({ reason: "extension", signal: feedbackSignal, delta }));
|
|
1157
|
+
}
|
|
1158
|
+
const result = await applyGeneratedCompaction(ctx, snapshot, () => speculativeGeneration, compaction, feedbackSignal);
|
|
1159
|
+
- endCompactionFeedback(ctx, feedbackSignal, result, remoteFallbackReason);
|
|
1160
|
+
+ endCompactionFeedback(ctx, feedbackSignal, result);
|
|
1161
|
+
return result;
|
|
1162
|
+
}
|
|
1163
|
+
catch (error) {
|
|
1164
|
+
- if (feedbackSignal?.aborted) {
|
|
1165
|
+
- // An aborted blocking compaction is a cancellation, not a failure: no
|
|
1166
|
+
- // red "Compaction failed" line, no breaker debit, no rethrow through
|
|
1167
|
+
- // the before_agent_start handler (issue #886). The faux-route flavor
|
|
1168
|
+
- // of this contract is pinned by blocking-compaction-review-hardening.
|
|
1169
|
+
- getLogger(ctx).debug("blocking_aborted", { route: "blocking" });
|
|
1170
|
+
- ctx.endCompaction?.({ reason: "extension", signal: feedbackSignal, aborted: true });
|
|
1171
|
+
- return { applied: false, reason: "rejected" };
|
|
1172
|
+
- }
|
|
1173
|
+
- const message = error instanceof Error ? error.message : String(error);
|
|
1174
|
+
- ctx.endCompaction?.({
|
|
1175
|
+
- reason: "extension",
|
|
1176
|
+
- signal: feedbackSignal,
|
|
1177
|
+
- aborted: feedbackSignal?.aborted,
|
|
1178
|
+
- errorMessage: `Compaction failed: ${stripTurnRetrySuppressionPrefix(message)}`,
|
|
1179
|
+
- });
|
|
1180
|
+
- const transient = isTransientSummarizationFailure(error, message);
|
|
1181
|
+
- if (transient) {
|
|
1182
|
+
+ const aborted = feedbackSignal?.aborted === true;
|
|
1183
|
+
+ const failure = aborted ? undefined : compactionFailure(error, ctx.model);
|
|
1184
|
+
+ ctx.endCompaction?.({ reason: "extension", signal: feedbackSignal, aborted,
|
|
1185
|
+
+ errorMessage: aborted ? undefined : "Compaction failed; previous context retained", failure });
|
|
1186
|
+
+ if (!aborted)
|
|
1187
|
+
state = breaker.recordFailure(state, Date.now(), { route: "extension" });
|
|
1188
|
+
- return { applied: false, reason: "failed" };
|
|
1189
|
+
- }
|
|
1190
|
+
- throw error;
|
|
1191
|
+
+ return { applied: false, reason: "failed", failure };
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
1195
|
+
- // A pre-aborted request (superseded admission or user cancel) must stand
|
|
1196
|
+
- // down before touching warm-job ownership: core's own post-emit abort
|
|
1197
|
+
- // checks turn it into a clean cancellation (issue #886).
|
|
1198
|
+
if (event.signal.aborted)
|
|
1199
|
+
- return undefined;
|
|
1200
|
+
+ return { cancel: true };
|
|
1201
|
+
const claimedWarmJob = claimWarmSummaryForCoreRoute(event, ctx);
|
|
1202
|
+
- // The claim detaches the job without aborting it, so ownership must survive every
|
|
1203
|
+
- // exit from this handler - including a throw - or the request it already paid for
|
|
1204
|
+
- // keeps streaming for a result nobody will read.
|
|
1205
|
+
- let warmJobConsumed = false;
|
|
1206
|
+
invalidateSpeculativeCompaction(ctx);
|
|
1207
|
+
- const claimedGeneration = speculativeGeneration;
|
|
1208
|
+
try {
|
|
1209
|
+
- if (!lanePolicy.ownsCompaction(ctx, event.reason)) {
|
|
1210
|
+
- return {
|
|
1211
|
+
- cancel: true,
|
|
1212
|
+
- rejectionCause: "external-owner",
|
|
1213
|
+
- reason: SDK_NATIVE_LANE_REJECTION_REASON,
|
|
1214
|
+
- };
|
|
1215
|
+
- }
|
|
1216
|
+
- if (cap.shouldRejectByCap(state).cancel) {
|
|
1217
|
+
- getLogger(ctx).debug("skip_cap", { reason: event.reason, count: state.acceptedAbsolute });
|
|
1218
|
+
- return {
|
|
1219
|
+
- cancel: true,
|
|
1220
|
+
- rejectionCause: "per-turn-cap",
|
|
1221
|
+
- reason: "absolute compaction cap reached for this session",
|
|
1222
|
+
- };
|
|
1223
|
+
- }
|
|
1224
|
+
- const now = Date.now();
|
|
1225
|
+
- if (breaker.isTripped(state, now) && !breaker.shouldBypass(state, { reason: event.reason })) {
|
|
1226
|
+
- const remainingMs = state.trippedAt !== null ? Math.max(0, state.trippedAt + breaker.COOLDOWN_MS - now) : 0;
|
|
1227
|
+
- getLogger(ctx).debug("skip_breaker", { reason: event.reason, remainingSec: Math.ceil(remainingMs / 1000) });
|
|
1228
|
+
- return {
|
|
1229
|
+
- cancel: true,
|
|
1230
|
+
- rejectionCause: "circuit-breaker",
|
|
1231
|
+
- reason: `compaction circuit breaker cooling down (${Math.ceil(remainingMs / 1000)}s left)`,
|
|
1232
|
+
- };
|
|
1233
|
+
- }
|
|
1234
|
+
+ if (!lanePolicy.ownsCompaction(ctx, event.reason))
|
|
1235
|
+
+ return { cancel: true, rejectionCause: "external-owner", reason: SDK_NATIVE_LANE_REJECTION_REASON };
|
|
1236
|
+
+ if (cap.shouldRejectByCap(state).cancel)
|
|
1237
|
+
+ return { cancel: true, rejectionCause: "per-turn-cap", failure: { kind: "provider" } };
|
|
1238
|
+
+ if (breaker.isTripped(state, Date.now()) && !breaker.shouldBypass(state, { reason: event.reason }))
|
|
1239
|
+
+ return { cancel: true, rejectionCause: "circuit-breaker", failure: { kind: "provider" } };
|
|
1240
|
+
capturePendingMetadata(event.requestId, ctx);
|
|
1241
|
+
const model = ctx.model;
|
|
1242
|
+
- if (!model) {
|
|
1243
|
+
- return undefined;
|
|
1244
|
+
- }
|
|
1245
|
+
- let remoteCompaction;
|
|
1246
|
+
- try {
|
|
1247
|
+
- // Native remote compaction is tied to the active provider's replay origin.
|
|
1248
|
+
- // A different summary model must use the bounded local summary route.
|
|
1249
|
+
- remoteCompaction =
|
|
1250
|
+
- (ctx.getCompactionModel?.() ?? model) !== model
|
|
1251
|
+
- ? undefined
|
|
1252
|
+
- : await runOpenAiRemoteCompaction(ctx, event, (data) => {
|
|
1253
|
+
- reportRemoteCompactionTimeout(ctx, event.reason, event.signal, data);
|
|
1254
|
+
- pi.events.emit(SENPI_COMPACTION_EVENT, data);
|
|
1255
|
+
- }, remoteCompactionDependencies);
|
|
1256
|
+
- }
|
|
1257
|
+
- catch (error) {
|
|
1258
|
+
- if (error instanceof AccountUsageLimitError)
|
|
1259
|
+
- return { cancel: true, reason: error.message };
|
|
1260
|
+
- // The remote route deliberately rethrows once its signal aborts; the
|
|
1261
|
+
- // handler converts that into a silent stand-down so the abort is not
|
|
1262
|
+
- // reported as an extension error with a stack (issue #886).
|
|
1263
|
+
- if (!event.signal.aborted)
|
|
1264
|
+
- throw error;
|
|
1265
|
+
- getLogger(ctx).debug("remote_aborted", { route: "core-route", requestId: event.requestId });
|
|
1266
|
+
- return undefined;
|
|
1267
|
+
- }
|
|
1268
|
+
- if (remoteCompaction) {
|
|
1269
|
+
- getLogger(ctx).debug("core_route_generated", { route: "core-route", requestId: event.requestId });
|
|
1270
|
+
- return { compaction: remoteCompaction };
|
|
1271
|
+
- }
|
|
1272
|
+
- let inheritedWarmFailure;
|
|
1273
|
+
+ if (!model)
|
|
1274
|
+
+ throw new SummaryGenerationError("model_unavailable", "Compaction model unavailable");
|
|
1275
|
+
if (claimedWarmJob) {
|
|
1276
|
+
- const unlinkAbort = linkAbortSignal(event.signal, claimedWarmJob.controller);
|
|
1277
|
+
- let warmCompaction;
|
|
1278
|
+
- let warmFailure;
|
|
1279
|
+
+ const unlink = linkAbortSignal(event.signal, claimedWarmJob.controller);
|
|
1280
|
+
try {
|
|
1281
|
+
- warmCompaction = await claimedWarmJob.promise;
|
|
1282
|
+
- warmFailure = await claimedWarmJob.failure;
|
|
1283
|
+
- }
|
|
1284
|
+
- finally {
|
|
1285
|
+
- unlinkAbort();
|
|
1286
|
+
- }
|
|
1287
|
+
- if (warmFailure === undefined && warmCompaction && !event.signal.aborted) {
|
|
1288
|
+
- getLogger(ctx).debug("warm_consumed", { generation: claimedWarmJob.generation, route: "core-route" });
|
|
1289
|
+
- warmJobConsumed = true;
|
|
1290
|
+
- return { compaction: warmCompaction };
|
|
1291
|
+
- }
|
|
1292
|
+
- if (warmFailure !== undefined &&
|
|
1293
|
+
- requiresDeterministicCompactionFallback(event, ctx.getContextUsage()) &&
|
|
1294
|
+
- classifyRequiredCompactionFallbackFailure(warmFailure) !== undefined &&
|
|
1295
|
+
- !event.signal.aborted &&
|
|
1296
|
+
- speculativeGeneration === claimedGeneration &&
|
|
1297
|
+
- claimedWarmJob.snapshot.expectedRevision === ctx.getMessageRevision()) {
|
|
1298
|
+
- inheritedWarmFailure = warmFailure;
|
|
1299
|
+
+ const compaction = await claimedWarmJob.promise;
|
|
1300
|
+
+ const failure = await claimedWarmJob.failure;
|
|
1301
|
+
+ if (failure !== undefined)
|
|
1302
|
+
+ throw failure;
|
|
1303
|
+
+ if (!compaction)
|
|
1304
|
+
+ throw new SummaryGenerationError("incomplete", "Warm compaction returned no result");
|
|
1305
|
+
+ return { compaction };
|
|
1306
|
+
}
|
|
1307
|
+
+ finally { unlink(); }
|
|
1308
|
+
+ }
|
|
1309
|
+
+ if ((ctx.getCompactionModel?.() ?? model) === model) {
|
|
1310
|
+
+ const remote = await runOpenAiRemoteCompaction(ctx, event,
|
|
1311
|
+
+ (data) => pi.events.emit(SENPI_COMPACTION_EVENT, data), remoteCompactionDependencies);
|
|
1312
|
+
+ if (remote.status === "failed")
|
|
1313
|
+
+ throw Object.assign(new Error("Remote compaction failed"), { failure: remote.failure });
|
|
1314
|
+
+ if (remote.status === "accepted")
|
|
1315
|
+
+ return { compaction: remote.compaction };
|
|
1316
|
+
}
|
|
1317
|
+
const snapshot = {
|
|
1318
|
+
- generation: ++speculativeGeneration,
|
|
1319
|
+
- expectedRevision: ctx.getMessageRevision(),
|
|
1320
|
+
- model,
|
|
1321
|
+
+ generation: ++speculativeGeneration, expectedRevision: ctx.getMessageRevision(), model,
|
|
1322
|
+
contextWindow: ctx.getContextUsage()?.contextWindow ?? model.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
1323
|
+
- preparation: event.preparation,
|
|
1324
|
+
- branchEntries: event.branchEntries,
|
|
1325
|
+
- promptVariant: getPromptVariant(event),
|
|
1326
|
+
- origin: "core-route",
|
|
1327
|
+
- customInstructions: event.customInstructions,
|
|
1328
|
+
- systemPrompt: ctx.getSystemPrompt(),
|
|
1329
|
+
- tools: getSummarizationTools(),
|
|
1330
|
+
- };
|
|
1331
|
+
- let compaction;
|
|
1332
|
+
- try {
|
|
1333
|
+
- if (inheritedWarmFailure)
|
|
1334
|
+
- throw inheritedWarmFailure;
|
|
1335
|
+
- compaction = await generateCompaction(ctx, snapshot, event.signal, (delta) => ctx.updateCompaction?.({ reason: event.reason, signal: event.signal, delta }));
|
|
1336
|
+
- }
|
|
1337
|
+
- catch (error) {
|
|
1338
|
+
- const message = error instanceof Error ? error.message : String(error);
|
|
1339
|
+
- const failureKind = classifyRequiredCompactionFallbackFailure(error);
|
|
1340
|
+
- // A terminal provider failure (dead model, dead credential) has no cheaper
|
|
1341
|
+
- // recovery on any compaction kind: without this, a soft-threshold pre_prompt
|
|
1342
|
+
- // compaction cancelled instead, the session rejected it, and the pending
|
|
1343
|
+
- // prompt failed with RequiredCompactionError while shouldCompact still said
|
|
1344
|
+
- // compact (the "Compaction rejected: compaction generator failed" wedge).
|
|
1345
|
+
- if ((requiresDeterministicCompactionFallback(event, ctx.getContextUsage()) ||
|
|
1346
|
+
- failureKind === "unsafe-retained-content" ||
|
|
1347
|
+
- failureKind === "summarization-provider-failure") &&
|
|
1348
|
+
- failureKind !== undefined &&
|
|
1349
|
+
- !event.signal.aborted) {
|
|
1350
|
+
- const recovery = recoverRequiredCompaction(ctx, snapshot, failureKind, error);
|
|
1351
|
+
- if (recovery.compaction)
|
|
1352
|
+
- return { compaction: recovery.compaction };
|
|
1353
|
+
- pendingMetadata.delete(event.requestId);
|
|
1354
|
+
- return {
|
|
1355
|
+
- cancel: true,
|
|
1356
|
+
- reason: recovery.rejectionReason,
|
|
1357
|
+
- };
|
|
1358
|
+
- }
|
|
1359
|
+
- pendingMetadata.delete(event.requestId);
|
|
1360
|
+
- if (error instanceof SummaryGenerationError) {
|
|
1361
|
+
- return { cancel: true, reason: stripTurnRetrySuppressionPrefix(error.message) };
|
|
1362
|
+
- }
|
|
1363
|
+
- return { cancel: true, reason: `compaction generator failed: ${stripTurnRetrySuppressionPrefix(message)}` };
|
|
1364
|
+
- }
|
|
1365
|
+
- if (!compaction) {
|
|
1366
|
+
- pendingMetadata.delete(event.requestId);
|
|
1367
|
+
- if (event.signal.aborted) {
|
|
1368
|
+
- return { cancel: true };
|
|
1369
|
+
- }
|
|
1370
|
+
- return { cancel: true, reason: "compaction generator returned no summary" };
|
|
1371
|
+
- }
|
|
1372
|
+
- return {
|
|
1373
|
+
- compaction,
|
|
1374
|
+
+ preparation: event.preparation, branchEntries: event.branchEntries, promptVariant: getPromptVariant(event),
|
|
1375
|
+
+ origin: "core-route", customInstructions: event.customInstructions,
|
|
1376
|
+
+ systemPrompt: ctx.getSystemPrompt(), tools: getSummarizationTools(),
|
|
1377
|
+
};
|
|
1378
|
+
+ const compaction = await generateCompaction(ctx, snapshot, event.signal,
|
|
1379
|
+
+ (delta) => ctx.updateCompaction?.({ reason: event.reason, signal: event.signal, delta }));
|
|
1380
|
+
+ if (!compaction)
|
|
1381
|
+
+ throw new SummaryGenerationError("incomplete", "Compaction returned no summary");
|
|
1382
|
+
+ return { compaction };
|
|
1383
|
+
}
|
|
1384
|
+
- finally {
|
|
1385
|
+
- if (!warmJobConsumed)
|
|
1386
|
+
- claimedWarmJob?.controller.abort();
|
|
1387
|
+
+ catch (error) {
|
|
1388
|
+
+ pendingMetadata.delete(event.requestId);
|
|
1389
|
+
+ return { cancel: true, ...(event.signal.aborted ? {} : {
|
|
1390
|
+
+ reason: "Compaction failed; previous context retained", failure: compactionFailure(error, ctx.model),
|
|
1391
|
+
+ }) };
|
|
1392
|
+
}
|
|
1393
|
+
+ finally { claimedWarmJob?.controller.abort(); }
|
|
1394
|
+
});
|
|
1395
|
+
pi.on("model_select", (event, ctx) => handleCompactionModelSelect({
|
|
1396
|
+
event,
|
|
1397
|
+
@@ -788,7 +450,8 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
1398
|
+
tokens: usage.tokens ?? 0,
|
|
1399
|
+
threshold: settings.reserveTokens,
|
|
1400
|
+
});
|
|
1401
|
+
- await applyBlockingCompaction(ctx, EMERGENCY_COMPACTION_INSTRUCTIONS);
|
|
1402
|
+
+ const result = await applyBlockingCompaction(ctx, EMERGENCY_COMPACTION_INSTRUCTIONS);
|
|
1403
|
+
+ if (!result.applied) { ctx.abort("system"); return; }
|
|
1404
|
+
}
|
|
1405
|
+
else if (!breakerCoolingDown &&
|
|
1406
|
+
usageWithPendingPrompt &&
|
|
1407
|
+
@@ -807,7 +470,8 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
1408
|
+
compactionInFlight: speculativeJob !== undefined && !speculativeJob.completed,
|
|
1409
|
+
graceBandEnabled: settings.graceBandEnabled,
|
|
1410
|
+
})) {
|
|
1411
|
+
- await applyBlockingCompaction(ctx, PROACTIVE_COMPACTION_INSTRUCTIONS);
|
|
1412
|
+
+ const result = await applyBlockingCompaction(ctx, PROACTIVE_COMPACTION_INSTRUCTIONS);
|
|
1413
|
+
+ if (!result.applied) { ctx.abort("system"); return; }
|
|
1414
|
+
}
|
|
1415
|
+
else {
|
|
1416
|
+
getLogger(ctx).debug("grace_deferred", {
|
|
1417
|
+
@@ -912,10 +576,8 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
1418
|
+
mode: ctx.mode,
|
|
1419
|
+
})) {
|
|
1420
|
+
getLogger(ctx).debug("idle_trigger", { contextWindow, tokens: usage?.tokens ?? 0 });
|
|
1421
|
+
- idleWarmupAttempt = 0;
|
|
1422
|
+
sessionIdleSinceAgentEnd = true;
|
|
1423
|
+
startSpeculativeCompaction(ctx, idle.IDLE_COMPACTION_INSTRUCTIONS);
|
|
1424
|
+
- armIdleWarmupRetry(ctx);
|
|
1425
|
+
armIdleApply(ctx);
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
@@ -940,11 +602,9 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
1429
|
+
if (warmAction === "replace")
|
|
1430
|
+
invalidateSpeculativeCompaction(ctx);
|
|
1431
|
+
if (warmAction !== "none") {
|
|
1432
|
+
- idleWarmupAttempt = 0;
|
|
1433
|
+
sessionIdleSinceAgentEnd = true;
|
|
1434
|
+
startSpeculativeCompaction(ctx, idle.IDLE_COMPACTION_INSTRUCTIONS);
|
|
1435
|
+
- armIdleWarmupRetry(ctx);
|
|
1436
|
+
- armIdleApply(ctx);
|
|
1437
|
+
+ armIdleApply(ctx);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
@@ -974,9 +634,8 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
|
|
1442
|
+
pi.on("session_shutdown", () => {
|
|
1443
|
+
sessionIdleSinceAgentEnd = false;
|
|
1444
|
+
cancelIdleWarmupRetry();
|
|
1445
|
+
- idleWarmupAttempt = 0;
|
|
1446
|
+
speculativeJob?.controller.abort();
|
|
1447
|
+
speculativeJob = undefined;
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
-//# sourceMappingURL=index.js.map
|
|
1451
|
+
|
|
1452
|
+
+//# sourceMappingURL=index.js.map
|
|
1453
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/lane-policy.js
|
|
1454
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/lane-policy.js
|
|
1455
|
+
@@ -93,7 +93,8 @@ export function createCompactionLanePolicy(options = {}) {
|
|
1456
|
+
// A configured compaction model override makes senpi own summarization
|
|
1457
|
+
// for the lane, so the SDK-native stand-down no longer applies. This is
|
|
1458
|
+
// the escape hatch for lanes whose SDK never fires native compaction.
|
|
1459
|
+
- if (context.getCompactionSettings?.().model)
|
|
1460
|
+
+ const override = context.getCompactionSettings?.().model;
|
|
1461
|
+
+ if (typeof override === "string" && /^[^/\s]+\/\S+$/.test(override))
|
|
1462
|
+
return false;
|
|
1463
|
+
// A settings read failure must never silently disable senpi compaction.
|
|
1464
|
+
const providerSettings = readProviderSettings(context.cwd);
|
|
1465
|
+
@@ -159,4 +160,4 @@ export function collectCompactBoundaryEntries(message) {
|
|
1466
|
+
}
|
|
1467
|
+
return entries;
|
|
1468
|
+
}
|
|
1469
|
+
-//# sourceMappingURL=lane-policy.js.map
|
|
1470
|
+
|
|
1471
|
+
+//# sourceMappingURL=lane-policy.js.map
|
|
1472
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-convert.js
|
|
1473
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-convert.js
|
|
1474
|
+
@@ -1,5 +1,6 @@
|
|
1475
|
+
+import { protectedRemoteInput } from "./task-intent.js";
|
|
1476
|
+
import { convertToLlm } from "../../../messages.js";
|
|
1477
|
+
-import { sessionEntryToContextMessages } from "../../../session-manager.js";
|
|
1478
|
+
+import { sessionEntryToContextMessages, getSessionContextEntryId, SESSION_CONTEXT_ENTRY_ID } from "../../../session-manager.js";
|
|
1479
|
+
import { getOpenAiRemoteCompactionDetails, isRecord, } from "./openai-remote-schema.js";
|
|
1480
|
+
export { getOpenAiRemoteCompactionDetails, isOpenAiContextCompactionItem, isOpenAiRemoteCompactionOutputItem, isRecord, isRetainedRemoteOutputItem, isRetainedResponsesStreamInputItem, OPENAI_REMOTE_COMPACTION_SCHEMA, } from "./openai-remote-schema.js";
|
|
1481
|
+
function parseJsonRecord(value) {
|
|
1482
|
+
@@ -139,8 +140,12 @@ function convertToolResultMessage(message, model) {
|
|
1483
|
+
}
|
|
1484
|
+
function convertLlmMessage(message, messageIndex, model) {
|
|
1485
|
+
switch (message.role) {
|
|
1486
|
+
- case "user":
|
|
1487
|
+
- return [{ role: "user", content: convertUserContent(message.content) }];
|
|
1488
|
+
+ case "user": {
|
|
1489
|
+
+ const item = { role: "user", content: convertUserContent(message.content) };
|
|
1490
|
+
+ const sourceId = getSessionContextEntryId(message) ?? message[SESSION_CONTEXT_ENTRY_ID];
|
|
1491
|
+
+ if (sourceId) Object.defineProperty(item, "__senpiCompactionSourceId", { value: sourceId });
|
|
1492
|
+
+ return [item];
|
|
1493
|
+
+ }
|
|
1494
|
+
case "assistant":
|
|
1495
|
+
return convertAssistantMessage(message, messageIndex);
|
|
1496
|
+
case "toolResult":
|
|
1497
|
+
@@ -185,7 +190,7 @@ export function convertBranchEntries(entries, model) {
|
|
1498
|
+
const details = getOpenAiRemoteCompactionDetails(entry.details);
|
|
1499
|
+
if (details) {
|
|
1500
|
+
flush();
|
|
1501
|
+
- items.push(...details.replacementInput);
|
|
1502
|
+
+ items.push(...protectedRemoteInput(details.replacementInput, details.intentProtection));
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
@@ -194,4 +199,4 @@ export function convertBranchEntries(entries, model) {
|
|
1507
|
+
flush();
|
|
1508
|
+
return items;
|
|
1509
|
+
}
|
|
1510
|
+
-//# sourceMappingURL=openai-remote-convert.js.map
|
|
1511
|
+
|
|
1512
|
+
+//# sourceMappingURL=openai-remote-convert.js.map
|
|
1513
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-responses-v2.js
|
|
1514
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-responses-v2.js
|
|
1515
|
+
@@ -1,3 +1,4 @@
|
|
1516
|
+
+import { compactionFailure } from "./speculative.js";
|
|
1517
|
+
import { isOpenAiRemoteCompactionOutputItem, isRecord, OPENAI_REMOTE_COMPACTION_SCHEMA, providerNativeItem, } from "./openai-remote-convert.js";
|
|
1518
|
+
import { openAiRemoteCompactionIdentity, } from "./openai-remote-model.js";
|
|
1519
|
+
import { runWithRemoteTimeout } from "./openai-remote-timeout.js";
|
|
1520
|
+
@@ -60,7 +61,8 @@ export async function runOpenAiResponsesV2Compaction(args) {
|
|
1521
|
+
extraBody: args.auth.extraBody,
|
|
1522
|
+
headers: withRemoteCompactionV2Header(args.headers),
|
|
1523
|
+
// One remote request per compaction on the subscription lane: a failure falls back to the local summary.
|
|
1524
|
+
- ...(args.model.api === "openai-codex-responses" ? { maxRetries: 0 } : {}),
|
|
1525
|
+
+ maxRetries: 0,
|
|
1526
|
+
+ streamKind: "auxiliary",
|
|
1527
|
+
sessionId: args.sessionId,
|
|
1528
|
+
signal: args.event.signal,
|
|
1529
|
+
transport: "sse",
|
|
1530
|
+
@@ -84,8 +86,10 @@ export async function runOpenAiResponsesV2Compaction(args) {
|
|
1531
|
+
},
|
|
1532
|
+
});
|
|
1533
|
+
const assistant = await stream.result();
|
|
1534
|
+
- if (assistant.stopReason === "error" || assistant.stopReason === "aborted")
|
|
1535
|
+
- return undefined;
|
|
1536
|
+
+ if (assistant.stopReason !== "stop" || assistant.stopDetails?.type === "refusal")
|
|
1537
|
+
+ throw Object.assign(new Error("Remote compaction did not finish normally"), {
|
|
1538
|
+
+ failure: assistant.stopDetails?.type === "refusal" ? { kind: "refused" } : assistant.failure ?? { kind: "incomplete" },
|
|
1539
|
+
+ });
|
|
1540
|
+
const compaction = findCompactionOutput(assistant);
|
|
1541
|
+
if (compaction === undefined)
|
|
1542
|
+
return undefined;
|
|
1543
|
+
@@ -102,6 +106,7 @@ export async function runOpenAiResponsesV2Compaction(args) {
|
|
1544
|
+
transport: "responses-v2",
|
|
1545
|
+
modelId: args.model.id,
|
|
1546
|
+
responseId,
|
|
1547
|
+
+ ...(assistant.responseModel ? { observedProvider: assistant.provider, observedModel: assistant.responseModel } : {}),
|
|
1548
|
+
createdAt,
|
|
1549
|
+
requestInputItemCount: args.request.inputItemCount,
|
|
1550
|
+
retainedInputItemCount: 1,
|
|
1551
|
+
@@ -170,9 +175,10 @@ export async function attemptOpenAiResponsesV2Compaction(args) {
|
|
1552
|
+
requestId: args.requestId,
|
|
1553
|
+
modelId: args.model.id,
|
|
1554
|
+
reason: "responses-v2-error",
|
|
1555
|
+
+ failure: compactionFailure(error, args.model),
|
|
1556
|
+
transport: "responses-v2",
|
|
1557
|
+
});
|
|
1558
|
+
return undefined;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
-//# sourceMappingURL=openai-remote-responses-v2.js.map
|
|
1562
|
+
|
|
1563
|
+
+//# sourceMappingURL=openai-remote-responses-v2.js.map
|
|
1564
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-schema.js
|
|
1565
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-schema.js
|
|
1566
|
+
@@ -49,6 +49,7 @@ export function getOpenAiRemoteCompactionDetails(value) {
|
|
1567
|
+
...(parseOpenAiRemoteCompactionOrigin(value.origin)
|
|
1568
|
+
? { origin: parseOpenAiRemoteCompactionOrigin(value.origin) }
|
|
1569
|
+
: {}),
|
|
1570
|
+
+ ...(isRecord(value.intentProtection) ? { intentProtection: value.intentProtection } : {}),
|
|
1571
|
+
...(isRecord(value.usage) ? { usage: value.usage } : {}),
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
@@ -71,4 +72,4 @@ export function isRetainedResponsesStreamInputItem(item) {
|
|
1575
|
+
return item.role === "user";
|
|
1576
|
+
return "role" in item && item.role === "user";
|
|
1577
|
+
}
|
|
1578
|
+
-//# sourceMappingURL=openai-remote-schema.js.map
|
|
1579
|
+
|
|
1580
|
+
+//# sourceMappingURL=openai-remote-schema.js.map
|
|
1581
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-timeout.d.ts
|
|
1582
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-timeout.d.ts
|
|
1583
|
+
@@ -11,7 +11,7 @@ import type { OpenAiRemoteCompactionModel } from "./openai-remote-model.ts";
|
|
1584
|
+
*/
|
|
1585
|
+
export declare function openAiRemoteCompactionTimeoutMs(model: OpenAiRemoteCompactionModel, compactionTokens: number): number;
|
|
1586
|
+
/** What the compaction does after a remote attempt runs out of its budget. */
|
|
1587
|
+
-export type RemoteCompactionTimeoutNextStep = "websocket" | "compact-endpoint" | "local-summary";
|
|
1588
|
+
+export type RemoteCompactionTimeoutNextStep = "websocket" | "compact-endpoint" | "local-summary" | "stop";
|
|
1589
|
+
/**
|
|
1590
|
+
* One deadline for every remote attempt of a compaction: a lane that falls through v2, WebSocket
|
|
1591
|
+
* and the compact endpoint never holds the session longer than {@link SUMMARIZATION_TOTAL_BUDGET_MS}
|
|
1592
|
+
@@ -35,4 +35,4 @@ export declare function runWithRemoteTimeout<T>(options: {
|
|
1593
|
+
run: (signal: AbortSignal) => Promise<T>;
|
|
1594
|
+
onTimeout: (waitedMs: number) => void;
|
|
1595
|
+
}): Promise<T | undefined>;
|
|
1596
|
+
-//# sourceMappingURL=openai-remote-timeout.d.ts.map
|
|
1597
|
+
|
|
1598
|
+
+//# sourceMappingURL=openai-remote-timeout.d.ts.map
|
|
1599
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-timeout.js
|
|
1600
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote-timeout.js
|
|
1601
|
+
@@ -39,6 +39,7 @@ export function createRemoteCompactionDeadline(attemptBudgetMs, now = Date.now)
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
const NEXT_STEP_TEXT = {
|
|
1605
|
+
+ stop: "stopping with previous context retained",
|
|
1606
|
+
websocket: "retrying over the WebSocket route",
|
|
1607
|
+
"compact-endpoint": "retrying through the compact endpoint",
|
|
1608
|
+
"local-summary": "falling back to a local summary",
|
|
1609
|
+
@@ -88,4 +89,4 @@ export async function runWithRemoteTimeout(options) {
|
|
1610
|
+
options.signal.removeEventListener("abort", abortFromSource);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
-//# sourceMappingURL=openai-remote-timeout.js.map
|
|
1614
|
+
|
|
1615
|
+
+//# sourceMappingURL=openai-remote-timeout.js.map
|
|
1616
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote.d.ts
|
|
1617
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote.d.ts
|
|
1618
|
+
@@ -2,7 +2,7 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
1619
|
+
import { type Api, type AssistantMessage, type Model, type ProviderHeaders } from "@earendil-works/pi-ai";
|
|
1620
|
+
import type { CompactionResult } from "../../../compaction/index.ts";
|
|
1621
|
+
import { type SessionEntry } from "../../../session-manager.ts";
|
|
1622
|
+
-import type { ProviderRequestPreparation, ServiceTier, SessionBeforeCompactEvent } from "../../types.ts";
|
|
1623
|
+
+import type { CompactionFailure, ProviderRequestPreparation, ServiceTier, SessionBeforeCompactEvent } from "../../types.ts";
|
|
1624
|
+
import type { OpenAiCompactBody, OpenAiRemoteCompactionDetails, OpenAiRemoteInputItem, OpenAiRemoteTransport } from "./openai-remote-convert.ts";
|
|
1625
|
+
import { type OpenAiRemoteCompactionModel, type OpenAiRemoteCompactionOrigin } from "./openai-remote-model.ts";
|
|
1626
|
+
import { type RemoteCompactionTimeout } from "./openai-remote-timeout.ts";
|
|
1627
|
+
@@ -121,7 +121,7 @@ export declare function buildOpenAiResponsesStreamCompactionResult(options: {
|
|
1628
|
+
now: () => number;
|
|
1629
|
+
origin?: OpenAiRemoteCompactionOrigin;
|
|
1630
|
+
}): OpenAiRemoteCompactionResult;
|
|
1631
|
+
-export declare function runOpenAiRemoteCompaction(ctx: OpenAiRemoteCompactionContext, event: SessionBeforeCompactEvent, emit?: EmitCompactionEvent, dependencies?: OpenAiRemoteCompactionDependencies): Promise<OpenAiRemoteCompactionResult | undefined>;
|
|
1632
|
+
+export declare function runOpenAiRemoteCompaction(ctx: OpenAiRemoteCompactionContext, event: SessionBeforeCompactEvent, emit?: EmitCompactionEvent, dependencies?: OpenAiRemoteCompactionDependencies): Promise<{ status: "ineligible" } | { status: "failed"; failure: CompactionFailure } | { status: "accepted"; compaction: OpenAiRemoteCompactionResult }>;
|
|
1633
|
+
/**
|
|
1634
|
+
* Mark the exact checkpoint-owned messages before later context hooks run.
|
|
1635
|
+
* The markers carry entry identity, not payload values; the Responses converter
|
|
1636
|
+
@@ -136,4 +136,4 @@ export declare function rewriteOpenAiPayloadWithRemoteCompaction(payload: unknow
|
|
1637
|
+
branchEntries: SessionEntry[];
|
|
1638
|
+
origin?: OpenAiRemoteCompactionOrigin;
|
|
1639
|
+
}, emit?: EmitCompactionEvent): unknown | undefined;
|
|
1640
|
+
-//# sourceMappingURL=openai-remote.d.ts.map
|
|
1641
|
+
|
|
1642
|
+
+//# sourceMappingURL=openai-remote.d.ts.map
|
|
1643
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote.js
|
|
1644
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/openai-remote.js
|
|
1645
|
+
@@ -1,6 +1,10 @@
|
|
1646
|
+
import { convertResponsesMessages, getContextProvenance, normalizeContext, } from "@earendil-works/pi-ai";
|
|
1647
|
+
import { deriveOpenAIPromptCacheKey } from "@earendil-works/pi-ai/api/openai-prompt-cache";
|
|
1648
|
+
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
1649
|
+
+import { compactionFailure } from "./speculative.js";
|
|
1650
|
+
+import { collectIntentProtection, protectedRemoteInput, taskIntentSummary, retainedUserText } from "./task-intent.js";
|
|
1651
|
+
+import { estimateTokens } from "../../../compaction/index.js";
|
|
1652
|
+
+import { resolveEffectiveReserveTokens } from "./policy.js";
|
|
1653
|
+
import { convertToLlm } from "../../../messages.js";
|
|
1654
|
+
import { buildContextEntries, buildSessionContext, getSessionContextEntryId, SESSION_CONTEXT_ENTRY_ID, sessionEntryToContextMessages, } from "../../../session-manager.js";
|
|
1655
|
+
import { convertBranchEntries, convertPendingMessages, getOpenAiRemoteCompactionDetails, isOpenAiContextCompactionItem, isOpenAiRemoteCompactionOutputItem, isRecord, isRetainedRemoteOutputItem, isRetainedResponsesStreamInputItem, OPENAI_REMOTE_COMPACTION_SCHEMA, providerNativeItem, } from "./openai-remote-convert.js";
|
|
1656
|
+
@@ -20,9 +24,10 @@ const OPENAI_RESPONSES_TOOL_CALL_PROVIDERS = new Set(["openai", "chatgpt-subscri
|
|
1657
|
+
export function createOpenAiRemoteCompactionRequest(options) {
|
|
1658
|
+
if (!isOpenAiRemoteCompactionModel(options.model))
|
|
1659
|
+
return undefined;
|
|
1660
|
+
- const input = options.messages
|
|
1661
|
+
+ const converted = options.messages
|
|
1662
|
+
? convertPendingMessages(options.messages, options.model)
|
|
1663
|
+
: convertBranchEntries(options.branchEntries, options.model);
|
|
1664
|
+
+ const input = protectedRemoteInput(converted, collectIntentProtection(options.branchEntries));
|
|
1665
|
+
if (input.length === 0)
|
|
1666
|
+
return undefined;
|
|
1667
|
+
return {
|
|
1668
|
+
@@ -53,6 +58,7 @@ function parseOpenAiCompactedResponse(options) {
|
|
1669
|
+
created_at: typeof options.value.created_at === "number" ? options.value.created_at : Math.floor(options.now() / 1000),
|
|
1670
|
+
object: "response.compaction",
|
|
1671
|
+
output: options.value.output.filter((item) => isRecord(item)),
|
|
1672
|
+
+ ...(typeof options.value.model === "string" ? { observedModel: options.value.model } : {}),
|
|
1673
|
+
...(isRecord(options.value.usage) ? { usage: options.value.usage } : {}),
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
@@ -69,6 +75,7 @@ export function buildOpenAiRemoteCompactionResult(options) {
|
|
1677
|
+
transport: "compact-endpoint",
|
|
1678
|
+
modelId: options.model.id,
|
|
1679
|
+
responseId: options.response.id,
|
|
1680
|
+
+ ...(options.response.observedModel ? { observedProvider: options.model.provider, observedModel: options.response.observedModel } : {}),
|
|
1681
|
+
createdAt: options.response.created_at,
|
|
1682
|
+
requestInputItemCount: options.requestInputItemCount,
|
|
1683
|
+
retainedInputItemCount: replacementInput.length,
|
|
1684
|
+
@@ -137,6 +144,7 @@ export function buildOpenAiResponsesStreamCompactionResult(options) {
|
|
1685
|
+
transport: "websocket",
|
|
1686
|
+
modelId: options.model.id,
|
|
1687
|
+
responseId: options.response.responseId ?? `response-${options.now()}`,
|
|
1688
|
+
+ ...(options.response.responseModel ? { observedProvider: options.response.provider, observedModel: options.response.responseModel } : {}),
|
|
1689
|
+
createdAt: Math.floor(options.response.timestamp / 1000),
|
|
1690
|
+
requestInputItemCount: options.requestInput.length,
|
|
1691
|
+
retainedInputItemCount: replacementInput.length,
|
|
1692
|
+
@@ -177,10 +185,14 @@ async function runOpenAiResponsesStreamCompaction(options) {
|
|
1693
|
+
sessionId: options.request.sessionId,
|
|
1694
|
+
signal: options.signal,
|
|
1695
|
+
transport: "websocket",
|
|
1696
|
+
+ streamKind: "auxiliary",
|
|
1697
|
+
+ maxRetries: 0,
|
|
1698
|
+
});
|
|
1699
|
+
const response = await stream.result();
|
|
1700
|
+
- if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
1701
|
+
- return undefined;
|
|
1702
|
+
+ if (response.stopReason !== "stop" || response.stopDetails?.type === "refusal") {
|
|
1703
|
+
+ throw Object.assign(new Error("Remote compaction did not finish normally"), {
|
|
1704
|
+
+ failure: response.stopDetails?.type === "refusal" ? { kind: "refused" } : response.failure ?? { kind: "incomplete" },
|
|
1705
|
+
+ });
|
|
1706
|
+
}
|
|
1707
|
+
return buildOpenAiResponsesStreamCompactionResult({
|
|
1708
|
+
model: options.model,
|
|
1709
|
+
@@ -221,6 +233,7 @@ async function runOpenAiCompactEndpointCompaction(options) {
|
|
1710
|
+
requestId: options.requestId,
|
|
1711
|
+
modelId: options.model.id,
|
|
1712
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1713
|
+
+ failure: compactionFailure(error, options?.model),
|
|
1714
|
+
transport: "compact-endpoint",
|
|
1715
|
+
});
|
|
1716
|
+
return undefined;
|
|
1717
|
+
@@ -233,6 +246,7 @@ async function runOpenAiCompactEndpointCompaction(options) {
|
|
1718
|
+
requestId: options.requestId,
|
|
1719
|
+
modelId: options.model.id,
|
|
1720
|
+
reason: `HTTP ${response.status}`,
|
|
1721
|
+
+ failure: { kind: response.status === 429 ? "quota" : response.status === 401 ? "auth_expired" : response.status === 403 ? "org_denied" : "provider" },
|
|
1722
|
+
transport: "compact-endpoint",
|
|
1723
|
+
});
|
|
1724
|
+
return undefined;
|
|
1725
|
+
@@ -249,6 +263,7 @@ async function runOpenAiCompactEndpointCompaction(options) {
|
|
1726
|
+
requestId: options.requestId,
|
|
1727
|
+
modelId: options.model.id,
|
|
1728
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1729
|
+
+ failure: compactionFailure(error, options?.model),
|
|
1730
|
+
transport: "compact-endpoint",
|
|
1731
|
+
});
|
|
1732
|
+
return undefined;
|
|
1733
|
+
@@ -290,6 +305,7 @@ async function runOpenAiCompactEndpointCompaction(options) {
|
|
1734
|
+
requestId: options.requestId,
|
|
1735
|
+
modelId: options.model.id,
|
|
1736
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1737
|
+
+ failure: compactionFailure(error, options?.model),
|
|
1738
|
+
transport: "compact-endpoint",
|
|
1739
|
+
});
|
|
1740
|
+
return undefined;
|
|
1741
|
+
@@ -323,6 +339,51 @@ function resolveRemoteStreamRunner(ctx, dependencies) {
|
|
1742
|
+
return (model, context, options) => streamSimple(model, context, options);
|
|
1743
|
+
}
|
|
1744
|
+
export async function runOpenAiRemoteCompaction(ctx, event, emit, dependencies = {}) {
|
|
1745
|
+
+ const protection = collectIntentProtection(event.branchEntries);
|
|
1746
|
+
+ const protectedInput = protectedRemoteInput([], protection);
|
|
1747
|
+
+ const model = ctx.model;
|
|
1748
|
+
+ const settings = ctx.getCompactionSettings?.() ?? event.preparation.settings;
|
|
1749
|
+
+ const protectionTokens = estimateTokens({ role: "user", content: JSON.stringify(protectedInput) });
|
|
1750
|
+
+ if (model && protectionTokens > model.contextWindow - resolveEffectiveReserveTokens(model.contextWindow, settings))
|
|
1751
|
+
+ return { status: "ineligible" }; // Before dispatch only: same-model text compaction is permitted.
|
|
1752
|
+
+ const attempt = await runOpenAiRemoteCompactionAttempt(ctx, event, emit, dependencies);
|
|
1753
|
+
+ if (attempt.status !== "accepted") return attempt;
|
|
1754
|
+
+ const result = attempt.compaction;
|
|
1755
|
+
+ const checkpointTexts = new Set(convertPendingMessages(buildSessionContext(event.branchEntries).messages.filter((message) => message.role === "compactionSummary"), model).map(retainedUserText));
|
|
1756
|
+
+ // The accepted native envelope already covers the request checkpoint. Keep its original intent outside it, not a second raw copy of the old summary.
|
|
1757
|
+
+ const retained = result.details.replacementInput.filter((item) => !checkpointTexts.has(retainedUserText(item)));
|
|
1758
|
+
+ const replacementInput = protectedRemoteInput(retained, protection);
|
|
1759
|
+
+ return { status: "accepted", compaction: { ...result,
|
|
1760
|
+
+ summary: taskIntentSummary(result.summary, protection.taskIntent) + (protection.sources.length ? `\n<source-user-text>\n${JSON.stringify(protection.sources)}\n</source-user-text>` : ""),
|
|
1761
|
+
+ details: { ...result.details, intentProtection: protection, replacementInput, retainedInputItemCount: replacementInput.length,
|
|
1762
|
+
+ selectionMode: settings.model === undefined || settings.model === "auto" ? "auto" : "explicit",
|
|
1763
|
+
+ requestedProvider: model.provider, requestedModel: model.id },
|
|
1764
|
+
+ } };
|
|
1765
|
+
+}
|
|
1766
|
+
+async function runOpenAiRemoteCompactionAttempt(ctx, event, emit, dependencies = {}) {
|
|
1767
|
+
+ let dispatched = false;
|
|
1768
|
+
+ let failure;
|
|
1769
|
+
+ try {
|
|
1770
|
+
+ const compaction = await runRemoteAttempt(ctx, event, (data) => {
|
|
1771
|
+
+ if (data.action === "remote_started")
|
|
1772
|
+
+ dispatched = true;
|
|
1773
|
+
+ if (data.failure)
|
|
1774
|
+
+ failure = data.failure;
|
|
1775
|
+
+ if (data.reason === REMOTE_COMPACTION_TIMEOUT_REASON || data.reason === REMOTE_COMPACTION_BUDGET_EXHAUSTED_REASON)
|
|
1776
|
+
+ failure = { kind: "timeout" };
|
|
1777
|
+
+ emit?.(data);
|
|
1778
|
+
+ }, dependencies);
|
|
1779
|
+
+ if (compaction)
|
|
1780
|
+
+ return { status: "accepted", compaction };
|
|
1781
|
+
+ return dispatched ? { status: "failed", failure: failure ?? { kind: "provider" } } : { status: "ineligible" };
|
|
1782
|
+
+ }
|
|
1783
|
+
+ catch (error) {
|
|
1784
|
+
+ if (event.signal.aborted)
|
|
1785
|
+
+ throw error;
|
|
1786
|
+
+ return { status: "failed", failure: compactionFailure(error, ctx.model) };
|
|
1787
|
+
+ }
|
|
1788
|
+
+}
|
|
1789
|
+
+async function runRemoteAttempt(ctx, event, emit, dependencies) {
|
|
1790
|
+
const model = ctx.model;
|
|
1791
|
+
if (!isOpenAiRemoteCompactionModel(model) || event.reason === "branch") {
|
|
1792
|
+
emit?.({
|
|
1793
|
+
@@ -376,12 +437,16 @@ export async function runOpenAiRemoteCompaction(ctx, event, emit, dependencies =
|
|
1794
|
+
});
|
|
1795
|
+
return undefined;
|
|
1796
|
+
}
|
|
1797
|
+
+ const requestTokens = estimateTokens({ role: "user", content: JSON.stringify(request.body) });
|
|
1798
|
+
+ const requestSettings = ctx.getCompactionSettings?.() ?? event.preparation.settings;
|
|
1799
|
+
+ if (requestTokens > requestModel.contextWindow - resolveEffectiveReserveTokens(requestModel.contextWindow, requestSettings))
|
|
1800
|
+
+ return undefined;
|
|
1801
|
+
const remoteTimeoutMs = dependencies.remoteTimeoutMs ?? openAiRemoteCompactionTimeoutMs(requestModel, request.tokensBefore);
|
|
1802
|
+
const remoteDeadline = createRemoteCompactionDeadline(remoteTimeoutMs);
|
|
1803
|
+
const timedOut = (waitedMs, next) => ({
|
|
1804
|
+
waitedMs,
|
|
1805
|
+
tokens: request.tokensBefore,
|
|
1806
|
+
- next: remoteDeadline.remainingMs() > 0 ? next : "local-summary",
|
|
1807
|
+
+ next: "stop",
|
|
1808
|
+
});
|
|
1809
|
+
const remoteBudgetExhausted = (transport) => {
|
|
1810
|
+
if (remoteDeadline.remainingMs() > 0)
|
|
1811
|
+
@@ -454,8 +519,7 @@ export async function runOpenAiRemoteCompaction(ctx, event, emit, dependencies =
|
|
1812
|
+
? "websocket"
|
|
1813
|
+
: "compact-endpoint"),
|
|
1814
|
+
});
|
|
1815
|
+
- if (result || requestModel.api === "openai-codex-responses")
|
|
1816
|
+
- return result;
|
|
1817
|
+
+ return result;
|
|
1818
|
+
}
|
|
1819
|
+
if (supportsOpenAiResponsesWebSocket(requestModel) && !remoteBudgetExhausted("websocket")) {
|
|
1820
|
+
const websocketHeaders = Object.fromEntries(requestHeaders.entries());
|
|
1821
|
+
@@ -529,9 +593,11 @@ export async function runOpenAiRemoteCompaction(ctx, event, emit, dependencies =
|
|
1822
|
+
requestId: event.requestId,
|
|
1823
|
+
modelId: requestModel.id,
|
|
1824
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1825
|
+
+ failure: compactionFailure(error, requestModel),
|
|
1826
|
+
transport: "websocket",
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
+ return undefined;
|
|
1830
|
+
}
|
|
1831
|
+
const transformedPayload = providerRequest ? await providerRequest.transformPayload(request.body) : request.body;
|
|
1832
|
+
if (!isOpenAiCompactBody(transformedPayload)) {
|
|
1833
|
+
@@ -730,7 +796,7 @@ export function rewriteOpenAiPayloadWithRemoteCompaction(payload, options, emit)
|
|
1834
|
+
}
|
|
1835
|
+
const input = [
|
|
1836
|
+
...payloadInput.slice(0, checkpointStart).filter(isRecord),
|
|
1837
|
+
- ...remote.details.replacementInput,
|
|
1838
|
+
+ ...protectedRemoteInput(remote.details.replacementInput, remote.details.intentProtection),
|
|
1839
|
+
...payloadInput.slice(checkpointStart + expectedOrdinals.length).filter(isRecord),
|
|
1840
|
+
];
|
|
1841
|
+
emit?.({
|
|
1842
|
+
@@ -743,4 +809,4 @@ export function rewriteOpenAiPayloadWithRemoteCompaction(payload, options, emit)
|
|
1843
|
+
});
|
|
1844
|
+
return { ...payload, input };
|
|
1845
|
+
}
|
|
1846
|
+
-//# sourceMappingURL=openai-remote.js.map
|
|
1847
|
+
|
|
1848
|
+
+//# sourceMappingURL=openai-remote.js.map
|
|
1849
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/prompts.js
|
|
1850
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/prompts.js
|
|
1851
|
+
@@ -6,12 +6,12 @@ Write one <task-intent> block with ORIGINAL_REQUEST, TASK_TYPE, MUST_PRESERVE, a
|
|
1852
|
+
const TASK_INTENT_UPDATE_ACQUIRE_GPT = `Write one <task-intent> block with ORIGINAL_REQUEST, TASK_TYPE, MUST_PRESERVE, and MUST_NOT_LOSE before <summary>.`;
|
|
1853
|
+
const TASK_INTENT_UPDATE_ANCHOR_CLAUDE = `<task-intent>
|
|
1854
|
+
{{taskIntent}}
|
|
1855
|
+
-Immutable provenance of the original task. Do not rewrite it. Newer explicit user steering overrides it.
|
|
1856
|
+
-</task-intent>`;
|
|
1857
|
+
+</task-intent>
|
|
1858
|
+
+Immutable provenance of the original task. Do not rewrite it. Newer explicit user steering overrides it.`;
|
|
1859
|
+
const TASK_INTENT_UPDATE_ANCHOR_GPT = `<task-intent>
|
|
1860
|
+
{{taskIntent}}
|
|
1861
|
+
-Immutable provenance of the original task. Do not rewrite it. Newer explicit user steering overrides it.
|
|
1862
|
+
-</task-intent>`;
|
|
1863
|
+
+</task-intent>
|
|
1864
|
+
+Immutable provenance of the original task. Do not rewrite it. Newer explicit user steering overrides it.`;
|
|
1865
|
+
export const MERGED_COMPACTION_PROMPT_SYSTEM = `[SYSTEM DIRECTIVE: OH-MY-OPENCODE - COMPACTION CONTEXT]
|
|
1866
|
+
|
|
1867
|
+
You are the COMPACTION ARCHIVIST. Create a structured handoff summary that lets the next agent continue this exact session without restarting, re-searching, or losing constraints.
|
|
1868
|
+
@@ -19,7 +19,7 @@ You are the COMPACTION ARCHIVIST. Create a structured handoff summary that lets
|
|
1869
|
+
Cardinal rules:
|
|
1870
|
+
R1. Quote user requests and constraints VERBATIM. Do not paraphrase.
|
|
1871
|
+
R2. If a section has no content, write "None." Never delete a section.
|
|
1872
|
+
-R3. Where a previous summary is supplied, treat its User Requests, Final Goal, and Constraints fields as IMMUTABLE. Append, never rewrite, those three sections.
|
|
1873
|
+
+R3. Preserve original requests and durable constraints as quoted provenance. Latest explicit user steering governs the current goal and next action. An answer-local restriction applies only to that answer.
|
|
1874
|
+
R4. Preserve every session_id, file path, and identifier byte-for-byte.
|
|
1875
|
+
|
|
1876
|
+
Do NOT use tools. Output only the requested summary block.`;
|
|
1877
|
+
@@ -83,7 +83,7 @@ export const MERGED_COMPACTION_PROMPT_UPDATE = `[USER]
|
|
1878
|
+
[INTERNAL COMPACTION UPDATE INSTRUCTION: NOT CONVERSATION HISTORY]
|
|
1879
|
+
The messages above are NEW conversation messages to incorporate into the existing summary provided in <previous-summary> tags.
|
|
1880
|
+
|
|
1881
|
+
-R3 enforcement: R3. Where a previous summary is supplied, treat its User Requests, Final Goal, and Constraints fields as IMMUTABLE. Append, never rewrite, those three sections.
|
|
1882
|
+
+Preserve original requests and durable constraints as quoted provenance. Latest explicit user steering governs the current goal and next action. Keep answer-local restrictions local to their answer.
|
|
1883
|
+
|
|
1884
|
+
PASS 1 - Internal task-intent extraction
|
|
1885
|
+
{{taskIntentInstruction}}
|
|
1886
|
+
@@ -176,7 +176,7 @@ export const MERGED_COMPACTION_PROMPT_TURN_PREFIX = `[USER]
|
|
1887
|
+
Create a compact prefix summary for the next turn. Emit only the sections listed below.
|
|
1888
|
+
|
|
1889
|
+
PASS 1 - Internal task-intent extraction
|
|
1890
|
+
-Silently determine the current task intent and the minimum context needed for the next turn.
|
|
1891
|
+
+Preserve the original request and durable constraints from the entire history together with the split turn. Latest explicit steering governs the current goal and next action. Do not promote answer-local restrictions into durable constraints.
|
|
1892
|
+
|
|
1893
|
+
PASS 2 - Emit summary biased toward Pass 1
|
|
1894
|
+
The structured output portion MUST be wrapped as \`<summary>...</summary>\` XML.
|
|
1895
|
+
@@ -214,9 +214,7 @@ function buildUserPrompt(options) {
|
|
1896
|
+
case "branch":
|
|
1897
|
+
return MERGED_COMPACTION_PROMPT_BRANCH;
|
|
1898
|
+
case "turn_prefix":
|
|
1899
|
+
- return options.taskIntent
|
|
1900
|
+
- ? MERGED_COMPACTION_PROMPT_TURN_PREFIX
|
|
1901
|
+
- : mergeTaskIntent(MERGED_COMPACTION_PROMPT_TURN_PREFIX, promptFamilyText(options.promptFamily, TASK_INTENT_ACQUISITION_CLAUDE, TASK_INTENT_ACQUISITION_GPT));
|
|
1902
|
+
+ return buildUserPrompt({ ...options, variant: options.previousSummary || options.taskIntent ? "update" : "default" });
|
|
1903
|
+
}
|
|
1904
|
+
const exhaustiveCheck = options.variant;
|
|
1905
|
+
return exhaustiveCheck;
|
|
1906
|
+
@@ -254,4 +252,4 @@ export function resolvePromptFamily(model) {
|
|
1907
|
+
function sanitizeTaskIntent(text) {
|
|
1908
|
+
return text.split("</task-intent>").join("[/task-intent]");
|
|
1909
|
+
}
|
|
1910
|
+
-//# sourceMappingURL=prompts.js.map
|
|
1911
|
+
|
|
1912
|
+
+//# sourceMappingURL=prompts.js.map
|
|
1913
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/rejected-recovery.d.ts
|
|
1914
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/rejected-recovery.d.ts
|
|
1915
|
+
@@ -3,9 +3,9 @@ import { type CompactionPreparation, type CompactionResult } from "../../../comp
|
|
1916
|
+
import { type SessionEntry } from "../../../session-manager.ts";
|
|
1917
|
+
export declare const REJECTED_RECOVERY_ENTRY = "compaction-recovery-rejected";
|
|
1918
|
+
/** Shared with core admission: a fallback's conservative estimate cannot overrule a usable summary. */
|
|
1919
|
+
-export declare function wouldCompactionOverflow(pathEntries: SessionEntry[], compactionResult: CompactionResult, fromExtension: boolean, model: Model<string>, settings: CompactionPreparation["settings"]): boolean;
|
|
1920
|
+
+export declare function wouldCompactionOverflow(pathEntries: SessionEntry[], compactionResult: CompactionResult, fromExtension: boolean, model: Model<string>, settings: CompactionPreparation["settings"], pendingMessages?: CompactionPreparation["messagesToSummarize"]): boolean;
|
|
1921
|
+
/** State, not elapsed time or a synthetic message revision, releases the latch. */
|
|
1922
|
+
export declare function compactionRecoveryStateKey(branch: readonly SessionEntry[], model: Model<string> | undefined, settings: CompactionPreparation["settings"]): string;
|
|
1923
|
+
/** Persisted on the branch, so extension reload and session reopen cannot retry it. */
|
|
1924
|
+
export declare function isAutomaticCompactionBlocked(branch: readonly SessionEntry[], model: Model<string> | undefined, settings: CompactionPreparation["settings"]): boolean;
|
|
1925
|
+
-//# sourceMappingURL=rejected-recovery.d.ts.map
|
|
1926
|
+
|
|
1927
|
+
+//# sourceMappingURL=rejected-recovery.d.ts.map
|
|
1928
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/rejected-recovery.js
|
|
1929
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/rejected-recovery.js
|
|
1930
|
+
@@ -3,10 +3,12 @@ import { estimateTokens } from "../../../compaction/index.js";
|
|
1931
|
+
import { admitCursorHistory, cursorAdmissionBudgetBytes } from "../../../cursor-history-admission.js";
|
|
1932
|
+
import { filterContextExcludedMessages } from "../../../messages.js";
|
|
1933
|
+
import { buildContextEntries, buildSessionContext, } from "../../../session-manager.js";
|
|
1934
|
+
+import { getOpenAiRemoteCompactionDetails, convertPendingMessages } from "./openai-remote-convert.js";
|
|
1935
|
+
+import { protectedRemoteInput } from "./task-intent.js";
|
|
1936
|
+
import { resolveEffectiveReserveTokens } from "./policy.js";
|
|
1937
|
+
export const REJECTED_RECOVERY_ENTRY = "compaction-recovery-rejected";
|
|
1938
|
+
/** Shared with core admission: a fallback's conservative estimate cannot overrule a usable summary. */
|
|
1939
|
+
-export function wouldCompactionOverflow(pathEntries, compactionResult, fromExtension, model, settings) {
|
|
1940
|
+
+export function wouldCompactionOverflow(pathEntries, compactionResult, fromExtension, model, settings, pendingMessages = []) {
|
|
1941
|
+
const currentLeaf = pathEntries.at(-1);
|
|
1942
|
+
if (!currentLeaf)
|
|
1943
|
+
return false;
|
|
1944
|
+
@@ -29,7 +31,13 @@ export function wouldCompactionOverflow(pathEntries, compactionResult, fromExten
|
|
1945
|
+
budgetBytes: cursorAdmissionBudgetBytes(model.contextWindow),
|
|
1946
|
+
}).messages ?? messages;
|
|
1947
|
+
}
|
|
1948
|
+
- const tokens = filterContextExcludedMessages(messages).reduce((total, message) => total + estimateTokens(message), 0);
|
|
1949
|
+
+ messages = [...messages, ...pendingMessages];
|
|
1950
|
+
+ const remote = getOpenAiRemoteCompactionDetails(compactionResult.details);
|
|
1951
|
+
+ const budgetMessages = remote ? [{ role: "user", content: JSON.stringify([
|
|
1952
|
+
+ ...protectedRemoteInput(remote.replacementInput, remote.intentProtection),
|
|
1953
|
+
+ ...convertPendingMessages(pendingMessages, model),
|
|
1954
|
+
+ ]) }] : messages;
|
|
1955
|
+
+ const tokens = filterContextExcludedMessages(budgetMessages).reduce((total, message) => total + estimateTokens(message), 0);
|
|
1956
|
+
return tokens > model.contextWindow - resolveEffectiveReserveTokens(model.contextWindow, settings);
|
|
1957
|
+
}
|
|
1958
|
+
/** State, not elapsed time or a synthetic message revision, releases the latch. */
|
|
1959
|
+
@@ -70,4 +78,4 @@ export function isAutomaticCompactionBlocked(branch, model, settings) {
|
|
1960
|
+
return false;
|
|
1961
|
+
return ("stateKey" in rejection.data && rejection.data.stateKey === compactionRecoveryStateKey(branch, model, settings));
|
|
1962
|
+
}
|
|
1963
|
+
-//# sourceMappingURL=rejected-recovery.js.map
|
|
1964
|
+
|
|
1965
|
+
+//# sourceMappingURL=rejected-recovery.js.map
|
|
1966
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative-summary.js
|
|
1967
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative-summary.js
|
|
1968
|
+
@@ -4,6 +4,7 @@ import { estimateTokens } from "../../../compaction/compaction.js";
|
|
1969
|
+
import { consumeStreamWithIdleTimeout, DEFAULT_SUMMARIZATION_IDLE_TIMEOUT_MS, summarizationMaxDurationMs, } from "../../../compaction/stream-watchdog.js";
|
|
1970
|
+
import { convertToLlm } from "../../../messages.js";
|
|
1971
|
+
import { repairOrphanedToolResults } from "./repair-tool-pairs.js";
|
|
1972
|
+
+import { estimateTotalTokens } from "./overflow-retry.js";
|
|
1973
|
+
import { normalizeSummarizationTurnOrder } from "./summarization-turn-order.js";
|
|
1974
|
+
const SUMMARY_TOKEN_HEADROOM = 32_768;
|
|
1975
|
+
const SUMMARY_CONTEXT_WINDOW_RESERVE_RATIO = 0.5;
|
|
1976
|
+
@@ -106,6 +107,15 @@ export async function generateSummaryMessage(options) {
|
|
1977
|
+
messages: repairOrphanedToolResults(normalizeSummarizationTurnOrder(convertToLlm(providerRequest?.messages ?? requestMessages))),
|
|
1978
|
+
...(options.snapshot.tools && options.snapshot.tools.length > 0 ? { tools: options.snapshot.tools } : {}),
|
|
1979
|
+
};
|
|
1980
|
+
+ const inputTokens = estimateTotalTokens([
|
|
1981
|
+
+ ...requestContext.messages,
|
|
1982
|
+
+ { role: "user", content: requestContext.systemPrompt },
|
|
1983
|
+
+ { role: "user", content: JSON.stringify(requestContext.tools ?? []) },
|
|
1984
|
+
+ ]);
|
|
1985
|
+
+ if (inputTokens + summaryMaxTokens(options.snapshot.model, options.snapshot.contextWindow) > options.snapshot.contextWindow) {
|
|
1986
|
+
+ throw Object.assign(new Error("Compaction request exceeds the selected model context budget"),
|
|
1987
|
+
+ { failure: { kind: "context_budget", provider: options.snapshot.model.provider, model: options.snapshot.model.id } });
|
|
1988
|
+
+ }
|
|
1989
|
+
const headers = providerRequest
|
|
1990
|
+
? await providerRequest.transformHeaders(options.auth.headers ?? {})
|
|
1991
|
+
: options.auth.headers;
|
|
1992
|
+
@@ -113,6 +123,8 @@ export async function generateSummaryMessage(options) {
|
|
1993
|
+
apiKey: options.auth.apiKey,
|
|
1994
|
+
headers,
|
|
1995
|
+
extraBody: options.auth.extraBody,
|
|
1996
|
+
+ env: options.auth.env,
|
|
1997
|
+
+ streamKind: "auxiliary",
|
|
1998
|
+
onPayload: async (payload, model) => {
|
|
1999
|
+
const sanitized = model.api === "anthropic-messages" ? sanitizeAnthropicPayload(payload) : payload;
|
|
2000
|
+
return providerRequest ? await providerRequest.transformPayload(sanitized) : sanitized;
|
|
2001
|
+
@@ -142,4 +154,4 @@ export async function generateSummaryMessage(options) {
|
|
2002
|
+
options.signal.removeEventListener("abort", onCallerAbort);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
-//# sourceMappingURL=speculative-summary.js.map
|
|
2006
|
+
|
|
2007
|
+
+//# sourceMappingURL=speculative-summary.js.map
|
|
2008
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative.d.ts
|
|
2009
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative.d.ts
|
|
2010
|
+
@@ -4,7 +4,7 @@ import { type CompactionPreparation, type CompactionResult } from "../../../comp
|
|
2011
|
+
import { type WarmAnchorSnapshot } from "../../../compaction/warm-anchor.ts";
|
|
2012
|
+
import type { ModelRegistry } from "../../../model-registry.ts";
|
|
2013
|
+
import type { ReadonlySessionManager } from "../../../session-manager.ts";
|
|
2014
|
+
-import type { ApplyCompactionResult, ContextUsage, ProviderRequestPreparation } from "../../types.ts";
|
|
2015
|
+
+import type { CompactionFailure, ApplyCompactionResult, ContextUsage, ProviderRequestPreparation } from "../../types.ts";
|
|
2016
|
+
import { type MergedCompactionPromptVariant } from "./prompts.ts";
|
|
2017
|
+
export { createEmergencyPruneLatch, type EmergencyPruneLatch, hardLimitEmergencyPrune, truncateContextMessages, } from "./emergency-prune.ts";
|
|
2018
|
+
type CompactionProgressCallback = (delta: string) => void;
|
|
2019
|
+
@@ -14,13 +14,14 @@ export interface SpeculativeCompactionContext {
|
|
2020
|
+
modelRegistry?: ModelRegistry;
|
|
2021
|
+
getContextUsage(): ContextUsage | undefined;
|
|
2022
|
+
getCompactionSettings?(): CompactionPreparation["settings"];
|
|
2023
|
+
- getCompactionModel?(): Model<any> | undefined;
|
|
2024
|
+
+ getCompactionModel?(inputTokens?: number): Model<any> | undefined;
|
|
2025
|
+
getMessageRevision(): number;
|
|
2026
|
+
getSystemPrompt?(): string;
|
|
2027
|
+
prepareProviderRequest?(messages: AgentMessage[]): Promise<ProviderRequestPreparation>;
|
|
2028
|
+
isIdle?(): boolean;
|
|
2029
|
+
applyCompaction(precomputed: CompactionResult, options: {
|
|
2030
|
+
reason: "extension";
|
|
2031
|
+
+ blocking?: boolean;
|
|
2032
|
+
expectedRevision?: number;
|
|
2033
|
+
expectedWarmAnchor?: WarmAnchorSnapshot;
|
|
2034
|
+
signal?: AbortSignal;
|
|
2035
|
+
@@ -56,7 +57,7 @@ export type SpeculativeCompactionResult = ApplyCompactionResult | {
|
|
2036
|
+
* text happens to look retryable must still surface loudly; the message
|
|
2037
|
+
* string alone cannot encode that.
|
|
2038
|
+
*/
|
|
2039
|
+
-export type SummaryRequestFailureKind = "upstream-stream-truncated";
|
|
2040
|
+
+export type SummaryRequestFailureKind = "upstream-stream-truncated" | CompactionFailure["kind"];
|
|
2041
|
+
export declare class SummaryRequestError extends Error {
|
|
2042
|
+
readonly transient: boolean;
|
|
2043
|
+
readonly failureKind?: SummaryRequestFailureKind;
|
|
2044
|
+
@@ -70,9 +71,12 @@ export declare class SummaryRequestError extends Error {
|
|
2045
|
+
constructor(message: string, transient: boolean, failureKind?: SummaryRequestFailureKind, refused?: boolean);
|
|
2046
|
+
}
|
|
2047
|
+
export declare class SummaryGenerationError extends Error {
|
|
2048
|
+
- readonly kind: "auth" | "empty-summary";
|
|
2049
|
+
- constructor(kind: "auth" | "empty-summary", message: string);
|
|
2050
|
+
+ readonly kind: CompactionFailure["kind"];
|
|
2051
|
+
+ readonly failure: CompactionFailure;
|
|
2052
|
+
+ constructor(kind: CompactionFailure["kind"], message: string);
|
|
2053
|
+
}
|
|
2054
|
+
+export declare function compactionFailure(error: unknown, model?: { provider: string; id: string }): CompactionFailure;
|
|
2055
|
+
+export declare function validateSummaryResponse(response: unknown): { summaryText: string; taskIntent?: string };
|
|
2056
|
+
/**
|
|
2057
|
+
* Output budget for one summarization request. Adaptive-thinking models emit
|
|
2058
|
+
* reasoning tokens before the summary text, so the legacy flat 8192 cap could
|
|
2059
|
+
@@ -104,6 +108,6 @@ export declare function createSpeculativeCompactionSnapshot(context: Speculative
|
|
2060
|
+
* misreports as an auth failure.
|
|
2061
|
+
*/
|
|
2062
|
+
export declare function runExtensionCompaction(context: SpeculativeCompactionContext, snapshot: SpeculativeCompactionSnapshot, signal?: AbortSignal, onProgress?: CompactionProgressCallback): Promise<CompactionResult | undefined>;
|
|
2063
|
+
-export declare function applyGeneratedCompaction(context: SpeculativeCompactionContext, snapshot: SpeculativeCompactionSnapshot | undefined, getCurrentGeneration: () => number, compaction: CompactionResult | undefined, signal?: AbortSignal): Promise<SpeculativeCompactionResult>;
|
|
2064
|
+
+export declare function applyGeneratedCompaction(context: SpeculativeCompactionContext, snapshot: SpeculativeCompactionSnapshot | undefined, getCurrentGeneration: () => number, compaction: CompactionResult | undefined, signal?: AbortSignal, blocking?: boolean): Promise<SpeculativeCompactionResult>;
|
|
2065
|
+
export declare function applySpeculativeCompaction(context: SpeculativeCompactionContext, snapshot: SpeculativeCompactionSnapshot | undefined, getCurrentGeneration: () => number, generate: () => Promise<CompactionResult | undefined>): Promise<SpeculativeCompactionResult>;
|
|
2066
|
+
-//# sourceMappingURL=speculative.d.ts.map
|
|
2067
|
+
|
|
2068
|
+
+//# sourceMappingURL=speculative.d.ts.map
|
|
2069
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative.js
|
|
2070
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/speculative.js
|
|
2071
|
+
@@ -5,12 +5,12 @@ import { createWarmAnchorSnapshot, isWarmSummaryAnchorValid, } from "../../../co
|
|
2072
|
+
import { CredentialFailoverError, TURN_RETRY_SUPPRESSION_PREFIX } from "../../../credential-pool/failover.js";
|
|
2073
|
+
import { convertToLlm } from "../../../messages.js";
|
|
2074
|
+
import { pruneToolResults } from "./emergency-prune.js";
|
|
2075
|
+
-import { allowOverflowRetry, boundSummarizationInput, SUMMARIZATION_INPUT_BUDGET_RATIO, SummarizationOverflowExhaustedError, shrinkSummarizationInputForOverflowRetry, } from "./overflow-retry.js";
|
|
2076
|
+
+import { estimateTotalTokens, SummarizationOverflowExhaustedError } from "./overflow-retry.js";
|
|
2077
|
+
import { computeEffectiveKeepRecentTokens, computeEffectiveThreshold } from "./policy.js";
|
|
2078
|
+
import { buildPrompt } from "./prompts.js";
|
|
2079
|
+
import { generateSummaryMessage, getSummaryText, hasSummarizationReasoningOverride, isAssistantMessage, } from "./speculative-summary.js";
|
|
2080
|
+
import { allowSummarizationRetry, DEFAULT_SUMMARIZATION_RETRY_POLICY } from "./summarization-retry.js";
|
|
2081
|
+
-import { extractTaskIntent, resolveInheritedTaskIntent } from "./task-intent.js";
|
|
2082
|
+
+import { extractTaskIntent, resolveInheritedTaskIntent, taskIntentSummary, taskIntentMetadata } from "./task-intent.js";
|
|
2083
|
+
export { createEmergencyPruneLatch, hardLimitEmergencyPrune, truncateContextMessages, } from "./emergency-prune.js";
|
|
2084
|
+
import { computeStructuralYield } from "./yield.js";
|
|
2085
|
+
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
2086
|
+
@@ -30,23 +30,35 @@ export class SummaryRequestError extends Error {
|
|
2087
|
+
this.transient = transient;
|
|
2088
|
+
this.failureKind = failureKind;
|
|
2089
|
+
this.refused = refused;
|
|
2090
|
+
+ this.failure = { kind: refused ? "refused" : failureKind === "upstream-stream-truncated" ? "incomplete" : failureKind ?? "provider" };
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
+export function compactionFailure(error, model) {
|
|
2094
|
+
+ const kind = error?.refused ? "refused" : error?.failure?.kind ?? error?.kind;
|
|
2095
|
+
+ const known = ["invalid_model", "auth_missing", "auth_expired", "quota", "org_denied", "model_unavailable", "refused", "timeout", "incomplete", "empty", "context_budget", "stale", "provider"];
|
|
2096
|
+
+ return { kind: known.includes(kind) ? kind
|
|
2097
|
+
+ : error instanceof StreamDurationBudgetError || error instanceof StreamIdleTimeoutError || error instanceof SummarizationTotalBudgetError || error?.name === "TimeoutError" ? "timeout"
|
|
2098
|
+
+ : error instanceof SummarizationOverflowExhaustedError ? "context_budget" : "provider",
|
|
2099
|
+
+ ...(model ? { provider: model.provider, model: model.id } : {}),
|
|
2100
|
+
+ ...(error?.failure?.provider ? { provider: error.failure.provider } : {}),
|
|
2101
|
+
+ ...(error?.failure?.model ? { model: error.failure.model } : {}) };
|
|
2102
|
+
+}
|
|
2103
|
+
+export function validateSummaryResponse(response) {
|
|
2104
|
+
+ if (response && isRefusalStop(response))
|
|
2105
|
+
+ throw new SummaryRequestError("Compaction summary refused", false, undefined, true);
|
|
2106
|
+
+ if (!response || !isAssistantMessage(response) || response.stopReason !== "stop" ||
|
|
2107
|
+
+ response.content.some((block) => block.type === "toolCall" || block.type === "toolUse"))
|
|
2108
|
+
+ throw new SummaryGenerationError("incomplete", "Compaction summary did not finish normally");
|
|
2109
|
+
+ const parsed = extractTaskIntent(getSummaryText(response));
|
|
2110
|
+
+ if (!parsed.summaryText.trim())
|
|
2111
|
+
+ throw new SummaryGenerationError("empty", "Compaction summary body is empty");
|
|
2112
|
+
+ return parsed;
|
|
2113
|
+
+}
|
|
2114
|
+
const UPSTREAM_STREAM_TRUNCATED_PATTERN = /(?:^|[^A-Za-z0-9_])upstream_stream_truncated(?:[^A-Za-z0-9_]|$)/;
|
|
2115
|
+
-/**
|
|
2116
|
+
- * Only failures with no cheaper recovery earn another billed request.
|
|
2117
|
+
- *
|
|
2118
|
+
- * Every class that `classifyRequiredCompactionFallbackFailure` recognizes
|
|
2119
|
+
- * (watchdog timeouts, the compaction-wide total budget, terminal provider and
|
|
2120
|
+
- * credential failures, `upstream-stream-truncated`, overflow exhaustion,
|
|
2121
|
+
- * empty-summary generation failures) already
|
|
2122
|
+
- * has a deterministic zero-LLM recovery, and context overflow is answered by
|
|
2123
|
+
- * shrinking the input in the surrounding loop - replaying those would pay for a
|
|
2124
|
+
- * summarization the fallback can rebuild for free. The classes checked here are
|
|
2125
|
+
- * mirrored from that classifier rather than imported, because
|
|
2126
|
+
- * `deterministic-fallback.ts` imports this module.
|
|
2127
|
+
- */
|
|
2128
|
+
+/** Only pre-output transient failures may retry the same selected model. */
|
|
2129
|
+
function isRetryableSummaryAttempt(error) {
|
|
2130
|
+
+ if (error?.failure?.kind && error.failure.kind !== "provider")
|
|
2131
|
+
+ return false;
|
|
2132
|
+
if (error instanceof StreamDurationBudgetError || error instanceof StreamIdleTimeoutError)
|
|
2133
|
+
return false;
|
|
2134
|
+
if (error instanceof SummarizationTotalBudgetError)
|
|
2135
|
+
@@ -55,17 +67,12 @@ function isRetryableSummaryAttempt(error) {
|
|
2136
|
+
return false;
|
|
2137
|
+
if (error instanceof SummaryGenerationError)
|
|
2138
|
+
return false;
|
|
2139
|
+
- // Mirrors the `summarization-provider-failure` class: credential rotation has
|
|
2140
|
+
- // already spent every slot it may spend, and the marker means output was
|
|
2141
|
+
- // committed, so another billed attempt buys nothing the fallback cannot
|
|
2142
|
+
- // rebuild for free. Message text alone must not re-authorize it - the wrapped
|
|
2143
|
+
- // provider detail can read as transient (#1741).
|
|
2144
|
+
if (error instanceof CredentialFailoverError)
|
|
2145
|
+
return false;
|
|
2146
|
+
if (error instanceof Error && error.message.startsWith(TURN_RETRY_SUPPRESSION_PREFIX))
|
|
2147
|
+
return false;
|
|
2148
|
+
if (error instanceof SummaryRequestError)
|
|
2149
|
+
- return error.failureKind === undefined && error.transient;
|
|
2150
|
+
+ return !error.refused && error.failureKind === undefined && error.transient;
|
|
2151
|
+
if (error instanceof Error)
|
|
2152
|
+
return isRetryableErrorMessage(error.message);
|
|
2153
|
+
return false;
|
|
2154
|
+
@@ -76,6 +83,9 @@ function isRefusalStop(response) {
|
|
2155
|
+
function summaryRequestFailureKind(response) {
|
|
2156
|
+
if (isRefusalStop(response))
|
|
2157
|
+
return undefined;
|
|
2158
|
+
+ const kind = response.failure?.kind;
|
|
2159
|
+
+ if (["auth_missing", "auth_expired", "quota", "org_denied", "model_unavailable", "timeout", "incomplete", "context_budget"].includes(kind))
|
|
2160
|
+
+ return kind;
|
|
2161
|
+
return UPSTREAM_STREAM_TRUNCATED_PATTERN.test(response.errorMessage ?? "") ? "upstream-stream-truncated" : undefined;
|
|
2162
|
+
}
|
|
2163
|
+
export class SummaryGenerationError extends Error {
|
|
2164
|
+
@@ -83,6 +93,7 @@ export class SummaryGenerationError extends Error {
|
|
2165
|
+
super(message);
|
|
2166
|
+
this.name = "SummaryGenerationError";
|
|
2167
|
+
this.kind = kind;
|
|
2168
|
+
+ this.failure = { kind };
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
@@ -131,7 +142,7 @@ export function createSpeculativeCompactionSnapshot(context, options) {
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Generate a compaction summary for the snapshot. Resolves `undefined` only
|
|
2176
|
+
- * when the request was aborted (caller signal or an aborted stream); every
|
|
2177
|
+
+ * when the caller signal was aborted; every
|
|
2178
|
+
* other failure throws — {@link SummaryGenerationError} for diagnosable
|
|
2179
|
+
* generation failures, the raw provider error otherwise. Abort is checked
|
|
2180
|
+
* before and after credential resolution so a cancelled request never
|
|
2181
|
+
@@ -140,51 +151,54 @@ export function createSpeculativeCompactionSnapshot(context, options) {
|
|
2182
|
+
export async function runExtensionCompaction(context, snapshot, signal, onProgress) {
|
|
2183
|
+
if (signal?.aborted)
|
|
2184
|
+
return undefined;
|
|
2185
|
+
- // A provider is authenticated for summarization by a resolved key, a credential
|
|
2186
|
+
- // request header, or ambient request-time auth. The ambient marker comes from the
|
|
2187
|
+
- // same provider resolution normal turns use; an unconfigured keyed provider still
|
|
2188
|
+
- // has none of these and is rejected before a request.
|
|
2189
|
+
- const authenticated = (resolved) => resolved?.ok === true && Boolean(resolved.apiKey || hasCredentialHeaders(resolved.headers) || resolved.ambient);
|
|
2190
|
+
- let selectedModel = context.getCompactionModel?.() ?? snapshot.model;
|
|
2191
|
+
- let auth = await context.modelRegistry?.getApiKeyAndHeaders(selectedModel);
|
|
2192
|
+
- if (signal?.aborted)
|
|
2193
|
+
- return undefined;
|
|
2194
|
+
- // A configured summary model whose credentials could not be confirmed earlier (#830) must not
|
|
2195
|
+
- // disable compaction: without credentials at request time, summarize on the session model.
|
|
2196
|
+
- if (selectedModel !== snapshot.model && !authenticated(auth)) {
|
|
2197
|
+
- selectedModel = snapshot.model;
|
|
2198
|
+
- auth = await context.modelRegistry?.getApiKeyAndHeaders(selectedModel);
|
|
2199
|
+
+ const prompt = buildPrompt({
|
|
2200
|
+
+ variant: snapshot.promptVariant,
|
|
2201
|
+
+ previousSummary: snapshot.preparation.previousSummary,
|
|
2202
|
+
+ taskIntent: resolveInheritedTaskIntent(snapshot.branchEntries ?? []),
|
|
2203
|
+
+ customInstructions: snapshot.customInstructions,
|
|
2204
|
+
+ });
|
|
2205
|
+
+ const messages = [...snapshot.preparation.messagesToSummarize, ...snapshot.preparation.turnPrefixMessages];
|
|
2206
|
+
+ const inputTokens = estimateTotalTokens([
|
|
2207
|
+
+ ...messages,
|
|
2208
|
+
+ { role: "user", content: prompt.user },
|
|
2209
|
+
+ { role: "user", content: snapshot.systemPrompt ?? prompt.system },
|
|
2210
|
+
+ { role: "user", content: JSON.stringify(snapshot.tools ?? []) },
|
|
2211
|
+
+ ]);
|
|
2212
|
+
+ const selectedModel = context.getCompactionModel?.(inputTokens) ?? snapshot.model;
|
|
2213
|
+
+ const selectedSnapshot = { ...snapshot, model: selectedModel, contextWindow: selectedModel.contextWindow, selectionMode: context.getCompactionSettings?.().model === undefined || context.getCompactionSettings?.().model === "auto" ? "auto" : "explicit" };
|
|
2214
|
+
+ let auth;
|
|
2215
|
+
+ try {
|
|
2216
|
+
+ const runtime = context.modelRegistry?.modelRuntime;
|
|
2217
|
+
+ if (runtime) {
|
|
2218
|
+
+ const prepared = await runtime.prepareRequest(selectedModel, { signal });
|
|
2219
|
+
+ auth = { ok: true, ...prepared.options, baseUrl: prepared.model.baseUrl, ambient: true };
|
|
2220
|
+
+ }
|
|
2221
|
+
+ else {
|
|
2222
|
+
+ auth = await context.modelRegistry?.getApiKeyAndHeaders(selectedModel);
|
|
2223
|
+
+ }
|
|
2224
|
+
+ }
|
|
2225
|
+
+ catch (error) {
|
|
2226
|
+
if (signal?.aborted)
|
|
2227
|
+
return undefined;
|
|
2228
|
+
+ const kind = error?.failure?.kind ?? error?.kind;
|
|
2229
|
+
+ const known = ["auth_missing", "auth_expired", "quota", "org_denied", "model_unavailable"];
|
|
2230
|
+
+ const failure = { kind: kind === "auth" ? "auth_missing" : known.includes(kind) ? kind : "provider",
|
|
2231
|
+
+ provider: selectedModel.provider, model: selectedModel.id };
|
|
2232
|
+
+ throw Object.assign(new SummaryGenerationError(failure.kind, "Compaction request authentication failed"), { failure, cause: error });
|
|
2233
|
+
}
|
|
2234
|
+
- const selectedSnapshot = selectedModel === snapshot.model
|
|
2235
|
+
- ? snapshot
|
|
2236
|
+
- : { ...snapshot, model: selectedModel, contextWindow: selectedModel.contextWindow };
|
|
2237
|
+
- if (!auth?.ok || !authenticated(auth)) {
|
|
2238
|
+
- const detail = auth && !auth.ok ? auth.error : `no credentials resolved for provider "${selectedModel.provider}"`;
|
|
2239
|
+
- throw new SummaryGenerationError("auth", `summarization credentials unavailable: ${detail}`);
|
|
2240
|
+
+ if (signal?.aborted)
|
|
2241
|
+
+ return undefined;
|
|
2242
|
+
+ if (!auth?.ok || !(auth.apiKey || hasCredentialHeaders(auth.headers) || auth.ambient)) {
|
|
2243
|
+
+ const failure = { kind: "auth_missing", provider: selectedModel.provider, model: selectedModel.id };
|
|
2244
|
+
+ throw Object.assign(new SummaryGenerationError(failure.kind, "Compaction request authentication unavailable"), { failure });
|
|
2245
|
+
}
|
|
2246
|
+
const requestSnapshot = auth.baseUrl
|
|
2247
|
+
? { ...selectedSnapshot, model: { ...selectedModel, baseUrl: auth.baseUrl } }
|
|
2248
|
+
: selectedSnapshot;
|
|
2249
|
+
- const prompt = buildPrompt({
|
|
2250
|
+
- variant: requestSnapshot.promptVariant,
|
|
2251
|
+
- previousSummary: requestSnapshot.preparation.previousSummary,
|
|
2252
|
+
- taskIntent: resolveInheritedTaskIntent(requestSnapshot.branchEntries ?? []),
|
|
2253
|
+
- customInstructions: requestSnapshot.customInstructions,
|
|
2254
|
+
- });
|
|
2255
|
+
- const promptTokens = approxTokens(prompt.user);
|
|
2256
|
+
- let messages = boundSummarizationInput(pruneToolResults([...requestSnapshot.preparation.messagesToSummarize, ...requestSnapshot.preparation.turnPrefixMessages], requestSnapshot.contextWindow, SUMMARIZATION_INPUT_BUDGET_RATIO), requestSnapshot.contextWindow, promptTokens);
|
|
2257
|
+
// One deadline for the whole compaction. The per-attempt budget scales with the
|
|
2258
|
+
// input and every retry re-arms it, so without this a large session could hold
|
|
2259
|
+
// the turn for attempt-budget x attempts with no bound the user can predict.
|
|
2260
|
+
const deadline = createSummarizationDeadline(summarizationTotalBudgetMs(requestSnapshot.preparation.settings.summarizationMaxDurationMs));
|
|
2261
|
+
- const overflowRetryStartMs = Date.now();
|
|
2262
|
+
- let overflowAttempts = 0;
|
|
2263
|
+
- const summarizationToolsOffered = (requestSnapshot.tools?.length ?? 0) > 0;
|
|
2264
|
+
- const reasoningOverrideOffered = hasSummarizationReasoningOverride(requestSnapshot.model);
|
|
2265
|
+
- let toolUseRetrySpent = false;
|
|
2266
|
+
- let reasoningOverrideRetrySpent = false;
|
|
2267
|
+
while (true) {
|
|
2268
|
+
if (signal?.aborted)
|
|
2269
|
+
return undefined;
|
|
2270
|
+
@@ -210,7 +224,7 @@ export async function runExtensionCompaction(context, snapshot, signal, onProgre
|
|
2271
|
+
response = await retryTransientCall(async () => {
|
|
2272
|
+
const attempt = await generateSummaryMessage({
|
|
2273
|
+
context,
|
|
2274
|
+
- forbidToolCalls: toolUseRetrySpent,
|
|
2275
|
+
+ forbidToolCalls: true,
|
|
2276
|
+
// Re-clamped per attempt, not per loop turn: a retry that starts
|
|
2277
|
+
// near the deadline gets only what is left, and none starts past it.
|
|
2278
|
+
maxDurationMs: deadline.attemptBudgetMs(attemptBudgetMs),
|
|
2279
|
+
@@ -223,15 +237,15 @@ export async function runExtensionCompaction(context, snapshot, signal, onProgre
|
|
2280
|
+
apiKey: auth.apiKey,
|
|
2281
|
+
headers: auth.headers,
|
|
2282
|
+
extraBody: auth.extraBody,
|
|
2283
|
+
+ env: auth.env,
|
|
2284
|
+
},
|
|
2285
|
+
- ...(reasoningOverrideRetrySpent ? { omitReasoningOptions: true } : {}),
|
|
2286
|
+
});
|
|
2287
|
+
if (attempt &&
|
|
2288
|
+
isAssistantMessage(attempt) &&
|
|
2289
|
+
- attempt.stopReason === "error" &&
|
|
2290
|
+
+ (attempt.stopReason === "error" || isRefusalStop(attempt)) &&
|
|
2291
|
+
!isContextOverflow(attempt, requestSnapshot.contextWindow)) {
|
|
2292
|
+
const failureKind = summaryRequestFailureKind(attempt);
|
|
2293
|
+
- throw new SummaryRequestError(attempt.errorMessage || "Compaction summary request failed", failureKind !== undefined || isRetryableAssistantError(attempt), failureKind, isRefusalStop(attempt));
|
|
2294
|
+
+ throw new SummaryRequestError(attempt.errorMessage || "Compaction summary request failed", attempt.content.length === 0 && failureKind === undefined && isRetryableAssistantError(attempt), failureKind, isRefusalStop(attempt));
|
|
2295
|
+
}
|
|
2296
|
+
return attempt;
|
|
2297
|
+
}, (error) => retryEligible &&
|
|
2298
|
+
@@ -244,88 +258,49 @@ export async function runExtensionCompaction(context, snapshot, signal, onProgre
|
|
2299
|
+
return undefined;
|
|
2300
|
+
throw error;
|
|
2301
|
+
}
|
|
2302
|
+
- if (!response)
|
|
2303
|
+
- return undefined;
|
|
2304
|
+
- if (isAssistantMessage(response) && isContextOverflow(response, requestSnapshot.contextWindow)) {
|
|
2305
|
+
- overflowAttempts++;
|
|
2306
|
+
- const elapsedMs = Date.now() - overflowRetryStartMs;
|
|
2307
|
+
- const retryMessages = allowOverflowRetry(overflowAttempts, elapsedMs)
|
|
2308
|
+
- ? shrinkSummarizationInputForOverflowRetry(messages, requestSnapshot.contextWindow, promptTokens)
|
|
2309
|
+
- : undefined;
|
|
2310
|
+
- if (!retryMessages) {
|
|
2311
|
+
- throw new SummarizationOverflowExhaustedError(overflowAttempts, elapsedMs);
|
|
2312
|
+
- }
|
|
2313
|
+
- messages = retryMessages;
|
|
2314
|
+
- continue;
|
|
2315
|
+
- }
|
|
2316
|
+
- if (isAssistantMessage(response) && response.stopReason === "aborted") {
|
|
2317
|
+
- // A partial summary from an aborted stream must never be applied.
|
|
2318
|
+
+ if (!response && signal?.aborted)
|
|
2319
|
+
return undefined;
|
|
2320
|
+
+ if (response && isAssistantMessage(response) && isContextOverflow(response, requestSnapshot.contextWindow)) {
|
|
2321
|
+
+ const failure = { kind: "context_budget", provider: selectedModel.provider, model: selectedModel.id };
|
|
2322
|
+
+ throw Object.assign(new SummaryGenerationError(failure.kind, "Compaction input exceeds the provider context budget"), { failure });
|
|
2323
|
+
}
|
|
2324
|
+
- if (isAssistantMessage(response) && response.stopReason === "error") {
|
|
2325
|
+
- // Surface the real provider failure instead of silently degrading
|
|
2326
|
+
- // into a generic "Compaction cancelled". Preserve the structured
|
|
2327
|
+
- // truncation class here so downstream recovery never trusts arbitrary
|
|
2328
|
+
- // thrown error text as authorization for destructive context reduction.
|
|
2329
|
+
- const failureKind = summaryRequestFailureKind(response);
|
|
2330
|
+
- throw new SummaryRequestError(response.errorMessage || "Compaction summary request failed", failureKind !== undefined || isRetryableAssistantError(response), failureKind, isRefusalStop(response));
|
|
2331
|
+
- }
|
|
2332
|
+
- const summary = getSummaryText(response);
|
|
2333
|
+
- if (!summary) {
|
|
2334
|
+
- const stopReason = isAssistantMessage(response) ? response.stopReason : "unknown";
|
|
2335
|
+
- // A summarizer can hijack the forwarded agent tools and answer with a
|
|
2336
|
+
- // bare tool call (observed on chatgpt-subscription gpt-5.6-sol, 2026-08-31),
|
|
2337
|
+
- // which used to surface as a terminal empty-summary failure. Spend one
|
|
2338
|
+
- // retry with tool calling forbidden; the tools stay in the request
|
|
2339
|
+
- // because Anthropic rejects tool_use history without the tools param.
|
|
2340
|
+
- if (stopReason === "toolUse" && summarizationToolsOffered && !toolUseRetrySpent) {
|
|
2341
|
+
- toolUseRetrySpent = true;
|
|
2342
|
+
- continue;
|
|
2343
|
+
- }
|
|
2344
|
+
- // Some OpenAI-completions relays complete with a normal stop but zero
|
|
2345
|
+
- // text when the summarization prompt pins an explicit reasoning effort
|
|
2346
|
+
- // (GLM-5.3-flash behind a custom relay, 2026-09-17: HTTP 200 carrying
|
|
2347
|
+
- // only the role prelude). Spend one retry without the reasoning
|
|
2348
|
+
- // override, but only when the first attempt carried one: a model with
|
|
2349
|
+
- // no override would just replay the identical prompt. On Anthropic the
|
|
2350
|
+
- // override is `thinkingEnabled: false`, so the retry runs with the
|
|
2351
|
+
- // provider's default thinking, clamped by the compaction deadline.
|
|
2352
|
+
- // Persistent emptiness still throws and the deterministic fallback
|
|
2353
|
+
- // owns recovery.
|
|
2354
|
+
- if (stopReason === "stop" && reasoningOverrideOffered && !reasoningOverrideRetrySpent) {
|
|
2355
|
+
- reasoningOverrideRetrySpent = true;
|
|
2356
|
+
- continue;
|
|
2357
|
+
- }
|
|
2358
|
+
- throw new SummaryGenerationError("empty-summary", `summarization response contained no text (stopReason: ${stopReason})`);
|
|
2359
|
+
- }
|
|
2360
|
+
+ const parsedSummary = validateSummaryResponse(response);
|
|
2361
|
+
+ const inheritedIntent = resolveInheritedTaskIntent(snapshot.branchEntries ?? []);
|
|
2362
|
+
+ if (inheritedIntent && parsedSummary.taskIntent && parsedSummary.taskIntent !== inheritedIntent)
|
|
2363
|
+
+ throw new SummaryGenerationError("incomplete", "Compaction changed protected task intent");
|
|
2364
|
+
+ const taskIntent = inheritedIntent ?? parsedSummary.taskIntent;
|
|
2365
|
+
+ const summary = taskIntentSummary(parsedSummary.summaryText, taskIntent);
|
|
2366
|
+
// Informational only: the core rejects an applied compaction that would
|
|
2367
|
+
// still overflow (_wouldCompactionOverflow). Rejecting here based on the
|
|
2368
|
+
// size of the *discarded* input made large sessions uncompactable.
|
|
2369
|
+
const tokenEstimate = estimateContextTokens(convertToLlm(messages)).tokens + approxTokens(summary);
|
|
2370
|
+
- const parsedSummary = extractTaskIntent(summary);
|
|
2371
|
+
- const taskIntent = parsedSummary.taskIntent ?? resolveInheritedTaskIntent(snapshot.branchEntries ?? []);
|
|
2372
|
+
return {
|
|
2373
|
+
- summary: parsedSummary.summaryText,
|
|
2374
|
+
+ summary,
|
|
2375
|
+
firstKeptEntryId: snapshot.preparation.firstKeptEntryId,
|
|
2376
|
+
tokensBefore: snapshot.preparation.tokensBefore,
|
|
2377
|
+
details: {
|
|
2378
|
+
schema: SUMMARY_SCHEMA,
|
|
2379
|
+
+ selectionMode: selectedSnapshot.selectionMode,
|
|
2380
|
+
+ requestedProvider: selectedModel.provider,
|
|
2381
|
+
+ requestedModel: selectedModel.id,
|
|
2382
|
+
+ ...(isAssistantMessage(response) && response.provider && response.responseModel
|
|
2383
|
+
+ ? { observedProvider: response.provider, observedModel: response.responseModel } : {}),
|
|
2384
|
+
promptVariant: snapshot.promptVariant,
|
|
2385
|
+
tokenEstimate,
|
|
2386
|
+
structuralYield: computeStructuralYield({
|
|
2387
|
+
previousSummary: snapshot.preparation.previousSummary ?? "",
|
|
2388
|
+
messagesToSummarize: snapshot.preparation.messagesToSummarize,
|
|
2389
|
+
turnPrefixMessages: snapshot.preparation.turnPrefixMessages,
|
|
2390
|
+
- summary: parsedSummary.summaryText,
|
|
2391
|
+
+ summary,
|
|
2392
|
+
tokensBefore: snapshot.preparation.tokensBefore,
|
|
2393
|
+
}),
|
|
2394
|
+
...(snapshot.origin ? { origin: snapshot.origin } : {}),
|
|
2395
|
+
- ...(taskIntent ? { taskIntent } : {}),
|
|
2396
|
+
+ ...(taskIntent ? { taskIntent: taskIntentMetadata(taskIntent) } : {}),
|
|
2397
|
+
},
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
-export async function applyGeneratedCompaction(context, snapshot, getCurrentGeneration, compaction, signal) {
|
|
2402
|
+
+export async function applyGeneratedCompaction(context, snapshot, getCurrentGeneration, compaction, signal, blocking = true) {
|
|
2403
|
+
const provider = context.model?.provider;
|
|
2404
|
+
if ((provider === "cursor" || provider === "cursor-cli-oauth") && context.isIdle && !context.isIdle()) {
|
|
2405
|
+
return { applied: false, reason: "rejected" };
|
|
2406
|
+
@@ -343,6 +318,7 @@ export async function applyGeneratedCompaction(context, snapshot, getCurrentGene
|
|
2407
|
+
}
|
|
2408
|
+
return await context.applyCompaction(compaction, {
|
|
2409
|
+
reason: "extension",
|
|
2410
|
+
+ blocking,
|
|
2411
|
+
...(revisionUnchanged || !warmAnchor
|
|
2412
|
+
? { expectedRevision: snapshot.expectedRevision }
|
|
2413
|
+
: { expectedWarmAnchor: warmAnchor }),
|
|
2414
|
+
@@ -355,4 +331,4 @@ export async function applySpeculativeCompaction(context, snapshot, getCurrentGe
|
|
2415
|
+
const compaction = await generate();
|
|
2416
|
+
return await applyGeneratedCompaction(context, snapshot, getCurrentGeneration, compaction);
|
|
2417
|
+
}
|
|
2418
|
+
-//# sourceMappingURL=speculative.js.map
|
|
2419
|
+
|
|
2420
|
+
+//# sourceMappingURL=speculative.js.map
|
|
2421
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/task-intent.js
|
|
2422
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/builtin/compaction/task-intent.js
|
|
2423
|
+
@@ -13,7 +13,7 @@ export function extractTaskIntent(responseText) {
|
|
2424
|
+
if (!block)
|
|
2425
|
+
break;
|
|
2426
|
+
if (taskIntent === undefined)
|
|
2427
|
+
- taskIntent = capUtf8Bytes(block.inner.trim(), TASK_INTENT_BYTE_CAP);
|
|
2428
|
+
+ taskIntent = block.inner.trim();
|
|
2429
|
+
remaining = `${remaining.slice(0, block.start)}${remaining.slice(block.end)}`;
|
|
2430
|
+
}
|
|
2431
|
+
const summaryBlocks = collectWellFormedBlocks(remaining, SUMMARY_OPEN, SUMMARY_CLOSE);
|
|
2432
|
+
@@ -41,15 +41,16 @@ export function resolveInheritedTaskIntent(branchEntries) {
|
|
2433
|
+
continue;
|
|
2434
|
+
const typedDetails = details;
|
|
2435
|
+
const schema = typeof typedDetails.schema === "string" ? typedDetails.schema : entry.schema;
|
|
2436
|
+
- if (schema === REMOTE_SCHEMA || schema !== SUMMARY_SCHEMA)
|
|
2437
|
+
+ if (schema !== REMOTE_SCHEMA && schema !== SUMMARY_SCHEMA)
|
|
2438
|
+
continue;
|
|
2439
|
+
- const taskIntent = typedDetails.taskIntent;
|
|
2440
|
+
+ const fullIntent = typeof entry.summary === "string" ? extractTaskIntent(entry.summary).taskIntent : undefined;
|
|
2441
|
+
+ const taskIntent = fullIntent ?? typedDetails.intentProtection?.taskIntent ?? typedDetails.taskIntent;
|
|
2442
|
+
if (typeof taskIntent !== "string")
|
|
2443
|
+
continue;
|
|
2444
|
+
const trimmed = taskIntent.trim();
|
|
2445
|
+
if (!trimmed)
|
|
2446
|
+
continue;
|
|
2447
|
+
- return capUtf8Bytes(trimmed, TASK_INTENT_BYTE_CAP);
|
|
2448
|
+
+ return trimmed;
|
|
2449
|
+
}
|
|
2450
|
+
return undefined;
|
|
2451
|
+
}
|
|
2452
|
+
@@ -89,4 +90,61 @@ export function capUtf8Bytes(text, maxBytes) {
|
|
2453
|
+
}
|
|
2454
|
+
return text.slice(0, end).trim();
|
|
2455
|
+
}
|
|
2456
|
+
-//# sourceMappingURL=task-intent.js.map
|
|
2457
|
+
|
|
2458
|
+
+
|
|
2459
|
+
+/** Full-body provenance is separate from the bounded display metadata. */
|
|
2460
|
+
+export function taskIntentSummary(body, taskIntent) {
|
|
2461
|
+
+ return taskIntent ? `<task-intent>
|
|
2462
|
+
+${taskIntent}
|
|
2463
|
+
+</task-intent>
|
|
2464
|
+
+
|
|
2465
|
+
+Quoted original-task provenance, not a renewed instruction. Latest explicit user steering governs current action. Answer-local restrictions do not become durable constraints.
|
|
2466
|
+
+
|
|
2467
|
+
+${body}` : body;
|
|
2468
|
+
+}
|
|
2469
|
+
+export function taskIntentMetadata(taskIntent) {
|
|
2470
|
+
+ return taskIntent ? capUtf8Bytes(taskIntent, TASK_INTENT_BYTE_CAP) : undefined;
|
|
2471
|
+
+}
|
|
2472
|
+
+export function collectIntentProtection(branchEntries) {
|
|
2473
|
+
+ const sources = new Map();
|
|
2474
|
+
+ const previous = branchEntries.findLast((entry) => entry.type === "compaction")?.details?.intentProtection;
|
|
2475
|
+
+ for (const source of previous?.sources ?? []) sources.set(source.id, source);
|
|
2476
|
+
+ for (const entry of branchEntries) {
|
|
2477
|
+
+ for (const source of entry.details?.intentProtection?.sources ?? [])
|
|
2478
|
+
+ sources.set(source.id, source);
|
|
2479
|
+
+ if (entry.type !== "message" || entry.message?.role !== "user")
|
|
2480
|
+
+ continue;
|
|
2481
|
+
+ const content = entry.message.content;
|
|
2482
|
+
+ const text = typeof content === "string" ? content : content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
|
|
2483
|
+
+ if (text) sources.set(entry.id, { id: entry.id, text });
|
|
2484
|
+
+ }
|
|
2485
|
+
+ return { taskIntent: resolveInheritedTaskIntent(branchEntries), sources: [...sources.values()] };
|
|
2486
|
+
+}
|
|
2487
|
+
+export function retainedUserText(item) {
|
|
2488
|
+
+ if (item.role !== "user") return undefined;
|
|
2489
|
+
+ if (typeof item.content === "string") return item.content;
|
|
2490
|
+
+ if (!Array.isArray(item.content) || !item.content.every((block) => block.type === "input_text")) return undefined;
|
|
2491
|
+
+ return item.content.map((block) => block.text).join("\n");
|
|
2492
|
+
+}
|
|
2493
|
+
+export function protectedRemoteInput(replacementInput, protection) {
|
|
2494
|
+
+ if (!protection) return replacementInput;
|
|
2495
|
+
+ const input = replacementInput.filter((item) => {
|
|
2496
|
+
+ const text = retainedUserText(item);
|
|
2497
|
+
+ if (!text) return true;
|
|
2498
|
+
+ if (protection.sources.some((source) => source.id === item.__senpiCompactionSourceId && source.text === text)) return false;
|
|
2499
|
+
+ if (protection.taskIntent && text === taskIntentSummary("", protection.taskIntent)) return false;
|
|
2500
|
+
+ const start = text.indexOf("<source-user-text>\n");
|
|
2501
|
+
+ const end = text.indexOf("\n</source-user-text>", start);
|
|
2502
|
+
+ if (start < 0 || end < 0) return true;
|
|
2503
|
+
+ let records;
|
|
2504
|
+
+ try { records = JSON.parse(text.slice(start + 19, end)); } catch { return true; }
|
|
2505
|
+
+ if (!Array.isArray(records) || !records.every((source) => protection.sources.some((known) => known.id === source.id && known.text === source.text))) return true;
|
|
2506
|
+
+ const canonical = taskIntentSummary(`Quoted user source records; newer explicit steering takes precedence.\n<source-user-text>\n${JSON.stringify(records)}\n</source-user-text>`, protection.taskIntent);
|
|
2507
|
+
+ return text !== canonical;
|
|
2508
|
+
+ });
|
|
2509
|
+
+ const remaining = protection.sources;
|
|
2510
|
+
+ const text = taskIntentSummary(remaining.length ? `Quoted user source records; newer explicit steering takes precedence.\n<source-user-text>
|
|
2511
|
+
+${JSON.stringify(remaining)}
|
|
2512
|
+
+</source-user-text>` : "", protection.taskIntent);
|
|
2513
|
+
+ return text ? [...input, { type: "message", role: "user", content: [{ type: "input_text", text }] }] : input;
|
|
2514
|
+
+}
|
|
2515
|
+
+//# sourceMappingURL=task-intent.js.map
|
|
2516
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/runner.js
|
|
2517
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/runner.js
|
|
2518
|
+
@@ -1215,9 +1215,9 @@ export class ExtensionRunner {
|
|
2519
|
+
get getCompactionModel() {
|
|
2520
|
+
runner.assertActive();
|
|
2521
|
+
return runner.getCompactionModelFn
|
|
2522
|
+
- ? () => {
|
|
2523
|
+
+ ? (inputTokens) => {
|
|
2524
|
+
runner.assertActive();
|
|
2525
|
+
- return runner.getCompactionModelFn?.();
|
|
2526
|
+
+ return runner.getCompactionModelFn?.(inputTokens);
|
|
2527
|
+
}
|
|
2528
|
+
: undefined;
|
|
2529
|
+
},
|
|
2530
|
+
@@ -1885,7 +1885,7 @@ export class ExtensionRunner {
|
|
2531
|
+
const event = {
|
|
2532
|
+
type: "before_provider_request",
|
|
2533
|
+
payload: currentPayload,
|
|
2534
|
+
- ...(request ? { model: request.model, headers: request.headers } : {}),
|
|
2535
|
+
+ ...(request ? { model: request.model, headers: request.headers, streamKind: request.streamKind } : {}),
|
|
2536
|
+
};
|
|
2537
|
+
const handlerResult = await handler(event, this.createContext(ext.path));
|
|
2538
|
+
if (handlerResult !== undefined) {
|
|
2539
|
+
@@ -2099,4 +2099,4 @@ export class ExtensionRunner {
|
|
2540
|
+
: { action: "continue" };
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
-//# sourceMappingURL=runner.js.map
|
|
2544
|
+
|
|
2545
|
+
+//# sourceMappingURL=runner.js.map
|
|
2546
|
+
--- a/@code-yeongyu/senpi/dist/core/extensions/types.d.ts
|
|
2547
|
+
+++ b/@code-yeongyu/senpi/dist/core/extensions/types.d.ts
|
|
2548
|
+
@@ -61,6 +61,14 @@ export type CompactionRejectionCause = "cancelled-by-extension" | "external-owne
|
|
2549
|
+
export interface ExtensionUIDialogOptions {
|
|
2550
|
+
/** Private local dialog: emits no generic blocked event or pending marker. Not an RPC field. */
|
|
2551
|
+
ephemeral?: boolean;
|
|
2552
|
+
+ /** Exact native permission identity; correlation only, never an approval. */
|
|
2553
|
+
+ nativePermission?: {
|
|
2554
|
+
+ sessionID: string;
|
|
2555
|
+
+ requestID: string;
|
|
2556
|
+
+ correlationId: string;
|
|
2557
|
+
+ };
|
|
2558
|
+
+ /** Whether answering this dialog is required; omitted means required. */
|
|
2559
|
+
+ required?: boolean;
|
|
2560
|
+
/** AbortSignal to programmatically dismiss the dialog. */
|
|
2561
|
+
signal?: AbortSignal;
|
|
2562
|
+
/** Timeout in milliseconds. Dialog auto-dismisses with live countdown display. */
|
|
2563
|
+
@@ -327,6 +335,7 @@ export interface CompactOptions {
|
|
2564
|
+
}
|
|
2565
|
+
export interface ApplyCompactionOptions {
|
|
2566
|
+
reason: CompactionReason;
|
|
2567
|
+
+ blocking?: boolean;
|
|
2568
|
+
expectedRevision?: number;
|
|
2569
|
+
/**
|
|
2570
|
+
* Content anchor for a warm summary: the compaction applies while this snapshot
|
|
2571
|
+
@@ -353,7 +362,13 @@ export interface UpdateCompactionOptions {
|
|
2572
|
+
delta?: string;
|
|
2573
|
+
text?: string;
|
|
2574
|
+
}
|
|
2575
|
+
+export interface CompactionFailure {
|
|
2576
|
+
+ kind: "invalid_model" | "auth_missing" | "auth_expired" | "quota" | "org_denied" | "model_unavailable" | "refused" | "timeout" | "incomplete" | "empty" | "context_budget" | "stale" | "provider";
|
|
2577
|
+
+ provider?: string;
|
|
2578
|
+
+ model?: string;
|
|
2579
|
+
+}
|
|
2580
|
+
export interface EndCompactionOptions {
|
|
2581
|
+
+ failure?: CompactionFailure;
|
|
2582
|
+
reason: CompactionReason;
|
|
2583
|
+
signal?: AbortSignal;
|
|
2584
|
+
aborted?: boolean;
|
|
2585
|
+
@@ -504,8 +519,8 @@ export interface ExtensionContext {
|
|
2586
|
+
getContextUsage(): ContextUsage | undefined;
|
|
2587
|
+
/** Get resolved compaction settings from global/project/user overrides. */
|
|
2588
|
+
getCompactionSettings(): CompactionPreparation["settings"];
|
|
2589
|
+
- /** Session-selected usable summarization model; throws when every authenticated account is spent. */
|
|
2590
|
+
- getCompactionModel?(): Model<Api> | undefined;
|
|
2591
|
+
+ /** Select a budget-fitting summary model; auto stays within the active provider and explicit failures never fall back. */
|
|
2592
|
+
+ getCompactionModel?(inputTokens?: number): Model<Api> | undefined;
|
|
2593
|
+
/**
|
|
2594
|
+
* Longest a tool may block in the foreground before the active model's prompt
|
|
2595
|
+
* cache expires, or `undefined` when no cache-derived budget applies. Reads the
|
|
2596
|
+
@@ -1198,6 +1213,7 @@ export interface SessionCompactAcceptedEvent {
|
|
2597
|
+
willRetry: boolean;
|
|
2598
|
+
}
|
|
2599
|
+
export interface SessionCompactRejectedEvent {
|
|
2600
|
+
+ failure?: CompactionFailure;
|
|
2601
|
+
type: "session_compact";
|
|
2602
|
+
reason: CompactionReason;
|
|
2603
|
+
requestId: string;
|
|
2604
|
+
@@ -1334,6 +1350,7 @@ export interface BeforeProviderRequestEvent {
|
|
2605
|
+
model?: Model<Api>;
|
|
2606
|
+
/** Final header transform output for this request. Values are never persisted. */
|
|
2607
|
+
headers?: ProviderHeaders;
|
|
2608
|
+
+ streamKind?: "main" | "auxiliary";
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Fired after request headers are assembled, before the provider HTTP call.
|
|
2612
|
+
@@ -1874,6 +1891,7 @@ export interface ReloadVetoDecision {
|
|
2613
|
+
reason?: string;
|
|
2614
|
+
}
|
|
2615
|
+
export interface SessionBeforeCompactResult {
|
|
2616
|
+
+ failure?: CompactionFailure;
|
|
2617
|
+
cancel?: boolean;
|
|
2618
|
+
compaction?: CompactionResult;
|
|
2619
|
+
/**
|
|
2620
|
+
@@ -2744,7 +2762,7 @@ export interface ExtensionContextActions {
|
|
2621
|
+
shutdown: () => void;
|
|
2622
|
+
getContextUsage: () => ContextUsage | undefined;
|
|
2623
|
+
getCompactionSettings: () => CompactionPreparation["settings"];
|
|
2624
|
+
- getCompactionModel?: () => Model<Api> | undefined;
|
|
2625
|
+
+ getCompactionModel?: (inputTokens?: number) => Model<Api> | undefined;
|
|
2626
|
+
getPromptCacheSafeWaitSeconds?: () => number | undefined;
|
|
2627
|
+
getPromptCacheGoalBackstopMaxSeconds?: () => number;
|
|
2628
|
+
onUserActivity?: (listener: (kind: UserActivityKind) => void) => () => void;
|
|
2629
|
+
@@ -2909,4 +2927,4 @@ export interface ExtensionError {
|
|
2630
|
+
error: string;
|
|
2631
|
+
stack?: string;
|
|
2632
|
+
}
|
|
2633
|
+
-//# sourceMappingURL=types.d.ts.map
|
|
2634
|
+
|
|
2635
|
+
+//# sourceMappingURL=types.d.ts.map
|