pi-ui-extend 0.1.56 → 0.1.59
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/dist/app/app.js +2 -0
- package/dist/app/commands/command-session-actions.js +3 -2
- package/dist/app/extensions/extension-actions-controller.js +1 -3
- package/dist/app/rendering/conversation-entry-renderer.d.ts +3 -0
- package/dist/app/rendering/conversation-entry-renderer.js +2 -0
- package/dist/app/rendering/conversation-viewport.d.ts +3 -0
- package/dist/app/rendering/conversation-viewport.js +4 -0
- package/dist/app/rendering/extension-entry-renderer.d.ts +6 -0
- package/dist/app/rendering/extension-entry-renderer.js +100 -0
- package/dist/app/rendering/tool-block-renderer.d.ts +5 -0
- package/dist/app/rendering/tool-block-renderer.js +1 -1
- package/dist/app/screen/mouse-controller.js +1 -1
- package/dist/app/session/lazy-session-manager.js +18 -1
- package/dist/app/session/pix-system-message.d.ts +1 -0
- package/dist/app/session/pix-system-message.js +4 -0
- package/dist/app/session/session-event-controller.js +7 -1
- package/dist/app/session/session-history.d.ts +3 -0
- package/dist/app/session/session-history.js +19 -2
- package/dist/app/session/session-search.js +2 -0
- package/dist/app/session/session-stats.d.ts +3 -0
- package/dist/app/session/session-stats.js +65 -0
- package/dist/app/types.d.ts +6 -1
- package/dist/bundled-extensions/terminal-bell/index.js +1 -37
- package/external/pi-tools-suite/README.md +17 -1
- package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +102 -0
- package/external/pi-tools-suite/src/antigravity-auth/payload.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +0 -33
- package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +1 -1
- package/external/pi-tools-suite/src/codex-reasoning-fix/index.ts +13 -121
- package/external/pi-tools-suite/src/dcp/config.ts +27 -0
- package/external/pi-tools-suite/src/dcp/debug-log.ts +2 -1
- package/external/pi-tools-suite/src/dcp/index.ts +193 -20
- package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +85 -0
- package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +254 -0
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -0
- package/external/pi-tools-suite/src/dcp/pruner-types.ts +30 -0
- package/external/pi-tools-suite/src/dcp/pruner.ts +8 -0
- package/external/pi-tools-suite/src/dcp/state.ts +21 -6
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +14 -0
- package/external/pi-tools-suite/src/index.ts +4 -2
- package/external/pi-tools-suite/src/telegram-mirror/events.ts +9 -4
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +1 -1
- package/external/pi-tools-suite/src/todo/index.ts +23 -27
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Spec: DCP emergency current-turn pruning
|
|
2
|
+
|
|
3
|
+
## Type
|
|
4
|
+
|
|
5
|
+
Change.
|
|
6
|
+
|
|
7
|
+
## Goal
|
|
8
|
+
|
|
9
|
+
Prevent one long active user turn from exhausting the provider context when an
|
|
10
|
+
active compression block already covers all older turns and the normal range
|
|
11
|
+
and message candidate detectors therefore return nothing.
|
|
12
|
+
|
|
13
|
+
## Scope
|
|
14
|
+
|
|
15
|
+
- Emergency context reminders without a normal candidate.
|
|
16
|
+
- Persisted emergency-pressure and provider-exposure state.
|
|
17
|
+
- Emergency-only same-turn message candidates for old large tool results.
|
|
18
|
+
- Deterministic placeholder pruning when reminders are ignored or usage reaches
|
|
19
|
+
the hard emergency threshold.
|
|
20
|
+
- Bounded private diagnostics for the decision and pruning result.
|
|
21
|
+
|
|
22
|
+
## Non-goals
|
|
23
|
+
|
|
24
|
+
- Summarising the unfinished active turn as one broad range.
|
|
25
|
+
- Changing normal multi-turn candidate or auto-compress behavior.
|
|
26
|
+
- Setting normal `autoToolPruning.keepRecentTurns` to zero.
|
|
27
|
+
- Mutating the raw session transcript; pruning affects provider context only.
|
|
28
|
+
|
|
29
|
+
## Behavior
|
|
30
|
+
|
|
31
|
+
1. Above the model-specific emergency threshold, DCP emits and re-applies a
|
|
32
|
+
context reminder even when normal candidates are absent.
|
|
33
|
+
2. Emergency-only message suggestions may select old, large, complete
|
|
34
|
+
assistant tool-call/result pairs from the active turn.
|
|
35
|
+
3. The newest configured number of complete pairs, results not yet included in
|
|
36
|
+
an accepted provider request, user messages, protected tools/files/tags, and messages
|
|
37
|
+
covered by compression blocks are never emergency candidates.
|
|
38
|
+
4. If emergency pressure persists beyond patience, or reaches the configured
|
|
39
|
+
hard context percentage, DCP replaces eligible result bodies with the
|
|
40
|
+
existing pruning placeholder. It processes deterministic oldest-first
|
|
41
|
+
candidates and stops once the target recovery budget is met.
|
|
42
|
+
5. The emergency counter resets only after pressure drops below the emergency
|
|
43
|
+
threshold, a successful compression/prune, a context-window change, or a
|
|
44
|
+
session reset.
|
|
45
|
+
|
|
46
|
+
## Contracts
|
|
47
|
+
|
|
48
|
+
The shared DCP config gains `strategies.emergencyCurrentTurnPruning` with an
|
|
49
|
+
enable flag for same-turn candidates/lossy pruning, hard and target context percentages, patience, recent-pair count,
|
|
50
|
+
minimum output size, suggestion cap, and extra protected tools. Missing config
|
|
51
|
+
uses safe defaults. Emergency reminders remain active when lossy pruning is
|
|
52
|
+
disabled. Sidecars accept missing new fields for backward
|
|
53
|
+
compatibility.
|
|
54
|
+
|
|
55
|
+
Diagnostics use distinct events:
|
|
56
|
+
|
|
57
|
+
- `context.strong_nudge_without_candidate`
|
|
58
|
+
- `compress.auto_blocked_no_candidate`
|
|
59
|
+
- `prune.emergency_current_turn`
|
|
60
|
+
|
|
61
|
+
## Invariants
|
|
62
|
+
|
|
63
|
+
- User messages are never selected or pruned.
|
|
64
|
+
- Tool-call/result structural pairs remain valid because only result content is
|
|
65
|
+
replaced.
|
|
66
|
+
- A new result is not pruned before an accepted provider request included it.
|
|
67
|
+
- The newest complete pairs and all configured/protected data survive.
|
|
68
|
+
- Pruning is deterministic, idempotently accounted, and stops at its budget.
|
|
69
|
+
|
|
70
|
+
## Related files
|
|
71
|
+
|
|
72
|
+
- `src/dcp/index.ts`
|
|
73
|
+
- `src/dcp/pruner-emergency.ts`
|
|
74
|
+
- `src/dcp/pruner-candidates.ts`
|
|
75
|
+
- `src/dcp/pruner-tools.ts`
|
|
76
|
+
- `src/dcp/config.ts`
|
|
77
|
+
- `src/dcp/state.ts`
|
|
78
|
+
- `test/compress-pruner.test.ts`
|
|
79
|
+
- `test/dcp-state-serialization.test.ts`
|
|
80
|
+
- `test/dcp-config.test.ts`
|
|
81
|
+
|
|
82
|
+
## Verification
|
|
83
|
+
|
|
84
|
+
Focused regression and safety tests, full suite tests, headless smoke, root
|
|
85
|
+
`npm run check`, SDK-pin check, `git diff --check`, and a headless one-turn
|
|
86
|
+
reproduction that confirms estimated provider-visible tokens fall before the
|
|
87
|
+
model limit.
|
|
88
|
+
|
|
89
|
+
## Risks / unknowns
|
|
90
|
+
|
|
91
|
+
Token recovery is estimated from message text, so the exact provider token
|
|
92
|
+
count can differ. The hard fallback therefore targets a margin below the
|
|
93
|
+
model-specific emergency threshold instead of an exact token boundary.
|
|
94
|
+
|
|
95
|
+
## Evidence
|
|
96
|
+
|
|
97
|
+
- Confirmed by current code: normal candidate detectors protect at least the
|
|
98
|
+
latest user turn; reminders and auto-compress are candidate-gated.
|
|
99
|
+
- Confirmed by regression test: an active block plus one long turn produces no
|
|
100
|
+
normal candidate at emergency pressure.
|
|
101
|
+
- Intended behavior: emergency candidate selection, persistence, fallback, and
|
|
102
|
+
diagnostics described above.
|
|
@@ -314,7 +314,7 @@ function convertTools(tools?: Tool[]): Array<{ functionDeclarations: unknown[] }
|
|
|
314
314
|
function reasoningLevel(options?: SimpleStreamOptions): "minimal" | "low" | "medium" | "high" | undefined {
|
|
315
315
|
const reasoning = options?.reasoning;
|
|
316
316
|
if (!reasoning) return undefined;
|
|
317
|
-
if (reasoning === "xhigh") return "high";
|
|
317
|
+
if (reasoning === "xhigh" || reasoning === "max") return "high";
|
|
318
318
|
return reasoning;
|
|
319
319
|
}
|
|
320
320
|
|
|
@@ -26,7 +26,6 @@ const AGENT_TIMEOUT_EXIT_CODE = 124;
|
|
|
26
26
|
const AGENT_TIMEOUT_KILL_GRACE_MS = 5_000;
|
|
27
27
|
const AGENT_SETTLED_TERMINATE_GRACE_MS = 50;
|
|
28
28
|
const AGENT_SETTLED_COMPLETION_FALLBACK_MS = 1_000;
|
|
29
|
-
const AGENT_END_BACKCOMPAT_SETTLE_FALLBACK_MS = 100;
|
|
30
29
|
const EXIT_STDIO_FLUSH_GRACE_MS = 10;
|
|
31
30
|
|
|
32
31
|
export function shouldPersistSubagentSessions(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
@@ -145,7 +144,6 @@ export function spawnAgent(
|
|
|
145
144
|
let timeoutKillTimer: NodeJS.Timeout | undefined;
|
|
146
145
|
let agentSettledKillTimer: NodeJS.Timeout | undefined;
|
|
147
146
|
let agentSettledCompletionFallbackTimer: NodeJS.Timeout | undefined;
|
|
148
|
-
let agentEndBackcompatFallbackTimer: NodeJS.Timeout | undefined;
|
|
149
147
|
let exitFinalizationTimer: NodeJS.Timeout | undefined;
|
|
150
148
|
const suppressedRpcEventCounts = new Map<string, number>();
|
|
151
149
|
|
|
@@ -157,7 +155,6 @@ export function spawnAgent(
|
|
|
157
155
|
if (timeoutKillTimer) clearTimeout(timeoutKillTimer);
|
|
158
156
|
if (agentSettledKillTimer) clearTimeout(agentSettledKillTimer);
|
|
159
157
|
if (agentSettledCompletionFallbackTimer) clearTimeout(agentSettledCompletionFallbackTimer);
|
|
160
|
-
if (agentEndBackcompatFallbackTimer) clearTimeout(agentEndBackcompatFallbackTimer);
|
|
161
158
|
if (exitFinalizationTimer) clearTimeout(exitFinalizationTimer);
|
|
162
159
|
if (!fs.existsSync(agentDir)) {
|
|
163
160
|
onComplete?.({
|
|
@@ -220,10 +217,6 @@ export function spawnAgent(
|
|
|
220
217
|
};
|
|
221
218
|
|
|
222
219
|
const scheduleAgentSettledTermination = () => {
|
|
223
|
-
if (agentEndBackcompatFallbackTimer) {
|
|
224
|
-
clearTimeout(agentEndBackcompatFallbackTimer);
|
|
225
|
-
agentEndBackcompatFallbackTimer = undefined;
|
|
226
|
-
}
|
|
227
220
|
if (agentSettledKillTimer) return;
|
|
228
221
|
agentSettledKillTimer = setTimeout(() => {
|
|
229
222
|
agentSettledKillTimer = undefined;
|
|
@@ -250,21 +243,6 @@ export function spawnAgent(
|
|
|
250
243
|
agentSettledCompletionFallbackTimer.unref?.();
|
|
251
244
|
};
|
|
252
245
|
|
|
253
|
-
const clearAgentEndBackcompatFallback = () => {
|
|
254
|
-
if (!agentEndBackcompatFallbackTimer) return;
|
|
255
|
-
clearTimeout(agentEndBackcompatFallbackTimer);
|
|
256
|
-
agentEndBackcompatFallbackTimer = undefined;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const scheduleAgentEndBackcompatFallback = () => {
|
|
260
|
-
if (agentEndBackcompatFallbackTimer || agentSettledKillTimer) return;
|
|
261
|
-
agentEndBackcompatFallbackTimer = setTimeout(() => {
|
|
262
|
-
agentEndBackcompatFallbackTimer = undefined;
|
|
263
|
-
scheduleAgentSettledTermination();
|
|
264
|
-
}, AGENT_END_BACKCOMPAT_SETTLE_FALLBACK_MS);
|
|
265
|
-
agentEndBackcompatFallbackTimer.unref?.();
|
|
266
|
-
};
|
|
267
|
-
|
|
268
246
|
const timeoutMs = options.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS;
|
|
269
247
|
if (timeoutMs > 0) {
|
|
270
248
|
timeoutTimer = setTimeout(() => {
|
|
@@ -305,15 +283,6 @@ export function spawnAgent(
|
|
|
305
283
|
}
|
|
306
284
|
try {
|
|
307
285
|
const event = JSON.parse(line) as RpcEventRecord;
|
|
308
|
-
if (
|
|
309
|
-
event.type === "agent_start"
|
|
310
|
-
|| event.type === "message_start"
|
|
311
|
-
|| event.type === "tool_execution_start"
|
|
312
|
-
|| event.type === "compaction_start"
|
|
313
|
-
|| event.type === "auto_retry_start"
|
|
314
|
-
) {
|
|
315
|
-
clearAgentEndBackcompatFallback();
|
|
316
|
-
}
|
|
317
286
|
const storedEvent = compactRpcEventForTranscript(event, Buffer.byteLength(line, "utf8"));
|
|
318
287
|
if (storedEvent) transcriptStream.write(serializeJsonLine(storedEvent));
|
|
319
288
|
onRpcEvent?.(event);
|
|
@@ -340,7 +309,6 @@ export function spawnAgent(
|
|
|
340
309
|
const result = extractAgentEndResult(event);
|
|
341
310
|
if (result.trim())
|
|
342
311
|
fs.writeFileSync(path.join(agentDir, "result.md"), result, "utf-8");
|
|
343
|
-
scheduleAgentEndBackcompatFallback();
|
|
344
312
|
}
|
|
345
313
|
if (event.type === "agent_settled") {
|
|
346
314
|
scheduleAgentSettledTermination();
|
|
@@ -368,7 +336,6 @@ export function spawnAgent(
|
|
|
368
336
|
lastAgentEndError = "Sub-agent produced an oversized agent_end RPC event before a final result could be captured.";
|
|
369
337
|
fs.writeFileSync(path.join(agentDir, "result.md"), lastAgentEndError, "utf-8");
|
|
370
338
|
}
|
|
371
|
-
scheduleAgentEndBackcompatFallback();
|
|
372
339
|
return true;
|
|
373
340
|
}
|
|
374
341
|
return false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { ignoreStaleExtensionContextError } from "../../context-usage";
|
|
2
|
+
import { ignoreStaleExtensionContextError } from "../../context-usage.js";
|
|
3
3
|
|
|
4
4
|
export const SUBAGENT_DENIED_TOOLS = new Set([
|
|
5
5
|
"question",
|
|
@@ -3,29 +3,16 @@
|
|
|
3
3
|
* rejects HTTP 400 `Unknown parameter: 'input[N].content'` when non-message
|
|
4
4
|
* items (reasoning, function_call_output) carry a spurious `content` field.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* (incl. 0.79.10) is affected.
|
|
6
|
+
* Stray fields can come from replayed pi-ai items or from another extension
|
|
7
|
+
* that modifies the provider payload. The sanitizer therefore must be the
|
|
8
|
+
* LAST `before_provider_request` handler registered by pi-tools-suite.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
10
|
+
* In pi-ai >= 0.80.6 the final `before_provider_request` payload feeds both the
|
|
11
|
+
* WebSocket delta builder and the zstd-compressed SSE fallback body. Running the
|
|
12
|
+
* sanitizer last therefore covers both transports without mutating global
|
|
13
|
+
* `fetch` or `WebSocket.prototype.send`.
|
|
12
14
|
*
|
|
13
|
-
*
|
|
14
|
-
* the body into a DELTA after `before_provider_request`, so the hook never
|
|
15
|
-
* sees the bytes actually sent. We wrap `WebSocket.prototype.send` once and
|
|
16
|
-
* strip `content` from every non-message input item of each
|
|
17
|
-
* `response.create` frame, on the exact bytes leaving the socket.
|
|
18
|
-
*
|
|
19
|
-
* 2. SSE/fetch fallback path: once a websocket attempt fails (common at large
|
|
20
|
-
* context — full body, ~100+ input items), the transport records an SSE
|
|
21
|
-
* fallback for the session and every subsequent request goes through a plain
|
|
22
|
-
* `fetch()` POST with the FULL body (`bodyJson`). The `before_provider_request`
|
|
23
|
-
* hook should clean this, but in practice it does not reliably catch every
|
|
24
|
-
* offending item, so we also wrap `globalThis.fetch` once and strip the same
|
|
25
|
-
* spurious `content` from the request body on the exact bytes leaving the
|
|
26
|
-
* HTTP client.
|
|
27
|
-
*
|
|
28
|
-
* The `before_provider_request` hook is kept as a third secondary guard.
|
|
15
|
+
* Module registration order is part of this workaround's correctness.
|
|
29
16
|
*
|
|
30
17
|
* Remove this whole module once an upstream pi-ai release carries the fix.
|
|
31
18
|
*/
|
|
@@ -40,12 +27,9 @@ type ProviderRequestContext = {
|
|
|
40
27
|
model?: unknown;
|
|
41
28
|
};
|
|
42
29
|
|
|
43
|
-
installWireStripper();
|
|
44
|
-
installFetchStripper();
|
|
45
|
-
|
|
46
30
|
/**
|
|
47
31
|
* Strip spurious `content` from any object that carries an `input` or
|
|
48
|
-
* `messages` array. Shared core for
|
|
32
|
+
* `messages` array. Shared core for payload sanitization.
|
|
49
33
|
* Returns the cleaned object plus a tally only when something changed;
|
|
50
34
|
* otherwise `undefined` (caller forwards the original untouched).
|
|
51
35
|
*/
|
|
@@ -87,101 +71,10 @@ function isSpuriousContentItem(item: unknown): boolean {
|
|
|
87
71
|
);
|
|
88
72
|
}
|
|
89
73
|
|
|
90
|
-
/**
|
|
91
|
-
* Wrap `WebSocket.prototype.send` once. For each `response.create` frame, strip
|
|
92
|
-
* the offending `content` from non-message input items and forward the cleaned
|
|
93
|
-
* frame. Forwards the original untouched on any parse/rewrite failure so the
|
|
94
|
-
* transport never breaks.
|
|
95
|
-
*/
|
|
96
|
-
function installWireStripper(): void {
|
|
97
|
-
try {
|
|
98
|
-
const WS = (globalThis as { WebSocket?: typeof WebSocket }).WebSocket;
|
|
99
|
-
if (!WS?.prototype) return;
|
|
100
|
-
const proto = WS.prototype as { send?: unknown };
|
|
101
|
-
if (typeof proto.send !== "function") return;
|
|
102
|
-
const original = proto.send as (data: unknown) => void;
|
|
103
|
-
if ((original as unknown as { __codexFixWrapped?: boolean }).__codexFixWrapped) return;
|
|
104
|
-
|
|
105
|
-
const wrapped = function (this: unknown, data: unknown): void {
|
|
106
|
-
let outgoing = data;
|
|
107
|
-
if (typeof data === "string") {
|
|
108
|
-
try {
|
|
109
|
-
const parsed = JSON.parse(data);
|
|
110
|
-
if (isRecord(parsed) && parsed.type === "response.create") {
|
|
111
|
-
const result = stripCarrier(parsed);
|
|
112
|
-
if (result) {
|
|
113
|
-
outgoing = JSON.stringify(result.obj);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} catch {
|
|
117
|
-
// Non-JSON or malformed: forward untouched.
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return original.call(this, outgoing);
|
|
121
|
-
};
|
|
122
|
-
(wrapped as unknown as { __codexFixWrapped?: boolean }).__codexFixWrapped = true;
|
|
123
|
-
proto.send = wrapped;
|
|
124
|
-
} catch {
|
|
125
|
-
// best-effort; never break on setup
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Wrap `globalThis.fetch` once. For JSON POST bodies carrying an `input` or
|
|
131
|
-
* `messages` array (Codex / OpenAI Responses shape), strip the spurious
|
|
132
|
-
* `content` from non-message items before the request leaves the HTTP client.
|
|
133
|
-
* This closes the SSE-fallback path that the WebSocket wrapper cannot see.
|
|
134
|
-
* Forwards the original request untouched on any non-matching/parse failure.
|
|
135
|
-
*/
|
|
136
|
-
function installFetchStripper(): void {
|
|
137
|
-
try {
|
|
138
|
-
const g = globalThis as { fetch?: typeof fetch };
|
|
139
|
-
const original = g.fetch;
|
|
140
|
-
if (typeof original !== "function") return;
|
|
141
|
-
if ((original as unknown as { __codexFixWrapped?: boolean }).__codexFixWrapped) return;
|
|
142
|
-
|
|
143
|
-
const wrapped = function fetchStripper(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
144
|
-
const nextInit = stripFetchInit(init);
|
|
145
|
-
return original.call(this, input, nextInit ?? init);
|
|
146
|
-
};
|
|
147
|
-
(wrapped as unknown as { __codexFixWrapped?: boolean }).__codexFixWrapped = true;
|
|
148
|
-
g.fetch = wrapped as typeof fetch;
|
|
149
|
-
} catch {
|
|
150
|
-
// best-effort; never break on setup
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* If `init` carries a JSON body shaped like a Responses request, return a new
|
|
156
|
-
* `init` with the stripped body. Returns `undefined` when nothing matched so
|
|
157
|
-
* the caller forwards the original `init` untouched.
|
|
158
|
-
*/
|
|
159
|
-
export function stripFetchInit(init: RequestInit | undefined): RequestInit | undefined {
|
|
160
|
-
if (!init || typeof init.body !== "string") return undefined;
|
|
161
|
-
try {
|
|
162
|
-
const parsed = JSON.parse(init.body);
|
|
163
|
-
const result = stripCarrier(parsed);
|
|
164
|
-
if (!result) return undefined;
|
|
165
|
-
return { ...init, body: JSON.stringify(result.obj) };
|
|
166
|
-
} catch {
|
|
167
|
-
return undefined;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* If `frame` is a Codex `response.create` carrying an `input` (or `messages`)
|
|
173
|
-
* array, return the cleaned frame plus a tally of stripped items. Returns
|
|
174
|
-
* `undefined` when nothing matched (caller forwards the original frame
|
|
175
|
-
* untouched). Exported for unit testing.
|
|
176
|
-
*/
|
|
177
|
-
export function stripContentFromWireFrame(frame: unknown): { frame: Record<string, unknown>; stripped: number } | undefined {
|
|
178
|
-
if (!isRecord(frame) || frame.type !== "response.create") return undefined;
|
|
179
|
-
const result = stripCarrier(frame);
|
|
180
|
-
if (!result) return undefined;
|
|
181
|
-
return { frame: result.obj, stripped: result.stripped };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
74
|
export default function codexReasoningFix(pi: ExtensionAPI): void {
|
|
75
|
+
// src/index.ts deliberately registers this module last. A later payload
|
|
76
|
+
// modifier could otherwise reintroduce invalid content after sanitization,
|
|
77
|
+
// and transport encoding happens after this hook.
|
|
185
78
|
pi.on("before_provider_request", async (event: ProviderRequestEvent, _ctx: ProviderRequestContext) => {
|
|
186
79
|
const result = stripReasoningContentFromPayload(event.payload);
|
|
187
80
|
return result === event.payload ? undefined : result;
|
|
@@ -189,8 +82,7 @@ export default function codexReasoningFix(pi: ExtensionAPI): void {
|
|
|
189
82
|
}
|
|
190
83
|
|
|
191
84
|
/**
|
|
192
|
-
* Strip spurious `content` from non-message items in a full payload.
|
|
193
|
-
* guard for the SSE-fallback / non-websocket path. Returns the same reference
|
|
85
|
+
* Strip spurious `content` from non-message items in a full payload. Returns the same reference
|
|
194
86
|
* when nothing changed; exported for unit testing.
|
|
195
87
|
*/
|
|
196
88
|
export function stripReasoningContentFromPayload(payload: unknown): unknown {
|
|
@@ -85,6 +85,23 @@ export interface DcpConfig {
|
|
|
85
85
|
readLikeTurns: number
|
|
86
86
|
protectedTools: string[]
|
|
87
87
|
}
|
|
88
|
+
emergencyCurrentTurnPruning: {
|
|
89
|
+
/** Enable same-turn candidates and lossy fallback pruning; emergency reminders remain active. */
|
|
90
|
+
enabled: boolean
|
|
91
|
+
/** Prune immediately at or above this model-independent context fraction. */
|
|
92
|
+
hardContextPercent: number
|
|
93
|
+
/** Recover enough estimated tokens to reach this fraction or a margin below the model emergency threshold. */
|
|
94
|
+
targetContextPercent: number
|
|
95
|
+
/** Emergency reminders allowed before pruning even below hardContextPercent. */
|
|
96
|
+
patience: number
|
|
97
|
+
/** Newest complete assistant tool-call/result pairs that are never selected. */
|
|
98
|
+
keepRecentToolPairs: number
|
|
99
|
+
/** Ignore small results whose replacement would recover little context. */
|
|
100
|
+
minOutputTokens: number
|
|
101
|
+
/** Maximum emergency same-turn message candidates shown in a reminder. */
|
|
102
|
+
maxSuggestions: number
|
|
103
|
+
protectedTools: string[]
|
|
104
|
+
}
|
|
88
105
|
}
|
|
89
106
|
protectedFilePatterns: string[]
|
|
90
107
|
pruneNotification: "off" | "minimal" | "detailed"
|
|
@@ -178,6 +195,16 @@ const DEFAULT_CONFIG: DcpConfig = {
|
|
|
178
195
|
readLikeTurns: 3,
|
|
179
196
|
protectedTools: [],
|
|
180
197
|
},
|
|
198
|
+
emergencyCurrentTurnPruning: {
|
|
199
|
+
enabled: true,
|
|
200
|
+
hardContextPercent: 0.82,
|
|
201
|
+
targetContextPercent: 0.70,
|
|
202
|
+
patience: 2,
|
|
203
|
+
keepRecentToolPairs: 8,
|
|
204
|
+
minOutputTokens: 500,
|
|
205
|
+
maxSuggestions: 8,
|
|
206
|
+
protectedTools: [],
|
|
207
|
+
},
|
|
181
208
|
},
|
|
182
209
|
protectedFilePatterns: [],
|
|
183
210
|
pruneNotification: "detailed",
|
|
@@ -128,6 +128,8 @@ export function summarizeDcpState(state: DcpState): Record<string, unknown> {
|
|
|
128
128
|
},
|
|
129
129
|
inactiveBlocksTail: inactiveBlocks,
|
|
130
130
|
prunedTools: state.prunedToolIds.size,
|
|
131
|
+
providerSeenTools: state.providerSeenToolIds.size,
|
|
132
|
+
consecutiveEmergencyPasses: state.consecutiveIgnoredStrongNudges,
|
|
131
133
|
nudgeAnchors: state.nudgeAnchors.map((anchor) => ({
|
|
132
134
|
id: anchor.id,
|
|
133
135
|
type: anchor.type,
|
|
@@ -232,4 +234,3 @@ export function writeDcpDebugLog(
|
|
|
232
234
|
export function dcpDebugLogDrain(): Promise<void> {
|
|
233
235
|
return logWriteChain
|
|
234
236
|
}
|
|
235
|
-
|