@howaboua/pi-codex-conversion 1.5.4 → 1.5.5-dev.25.f80a775
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/adapter/activation.ts +37 -13
- package/src/adapter/compact-client.ts +257 -0
- package/src/adapter/compaction-output.ts +80 -0
- package/src/adapter/compaction-runtime.ts +272 -0
- package/src/adapter/compaction.ts +261 -0
- package/src/adapter/config.ts +27 -0
- package/src/adapter/context-filter.ts +20 -0
- package/src/adapter/details-store.ts +151 -0
- package/src/adapter/payload-rewrite.ts +550 -0
- package/src/adapter/provider-request.ts +4 -2
- package/src/adapter/serializer.ts +288 -0
- package/src/adapter/state.ts +1 -0
- package/src/adapter/tool-set.ts +2 -1
- package/src/adapter/types.ts +220 -0
- package/src/codex-settings/command.ts +19 -4
- package/src/codex-settings/ui.ts +126 -46
- package/src/index.ts +50 -21
- package/src/providers/openai-codex-custom-provider.ts +1 -1
- package/src/providers/openai-responses-shared.ts +2 -0
- package/vendor/apply-patch/win32-arm64/apply_patch.exe +0 -0
- package/vendor/apply-patch/win32-x64/apply_patch.exe +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.5
|
|
4
|
+
|
|
5
|
+
- Avoid registering disabled native `web_search` and `image_generation` tools so other extensions can own those names.
|
|
6
|
+
- Preserve other extensions' `web_search` and `image_generation` tools when the matching Codex feature is off.
|
|
7
|
+
- Added a `/codex status` toggle and settings UI option for hiding the Codex footer/statusline.
|
|
8
|
+
|
|
3
9
|
## 1.5.4
|
|
4
10
|
|
|
5
11
|
- Added `/codex` settings UI.
|
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ Notably:
|
|
|
42
42
|
Use `/codex` to change adapter settings.
|
|
43
43
|
|
|
44
44
|
- `/codex all` — use the Codex tool and prompt adapter on every model
|
|
45
|
+
- `/codex status` — toggle the footer/statusline entry
|
|
45
46
|
- `/codex fast` — toggle priority service tier for the OpenAI Codex provider
|
|
46
47
|
- `/codex search` — toggle native Codex web search
|
|
47
48
|
- `/codex image` — toggle native Codex image generation
|
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@ import { supportsNativeImageGeneration } from "../tools/image-generation-tool.ts
|
|
|
15
15
|
import { supportsNativeWebSearch } from "../tools/web-search-tool.ts";
|
|
16
16
|
|
|
17
17
|
const ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, WEB_SEARCH_TOOL_NAME, IMAGE_GENERATION_TOOL_NAME, VIEW_IMAGE_TOOL_NAME];
|
|
18
|
+
const ALWAYS_OWNED_ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, VIEW_IMAGE_TOOL_NAME];
|
|
18
19
|
|
|
19
20
|
export function syncAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
|
|
20
21
|
if (shouldUseCodexAdapter(ctx, state.config)) {
|
|
@@ -29,32 +30,41 @@ export function shouldUseCodexAdapter(ctx: ExtensionContext, config: CodexConver
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
function enableAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
|
|
32
|
-
const
|
|
33
|
+
const currentAdapterOwnedTools = getAdapterOwnedToolNames(state.config);
|
|
34
|
+
const adapterOwnedTools = state.enabled ? mergeToolNames(state.adapterOwnedToolNames ?? currentAdapterOwnedTools, currentAdapterOwnedTools) : currentAdapterOwnedTools;
|
|
35
|
+
const toolNames = mergeAdapterTools(pi.getActiveTools(), getAdapterToolNames(ctx, state.config), adapterOwnedTools);
|
|
33
36
|
if (!state.enabled) {
|
|
34
37
|
// Preserve the previous active set once so switching away from Codex-like
|
|
35
38
|
// models restores the user's existing Pi tool configuration. Strip adapter
|
|
36
39
|
// tools in case a fresh session starts from persisted/mixed active tools.
|
|
37
|
-
state.previousToolNames = stripAdapterTools(pi.getActiveTools());
|
|
40
|
+
state.previousToolNames = stripAdapterTools(pi.getActiveTools(), adapterOwnedTools);
|
|
38
41
|
state.enabled = true;
|
|
39
42
|
}
|
|
43
|
+
state.adapterOwnedToolNames = currentAdapterOwnedTools;
|
|
40
44
|
pi.setActiveTools(toolNames);
|
|
41
45
|
setStatus(ctx, true, state.config);
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
function disableAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
|
|
45
49
|
const previousToolNames = state.previousToolNames && state.previousToolNames.length > 0 ? state.previousToolNames : DEFAULT_TOOL_NAMES;
|
|
46
|
-
const
|
|
47
|
-
|
|
50
|
+
const adapterOwnedTools = state.adapterOwnedToolNames ?? getAdapterOwnedToolNames(state.config);
|
|
51
|
+
const restoredTools = restoreTools(previousToolNames, pi.getActiveTools(), adapterOwnedTools);
|
|
52
|
+
if (state.enabled || hasAdapterTools(pi.getActiveTools(), adapterOwnedTools)) {
|
|
48
53
|
pi.setActiveTools(restoredTools);
|
|
49
54
|
}
|
|
50
55
|
if (state.enabled) {
|
|
51
56
|
state.enabled = false;
|
|
57
|
+
state.adapterOwnedToolNames = undefined;
|
|
52
58
|
}
|
|
53
59
|
setStatus(ctx, false, state.config);
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
function setStatus(ctx: ExtensionContext, enabled: boolean, config: CodexConversionConfig): void {
|
|
57
63
|
if (!ctx.hasUI) return;
|
|
64
|
+
if (!config.statusLine) {
|
|
65
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
58
68
|
const statusConfig = getStatusConfig(ctx, config);
|
|
59
69
|
ctx.ui.setStatus(STATUS_KEY, enabled ? buildStatusText(statusConfig) : undefined);
|
|
60
70
|
}
|
|
@@ -67,6 +77,7 @@ function getStatusConfig(ctx: ExtensionContext, config: CodexConversionConfig):
|
|
|
67
77
|
fast: showOpenAICodexFlags && config.fast,
|
|
68
78
|
webSearch: showOpenAICodexFlags && config.webSearch && supportsNativeWebSearch(ctx.model),
|
|
69
79
|
imageGeneration: showOpenAICodexFlags && config.imageGeneration && supportsNativeImageGeneration(ctx.model),
|
|
80
|
+
compaction: { enabled: Boolean(config.responsesCompaction), model: config.compactionModel, reasoning: config.compactionReasoning },
|
|
70
81
|
...(showResponsesVerbosity ? { verbosity: config.verbosity } : {}),
|
|
71
82
|
};
|
|
72
83
|
}
|
|
@@ -85,25 +96,38 @@ function getAdapterToolNames(ctx: ExtensionContext, config: CodexConversionConfi
|
|
|
85
96
|
return toolNames;
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
function getAdapterOwnedToolNames(config: CodexConversionConfig): string[] {
|
|
100
|
+
return [
|
|
101
|
+
...ALWAYS_OWNED_ADAPTER_TOOL_NAMES,
|
|
102
|
+
...(config.webSearch ? [WEB_SEARCH_TOOL_NAME] : []),
|
|
103
|
+
...(config.imageGeneration ? [IMAGE_GENERATION_TOOL_NAME] : []),
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function mergeToolNames(...toolNameGroups: string[][]): string[] {
|
|
108
|
+
return [...new Set(toolNameGroups.flat())];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function mergeAdapterTools(activeTools: string[], adapterTools: string[], adapterOwnedTools: string[] = adapterTools): string[] {
|
|
112
|
+
const ownedTools = new Set([...ALWAYS_OWNED_ADAPTER_TOOL_NAMES, ...adapterTools, ...adapterOwnedTools]);
|
|
113
|
+
const preservedTools = activeTools.filter((toolName) => !DEFAULT_TOOL_NAMES.includes(toolName) && !ownedTools.has(toolName));
|
|
90
114
|
return [...adapterTools, ...preservedTools];
|
|
91
115
|
}
|
|
92
116
|
|
|
93
|
-
export function restoreTools(previousTools: string[], activeTools: string[]): string[] {
|
|
94
|
-
const restored = stripAdapterTools(previousTools);
|
|
117
|
+
export function restoreTools(previousTools: string[], activeTools: string[], adapterOwnedTools: string[] = ADAPTER_TOOL_NAMES): string[] {
|
|
118
|
+
const restored = stripAdapterTools(previousTools, adapterOwnedTools);
|
|
95
119
|
for (const toolName of activeTools) {
|
|
96
|
-
if (!
|
|
120
|
+
if (!adapterOwnedTools.includes(toolName) && !restored.includes(toolName)) {
|
|
97
121
|
restored.push(toolName);
|
|
98
122
|
}
|
|
99
123
|
}
|
|
100
124
|
return restored;
|
|
101
125
|
}
|
|
102
126
|
|
|
103
|
-
export function stripAdapterTools(toolNames: string[]): string[] {
|
|
104
|
-
return toolNames.filter((toolName) => !
|
|
127
|
+
export function stripAdapterTools(toolNames: string[], adapterOwnedTools: string[] = ADAPTER_TOOL_NAMES): string[] {
|
|
128
|
+
return toolNames.filter((toolName) => !adapterOwnedTools.includes(toolName));
|
|
105
129
|
}
|
|
106
130
|
|
|
107
|
-
function hasAdapterTools(activeTools: string[]): boolean {
|
|
108
|
-
return activeTools.some((toolName) =>
|
|
131
|
+
function hasAdapterTools(activeTools: string[], adapterOwnedTools: string[]): boolean {
|
|
132
|
+
return activeTools.some((toolName) => adapterOwnedTools.includes(toolName));
|
|
109
133
|
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type { NativeCompactionRuntime } from "./compaction-runtime.ts";
|
|
2
|
+
import type { NativeCompactionRequestBody } from "./serializer.ts";
|
|
3
|
+
|
|
4
|
+
const JSON_CONTENT_TYPE = "application/json";
|
|
5
|
+
|
|
6
|
+
type CompactResponseEnvelope = {
|
|
7
|
+
id?: string;
|
|
8
|
+
created_at?: number | string;
|
|
9
|
+
output: unknown[];
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type NativeCompactionClientFailureReason =
|
|
14
|
+
| "aborted"
|
|
15
|
+
| "network-error"
|
|
16
|
+
| "non-2xx"
|
|
17
|
+
| "empty-body"
|
|
18
|
+
| "invalid-json"
|
|
19
|
+
| "malformed-response"
|
|
20
|
+
| "empty-output";
|
|
21
|
+
|
|
22
|
+
export type NativeCompactionClientSuccess = {
|
|
23
|
+
ok: true;
|
|
24
|
+
status: number;
|
|
25
|
+
compactedWindow: unknown[];
|
|
26
|
+
compactResponseId?: string;
|
|
27
|
+
createdAt?: string;
|
|
28
|
+
response: CompactResponseEnvelope;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type NativeCompactionClientFailure = {
|
|
32
|
+
ok: false;
|
|
33
|
+
reason: NativeCompactionClientFailureReason;
|
|
34
|
+
status?: number;
|
|
35
|
+
errorMessage?: string;
|
|
36
|
+
responseText?: string;
|
|
37
|
+
responseJson?: unknown;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type NativeCompactionClientResult = NativeCompactionClientSuccess | NativeCompactionClientFailure;
|
|
41
|
+
|
|
42
|
+
export type ExecuteNativeCompactionOptions = {
|
|
43
|
+
runtime: NativeCompactionRuntime;
|
|
44
|
+
request: NativeCompactionRequestBody;
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
49
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isAbortError(error: unknown): boolean {
|
|
53
|
+
return (
|
|
54
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
55
|
+
(error instanceof Error && (error.name === "AbortError" || error.name === "ABORT_ERR"))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeResponseTimestamp(value: unknown): string | undefined {
|
|
60
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
61
|
+
const milliseconds = value > 1_000_000_000_000 ? value : value * 1000;
|
|
62
|
+
return new Date(milliseconds).toISOString();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof value !== "string") {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const trimmed = value.trim();
|
|
70
|
+
if (!trimmed) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const parsed = Date.parse(trimmed);
|
|
75
|
+
return Number.isNaN(parsed) ? trimmed : new Date(parsed).toISOString();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isCompactOutputItem(value: unknown): value is Record<string, unknown> {
|
|
79
|
+
return isRecord(value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isCompactResponseEnvelope(value: unknown): value is CompactResponseEnvelope {
|
|
83
|
+
return isRecord(value) && Array.isArray(value.output) && value.output.every(isCompactOutputItem);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
|
|
87
|
+
const parts = token.split(".");
|
|
88
|
+
if (parts.length !== 3) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const payloadText = Buffer.from(parts[1]!, "base64url").toString("utf8");
|
|
94
|
+
const payload = JSON.parse(payloadText);
|
|
95
|
+
return isRecord(payload) ? payload : undefined;
|
|
96
|
+
} catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function extractCodexAccountId(token: string): string | undefined {
|
|
102
|
+
const payload = decodeJwtPayload(token);
|
|
103
|
+
const authClaims = payload?.["https://api.openai.com/auth"];
|
|
104
|
+
if (!isRecord(authClaims)) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const accountId = authClaims.chatgpt_account_id;
|
|
109
|
+
return typeof accountId === "string" && accountId.trim().length > 0 ? accountId.trim() : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildCodexUserAgent(): string {
|
|
113
|
+
const platform = typeof process !== "undefined" ? process.platform : "browser";
|
|
114
|
+
const arch = typeof process !== "undefined" ? process.arch : "unknown";
|
|
115
|
+
return `pi (${platform}; ${arch})`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function extractBearerToken(headers: Headers): string | undefined {
|
|
119
|
+
const authorization = headers.get("authorization")?.trim();
|
|
120
|
+
const match = authorization?.match(/^Bearer\s+(.+)$/i);
|
|
121
|
+
return match?.[1]?.trim() || undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function toHeaders(runtime: NativeCompactionRuntime): Record<string, string> {
|
|
125
|
+
const headers = new Headers(runtime.currentModel.headers ?? {});
|
|
126
|
+
for (const [key, value] of Object.entries(runtime.headers ?? {})) {
|
|
127
|
+
headers.set(key, value);
|
|
128
|
+
}
|
|
129
|
+
headers.set("accept", JSON_CONTENT_TYPE);
|
|
130
|
+
headers.set("content-type", JSON_CONTENT_TYPE);
|
|
131
|
+
if (runtime.apiKey) {
|
|
132
|
+
headers.set("authorization", `Bearer ${runtime.apiKey}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (runtime.provider === "openai-codex") {
|
|
136
|
+
const accountId = extractCodexAccountId(runtime.apiKey ?? extractBearerToken(headers) ?? "");
|
|
137
|
+
if (accountId) {
|
|
138
|
+
headers.set("chatgpt-account-id", accountId);
|
|
139
|
+
}
|
|
140
|
+
headers.set("originator", "pi");
|
|
141
|
+
headers.set("user-agent", buildCodexUserAgent());
|
|
142
|
+
headers.set("openai-beta", "responses=experimental");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return Object.fromEntries(headers.entries());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function executeNativeCompaction(
|
|
149
|
+
options: ExecuteNativeCompactionOptions,
|
|
150
|
+
): Promise<NativeCompactionClientResult> {
|
|
151
|
+
const { runtime, request, signal } = options;
|
|
152
|
+
const headers = toHeaders(runtime);
|
|
153
|
+
|
|
154
|
+
if (signal?.aborted) {
|
|
155
|
+
const aborted: NativeCompactionClientFailure = {
|
|
156
|
+
ok: false,
|
|
157
|
+
reason: "aborted",
|
|
158
|
+
};
|
|
159
|
+
return aborted;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const response = await fetch(runtime.compactUrl, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers,
|
|
166
|
+
body: JSON.stringify(request),
|
|
167
|
+
signal,
|
|
168
|
+
});
|
|
169
|
+
const responseText = await response.text();
|
|
170
|
+
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
let responseJson: unknown;
|
|
173
|
+
if (responseText.trim().length > 0) {
|
|
174
|
+
try {
|
|
175
|
+
responseJson = JSON.parse(responseText);
|
|
176
|
+
} catch {
|
|
177
|
+
responseJson = undefined;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const failure: NativeCompactionClientFailure = {
|
|
182
|
+
ok: false,
|
|
183
|
+
reason: "non-2xx",
|
|
184
|
+
status: response.status,
|
|
185
|
+
responseText: responseText || undefined,
|
|
186
|
+
responseJson,
|
|
187
|
+
};
|
|
188
|
+
return failure;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!responseText.trim()) {
|
|
192
|
+
const failure: NativeCompactionClientFailure = {
|
|
193
|
+
ok: false,
|
|
194
|
+
reason: "empty-body",
|
|
195
|
+
status: response.status,
|
|
196
|
+
};
|
|
197
|
+
return failure;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let parsed: unknown;
|
|
201
|
+
try {
|
|
202
|
+
parsed = JSON.parse(responseText);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
const failure: NativeCompactionClientFailure = {
|
|
205
|
+
ok: false,
|
|
206
|
+
reason: "invalid-json",
|
|
207
|
+
status: response.status,
|
|
208
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
209
|
+
responseText,
|
|
210
|
+
};
|
|
211
|
+
return failure;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!isCompactResponseEnvelope(parsed)) {
|
|
215
|
+
const failure: NativeCompactionClientFailure = {
|
|
216
|
+
ok: false,
|
|
217
|
+
reason: "malformed-response",
|
|
218
|
+
status: response.status,
|
|
219
|
+
responseJson: parsed,
|
|
220
|
+
};
|
|
221
|
+
return failure;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (parsed.output.length === 0) {
|
|
225
|
+
const failure: NativeCompactionClientFailure = {
|
|
226
|
+
ok: false,
|
|
227
|
+
reason: "empty-output",
|
|
228
|
+
status: response.status,
|
|
229
|
+
responseJson: parsed,
|
|
230
|
+
};
|
|
231
|
+
return failure;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const success: NativeCompactionClientSuccess = {
|
|
235
|
+
ok: true,
|
|
236
|
+
status: response.status,
|
|
237
|
+
compactedWindow: [...parsed.output],
|
|
238
|
+
compactResponseId: typeof parsed.id === "string" && parsed.id.trim() ? parsed.id.trim() : undefined,
|
|
239
|
+
createdAt: normalizeResponseTimestamp(parsed.created_at),
|
|
240
|
+
response: parsed,
|
|
241
|
+
};
|
|
242
|
+
return success;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
const failure: NativeCompactionClientFailure = isAbortError(error)
|
|
245
|
+
? {
|
|
246
|
+
ok: false,
|
|
247
|
+
reason: "aborted",
|
|
248
|
+
}
|
|
249
|
+
: {
|
|
250
|
+
ok: false,
|
|
251
|
+
reason: "network-error",
|
|
252
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
return failure;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const COMPACTION_ITEM_TYPES = new Set(["compaction", "compaction_summary"]);
|
|
2
|
+
|
|
3
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function cloneStructuredValue(value: unknown): unknown {
|
|
8
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
return value.map(cloneStructuredValue);
|
|
13
|
+
}
|
|
14
|
+
if (isRecord(value)) {
|
|
15
|
+
const clone: Record<string, unknown> = {};
|
|
16
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
17
|
+
clone[key] = cloneStructuredValue(nested);
|
|
18
|
+
}
|
|
19
|
+
return clone;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Unsupported structured compact output value: ${typeof value}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cloneCompactedOutputItem(item: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
25
|
+
try {
|
|
26
|
+
return cloneStructuredValue(item) as Record<string, unknown>;
|
|
27
|
+
} catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function shouldKeepCompactedOutputItem(item: unknown): item is Record<string, unknown> {
|
|
33
|
+
return isRecord(item) && typeof item.type === "string";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sanitizeCompactedWindow(output: readonly unknown[]): Record<string, unknown>[] {
|
|
37
|
+
const sanitized: Record<string, unknown>[] = [];
|
|
38
|
+
for (const item of output) {
|
|
39
|
+
if (!shouldKeepCompactedOutputItem(item)) continue;
|
|
40
|
+
const cloned = cloneCompactedOutputItem(item);
|
|
41
|
+
if (cloned) sanitized.push(cloned);
|
|
42
|
+
}
|
|
43
|
+
return sanitized;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function extractCompactionSummaryText(compactedWindow: readonly unknown[]): string | undefined {
|
|
47
|
+
for (const item of compactedWindow) {
|
|
48
|
+
if (!isRecord(item) || typeof item.type !== "string" || !COMPACTION_ITEM_TYPES.has(item.type)) continue;
|
|
49
|
+
if (typeof item.encrypted_content === "string" && item.encrypted_content.trim().length > 0) return item.encrypted_content.trim();
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function hasCompactionOutputItem(compactedWindow: readonly unknown[]): boolean {
|
|
55
|
+
return compactedWindow.some((item) => isRecord(item) && typeof item.type === "string" && COMPACTION_ITEM_TYPES.has(item.type));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function describeOutputItem(item: unknown): string {
|
|
59
|
+
if (!isRecord(item)) return typeof item;
|
|
60
|
+
const type = typeof item.type === "string" ? item.type : "<missing-type>";
|
|
61
|
+
const role = typeof item.role === "string" ? `/${item.role}` : "";
|
|
62
|
+
const content = Array.isArray(item.content) ? ` content=${item.content.length}` : "";
|
|
63
|
+
const keys = Object.keys(item).sort().slice(0, 8).join(",");
|
|
64
|
+
return `${type}${role}${content} keys=[${keys}]`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function summarizeCompactionOutputForDiagnostics(rawOutput: readonly unknown[], sanitizedOutput: readonly unknown[]): string {
|
|
68
|
+
const rawTypes = rawOutput.map((item) => isRecord(item) && typeof item.type === "string" ? item.type : typeof item);
|
|
69
|
+
const sanitizedTypes = sanitizedOutput.map((item) => isRecord(item) && typeof item.type === "string" ? item.type : typeof item);
|
|
70
|
+
const rawCounts = countValues(rawTypes);
|
|
71
|
+
const sanitizedCounts = countValues(sanitizedTypes);
|
|
72
|
+
const sample = rawOutput.slice(0, 8).map((item, index) => `${index}: ${describeOutputItem(item)}`).join("; ");
|
|
73
|
+
return `raw=${rawOutput.length} {${rawCounts}}; sanitized=${sanitizedOutput.length} {${sanitizedCounts}}; sample=${sample || "<empty>"}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function countValues(values: readonly string[]): string {
|
|
77
|
+
const counts = new Map<string, number>();
|
|
78
|
+
for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
79
|
+
return Array.from(counts.entries()).map(([value, count]) => `${value}:${count}`).join(", ");
|
|
80
|
+
}
|