dsh-lcx-codex 0.3.4 → 0.4.0-rc.13

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/lib/index.js CHANGED
@@ -1,31 +1,46 @@
1
1
  import z from '@deepseek-ai/schemastery'
2
- import { attributionHeaders, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
3
- import { settingsNamespace } from '@deepseek-ai/dsh-settings'
4
- import { clampOpenAIPromptCacheKey } from '@earendil-works/pi-ai/api/openai-prompt-cache'
2
+ import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
5
3
  import { randomUUID } from 'node:crypto'
6
4
  import { homedir } from 'node:os'
7
5
  import { join } from 'node:path'
8
- import { CheckpointV3Store, CHECKPOINT_V3_VERSION } from './checkpoint-store-v3.js'
6
+ import { AsyncLocalStorage } from 'node:async_hooks'
7
+ import { symbols as cordisSymbols } from '@deepseek-ai/cordis'
8
+ import { fetchJsonWithRetry } from './transport.js'
9
9
  import {
10
- baseURLFingerprint,
11
- buildNativeReplacementHistory,
12
- buildPortableHistory,
13
- assertCheckpointRoute,
14
- buildPortableResponsesInput,
15
- buildPortableResponsesInputWithImages,
16
- hasPortableCheckpoint,
17
- hydrateNativeImageReferences,
18
- inputImageCount,
19
- checkpointNativeReplacementHistory,
20
- latestPortableMarker,
21
- normalizeCompactionResponse,
22
- persistNativeImageReferences,
10
+ authenticatedHeaders,
11
+ currentRoute,
12
+ generationControlsFromHeader,
13
+ generationControlsFromSession,
14
+ promptCacheKey,
15
+ promptCacheRetention,
16
+ promptCacheSessionId,
17
+ resolveResponsesRouteConfig,
23
18
  routeFingerprint,
24
- textOfMessage,
25
- } from './compact.js'
26
- import { DEFAULT_MAX_REQUEST_IMAGE_BYTES, resolveModelImageSupport } from './dsh-pi-responses.js'
27
- import { abortIfNeeded, fetchJsonWithRetry, fetchSse } from './transport.js'
28
- import { mergeFeatureHeader, requestNativeCompaction, responsesTools } from './compact-v2.js'
19
+ updateRequestHeaderCache,
20
+ } from './route.js'
21
+ import {
22
+ DEFAULT_MAX_REQUEST_IMAGE_BYTES,
23
+ DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
24
+ DEFAULT_REQUEST_IMAGE_MAX_BYTES,
25
+ hydrateNativeImageReferences,
26
+ resolveModelImageSupport,
27
+ serializeDshMessages,
28
+ } from './dsh-responses.js'
29
+ import { requestNativeCompaction } from './compact-v2.js'
30
+ import { requestNativeReplay } from './responses-replay.js'
31
+ import {
32
+ checkpointStateForMessage,
33
+ compactCheckpointId,
34
+ createNativeCheckpointBlock,
35
+ legacyCheckpointId,
36
+ nativeCheckpointChunks,
37
+ portableMessagesForCheckpoint,
38
+ retainedConversationInput,
39
+ shadowedMessagesForCheckpoint,
40
+ rewriteCheckpointsPortable,
41
+ stateRouteCompatible,
42
+ } from './native-checkpoint.js'
43
+ import { legacyRouteCompatible, legacyV3Id, loadLegacyRecord } from './legacy-v3.js'
29
44
  import {
30
45
  HOSTED_SEARCH_OUTPUT,
31
46
  HOSTED_SEARCH_PARAMETERS,
@@ -45,28 +60,28 @@ import {
45
60
  } from './web-search-alpha.js'
46
61
  import { AlphaCapabilityStore, alphaCapabilityFingerprint, alphaCapabilityUsable } from './web-search-capability.js'
47
62
  import { AlphaRefStore } from './web-search-ref-store.js'
48
- import { createSessionGenerationTracker } from './session-lease.js'
63
+ import { ServiceMutex } from './service-mutex.js'
49
64
 
50
- const name = 'lcx-codex'
51
- const inject = ['llm', 'web', 'credentials', 'settings', 'tools']
52
- const WEB_SEARCH_TOOL_NAME = 'websearch_gpt'
53
- const ALPHA_SEARCH_TOOL_NAME = 'websearch_alpha'
65
+ export const name = 'lcx-codex'
66
+ export const inject = ['llm', 'web', 'sessions']
67
+ const SETTINGS_NS = settingsNamespace('lcx-codex')
68
+ const ADVANCED_HOSTED_TOOL = 'websearch_gpt_advanced'
69
+ const ALPHA_TOOL = 'websearch_alpha'
54
70
  const COMPACTION_DIRECTIVE = 'You are now acting as a compaction engine'
55
- const SETTINGS_NAMESPACE = 'lcx-codex'
56
- const checkpointReplayOptions = new WeakSet()
57
- const PORTABLE_REPLAY_TEXT_MAX_CHARS = 32_000
58
- const PORTABLE_REPLAY_TOTAL_MAX_CHARS = 80_000
59
- const PORTABLE_REPLAY_TOTAL_MAX_BYTES = 2 * 1024 * 1024
60
- const V3_CHECKPOINT_MARKER_PATTERN = /\[dsh-lcx-codex-v3-checkpoint:[0-9a-f-]{36}\]/giu
61
- const CHECKPOINT_REPLAY_UNAVAILABLE_CODE = 'LCX_CHECKPOINT_REPLAY_UNAVAILABLE'
62
- const UNSUPPORTED_IMAGE_PLACEHOLDER = '[image omitted because the target model does not support image input]'
71
+ const bypassReplayOptions = new WeakSet()
72
+ const hostedSearchRouteContext = new AsyncLocalStorage()
73
+
74
+ function dshHome() { return process.env.DSH_HOME ?? join(homedir(), '.dsh') }
75
+ function defaultLegacyCheckpointPath() { return join(dshHome(), 'storages', 'lcx-codex', 'checkpoints-v3.json') }
76
+ function defaultAlphaCapabilityPath() { return join(dshHome(), 'storages', 'lcx-codex', 'web-alpha-capabilities.json') }
77
+ function defaultAlphaRefPath() { return join(dshHome(), 'storages', 'lcx-codex', 'web-alpha-refs.json') }
63
78
 
64
- const Config = z.object({
79
+ export const Config = z.object({
65
80
  provider: z.string().default('lcx'),
66
81
  baseURL: z.string().default('https://api.lcxbot.com/v1'),
67
82
  apiKeyEnv: z.string().default('LCX_API_KEY'),
68
83
  model: z.string().default('gpt-5.6-sol'),
69
- compactTransport: z.string().default('native-v2'),
84
+ legacyCheckpointPath: z.string().default(''),
70
85
  checkpointPath: z.string().default(''),
71
86
  alphaCapabilityPath: z.string().default(''),
72
87
  alphaRefPath: z.string().default(''),
@@ -76,80 +91,46 @@ const Config = z.object({
76
91
  webSearchProvider: z.string().default('lcx-responses'),
77
92
  webMaxResults: z.number().default(8),
78
93
  timeoutMs: z.number().default(300000),
79
- maxResponseBytes: z.number().default(4 * 1024 * 1024),
94
+ maxResponseBytes: z.number().default(8 * 1024 * 1024),
80
95
  maxAttempts: z.number().default(3),
81
96
  maxRequestImageBytes: z.number().default(DEFAULT_MAX_REQUEST_IMAGE_BYTES),
97
+ requestImagePixelBudget: z.number().default(DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET),
98
+ requestImageMaxBytes: z.number().default(DEFAULT_REQUEST_IMAGE_MAX_BYTES),
99
+ portableReplayMaxChars: z.number().default(80_000),
100
+ nativeRetentionTokenBudget: z.number().default(64_000),
101
+ assistantRetentionTokenReserve: z.number().default(24_000),
102
+ assistantRetentionPerMessageTokenCap: z.number().default(3_000),
103
+ webSearchTimeoutMs: z.number().default(240_000),
104
+ autoCompactionThresholdPercent: z.number().default(90),
105
+ emergencyPruneThresholdPercent: z.number().default(95),
82
106
  })
83
107
 
84
108
  const SettingsSchema = z.object({
85
109
  enabled: z.boolean().default(false),
86
110
  webSearch: z.boolean().default(false),
111
+ advancedHostedSearch: z.boolean().default(false),
87
112
  alphaSearch: z.boolean().default(false),
88
113
  remoteCompaction: z.boolean().default(false),
89
114
  fallbackToBasicCompaction: z.boolean().default(true),
115
+ autoCompaction: z.boolean().default(true),
116
+ webSearchTimeoutSeconds: z.number().default(240),
117
+ autoCompactionThresholdPercent: z.number().default(90),
118
+ emergencyPruneThresholdPercent: z.number().default(95),
90
119
  provider: z.string().default('lcx'),
91
120
  baseURL: z.string().default('https://api.lcxbot.com/v1'),
92
121
  apiKeyEnv: z.string().default('LCX_API_KEY'),
93
122
  model: z.string().default('gpt-5.6-sol'),
94
- compactTransport: z.string().default('native-v2'),
95
123
  })
96
124
 
97
- function webError(message, code, cause) {
98
- const error = new Error(message, cause === undefined ? undefined : { cause })
99
- error.name = 'WebError'
100
- error.code = code
101
- return error
102
- }
103
-
104
- function defaultCheckpointPath() {
105
- return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'storages', 'lcx-codex', 'checkpoints-v3.json')
106
- }
107
-
108
- function defaultAlphaCapabilityPath() {
109
- return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'storages', 'lcx-codex', 'web-alpha-capabilities.json')
110
- }
111
-
112
- function defaultAlphaRefPath() {
113
- return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'storages', 'lcx-codex', 'web-alpha-refs.json')
114
- }
115
-
116
- function portableCheckpointPath(checkpointPath) {
117
- const value = String(checkpointPath)
118
- if (/-v3\.json$/iu.test(value)) return value
119
- return /\.json$/iu.test(value) ? value.replace(/\.json$/iu, '-v3.json') : `${value}-v3.json`
120
- }
121
-
122
- const COMPACT_TRANSPORT_UNSUPPORTED_CODE = 'LCX_COMPACT_TRANSPORT_UNSUPPORTED'
123
- const COMPACT_TRANSPORT_INVALID_CODE = 'LCX_COMPACT_TRANSPORT_INVALID'
124
-
125
- function normalizeCompactTransport(value) {
126
- const normalized = String(value ?? 'native-v2').trim().toLowerCase()
127
- if (normalized === 'legacy') {
128
- const error = new Error('Legacy compact transport is unsupported; only native-v2 is available')
129
- error.code = COMPACT_TRANSPORT_UNSUPPORTED_CODE
130
- throw error
131
- }
132
- if (normalized !== 'native-v2') {
133
- const error = new Error(`Unsupported compact transport: ${String(value)}`)
134
- error.code = COMPACT_TRANSPORT_INVALID_CODE
135
- throw error
136
- }
137
- return normalized
138
- }
139
-
140
125
  function normalizeConfig(input = {}) {
141
- const cacheRetention = ['none', 'short', 'long'].includes(input.cacheRetention) ? input.cacheRetention : 'short'
126
+ const legacy = input.legacyCheckpointPath || input.checkpointPath || defaultLegacyCheckpointPath()
142
127
  return {
143
128
  provider: input.provider || 'lcx',
144
129
  baseURL: String(input.baseURL || 'https://api.lcxbot.com/v1').replace(/\/+$/u, ''),
145
130
  apiKeyEnv: input.apiKeyEnv || 'LCX_API_KEY',
146
131
  model: input.model || 'gpt-5.6-sol',
147
- compactTransport: normalizeCompactTransport(input.compactTransport),
148
132
  headers: input.headers && typeof input.headers === 'object' ? { ...input.headers } : {},
149
- cacheRetention,
150
- supportsLongCacheRetention: input.supportsLongCacheRetention !== false,
151
- checkpointPath: input.checkpointPath || defaultCheckpointPath(),
152
- portableCheckpointPath: input.portableCheckpointPath || portableCheckpointPath(input.checkpointPath || defaultCheckpointPath()),
133
+ legacyCheckpointPath: legacy,
153
134
  alphaCapabilityPath: input.alphaCapabilityPath || defaultAlphaCapabilityPath(),
154
135
  alphaRefPath: input.alphaRefPath || defaultAlphaRefPath(),
155
136
  alphaProfile: String(input.alphaProfile ?? ''),
@@ -158,1626 +139,600 @@ function normalizeConfig(input = {}) {
158
139
  webSearchProvider: input.webSearchProvider || 'lcx-responses',
159
140
  webMaxResults: Number.isInteger(input.webMaxResults) && input.webMaxResults > 0 ? input.webMaxResults : 8,
160
141
  timeoutMs: Number.isInteger(input.timeoutMs) && input.timeoutMs > 0 ? input.timeoutMs : 300000,
161
- maxResponseBytes: Number.isInteger(input.maxResponseBytes) && input.maxResponseBytes > 0 ? input.maxResponseBytes : 4 * 1024 * 1024,
142
+ maxResponseBytes: Number.isInteger(input.maxResponseBytes) && input.maxResponseBytes > 0 ? input.maxResponseBytes : 8 * 1024 * 1024,
162
143
  maxAttempts: Number.isInteger(input.maxAttempts) && input.maxAttempts > 0 ? Math.min(input.maxAttempts, 6) : 3,
163
144
  maxRequestImageBytes: Number.isSafeInteger(input.maxRequestImageBytes) && input.maxRequestImageBytes > 0 ? input.maxRequestImageBytes : DEFAULT_MAX_REQUEST_IMAGE_BYTES,
145
+ requestImagePixelBudget: Number.isSafeInteger(input.requestImagePixelBudget) && input.requestImagePixelBudget > 0 ? input.requestImagePixelBudget : DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
146
+ requestImageMaxBytes: Number.isSafeInteger(input.requestImageMaxBytes) && input.requestImageMaxBytes > 0 ? input.requestImageMaxBytes : DEFAULT_REQUEST_IMAGE_MAX_BYTES,
147
+ portableReplayMaxChars: Number.isSafeInteger(input.portableReplayMaxChars) && input.portableReplayMaxChars > 0 ? input.portableReplayMaxChars : 80_000,
148
+ nativeRetentionTokenBudget: Number.isSafeInteger(input.nativeRetentionTokenBudget) && input.nativeRetentionTokenBudget > 0 ? input.nativeRetentionTokenBudget : 64_000,
149
+ assistantRetentionTokenReserve: Number.isSafeInteger(input.assistantRetentionTokenReserve) && input.assistantRetentionTokenReserve >= 0 ? input.assistantRetentionTokenReserve : 24_000,
150
+ assistantRetentionPerMessageTokenCap: Number.isSafeInteger(input.assistantRetentionPerMessageTokenCap) && input.assistantRetentionPerMessageTokenCap > 0 ? input.assistantRetentionPerMessageTokenCap : 3_000,
151
+ webSearchTimeoutMs: Number.isSafeInteger(input.webSearchTimeoutMs) && input.webSearchTimeoutMs >= 30_000 ? Math.min(input.webSearchTimeoutMs, 600_000) : 240_000,
152
+ autoCompactionThresholdPercent: Number.isFinite(input.autoCompactionThresholdPercent) ? Math.min(95, Math.max(85, Number(input.autoCompactionThresholdPercent))) : 90,
153
+ emergencyPruneThresholdPercent: Number.isFinite(input.emergencyPruneThresholdPercent) ? Math.min(99, Math.max(90, Number(input.emergencyPruneThresholdPercent))) : 95,
164
154
  }
165
155
  }
166
156
 
167
- function isGptModel(model) {
168
- return /(^|[^a-z])gpt(?:[^a-z]|$)/iu.test(String(model ?? ''))
169
- }
170
-
171
- function settingsValue(ctx, namespace) {
172
- const settings = ctx?.get?.('settings') ?? ctx?.settings
173
- return settings?.get?.(settingsNamespace(namespace))
174
- }
175
-
176
- function resolveResponsesRouteConfig(ctx, options, fallbackConfig) {
177
- if (!isGptModel(options.model)) return undefined
178
- const provider = String(options.provider ?? '')
179
- const section = settingsValue(ctx, 'llm-pi-ai')
180
- const profile = section?.providers?.[provider]
181
- if (profile === undefined) {
182
- if (provider !== fallbackConfig.provider) return undefined
183
- return normalizeConfig({ ...fallbackConfig, provider, model: options.model })
184
- }
185
- if (profile.api !== 'openai-responses') return undefined
186
- const baseURL = profile.baseURL ?? (provider === fallbackConfig.provider ? fallbackConfig.baseURL : undefined)
187
- const apiKeyEnv = profile.apiKeyEnv ?? (provider === fallbackConfig.provider ? fallbackConfig.apiKeyEnv : undefined)
188
- if (!baseURL || !apiKeyEnv) return undefined
189
- return normalizeConfig({
190
- ...fallbackConfig,
191
- provider,
192
- baseURL,
193
- apiKeyEnv,
194
- headers: profile.headers ?? fallbackConfig.headers,
195
- cacheRetention: profile.cacheRetention ?? fallbackConfig.cacheRetention,
196
- supportsLongCacheRetention: profile.compat?.supportsLongCacheRetention ?? fallbackConfig.supportsLongCacheRetention,
197
- model: options.model,
198
- compactTransport: profile.compactTransport ?? fallbackConfig.compactTransport,
199
- timeoutMs: profile.timeoutMs ?? fallbackConfig.timeoutMs,
200
- maxRequestImageBytes: profile.maxRequestImageBytes ?? fallbackConfig.maxRequestImageBytes,
201
- maxAttempts: retryAttempts(profile.retryPolicy, fallbackConfig.maxAttempts),
202
- })
203
- }
204
-
205
- function retryAttempts(policy, fallback) {
206
- if (!policy || typeof policy !== 'object') return fallback
207
- try {
208
- const resolved = resolveRetryPolicy(policy, 'llm-pi-ai provider retryPolicy')
209
- if (resolved.mode === 'normal' && Number.isSafeInteger(resolved.maxRetries)) return Math.min(resolved.maxRetries + 1, 6)
210
- } catch {
211
- return fallback
212
- }
213
- return fallback
214
- }
215
-
216
- function nativeRouteError(options) {
217
- const error = new Error(`【GPT 专属原生远程压缩】当前路由不满足 GPT + openai-responses 条件:${String(options.provider)}/${String(options.model)}`)
218
- error.code = 'LCX_CHECKPOINT_ROUTE_MISMATCH'
219
- return error
220
- }
221
-
222
- function ambientApiKey(config) {
223
- const key = String(process.env[config.apiKeyEnv] ?? '').trim()
224
- if (!key) throw new Error(`DSH provider credential is unavailable: ${config.apiKeyEnv}`)
225
- return key
226
- }
227
-
228
- async function resolveApiKey(ctx, config) {
229
- const credentials = ctx?.get?.('credentials') ?? ctx?.credentials
230
- if (credentials?.resolve) {
231
- const resolved = await credentials.resolve(config.apiKeyEnv)
232
- if (typeof resolved?.value === 'string' && resolved.value.length > 0) return resolved.value
233
- }
234
- return ambientApiKey(config)
235
- }
236
-
237
- function attachmentImageResolver(ctx) {
238
- const attachments = ctx?.get?.('attachments') ?? ctx?.attachments
239
- return async (block, signal) => {
240
- if (!attachments?.readImage) {
241
- const error = new Error('LCX Compact image input requires the DSH attachment service')
242
- error.code = 'LCX_COMPACT_IMAGE_UNAVAILABLE'
243
- throw error
244
- }
245
- try {
246
- const stored = await attachments.readImage(block?.attachment, signal)
247
- return { data: stored?.data, mediaType: stored?.ref?.mediaType ?? block?.attachment?.mediaType }
248
- } catch (error) {
249
- const wrapped = new Error('LCX Compact failed to read image attachment', { cause: error })
250
- wrapped.code = 'LCX_COMPACT_IMAGE_UNAVAILABLE'
251
- throw wrapped
252
- }
253
- }
254
- }
255
-
256
- function usageFrom(value, seen = new Set()) {
257
- if (!value || typeof value !== 'object' || seen.has(value)) return undefined
258
- seen.add(value)
259
- if (Number.isFinite(value.input_tokens) || Number.isFinite(value.output_tokens)) {
260
- const totalInputTokens = Number(value.input_tokens ?? 0)
261
- const outputTokens = Number(value.output_tokens ?? 0)
262
- const cachedValue = Number(value.input_tokens_details?.cached_tokens ?? value.input_token_details?.cached_tokens ?? value.prompt_tokens_details?.cached_tokens ?? 0)
263
- const cacheWriteValue = Number(value.input_tokens_details?.cache_write_tokens ?? value.input_token_details?.cache_write_tokens ?? value.prompt_tokens_details?.cache_write_tokens ?? 0)
264
- const cacheReadTokens = Number.isFinite(cachedValue) && cachedValue > 0 ? cachedValue : 0
265
- const cacheWriteTokens = Number.isFinite(cacheWriteValue) && cacheWriteValue > 0 ? cacheWriteValue : 0
266
- const inputTokens = Number.isFinite(totalInputTokens)
267
- ? Math.max(0, totalInputTokens - cacheReadTokens - cacheWriteTokens)
268
- : 0
269
- const normalizedOutputTokens = Number.isFinite(outputTokens) && outputTokens > 0 ? outputTokens : 0
270
- const reasoningValue = Number(value.output_token_details?.reasoning_tokens ?? 0)
271
- if (inputTokens <= 0 && cacheReadTokens <= 0 && cacheWriteTokens <= 0 && normalizedOutputTokens <= 0) return undefined
272
- const usage = { inputTokens, outputTokens: normalizedOutputTokens }
273
- if (cacheReadTokens > 0) usage.cacheReadTokens = cacheReadTokens
274
- if (cacheWriteTokens > 0) usage.cacheWriteTokens = cacheWriteTokens
275
- if (Number.isFinite(reasoningValue) && reasoningValue > 0) usage.reasoningTokens = reasoningValue
276
- return usage
277
- }
278
- if (Array.isArray(value)) {
279
- for (const item of value) {
280
- const found = usageFrom(item, seen)
281
- if (found) return found
282
- }
283
- } else {
284
- for (const item of Object.values(value)) {
285
- const found = usageFrom(item, seen)
286
- if (found) return found
287
- }
288
- }
289
- return undefined
290
- }
291
-
292
- function responseTextParts(item) {
293
- return (Array.isArray(item?.content) ? item.content : [])
294
- .filter((part) => part?.type === 'output_text' && typeof part.text === 'string')
295
- .map((part) => part.text)
296
- }
297
-
298
- function responseReasoningText(item) {
299
- const textOf = (parts) => (Array.isArray(parts) ? parts : [])
300
- .filter((part) => typeof part?.text === 'string')
301
- .map((part) => part.text)
302
- .filter((text) => text.length > 0)
303
- .join('\n\n')
304
- return textOf(item?.summary) || textOf(item?.content)
157
+ function webError(message, code, cause) {
158
+ const error = new Error(message, cause === undefined ? undefined : { cause })
159
+ error.name = 'WebError'; error.code = code; return error
305
160
  }
306
161
 
307
- async function authenticatedHeaders(ctx, config, sessionId, clientRequestId) {
162
+ function activeAgentRoute(exec, fallback) {
163
+ const agent = exec?.agent
164
+ const latest = agent?.session?.requestHeader?.()?.config
308
165
  return {
309
- ...config.headers,
310
- ...attributionHeaders(),
311
- authorization: `Bearer ${await resolveApiKey(ctx, config)}`,
312
- ...(clientRequestId === null ? {} : { 'x-client-request-id': clientRequestId ?? (sessionId ? String(sessionId) : randomUUID()) }),
313
- ...(sessionId ? { 'session-id': String(sessionId) } : {}),
314
- }
315
- }
316
-
317
- const REMOTE_COMPACTION_ERROR_CODES = new Set([
318
- 'LCX_HTTP_RETRYABLE',
319
- 'LCX_TIMEOUT',
320
- 'LCX_RETRY_EXHAUSTED',
321
- ])
322
- const TRANSIENT_NETWORK_ERROR_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EAI_AGAIN', 'UND_ERR_CONNECT_TIMEOUT'])
323
-
324
- function hasRemoteCompactionErrorCode(error) {
325
- const seen = new Set()
326
- let current = error
327
- while (current && !seen.has(current)) {
328
- seen.add(current)
329
- const statusRetryable = Number.isInteger(current.status) && (current.status === 429 || (current.status >= 500 && current.status <= 599))
330
- if (current instanceof TypeError || current.name === 'TypeError' || statusRetryable || current.retryable === true || REMOTE_COMPACTION_ERROR_CODES.has(current.code) || TRANSIENT_NETWORK_ERROR_CODES.has(current.code)) return true
331
- current = current.cause
166
+ provider: latest?.provider ?? agent?.options?.provider ?? fallback.provider,
167
+ model: latest?.model ?? agent?.options?.model ?? fallback.model,
168
+ sessionId: String(agent?.session?.id ?? ''),
332
169
  }
333
- return false
334
170
  }
335
171
 
336
- function markRemoteCompactionRequestError(error) {
337
- const failure = error instanceof Error ? error : new Error(String(error))
338
- failure.remoteCompactionRequest = hasRemoteCompactionErrorCode(failure)
339
- return failure
340
- }
172
+ function sessionsService(ctx) { return ctx?.get?.('sessions') ?? ctx?.sessions }
173
+ function sessionFor(ctx, sessionId) { return sessionId ? sessionsService(ctx)?.get?.(sessionId) : undefined }
341
174
 
342
- function currentRoute(options, config) {
175
+ function requestImageOptions(routeConfig, imageSupport, signal, imageMap, extra = {}) {
343
176
  return {
344
- provider: String(options.provider ?? config.provider ?? ''),
345
- model: String(options.model ?? config.model ?? ''),
346
- baseURL: config.baseURL,
347
- sessionId: options.sessionId ? String(options.sessionId) : '',
348
- }
349
- }
350
-
351
- function sessionAncestry(ctx, sessionId) {
352
- const id = typeof sessionId === 'string' ? sessionId : ''
353
- const sessions = ctx?.get?.('sessions') ?? ctx?.sessions
354
- if (!id || !sessions?.get) return []
355
- const ancestors = []
356
- const seen = new Set([id])
357
- let current = sessions.get(id)
358
- while (current && ancestors.length < 32) {
359
- const parent = current.header?.parentSession
360
- if (typeof parent !== 'string' || parent.length === 0 || seen.has(parent)) break
361
- ancestors.push(parent)
362
- seen.add(parent)
363
- current = sessions.get(parent)
177
+ imageSupport, signal, imageMap,
178
+ maxRequestImageBytes: routeConfig.maxRequestImageBytes,
179
+ requestImagePixelBudget: routeConfig.requestImagePixelBudget,
180
+ requestImageMaxBytes: routeConfig.requestImageMaxBytes,
181
+ ...extra,
364
182
  }
365
- return ancestors
366
183
  }
367
184
 
368
- function routeWithSessionAncestry(ctx, route) {
369
- return { ...route, ancestorSessionIds: sessionAncestry(ctx, route.sessionId) }
185
+ function readWebSearchProvider(ctx) {
186
+ try { return Reflect.get(ctx?.web, 'searchProviderId') } catch { return undefined }
370
187
  }
371
-
372
- function promptCacheSessionId(route, config) {
373
- if (config?.cacheRetention === 'none') return undefined
374
- return route?.sessionId ? String(route.sessionId) : undefined
375
- }
376
-
377
- function promptCacheKey(route, config) {
378
- const sessionId = promptCacheSessionId(route, config)
379
- return sessionId ? clampOpenAIPromptCacheKey(sessionId) : undefined
380
- }
381
-
382
- function promptCacheRetention(config) {
383
- return config?.cacheRetention === 'long' && config.supportsLongCacheRetention !== false ? '24h' : undefined
384
- }
385
-
386
- function promptCacheHeaders(ctx, config, route) {
387
- const sessionId = promptCacheSessionId(route, config)
388
- return authenticatedHeaders(ctx, config, sessionId, sessionId === undefined ? null : undefined)
389
- }
390
-
391
- async function requestRemoteCompaction(options, input, config, signal, ctx, route, preflightHeaders) {
188
+ function writeWebSearchProvider(ctx, providerId) {
189
+ if (!ctx?.web) return false
392
190
  try {
393
- const headers = preflightHeaders ?? await promptCacheHeaders(ctx, config, route)
394
- return await requestNativeCompaction({
395
- baseURL: config.baseURL,
396
- model: options.model,
397
- input,
398
- instructions: options.system,
399
- promptCacheKey: promptCacheKey(route, config),
400
- promptCacheRetention: promptCacheRetention(config),
401
- idempotencyKey: randomUUID(),
402
- tools: options.tools,
403
- headers,
404
- signal,
405
- timeoutMs: config.timeoutMs,
406
- maxAttempts: config.maxAttempts,
407
- maxResponseBytes: config.maxResponseBytes,
408
- })
409
- } catch (error) {
410
- throw markRemoteCompactionRequestError(error)
411
- }
191
+ if (!Reflect.set(ctx.web, 'searchProviderId', providerId)) return false
192
+ return Reflect.get(ctx.web, 'searchProviderId') === providerId
193
+ } catch { return false }
412
194
  }
413
195
 
414
- function portableSummaryText(model, usage) {
415
- const input = Number.isFinite(usage?.inputTokens) ? usage.inputTokens : undefined
416
- const output = Number.isFinite(usage?.outputTokens) ? usage.outputTokens : undefined
417
- const usageText = input === undefined || output === undefined ? 'usage:未知' : `usage:${input}/${output} tokens`
418
- return `LCX 压缩完成 · Native V2 · v3 已保存 · 模型:${String(model)} · ${usageText}`
196
+ async function executeHostedSearch(ctx, routeConfig, args, signal, sessionId = '') {
197
+ const normalized = normalizeHostedSearchArgs(args)
198
+ const requestId = randomUUID()
199
+ const route = currentRoute({ provider: routeConfig.provider, model: routeConfig.model, sessionId }, routeConfig)
200
+ const headers = await authenticatedHeaders(ctx, routeConfig, sessionId, requestId)
201
+ const searchCacheKey = sessionId ? `dsh-lcx-search:${routeFingerprint(route)}` : undefined
202
+ const body = buildHostedSearchBody(normalized, routeConfig.model, { promptCacheKey: searchCacheKey })
203
+ const response = await fetchJsonWithRetry(`${routeConfig.baseURL}/responses`, body, headers, signal, routeConfig.timeoutMs, { maxAttempts: routeConfig.maxAttempts, maxResponseBytes: routeConfig.maxResponseBytes })
204
+ return parseHostedSearchResponse(response, requestId, routeConfig.webMaxResults)
419
205
  }
420
206
 
421
- function checkpointReplaySource(checkpointId) {
422
- return {
423
- kind: 'plugin',
424
- plugin: 'dsh-lcx-codex',
425
- purpose: 'checkpoint-recall',
426
- ...(checkpointId ? { checkpointId } : {}),
207
+ class LcxResponsesSearchProvider {
208
+ constructor(ctx, getConfig, enabled) { this.ctx = ctx; this.getConfig = getConfig; this.enabled = enabled; this.id = getConfig().webSearchProvider }
209
+ available() {
210
+ const config = this.getConfig()
211
+ const route = resolveResponsesRouteConfig(this.ctx, { provider: config.provider, model: config.model }, config)
212
+ return this.enabled() && Boolean(route)
213
+ }
214
+ async search(request, signal) {
215
+ const config = this.getConfig()
216
+ const active = hostedSearchRouteContext.getStore()
217
+ const requested = active ?? { provider: config.provider, model: config.model, sessionId: '' }
218
+ let route = resolveResponsesRouteConfig(this.ctx, requested, config)
219
+ let source = active ? 'active-agent' : 'fallback'
220
+ if (!route && active) {
221
+ route = resolveResponsesRouteConfig(this.ctx, { provider: config.provider, model: config.model }, config)
222
+ source = 'fallback'
223
+ }
224
+ if (!route) throw webError('LCX Hosted Search requires a configured GPT openai-responses route', 'LCX_WEB_ROUTE_UNAVAILABLE')
225
+ const sessionId = String((source === 'active-agent' ? active?.sessionId : '') ?? '')
226
+ this.ctx.logger?.info?.(`[lcx-codex] web_search route: ${route.provider}/${route.model} (${source})`)
227
+ const result = await executeHostedSearch(this.ctx, route, { query: request.query }, signal, sessionId)
228
+ const max = Number.isInteger(request.maxResults) && request.maxResults > 0 ? request.maxResults : route.webMaxResults
229
+ return { content: result.content || undefined, sources: result.sources.slice(0, max), truncated: result.truncated || result.sources.length > max }
427
230
  }
428
231
  }
429
232
 
430
- function stripCheckpointMarker(text) {
431
- return String(text).replace(V3_CHECKPOINT_MARKER_PATTERN, '').trim()
432
- }
433
-
434
- function replayBudget() {
233
+ function createAdvancedHostedTool(ctx, state, getConfig) {
435
234
  return {
436
- chars: PORTABLE_REPLAY_TOTAL_MAX_CHARS,
437
- bytes: PORTABLE_REPLAY_TOTAL_MAX_BYTES,
235
+ name: ADVANCED_HOSTED_TOOL,
236
+ description: 'Advanced GPT Responses Hosted Search. Use DSH web_search for ordinary lookup. Call this only when you need domain allow/block filters, approximate user location, search-context size, image search, external-web-access or return-token-budget controls. It is one-shot and has no open/find/click state.',
237
+ parameters: HOSTED_SEARCH_PARAMETERS,
238
+ output: { schema: HOSTED_SEARCH_OUTPUT, render: (_args, value) => renderHostedSearchResult(value) },
239
+ async execute(args, exec) {
240
+ if (!state.enabled || !state.webSearch || !state.advancedHostedSearch) throw webError(`${ADVANCED_HOSTED_TOOL} is disabled`, 'LCX_WEB_DISABLED')
241
+ const config = getConfig(); const active = activeAgentRoute(exec, config)
242
+ const route = resolveResponsesRouteConfig(ctx, active, config)
243
+ if (!route) throw webError('Advanced Hosted Search requires the active GPT openai-responses route', 'LCX_WEB_ROUTE_UNAVAILABLE')
244
+ return executeHostedSearch(ctx, route, args, exec?.signal, active.sessionId)
245
+ },
438
246
  }
439
247
  }
440
248
 
441
- function boundedReplayText(value, budget) {
442
- if (typeof value !== 'string') return undefined
443
- const normalized = stripCheckpointMarker(value)
444
- if (!normalized || budget.chars <= 0 || budget.bytes <= 0) return undefined
445
- let text = normalized.slice(0, Math.min(PORTABLE_REPLAY_TEXT_MAX_CHARS, budget.chars))
446
- while (text.length > 0 && Buffer.byteLength(text, 'utf8') > budget.bytes) text = text.slice(0, -1)
447
- if (!text) return undefined
448
- budget.chars -= text.length
449
- budget.bytes -= Buffer.byteLength(text, 'utf8')
450
- return text
249
+ function alphaCapabilityFor(config, store) {
250
+ const fingerprint = alphaCapabilityFingerprint({ baseURL: config.baseURL, provider: config.provider, model: config.model, profile: config.alphaProfile, group: config.alphaGroup, schemaFingerprint: ALPHA_SCHEMA_FINGERPRINT })
251
+ return { fingerprint, record: store.get(fingerprint) }
451
252
  }
452
253
 
453
- function exactReplayText(value, budget) {
454
- if (typeof value !== 'string' || value.length === 0 || value.length > PORTABLE_REPLAY_TEXT_MAX_CHARS) return undefined
455
- const byteLength = Buffer.byteLength(value, 'utf8')
456
- if (value.length > budget.chars || byteLength > budget.bytes) return undefined
457
- budget.chars -= value.length
458
- budget.bytes -= byteLength
459
- return value
460
- }
461
-
462
- function isPortableReplayItem(item) {
463
- return item !== null && typeof item === 'object' && !Array.isArray(item) &&
464
- typeof item.type === 'string' && !item.type.startsWith('response.') &&
465
- item.type !== 'compaction' && item.type !== 'context_compaction' && item.type !== 'compaction_trigger' &&
466
- !Object.prototype.hasOwnProperty.call(item, 'encrypted_content')
467
- }
468
-
469
- function portableReplayUnsupportedContentError(type) {
470
- const error = new Error(`LCX checkpoint portable replay cannot represent Responses item type: ${String(type)}`)
471
- error.code = 'LCX_CHECKPOINT_PORTABLE_UNSUPPORTED_CONTENT'
472
- return error
473
- }
474
-
475
- function safePortableCall(item) {
476
- if (!isPortableReplayItem(item) || item.type !== 'function_call') return undefined
477
- if (typeof item.call_id !== 'string' || item.call_id.length === 0 || item.call_id.length > 256) return undefined
478
- if (typeof item.name !== 'string' || item.name.trim().length === 0 || item.name.length > 256) return undefined
479
- if (typeof item.arguments !== 'string' || item.arguments.length === 0 || item.arguments.length > PORTABLE_REPLAY_TEXT_MAX_CHARS) return undefined
254
+ async function executeAlpha(ctx, routeConfig, capability, refStore, args, exec) {
255
+ const normalized = normalizeAlphaSearchArgs(args)
256
+ const sessionId = String(exec?.agent?.session?.id ?? '')
257
+ if (!sessionId) throw webError('Alpha Search requires a DSH session', 'LCX_ALPHA_SESSION_REQUIRED')
258
+ const routeFp = routeFingerprint({ provider: routeConfig.provider, model: routeConfig.model, baseURL: routeConfig.baseURL, sessionId })
259
+ if (['open','find','click','screenshot'].includes(normalized.action)) refStore.assertUsable(sessionId, routeFp, normalized.refId)
260
+ const requestId = randomUUID(); const headers = await authenticatedHeaders(ctx, routeConfig, sessionId, requestId)
261
+ let response
480
262
  try {
481
- JSON.parse(item.arguments)
482
- } catch {
483
- return undefined
484
- }
485
- return { callId: item.call_id, name: item.name, arguments: item.arguments }
486
- }
487
-
488
- function safePortableOutput(item) {
489
- if (!isPortableReplayItem(item) || item.type !== 'function_call_output') return undefined
490
- if (typeof item.call_id !== 'string' || item.call_id.length === 0 || item.call_id.length > 256) return undefined
491
- let outputParts
492
- if (typeof item.output === 'string') {
493
- outputParts = [{ type: 'input_text', text: item.output }]
494
- } else if (Array.isArray(item.output)) {
495
- for (const part of item.output) {
496
- if (part === null || typeof part !== 'object' || Array.isArray(part) ||
497
- (part.type !== 'dsh_image_attachment' &&
498
- ((part.type !== 'input_text' && part.type !== 'output_text') || typeof part.text !== 'string'))) {
499
- throw portableReplayUnsupportedContentError(part?.type)
500
- }
501
- }
502
- outputParts = item.output
503
- } else {
504
- return undefined
263
+ response = await fetchJsonWithRetry(`${routeConfig.baseURL}/alpha/search`, buildAlphaSearchBody(normalized, routeConfig.model, sessionId, true, routeConfig.alphaMaxOutputTokens), headers, exec?.signal, routeConfig.timeoutMs, { maxAttempts: routeConfig.maxAttempts, maxResponseBytes: routeConfig.maxResponseBytes })
264
+ } catch (error) {
265
+ if ([404,405].includes(error?.status) || /channel does not support/iu.test(String(error?.message ?? ''))) throw webError('Alpha Search is not supported by this route', 'LCX_ALPHA_UNAVAILABLE', error)
266
+ throw webError('Alpha Search provider request failed', 'LCX_ALPHA_PROVIDER_ERROR', error)
505
267
  }
506
- const textLength = outputParts.reduce((sum, part) => sum + (typeof part.text === 'string' ? part.text.length : 0), 0)
507
- if (textLength > PORTABLE_REPLAY_TEXT_MAX_CHARS) return undefined
508
- return { callId: item.call_id, outputParts }
509
- }
510
-
511
- function portableImageCapabilityError() {
512
- const error = new Error('LCX Compact cannot determine whether the target model accepts checkpoint images')
513
- error.code = 'LCX_COMPACT_IMAGE_CAPABILITY_UNKNOWN'
514
- return error
268
+ const result = parseAlphaSearchResponse(response, { action: normalized.action, capability: capability.classification, requestId })
269
+ refStore.record(sessionId, routeFp, result.refRecords ?? result.refs.map((refId) => ({ refId })))
270
+ delete result.refRecords
271
+ return result
515
272
  }
516
273
 
517
- function portableImageBlock(part, imageSupport) {
518
- if (!part?.attachment || typeof part.attachment !== 'object' || Array.isArray(part.attachment)) {
519
- throw portableReplayUnsupportedContentError('image')
274
+ function createAlphaTool(ctx, state, getConfig, capabilityStore, refStore) {
275
+ return {
276
+ name: ALPHA_TOOL,
277
+ description: 'Stateful Codex/Alpha web command tool. Use it for search/open/find/click/PDF screenshot and the structured image/finance/weather/sports/time actions. Use DSH web_search for ordinary search, and websearch_gpt_advanced only for Hosted Search controls.',
278
+ parameters: ALPHA_SEARCH_PARAMETERS,
279
+ output: { schema: ALPHA_SEARCH_OUTPUT, render: (_args, value) => renderAlphaSearchResult(value) },
280
+ async execute(args, exec) {
281
+ if (!state.enabled || !state.alphaSearch) throw webError(`${ALPHA_TOOL} is disabled`, 'LCX_ALPHA_DISABLED')
282
+ const config = getConfig(); const active = activeAgentRoute(exec, config); const route = resolveResponsesRouteConfig(ctx, active, config)
283
+ if (!route) throw webError('Alpha Search requires the active GPT openai-responses route', 'LCX_ALPHA_ROUTE_UNAVAILABLE')
284
+ const fingerprint = alphaCapabilityFingerprint({ baseURL: route.baseURL, provider: route.provider, model: route.model, profile: route.alphaProfile, group: route.alphaGroup, schemaFingerprint: ALPHA_SCHEMA_FINGERPRINT })
285
+ const capability = capabilityStore.get(fingerprint)
286
+ if (!alphaCapabilityUsable(capability) || capability?.schemaFingerprint !== ALPHA_SCHEMA_FINGERPRINT) throw webError('Alpha Search capability has not been verified for this exact route/schema', 'LCX_ALPHA_CAPABILITY_UNVERIFIED')
287
+ return executeAlpha(ctx, route, capability, refStore, args, exec)
288
+ },
520
289
  }
521
- if (imageSupport === 'unknown') throw portableImageCapabilityError()
522
- return imageSupport === 'supported'
523
- ? { type: 'image', attachment: structuredClone(part.attachment) }
524
- : { type: 'text', text: UNSUPPORTED_IMAGE_PLACEHOLDER }
525
290
  }
526
291
 
527
- function dshBlocksContainImage(blocks) {
528
- return (blocks ?? []).some((block) => block?.type === 'image' ||
529
- (block?.type === 'tool-result' && dshBlocksContainImage(block.content)))
292
+ function isDshCompactionDirective(message) {
293
+ if (message?.role !== 'user') return false
294
+ if (message?.source?.kind === 'plugin' && message?.source?.plugin === 'dsh-compaction-basic') return true
295
+ const text = (message?.content ?? []).filter((b) => b?.type === 'text').map((b) => b.text).join('')
296
+ return text.includes(COMPACTION_DIRECTIVE)
530
297
  }
531
-
532
- function dshMessagesContainImage(messages) {
533
- return (messages ?? []).some((message) => dshBlocksContainImage(message?.content))
298
+ function stripCompactionDirective(messages) {
299
+ if (!Array.isArray(messages) || messages.length === 0) return []
300
+ return isDshCompactionDirective(messages.at(-1)) ? messages.slice(0, -1) : messages
534
301
  }
535
302
 
536
- function portableContentBlocks(parts, expectedTypes, budget, imageSupport) {
537
- const content = []
538
- let pendingText = ''
539
- const flushText = () => {
540
- if (!pendingText) return
541
- const text = boundedReplayText(pendingText, budget)
542
- if (text !== undefined) content.push({ type: 'text', text })
543
- pendingText = ''
544
- }
545
- for (const part of parts ?? []) {
546
- if (expectedTypes.has(part?.type) && typeof part.text === 'string') {
547
- pendingText += part.text
548
- continue
549
- }
550
- if (part?.type === 'dsh_image_attachment' && part.attachment) {
551
- flushText()
552
- content.push(portableImageBlock(part, imageSupport))
553
- continue
554
- }
555
- throw portableReplayUnsupportedContentError(part?.type)
556
- }
557
- flushText()
558
- return content
559
- }
560
-
561
- function portableTextMessage(item, budget, checkpointId, imageSupport) {
562
- if (!isPortableReplayItem(item) || item.type !== 'message' || (item.role !== 'user' && item.role !== 'assistant')) return undefined
563
- const expectedType = item.role === 'assistant' ? 'output_text' : 'input_text'
564
- const parts = Array.isArray(item.content) ? item.content : []
565
- const content = portableContentBlocks(parts, new Set([expectedType]), budget, imageSupport)
566
- if (content.length === 0) return undefined
567
- return {
568
- id: randomUUID(),
569
- role: item.role,
570
- content,
571
- source: checkpointReplaySource(checkpointId),
572
- }
573
- }
303
+ function mergeMap(target, source) { for (const [key, value] of source ?? []) target.set(key, value) }
574
304
 
575
- function cloneReplayBlock(block, imageSupport) {
576
- const supported = new Set(['text', 'tool-call', 'tool-result', 'reasoning', 'image'])
577
- if (!supported.has(block?.type)) throw portableReplayUnsupportedContentError(block?.type)
578
- if (block.type === 'reasoning') return undefined
579
- if (block.type === 'image') return portableImageBlock({ attachment: block.attachment }, imageSupport)
580
- if (block.type === 'tool-result') {
581
- return {
582
- ...structuredClone(block),
583
- content: (block.content ?? []).map((part) => cloneReplayBlock(part, imageSupport)).filter(Boolean),
584
- }
305
+ async function serializeNativeAware(messages, route, routeConfig, ctx, options = {}) {
306
+ const session = sessionFor(ctx, route.sessionId)
307
+ const imageSupport = await resolveModelImageSupport(ctx, route, options.signal)
308
+ const input = []; const imageMap = new Map(); let nativeTools
309
+ let normal = []
310
+ const serializeOptions = (imageMapOverride, extra = {}) => requestImageOptions(routeConfig, imageSupport, options.signal, imageMapOverride, { route, tools: options.tools, responsesCompat: routeConfig.responsesCompat, ...extra })
311
+ const prelude = await serializeDshMessages([], ctx, serializeOptions(undefined, { systemPrompt: options.system, includeSystemPrompt: true }))
312
+ input.push(...prelude.input); nativeTools = prelude.tools
313
+ const flush = async () => {
314
+ if (!normal.length) return
315
+ const serialized = await serializeDshMessages(normal, ctx, serializeOptions())
316
+ nativeTools = serialized.tools ?? nativeTools
317
+ input.push(...serialized.input); mergeMap(imageMap, serialized.imageMap); normal = []
585
318
  }
586
- return structuredClone(block)
587
- }
588
-
589
- function cloneReplayTail(messages, imageSupport = 'unknown') {
590
- const projected = []
591
319
  for (const message of messages ?? []) {
592
- if (message === null || typeof message !== 'object' || Array.isArray(message) ||
593
- typeof message.role !== 'string' || !Array.isArray(message.content)) continue
594
- const content = []
595
- for (const block of message.content) {
596
- const cloned = cloneReplayBlock(block, imageSupport)
597
- if (cloned) content.push(cloned)
598
- }
599
- if (content.length === 0) continue
600
- projected.push({ ...structuredClone(message), content })
601
- }
602
- return projected
603
- }
604
-
605
- function portableResponsesToMessages(input, options = {}) {
606
- const budget = options.budget ?? replayBudget()
607
- const checkpointId = options.checkpointId
608
- const imageSupport = options.imageSupport ?? 'unknown'
609
- const items = Array.isArray(input) ? input : []
610
- for (const item of items) {
611
- if (!isPortableReplayItem(item)) continue
612
- if (item.type === 'message') {
613
- const expectedType = item.role === 'assistant' ? 'output_text' : item.role === 'user' ? 'input_text' : undefined
614
- if (!expectedType || !Array.isArray(item.content) || item.content.some((part) =>
615
- part?.type !== expectedType && part?.type !== 'dsh_image_attachment')) {
616
- throw portableReplayUnsupportedContentError(item.type)
320
+ const state = session ? checkpointStateForMessage(session, message) : undefined
321
+ if (state) {
322
+ await flush()
323
+ if (stateRouteCompatible(state, route, ctx)) {
324
+ let nativeOutput = state.nativeOutput
325
+ if (state.version === 4) {
326
+ const checkpointId = compactCheckpointId(message)
327
+ const shadowed = shadowedMessagesForCheckpoint(session, checkpointId)
328
+ if (shadowed.length > 0) {
329
+ const serializedShadowed = await serializeDshMessages(shadowed, ctx, serializeOptions())
330
+ const repairedRetained = retainedConversationInput(serializedShadowed.input, {
331
+ tokenBudget: routeConfig.nativeRetentionTokenBudget,
332
+ assistantTokenReserve: routeConfig.assistantRetentionTokenReserve,
333
+ assistantPerMessageTokenCap: routeConfig.assistantRetentionPerMessageTokenCap,
334
+ })
335
+ const opaque = state.nativeCompaction ?? nativeOutput.find((item) => item?.type === 'compaction')
336
+ if (repairedRetained.length > 0 && opaque) {
337
+ nativeOutput = [...repairedRetained, structuredClone(opaque)]
338
+ mergeMap(imageMap, serializedShadowed.imageMap)
339
+ } else if (state.retainedInputCount === undefined) {
340
+ const portable = portableMessagesForCheckpoint(session, checkpointId, { maxChars: routeConfig.portableReplayMaxChars })
341
+ const serializedPortable = await serializeDshMessages(portable, ctx, serializeOptions())
342
+ input.push(...serializedPortable.input); mergeMap(imageMap, serializedPortable.imageMap)
343
+ continue
344
+ }
345
+ } else if (state.retainedInputCount === undefined) {
346
+ const portable = portableMessagesForCheckpoint(session, checkpointId, { maxChars: routeConfig.portableReplayMaxChars })
347
+ const serializedPortable = await serializeDshMessages(portable, ctx, serializeOptions())
348
+ input.push(...serializedPortable.input); mergeMap(imageMap, serializedPortable.imageMap)
349
+ continue
350
+ }
351
+ }
352
+ const hydratedMap = new Map()
353
+ const hydrated = await hydrateNativeImageReferences(nativeOutput, ctx, requestImageOptions(routeConfig, imageSupport, options.signal, hydratedMap))
354
+ input.push(...hydrated); mergeMap(imageMap, hydratedMap)
355
+ } else {
356
+ const portable = portableMessagesForCheckpoint(session, compactCheckpointId(message), { maxChars: routeConfig.portableReplayMaxChars })
357
+ const serialized = await serializeDshMessages(portable, ctx, serializeOptions())
358
+ input.push(...serialized.input); mergeMap(imageMap, serialized.imageMap)
617
359
  }
618
- } else if (item.type !== 'function_call' && item.type !== 'function_call_output') {
619
- throw portableReplayUnsupportedContentError(item.type)
620
- }
621
- }
622
- const calls = new Map()
623
- const outputs = new Map()
624
- for (let index = 0; index < items.length; index += 1) {
625
- const call = safePortableCall(items[index])
626
- const output = safePortableOutput(items[index])
627
- if (call) calls.set(call.callId, [...(calls.get(call.callId) ?? []), { ...call, index }])
628
- if (output) outputs.set(output.callId, [...(outputs.get(output.callId) ?? []), { ...output, index }])
629
- }
630
- const pairs = new Map()
631
- for (const [callId, callItems] of calls) {
632
- const outputItems = outputs.get(callId)
633
- if (callItems.length === 1 && outputItems?.length === 1 && callItems[0].index < outputItems[0].index) {
634
- pairs.set(callId, { call: callItems[0], output: outputItems[0] })
635
- }
636
- }
637
-
638
- const messages = []
639
- const reservedPairs = new Map()
640
- for (const item of items) {
641
- const textMessage = portableTextMessage(item, budget, checkpointId, imageSupport)
642
- if (textMessage) {
643
- messages.push(textMessage)
644
360
  continue
645
361
  }
646
- const call = safePortableCall(item)
647
- const pair = call ? pairs.get(call.callId) : undefined
648
- if (pair) {
649
- const before = { ...budget }
650
- const argumentsText = exactReplayText(pair.call.arguments, budget)
651
- let outputContent = portableContentBlocks(
652
- pair.output.outputParts,
653
- new Set(['input_text', 'output_text']),
654
- budget,
655
- imageSupport,
656
- )
657
- if (outputContent.length === 0) {
658
- const emptyOutput = exactReplayText('(no output)', budget)
659
- outputContent = emptyOutput ? [{ type: 'text', text: emptyOutput }] : []
660
- }
661
- if (!argumentsText || outputContent.length === 0) {
662
- budget.chars = before.chars
663
- budget.bytes = before.bytes
362
+ const legacyId = legacyV3Id(message)
363
+ if (legacyId) {
364
+ const legacy = loadLegacyRecord(routeConfig.legacyCheckpointPath, legacyId)
365
+ if (legacy) {
366
+ await flush()
367
+ const legacyItems = legacyRouteCompatible(legacy, route, ctx) ? legacy.nativeOutput : (legacy.portableHistory ?? [])
368
+ const hydratedMap = new Map()
369
+ const hydrated = await hydrateNativeImageReferences(legacyItems, ctx, requestImageOptions(routeConfig, imageSupport, options.signal, hydratedMap))
370
+ input.push(...hydrated); mergeMap(imageMap, hydratedMap)
664
371
  continue
665
372
  }
666
- reservedPairs.set(call.callId, outputContent)
667
- messages.push({
668
- id: randomUUID(),
669
- role: 'assistant',
670
- content: [{ type: 'tool-call', id: call.callId, name: call.name, arguments: argumentsText }],
671
- source: checkpointReplaySource(checkpointId),
672
- })
673
- continue
674
- }
675
- const output = safePortableOutput(item)
676
- const outputContent = output ? reservedPairs.get(output.callId) : undefined
677
- if (output && outputContent !== undefined) {
678
- messages.push({
679
- id: randomUUID(),
680
- role: 'user',
681
- content: [{ type: 'tool-result', toolCallId: output.callId, content: outputContent }],
682
- source: checkpointReplaySource(checkpointId),
683
- })
684
- reservedPairs.delete(output.callId)
685
373
  }
374
+ normal.push(message)
686
375
  }
687
- return messages
688
- }
689
-
690
- function portableReplayRouteError(record, route) {
691
- const dimensions = []
692
- if (record.provider !== String(route.provider ?? '')) dimensions.push('provider')
693
- if (record.baseURLFingerprint !== baseURLFingerprint(route.baseURL)) dimensions.push('endpoint')
694
- const sessionId = String(route.sessionId ?? '')
695
- const sameSessionLineage = !sessionId || record.lineageId === sessionId ||
696
- (Array.isArray(route.ancestorSessionIds) && route.ancestorSessionIds.includes(record.lineageId))
697
- if (!sameSessionLineage) dimensions.push('session')
698
- const error = new Error(`LCX v3 checkpoint cannot migrate; route mismatch dimensions: ${(dimensions.length > 0 ? dimensions : ['route']).join(', ')}`)
699
- error.code = 'LCX_CHECKPOINT_ROUTE_MISMATCH'
700
- return error
701
- }
702
-
703
- function buildPortableReplayMessages(messages, store, route, options = {}) {
704
- const marker = latestPortableMarker(messages)
705
- if (!marker) return cloneReplayTail(messages)
706
- const record = store?.get?.(marker.id)
707
- if (!record) {
708
- const error = new Error(`LCX v3 checkpoint ${marker.id} is missing from ${store?.file ?? 'checkpoint store'}`)
709
- error.code = 'LCX_CHECKPOINT_V3_CORRUPT'
710
- throw error
711
- }
712
- const exactRoute = Boolean(route.sessionId) && record.routeFingerprint === routeFingerprint(route)
713
- const sessionId = String(route.sessionId ?? '')
714
- const sameSessionLineage = !sessionId || record.lineageId === sessionId ||
715
- (Array.isArray(route.ancestorSessionIds) && route.ancestorSessionIds.includes(record.lineageId))
716
- const portableRoute = record.provider === String(route.provider ?? '') &&
717
- record.baseURLFingerprint === baseURLFingerprint(route.baseURL) &&
718
- sameSessionLineage
719
- if (!exactRoute && !portableRoute) throw portableReplayRouteError(record, route)
720
- const portableImages = Number(record.portableImageCount ?? 0)
721
- const durableImages = inputImageCount(record.portableHistory)
722
- if (portableImages > durableImages) {
723
- throw portableReplayUnsupportedContentError('image')
724
- }
725
- const imageSupport = options.imageSupport ?? 'unknown'
726
- const budget = replayBudget()
727
- const summary = boundedReplayText(record.portableSummary, budget)
728
- const recalled = []
729
- if (summary) recalled.push({
730
- id: randomUUID(),
731
- role: 'assistant',
732
- content: [{ type: 'text', text: summary }],
733
- source: checkpointReplaySource(marker.id),
734
- })
735
- recalled.push(...portableResponsesToMessages(record.portableHistory, { budget, checkpointId: marker.id, imageSupport }))
736
- recalled.push(...cloneReplayTail((messages ?? []).slice(marker.index + 1), imageSupport))
737
- return recalled
738
- }
739
-
740
- function checkpointReplayUnavailableError() {
741
- const error = new Error('LCX checkpoint replay requires the public DSH ctx.llm.stream API')
742
- error.code = CHECKPOINT_REPLAY_UNAVAILABLE_CODE
743
- return error
744
- }
745
-
746
- function checkpointReplayRecord(messages, store) {
747
- const marker = latestPortableMarker(messages)
748
- if (!marker) return undefined
749
- const record = store?.get?.(marker.id)
750
- if (!record) {
751
- const error = new Error(`LCX v3 checkpoint ${marker.id} is missing from ${store?.file ?? 'checkpoint store'}`)
752
- error.code = 'LCX_CHECKPOINT_V3_CORRUPT'
753
- throw error
754
- }
755
- return { marker, record }
376
+ await flush()
377
+ return { input, imageMap, imageSupport, tools: nativeTools }
756
378
  }
757
379
 
758
- function nativeReplayBody(options, route, config, nativeOutput, tailInput) {
759
- return {
760
- model: options.model,
761
- input: [...structuredClone(nativeOutput), ...structuredClone(tailInput)],
762
- stream: true,
763
- store: false,
764
- ...(options.tools !== undefined ? { tools: responsesTools(options.tools) } : {}),
765
- ...(options.system !== undefined ? { instructions: options.system } : {}),
766
- ...(promptCacheKey(route, config) ? { prompt_cache_key: promptCacheKey(route, config) } : {}),
767
- ...(promptCacheRetention(config) ? { prompt_cache_retention: promptCacheRetention(config) } : {}),
768
- }
380
+ function fallbackEligible(error, signal) {
381
+ if (signal?.aborted) return false
382
+ if (error?.name === 'AbortError' || error?.code === 'LCX_ABORTED') return false
383
+ if (error?.name === 'TimeoutError') return true
384
+ return ['LCX_HTTP_RETRYABLE', 'LCX_RETRY_EXHAUSTED'].includes(error?.code)
769
385
  }
770
386
 
771
- async function* nativeCheckpointReplayStream(options, config, portableStore, ctx, record, marker) {
772
- const route = routeWithSessionAncestry(ctx, currentRoute(options, config))
773
- assertCheckpointRoute(record, route)
774
- const llm = ctx?.llm ?? ctx?.get?.('llm')
775
- const imageSupport = typeof llm?.resolveModelInfo === 'function'
776
- ? await resolveModelImageSupport(llm, route, options.signal)
777
- : 'supported'
778
- const imageOptions = { resolveImage: attachmentImageResolver(ctx), imageSupport, signal: options.signal, maxRequestImageBytes: config.maxRequestImageBytes }
779
- const nativeOutput = await hydrateNativeImageReferences(checkpointNativeReplacementHistory(record), imageOptions)
780
- const tailInput = await buildPortableResponsesInputWithImages(
781
- (options.messages ?? []).slice(marker.index + 1),
782
- portableStore,
783
- route,
784
- imageOptions,
785
- )
786
- const replaySignals = replayEffectiveSignal(options.signal, config.timeoutMs)
387
+ async function* remoteCompactionStream(options, routeConfig, state, ctx, next, requestHeaders) {
388
+ const history = stripCompactionDirective(options.messages)
389
+ const route = currentRoute(options, routeConfig)
787
390
  try {
788
- const response = await fetchSse(
789
- `${config.baseURL}/responses`,
790
- nativeReplayBody(options, route, config, nativeOutput, tailInput),
791
- mergeFeatureHeader(await promptCacheHeaders(ctx, config, route)),
792
- replaySignals.signal,
793
- config.timeoutMs + 1000,
794
- { maxResponseBytes: config.maxResponseBytes },
795
- )
796
- yield* responsesSseChunks(response, {
797
- signal: replaySignals.signal,
798
- requestSignal: options.signal,
799
- timeoutSignal: replaySignals.timeoutSignal,
800
- timeoutMs: config.timeoutMs,
801
- maxResponseBytes: config.maxResponseBytes,
391
+ const prepared = await serializeNativeAware(history, route, routeConfig, ctx, { signal: options.signal, system: options.system, tools: options.tools })
392
+ const cacheSessionId = promptCacheSessionId(route, routeConfig)
393
+ const headers = await authenticatedHeaders(ctx, routeConfig, cacheSessionId, cacheSessionId === undefined ? null : undefined)
394
+ const cachedGeneration = generationControlsFromHeader(requestHeaders?.get(route.sessionId), route)
395
+ const generation = Object.keys(cachedGeneration).length > 0 ? cachedGeneration : generationControlsFromSession(sessionFor(ctx, route.sessionId), route)
396
+ const result = await requestNativeCompaction({
397
+ baseURL: routeConfig.baseURL,
398
+ model: route.model,
399
+ input: prepared.input,
400
+ tools: prepared.tools ?? options.tools,
401
+ promptCacheKey: promptCacheKey(route, routeConfig),
402
+ promptCacheRetention: promptCacheRetention(routeConfig),
403
+ reasoningEffort: generation.reasoningEffort,
404
+ temperature: generation.temperature,
405
+ maxTokens: generation.maxTokens,
406
+ idempotencyKey: randomUUID(),
407
+ headers,
408
+ signal: options.signal,
409
+ timeoutMs: routeConfig.timeoutMs,
410
+ maxAttempts: routeConfig.maxAttempts,
411
+ maxResponseBytes: routeConfig.maxResponseBytes,
802
412
  })
413
+ const session = sessionFor(ctx, route.sessionId)
414
+ const block = createNativeCheckpointBlock({
415
+ session, route, result, input: prepared.input, imageMap: prepared.imageMap,
416
+ retentionOptions: {
417
+ tokenBudget: routeConfig.nativeRetentionTokenBudget,
418
+ assistantTokenReserve: routeConfig.assistantRetentionTokenReserve,
419
+ assistantPerMessageTokenCap: routeConfig.assistantRetentionPerMessageTokenCap,
420
+ },
421
+ })
422
+ ctx.logger?.info?.(`[lcx-codex] native V2 compaction succeeded; retained ${block.retainedClientCount ?? 0} client + ${block.retainedAssistantCount ?? 0} assistant-visible item(s) (~${block.retainedEstimatedTokens ?? 0} tokens) with the opaque checkpoint`)
423
+ for (const chunk of nativeCheckpointChunks(block, result.usage)) yield chunk
803
424
  } catch (error) {
804
- if (replaySignals.timeoutSignal.aborted && !options.signal?.aborted) {
805
- throw replayTimeoutError(config.timeoutMs, error)
806
- }
807
- throw error
808
- }
809
- }
810
-
811
- async function* textChunks(text, usage) {
812
- yield { type: 'block-start', index: 0, blockType: 'text' }
813
- yield { type: 'text-delta', index: 0, text }
814
- yield { type: 'block-end', index: 0, block: { type: 'text', text } }
815
- if (usage) yield { type: 'usage', usage }
816
- yield { type: 'finish', reason: { kind: 'stop' } }
817
- }
818
-
819
- function isCompactionDirective(message) {
820
- return textOfMessage(message).includes(COMPACTION_DIRECTIVE)
821
- }
822
-
823
- function diagnosticEndpoint(baseURL) {
824
- try {
825
- const url = new URL(`${String(baseURL).replace(/\/+$/u, '')}/responses`)
826
- return `${url.origin}${url.pathname}`
827
- } catch {
828
- return 'invalid'
829
- }
830
- }
831
-
832
- function logCompactionDiagnostic(ctx, options, config, error, phase) {
833
- const failure = failureOf(error)
834
- const status = failure.status === undefined ? '' : ` status=${failure.status}`
835
- const detail = `dsh-lcx-codex compaction ${phase}: code=${failure.code}${status} provider=${String(options.provider ?? config.provider)} model=${String(options.model ?? config.model)} transport=${config.compactTransport} endpoint=${diagnosticEndpoint(config.baseURL)} inputItems=${Array.isArray(options.messages) ? options.messages.length : 0}`
836
- try {
837
- if (typeof ctx?.logger?.error === 'function') ctx.logger.error(detail)
838
- else if (typeof console?.error === 'function') console.error(detail)
839
- } catch {
840
- // Diagnostics must never change compaction behavior.
841
- }
842
- }
843
-
844
- async function prepareRemoteCompaction(options, config, portableStore, ctx, sessionTracker) {
845
- const last = options.messages?.[options.messages.length - 1]
846
- const history = last && isCompactionDirective(last) ? options.messages.slice(0, -1) : options.messages
847
- const route = currentRoute(options, config)
848
- const lease = sessionTracker?.capture(route.sessionId)
849
- lease?.assert('request-start')
850
- const headers = await promptCacheHeaders(ctx, config, route)
851
- responsesTools(options.tools)
852
- const llm = ctx?.llm ?? ctx?.get?.('llm')
853
- const imageSupport = typeof llm?.resolveModelInfo === 'function'
854
- ? await resolveModelImageSupport(llm, route, options.signal)
855
- : 'supported'
856
- const imageReferences = new Map()
857
- const input = await buildPortableResponsesInputWithImages(history, portableStore, route, {
858
- resolveImage: attachmentImageResolver(ctx),
859
- imageSupport,
860
- signal: options.signal,
861
- maxRequestImageBytes: config.maxRequestImageBytes,
862
- onImageResolved: ({ imageUrl, attachment }) => imageReferences.set(imageUrl, attachment),
863
- })
864
- lease?.assert('preflight')
865
- return { history, route, lease, input, imageReferences, headers }
866
- }
867
-
868
- function mergeUsage(...values) {
869
- const keys = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens']
870
- const merged = {}
871
- for (const key of keys) {
872
- const total = values.reduce((sum, value) => sum + (Number.isFinite(value?.[key]) ? value[key] : 0), 0)
873
- if (total > 0) merged[key] = total
874
- }
875
- return Object.keys(merged).length > 0 ? merged : undefined
876
- }
877
-
878
- function readableSummaryText(text, model, usage) {
879
- const summary = stripCheckpointMarker(String(text ?? '')).trim().slice(0, 16 * 1024)
880
- return summary || portableSummaryText(model, usage)
881
- }
882
-
883
- function portableMarkerTextWithSummary(id, model, usage, summary) {
884
- const summaryText = stripCheckpointMarker(String(summary ?? '')).trim().slice(0, 16 * 1024)
885
- const status = portableSummaryText(model, usage)
886
- return `${summaryText ? `${summaryText}\n` : ''}${status}\n[dsh-lcx-codex-v3-checkpoint:${id}]`
887
- }
888
-
889
- async function commitRemoteCompaction(prepared, result, portableStore, config, summary, usage) {
890
- const { history, route, lease, input, imageReferences } = prepared
891
- lease?.assert('commit')
892
- const replacementHistory = buildNativeReplacementHistory(input, result.compaction)
893
- const persistedNativeOutput = persistNativeImageReferences(replacementHistory, imageReferences)
894
- const persistedPortableInput = persistNativeImageReferences(input, imageReferences)
895
- const id = randomUUID()
896
- const parent = latestPortableMarker(history)
897
- const portableImageCount = inputImageCount(input)
898
- const portableSummary = readableSummaryText(summary, route.model, usage)
899
- portableStore.put(id, {
900
- version: CHECKPOINT_V3_VERSION,
901
- checkpointId: id,
902
- ...(parent ? { parentCheckpointId: parent.id } : {}),
903
- lineageId: route.sessionId || `${route.provider}:${route.baseURL}`,
904
- sourceSessionId: route.sessionId || `${route.provider}:${route.baseURL}`,
905
- provider: route.provider,
906
- model: route.model,
907
- modelKey: `${route.provider}:${route.model}`,
908
- transport: config.compactTransport,
909
- baseURLFingerprint: baseURLFingerprint(route.baseURL),
910
- routeFingerprint: routeFingerprint(route),
911
- nativeOutput: persistedNativeOutput,
912
- nativeCompaction: result.compaction,
913
- portableHistory: buildPortableHistory(persistedPortableInput),
914
- ...(portableImageCount > 0 ? { portableImageCount } : {}),
915
- portableSummary,
916
- createdAt: Date.now(),
917
- usage,
918
- })
919
- return { id, route, usage, text: portableMarkerTextWithSummary(id, route.model, usage, summary) }
920
- }
921
-
922
- async function* remoteCompactionStream(options, config, portableStore, ctx, sessionTracker) {
923
- try {
924
- const prepared = await prepareRemoteCompaction(options, config, portableStore, ctx, sessionTracker)
925
- const result = await requestRemoteCompaction(options, prepared.input, config, options.signal, ctx, prepared.route, prepared.headers)
926
- const committed = await commitRemoteCompaction(prepared, result, portableStore, config, undefined, result.usage)
927
- yield* textChunks(committed.text, committed.usage)
928
- } catch (error) {
929
- logCompactionDiagnostic(ctx, options, config, error, 'failed')
930
- throw error
425
+ const session = sessionFor(ctx, route.sessionId)
426
+ const hasExistingCheckpoint = messagesContainNativeCheckpoint(history, session) || messagesContainLegacyCheckpoint(history)
427
+ const code = error?.code ?? error?.name ?? 'ERROR'
428
+ const status = Number.isInteger(error?.status) ? ` status=${error.status}` : ''
429
+ const requestId = error?.requestId ? ` requestId=${String(error.requestId)}` : ''
430
+ const providerCode = error?.providerCode ? ` providerCode=${error.providerCode}` : ''
431
+ const providerType = error?.providerType ? ` providerType=${error.providerType}` : ''
432
+ const providerParam = error?.providerParam ? ` providerParam=${error.providerParam}` : ''
433
+ ctx.logger?.warn?.(`[lcx-codex] native V2 compaction failed: code=${code}${status}${requestId}${providerCode}${providerType}${providerParam}`)
434
+ if (!state.fallbackToBasicCompaction || hasExistingCheckpoint || !fallbackEligible(error, options.signal)) throw error
435
+ ctx.logger?.info?.('[lcx-codex] falling back to DSH basic compaction after allowlisted first-checkpoint native failure')
436
+ const stream = await next()
437
+ for await (const chunk of stream) yield chunk
931
438
  }
932
439
  }
933
440
 
934
- async function collectLocalCompaction(next) {
935
- const chunks = []
936
- const stream = await next()
937
- for await (const chunk of stream) chunks.push(chunk)
938
- const text = chunks.filter((chunk) => chunk?.type === 'text-delta' && typeof chunk.text === 'string').map((chunk) => chunk.text).join('')
939
- const usage = chunks.find((chunk) => chunk?.type === 'usage')?.usage
940
- return { chunks, text, usage }
441
+ function routedTargetForAgent(agent, fallback) {
442
+ const latest = agent?.session?.requestHeader?.()?.config
443
+ const provider = latest?.provider ?? agent?.options?.provider ?? fallback.provider
444
+ const model = latest?.model ?? agent?.options?.model ?? fallback.model
445
+ return provider && model ? { provider, model } : undefined
941
446
  }
942
447
 
943
- async function* parallelCompactionStream(options, config, portableStore, ctx, sessionTracker, next, diagnostic) {
944
- let prepared
945
- try {
946
- prepared = await prepareRemoteCompaction(options, config, portableStore, ctx, sessionTracker)
947
- } catch (error) {
948
- logCompactionDiagnostic(ctx, options, config, error, 'failed')
949
- throw error
950
- }
951
- const localPromise = collectLocalCompaction(next)
952
- const remotePromise = requestRemoteCompaction(options, prepared.input, config, options.signal, ctx, prepared.route, prepared.headers)
953
- .then((result) => ({ result }), (error) => ({ error }))
954
- const [localOutcome, remoteOutcome] = await Promise.allSettled([localPromise, remotePromise])
955
- if (remoteOutcome.status === 'fulfilled' && remoteOutcome.value.result) {
956
- const local = localOutcome.status === 'fulfilled' ? localOutcome.value : { text: '', usage: undefined }
957
- const usage = mergeUsage(remoteOutcome.value.result.usage, local.usage)
958
- try {
959
- const committed = await commitRemoteCompaction(prepared, remoteOutcome.value.result, portableStore, config, local.text, usage)
960
- yield* textChunks(committed.text, committed.usage)
961
- return
962
- } catch (error) {
963
- logCompactionDiagnostic(ctx, options, config, error, 'failed')
964
- throw error
965
- }
966
- }
967
- const remoteError = remoteOutcome.status === 'fulfilled' ? remoteOutcome.value.error : remoteOutcome.reason
968
- if (options.signal?.aborted || remoteError?.remoteCompactionRequest !== true) throw remoteError
969
- diagnostic?.(remoteError, 'failed')
970
- diagnostic?.(remoteError, 'fallback')
971
- if (localOutcome.status !== 'fulfilled') throw localOutcome.reason
972
- for (const chunk of localOutcome.value.chunks) yield chunk
448
+ function clampPercent(value, fallback, min, max) {
449
+ const number = Number(value)
450
+ return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : fallback
973
451
  }
974
452
 
975
- function replaySseError(message, code = 'LCX_RESPONSES_UPSTREAM_ERROR') {
976
- const error = new Error(message)
977
- error.code = code
978
- return error
453
+ function pressurePolicy(state, config) {
454
+ const auto = clampPercent(state.autoCompactionThresholdPercent, config.autoCompactionThresholdPercent, 85, 95)
455
+ const emergencyRaw = clampPercent(state.emergencyPruneThresholdPercent, config.emergencyPruneThresholdPercent, 90, 99)
456
+ const emergency = Math.max(auto + 1, emergencyRaw)
457
+ return { auto, emergency: Math.min(99, emergency) }
979
458
  }
980
459
 
981
- function replayTimeoutError(timeoutMs, cause) {
982
- const error = replaySseError(`LCX Responses replay timed out after ${timeoutMs} ms`, 'LCX_TIMEOUT')
983
- if (cause !== undefined) error.cause = cause
984
- return error
460
+ function adjustedCompactionConfig(config, target, thresholdRatio) {
461
+ if (!config || typeof config !== 'object') return config
462
+ const modelPolicies = Array.isArray(config.modelPolicies)
463
+ ? config.modelPolicies.map((policy) => policy?.provider === target.provider && policy?.model === target.model ? { ...policy, thresholdRatio } : policy)
464
+ : config.modelPolicies
465
+ return { ...config, thresholdRatio, ...(modelPolicies === undefined ? {} : { modelPolicies }) }
985
466
  }
986
467
 
987
- function replayAbortError(options, cause) {
988
- if (options.timeoutSignal?.aborted && !options.requestSignal?.aborted) {
989
- return replayTimeoutError(options.timeoutMs, cause)
990
- }
991
- return cause ?? options.signal?.reason ?? new Error('request aborted')
468
+ function resolveContextService(ctx, name) {
469
+ try { return ctx?.get?.(name) ?? ctx?.[name] } catch { return undefined }
992
470
  }
993
471
 
994
- function replayEffectiveSignal(signal, timeoutMs) {
995
- const timeoutSignal = AbortSignal.timeout(timeoutMs)
996
- return {
997
- timeoutSignal,
998
- signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
999
- }
1000
- }
472
+ function resolveScopedService(agent, name) { return resolveContextService(agent?.ctx, name) }
1001
473
 
1002
- function replaySseEvent(dataLines) {
1003
- if (dataLines.length === 0) return undefined
1004
- const data = dataLines.join('\n')
1005
- if (data === '[DONE]') return undefined
474
+ function resolveAgentService(ctx, agent, name) {
475
+ const agentPresets = resolveContextService(ctx, 'agentPresets')
1006
476
  try {
1007
- const parsed = JSON.parse(data)
1008
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('event is not an object')
1009
- return parsed
1010
- } catch (error) {
1011
- throw replaySseError(`LCX Responses replay returned malformed SSE JSON: ${String(error)}`, 'LCX_INVALID_SSE')
1012
- }
1013
- }
1014
-
1015
- function replayOutputIndex(event, item) {
1016
- const value = event?.output_index ?? item?.output_index
1017
- return Number.isInteger(value) ? value : undefined
477
+ const service = agentPresets?.serviceFor?.(agent, name)
478
+ if (service !== undefined) return service
479
+ } catch {}
480
+ return resolveScopedService(agent, name)
1018
481
  }
1019
482
 
1020
- function replayItemId(event, item) {
1021
- const value = event?.item_id ?? item?.id
1022
- return typeof value === 'string' && value.length > 0 ? value : undefined
483
+ function concreteService(value) {
484
+ try { return value?.[cordisSymbols.original] ?? value } catch { return value }
1023
485
  }
1024
486
 
1025
- function replayItemKey(event, item) {
1026
- const outputIndex = replayOutputIndex(event, item)
1027
- if (outputIndex !== undefined) return `output:${outputIndex}`
1028
- const itemId = replayItemId(event, item)
1029
- return itemId ? `item:${itemId}` : undefined
487
+ function combinedAbortSignal(primary, lifecycle) {
488
+ if (!primary) return lifecycle
489
+ if (!lifecycle) return primary
490
+ if (primary === lifecycle) return primary
491
+ return AbortSignal.any([primary, lifecycle])
1030
492
  }
1031
493
 
1032
- function replayKind(type) {
1033
- if (type === 'function_call' || type === 'response.function_call' || type === 'tool-call') return 'tool-call'
1034
- if (type === 'reasoning') return 'reasoning'
1035
- return 'text'
1036
- }
1037
-
1038
- function replayItemText(item, kind) {
1039
- if (kind === 'reasoning') return responseReasoningText(item)
1040
- return responseTextParts(item).join('')
1041
- }
1042
-
1043
- function replayEnsureOpen(state) {
1044
- if (state.open) return []
1045
- state.open = true
1046
- state.started = true
1047
- return [{ type: 'block-start', index: state.index, blockType: state.kind }]
1048
- }
1049
-
1050
- function replayClose(state, finalText) {
1051
- const chunks = []
1052
- if (!state.open) {
1053
- if (!state.started && typeof finalText === 'string' && finalText.length > 0) chunks.push(...replayEnsureOpen(state))
1054
- else return []
1055
- }
1056
- if (typeof finalText === 'string' && (state.text.length === 0 || finalText.length >= state.text.length)) state.text = finalText
1057
- state.open = false
1058
- state.closed = true
1059
- return [...chunks, {
1060
- type: 'block-end',
1061
- index: state.index,
1062
- block: state.kind === 'tool-call'
1063
- ? { type: 'tool-call', id: state.id, name: state.name, arguments: state.arguments }
1064
- : { type: state.kind, text: state.text },
1065
- }]
1066
- }
1067
-
1068
- function replayCloseItem(state, item) {
1069
- const kind = replayKind(item?.type ?? state.kind)
1070
- state.kind = kind
1071
- if (kind === 'tool-call') {
1072
- if (typeof item?.call_id === 'string' && item.call_id.length > 0) state.id = item.call_id
1073
- if (!state.id) state.id = randomUUID()
1074
- state.name = String(item?.name ?? state.name ?? '')
1075
- const finalArguments = typeof item?.arguments === 'string' ? item.arguments : undefined
1076
- const chunks = []
1077
- if (!state.started && (state.name || finalArguments)) {
1078
- chunks.push(...replayEnsureOpen(state))
1079
- if (finalArguments) {
1080
- state.arguments = finalArguments
1081
- chunks.push({ type: 'tool-call-delta', index: state.index, id: state.id, name: state.name, argumentsDelta: finalArguments })
1082
- }
1083
- } else if (finalArguments && state.arguments.length === 0) {
1084
- state.arguments = finalArguments
494
+ function patchCompactionPressureService(compactionValue, state, getConfig, ctx, records, requestHeaders) {
495
+ const compaction = concreteService(compactionValue)
496
+ if (!compaction || typeof compaction.compactIfNeeded !== 'function' || records.has(compaction)) return false
497
+ const original = compaction.compactIfNeeded
498
+ const record = { compaction, original, wrapper: undefined, mutex: new ServiceMutex(), lifecycle: new AbortController() }
499
+ const wrapper = async function(agentArg, trigger, signal) {
500
+ const activeSignal = combinedAbortSignal(signal, record.lifecycle.signal)
501
+ const callOriginal = () => {
502
+ updateRequestHeaderCache(requestHeaders, agentArg?.session, { type: 'compaction/start' })
503
+ return original.call(this, agentArg, trigger, activeSignal)
1085
504
  }
1086
- chunks.push(...replayClose(state))
1087
- return chunks
1088
- }
1089
- const finalText = replayItemText(item, kind)
1090
- const chunks = []
1091
- if (!state.started && finalText) {
1092
- chunks.push(...replayEnsureOpen(state))
1093
- state.text = finalText
1094
- chunks.push({ type: kind === 'reasoning' ? 'reasoning-delta' : 'text-delta', index: state.index, text: finalText })
1095
- }
1096
- chunks.push(...replayClose(state, finalText))
1097
- return chunks
1098
- }
1099
-
1100
- async function* responsesSseChunks(response, options = {}) {
1101
- if (!response?.body) throw replaySseError('LCX Responses replay response did not include an SSE body', 'LCX_INVALID_SSE')
1102
- const maxBytes = options.maxResponseBytes ?? 8 * 1024 * 1024
1103
- const reader = response.body.getReader()
1104
- const decoder = new TextDecoder()
1105
- const states = new Map()
1106
- let nextIndex = 0
1107
- let pending = ''
1108
- let dataLines = []
1109
- let bytes = 0
1110
- let sawCompleted = false
1111
- let hasToolCall = false
1112
- let completedResponse
1113
- const cancel = () => { reader.cancel(options.signal?.reason).catch(() => undefined) }
1114
- options.signal?.addEventListener('abort', cancel, { once: true })
1115
-
1116
- const stateFor = (event, item, kindHint) => {
1117
- const outputIndex = replayOutputIndex(event, item)
1118
- const itemId = replayItemId(event, item)
1119
- const kind = kindHint ?? replayKind(item?.type)
1120
- const key = replayItemKey(event, item)
1121
- let state = key ? states.get(key) : undefined
1122
- if (!state) {
1123
- state = [...states.values()].find((candidate) =>
1124
- !candidate.closed &&
1125
- ((outputIndex !== undefined && candidate.outputIndex === outputIndex) ||
1126
- (itemId !== undefined && candidate.itemId === itemId) ||
1127
- (outputIndex === undefined && itemId === undefined && candidate.kind === kind)))
1128
- }
1129
- if (!state) {
1130
- state = {
1131
- index: nextIndex++,
1132
- kind,
1133
- open: false,
1134
- closed: false,
1135
- started: false,
1136
- text: '',
1137
- arguments: '',
1138
- id: undefined,
1139
- itemId: undefined,
1140
- outputIndex: undefined,
1141
- name: '',
1142
- }
1143
- states.set(key ?? `anonymous:${state.index}`, state)
1144
- }
1145
- if (outputIndex !== undefined) state.outputIndex = outputIndex
1146
- if (itemId !== undefined) state.itemId = itemId
1147
- if (item?.type) state.kind = replayKind(item.type)
1148
- if (typeof event?.call_id === 'string' && event.call_id.length > 0) state.id = event.call_id
1149
- if (typeof item?.call_id === 'string' && item.call_id.length > 0) state.id = item.call_id
1150
- if (item?.name) state.name = String(item.name)
1151
- return state
1152
- }
1153
-
1154
- const stateForCompleted = (item, outputIndex, consumed) => {
1155
- const kind = replayKind(item?.type)
1156
- const itemId = replayItemId(undefined, item)
1157
- let state = [...states.values()].find((candidate) =>
1158
- !consumed.has(candidate) &&
1159
- ((outputIndex !== undefined && candidate.outputIndex === outputIndex) ||
1160
- (itemId !== undefined && candidate.itemId === itemId)))
1161
- if (!state && itemId === undefined) {
1162
- state = [...states.values()].find((candidate) => !consumed.has(candidate) && candidate.kind === kind)
1163
- }
1164
- if (!state) state = stateFor({ output_index: outputIndex }, item, kind)
1165
- consumed.add(state)
1166
- return state
1167
- }
1168
-
1169
- const eventChunks = (event) => {
1170
- if (event?.type === 'error' || event?.type === 'response.failed' || event?.type === 'response.incomplete' || event?.type === 'response.error') {
1171
- const message = event.error?.message ?? event.response?.error?.message ?? `LCX Responses replay upstream error (${String(event.type)})`
1172
- throw replaySseError(message)
1173
- }
1174
- if (event?.type === 'response.output_item.added') {
1175
- const item = event.item
1176
- const state = stateFor(event, item)
1177
- if (item?.type === 'function_call') hasToolCall = true
1178
- return []
1179
- }
1180
- if (event?.type === 'response.output_text.delta') {
1181
- const state = stateFor(event, undefined, 'text')
1182
- const delta = typeof event.delta === 'string' ? event.delta : ''
1183
- if (!delta) return []
1184
- const chunks = replayEnsureOpen(state)
1185
- state.text += delta
1186
- chunks.push({ type: 'text-delta', index: state.index, text: delta })
1187
- return chunks
1188
- }
1189
- if (event?.type === 'response.output_text.done') {
1190
- const state = stateFor(event, undefined, 'text')
1191
- return replayClose(state, typeof event.text === 'string' ? event.text : undefined)
1192
- }
1193
- if (event?.type === 'response.function_call_arguments.delta') {
1194
- const state = stateFor(event, undefined, 'tool-call')
1195
- state.kind = 'tool-call'
1196
- if (typeof event.call_id === 'string' && event.call_id.length > 0) state.id = event.call_id
1197
- if (!state.id) state.id = randomUUID()
1198
- if (typeof event.name === 'string') state.name = event.name
1199
- const delta = typeof event.delta === 'string' ? event.delta : ''
1200
- if (!delta) return []
1201
- hasToolCall = true
1202
- const chunks = replayEnsureOpen(state)
1203
- state.arguments += delta
1204
- chunks.push({ type: 'tool-call-delta', index: state.index, id: state.id, name: state.name, argumentsDelta: delta })
1205
- return chunks
1206
- }
1207
- if (event?.type === 'response.function_call_arguments.done') {
1208
- const state = stateFor(event, undefined, 'tool-call')
1209
- state.kind = 'tool-call'
1210
- hasToolCall = true
1211
- if (typeof event.call_id === 'string' && event.call_id.length > 0) state.id = event.call_id
1212
- if (!state.id) state.id = randomUUID()
1213
- if (typeof event.name === 'string') state.name = event.name
1214
- const chunks = []
1215
- const finalArguments = typeof event.arguments === 'string' ? event.arguments : ''
1216
- if (!state.started && (state.name || finalArguments)) {
1217
- chunks.push(...replayEnsureOpen(state))
1218
- if (finalArguments) {
1219
- state.arguments = finalArguments
1220
- chunks.push({ type: 'tool-call-delta', index: state.index, id: state.id, name: state.name, argumentsDelta: finalArguments })
1221
- }
1222
- } else if (finalArguments && state.arguments.length === 0) {
1223
- state.arguments = finalArguments
1224
- }
1225
- chunks.push(...replayClose(state))
1226
- return chunks
1227
- }
1228
- if (event?.type === 'response.output_item.done') {
1229
- const state = stateFor(event, event.item)
1230
- if (event.item?.type === 'function_call' || state.kind === 'tool-call') hasToolCall = true
1231
- if (state.kind === 'text') {
1232
- const finalText = replayItemText(event.item, 'text')
1233
- if (finalText && [...states.values()].some((candidate) =>
1234
- candidate !== state && candidate.closed && candidate.kind === 'text' && candidate.text === finalText)) return []
1235
- }
1236
- return replayCloseItem(state, event.item)
1237
- }
1238
- if (event?.type === 'response.completed') {
1239
- sawCompleted = true
1240
- completedResponse = event.response && typeof event.response === 'object' ? event.response : {}
1241
- const chunks = []
1242
- const completedStates = new Set()
1243
- if (Array.isArray(completedResponse.output)) {
1244
- for (let outputIndex = 0; outputIndex < completedResponse.output.length; outputIndex += 1) {
1245
- const item = completedResponse.output[outputIndex]
1246
- if (item?.type === 'function_call') hasToolCall = true
1247
- const completedText = replayItemText(item, 'text')
1248
- if (completedText && [...states.values()].some((candidate) =>
1249
- candidate.closed && candidate.kind === 'text' && candidate.text === completedText)) {
1250
- continue
1251
- }
1252
- const state = stateForCompleted(item, outputIndex, completedStates)
1253
- // A provider may emit output_text.done/output_item.done before the
1254
- // authoritative response.completed event. The completed event closes
1255
- // the response, but must not create a second visible DSH block for an
1256
- // item that has already been closed on the stream.
1257
- if (state.closed) continue
1258
- // Some gateways change output indexes or omit item ids between
1259
- // streaming events and response.completed. If the completed text
1260
- // exactly matches an already closed text state, it is the same
1261
- // visible item and must not be projected a second time.
1262
- chunks.push(...replayCloseItem(state, item))
1263
- }
505
+ return record.mutex.run(activeSignal, async () => {
506
+ if (trigger !== 'pressure' || !state.enabled || !state.remoteCompaction || !state.autoCompaction) return callOriginal()
507
+ const config = getConfig()
508
+ const target = routedTargetForAgent(agentArg, config)
509
+ if (!target || !resolveResponsesRouteConfig(ctx, target, config)) return callOriginal()
510
+ const tokenMeter = resolveScopedService(agentArg, 'tokenMeter') ?? resolveContextService(this?.ctx, 'tokenMeter')
511
+ const llm = resolveScopedService(agentArg, 'llm') ?? resolveContextService(ctx, 'llm')
512
+ if (!tokenMeter?.measure || !llm?.resolveModelInfo) return callOriginal()
513
+ let contextWindow
514
+ try { contextWindow = (await llm.resolveModelInfo(target.provider, target.model, activeSignal))?.context?.contextWindow } catch { return callOriginal() }
515
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0) return callOriginal()
516
+ const totalTokens = Number(tokenMeter.measure(agentArg.session)?.totalTokens ?? 0)
517
+ const { auto, emergency } = pressurePolicy(state, config)
518
+ const ratioPercent = totalTokens / contextWindow * 100
519
+ if (ratioPercent < auto) return null
520
+ const pruner = concreteService(resolveAgentService(ctx, agentArg, 'toolResultPruner') ?? resolveContextService(this?.ctx, 'toolResultPruner'))
521
+ const originalPrune = pruner?.pruneSession
522
+ const originalConfig = this?.config
523
+ const nativeFirst = ratioPercent < emergency
524
+ const noOpPrune = () => ({ pruned: [], charsRemoved: 0 })
525
+ let installedConfig
526
+ if (nativeFirst && pruner && typeof originalPrune === 'function') pruner.pruneSession = noOpPrune
527
+ if (originalConfig && this && Object.prototype.hasOwnProperty.call(this, 'config')) {
528
+ try { installedConfig = adjustedCompactionConfig(originalConfig, target, auto / 100); this.config = installedConfig } catch {}
1264
529
  }
1265
- for (const state of states.values()) {
1266
- if (!completedStates.has(state)) chunks.push(...replayClose(state))
530
+ ctx.logger?.info?.(`[lcx-codex] auto pressure ${ratioPercent.toFixed(1)}%: ${nativeFirst ? 'Native V2 first' : 'emergency DSH prune allowed'} (native ${auto}%, emergency ${emergency}%)`)
531
+ try { return await callOriginal() }
532
+ finally {
533
+ if (installedConfig && this?.config === installedConfig) { try { this.config = originalConfig } catch {} }
534
+ if (nativeFirst && pruner?.pruneSession === noOpPrune) { try { pruner.pruneSession = originalPrune } catch {} }
1267
535
  }
1268
- const usage = usageFrom(completedResponse)
1269
- if (usage) chunks.push({ type: 'usage', usage })
1270
- chunks.push({ type: 'finish', reason: { kind: hasToolCall ? 'tool-calls' : 'stop' } })
1271
- return chunks
1272
- }
1273
- if (event?.type === 'response.reasoning_summary_text.delta' || event?.type === 'response.reasoning_text.delta' || event?.type === 'response.reasoning.delta') {
1274
- const state = stateFor(event, undefined, 'reasoning')
1275
- state.kind = 'reasoning'
1276
- const delta = typeof event.delta === 'string' ? event.delta : ''
1277
- if (!delta) return []
1278
- const chunks = replayEnsureOpen(state)
1279
- state.text += delta
1280
- chunks.push({ type: 'reasoning-delta', index: state.index, text: delta })
1281
- return chunks
1282
- }
1283
- if (event?.type === 'response.reasoning_summary_text.done' || event?.type === 'response.reasoning_text.done' || event?.type === 'response.reasoning.done') {
1284
- const state = stateFor(event, undefined, 'reasoning')
1285
- state.kind = 'reasoning'
1286
- return replayClose(state, typeof event.text === 'string' ? event.text : undefined)
1287
- }
1288
- return []
1289
- }
1290
-
1291
- const dispatch = () => {
1292
- const event = replaySseEvent(dataLines)
1293
- dataLines = []
1294
- return event ? eventChunks(event) : []
1295
- }
1296
-
1297
- try {
1298
- while (true) {
1299
- if (options.signal?.aborted) throw replayAbortError(options)
1300
- const result = await reader.read()
1301
- if (result.done) break
1302
- bytes += result.value.byteLength
1303
- if (bytes > maxBytes) throw replaySseError(`LCX Responses replay SSE response exceeds ${maxBytes} bytes`, 'LCX_RESPONSE_TOO_LARGE')
1304
- pending += decoder.decode(result.value, { stream: true })
1305
- let newline
1306
- while ((newline = pending.indexOf('\n')) >= 0) {
1307
- const line = pending.slice(0, newline).replace(/\r$/u, '')
1308
- pending = pending.slice(newline + 1)
1309
- if (line === '') {
1310
- for (const chunk of dispatch()) yield chunk
1311
- } else if (!line.startsWith(':')) {
1312
- if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /u, ''))
1313
- }
1314
- }
1315
- }
1316
- pending += decoder.decode()
1317
- if (pending.length > 0) {
1318
- const line = pending.replace(/\r$/u, '')
1319
- if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /u, ''))
1320
- }
1321
- for (const chunk of dispatch()) yield chunk
1322
- } catch (error) {
1323
- throw replayAbortError(options, error)
1324
- } finally {
1325
- options.signal?.removeEventListener('abort', cancel)
1326
- await reader.cancel().catch(() => undefined)
1327
- }
1328
- if (!sawCompleted && options.timeoutSignal?.aborted && !options.requestSignal?.aborted) {
1329
- throw replayTimeoutError(options.timeoutMs)
1330
- }
1331
- if (!sawCompleted) throw replaySseError('LCX Responses replay SSE ended without response.completed', 'LCX_INCOMPLETE_SSE')
1332
- if (!completedResponse) throw replaySseError('LCX Responses replay did not return a completed response', 'LCX_INCOMPLETE_SSE')
1333
- }
1334
-
1335
- function failureOf(error) {
1336
- return {
1337
- message: error instanceof Error ? error.message : String(error),
1338
- code: typeof error?.code === 'string' ? error.code : 'LCX_CODEX_ERROR',
1339
- ...(Number.isInteger(error?.status) ? { status: error.status } : {}),
536
+ })
1340
537
  }
538
+ record.wrapper = wrapper
539
+ try { compaction.compactIfNeeded = wrapper } catch { return false }
540
+ records.set(compaction, record)
541
+ return true
1341
542
  }
1342
543
 
1343
- function guardedStream(factory, signal) {
1344
- return (async function* () {
1345
- try {
1346
- yield* factory()
1347
- } catch (error) {
1348
- const failure = failureOf(error)
1349
- yield {
1350
- type: 'finish',
1351
- reason: signal?.aborted ? { kind: 'aborted', failure } : { kind: 'error', failure },
1352
- }
1353
- }
1354
- })()
544
+ function patchCompactionPressureForAgent(agent, state, getConfig, ctx, records, requestHeaders) {
545
+ return patchCompactionPressureService(resolveAgentService(ctx, agent, 'compaction'), state, getConfig, ctx, records, requestHeaders)
1355
546
  }
1356
547
 
1357
- async function executeHostedSearch(ctx, config, args, signal) {
1358
- abortIfNeeded(signal)
1359
- const normalized = normalizeHostedSearchArgs(args)
1360
- const requestId = randomUUID()
1361
- try {
1362
- const response = await fetchJsonWithRetry(
1363
- `${config.baseURL}/responses`,
1364
- buildHostedSearchBody(normalized, config.model),
1365
- await authenticatedHeaders(ctx, config, undefined, requestId),
1366
- signal,
1367
- config.timeoutMs,
1368
- { maxAttempts: config.maxAttempts, maxResponseBytes: config.maxResponseBytes },
1369
- )
1370
- return parseHostedSearchResponse(response, requestId, config.webMaxResults)
1371
- } catch (error) {
1372
- if (signal?.aborted) throw webError('LCX hosted Web Search aborted', 'LCX_WEB_ABORTED', error)
1373
- if (error?.code === 'LCX_TIMEOUT') throw webError(String(error), 'LCX_WEB_TIMEOUT', error)
1374
- if (error?.code?.startsWith?.('WEB_')) throw error
1375
- throw webError(String(error), 'LCX_WEB_PROVIDER_ERROR', error)
548
+ async function restoreCompactionPressure(records) {
549
+ const reason = new Error('lcx-codex pressure coordination is shutting down')
550
+ const entries = [...records.values()]
551
+ for (const record of entries) {
552
+ if (!record.lifecycle.signal.aborted) record.lifecycle.abort(reason)
1376
553
  }
554
+ await Promise.allSettled(entries.map((record) => record.mutex.close(reason)))
555
+ for (const record of entries) if (record.compaction?.compactIfNeeded === record.wrapper) { try { record.compaction.compactIfNeeded = record.original } catch {} }
556
+ records.clear()
1377
557
  }
1378
558
 
1379
- function configuredResponsesRoute(ctx, config) {
1380
- const normalized = normalizeConfig(config)
1381
- return resolveResponsesRouteConfig(ctx, { provider: normalized.provider, model: normalized.model }, normalized)
559
+ function patchVisibleWebSearchTimeout(agent, state, getConfig, patchedDefinitions) {
560
+ const tools = resolveScopedService(agent, 'tools')
561
+ const definition = tools?.get?.('web_search', agent)
562
+ if (!definition || typeof definition !== 'object') return
563
+ if (!patchedDefinitions.has(definition)) patchedDefinitions.set(definition, definition.timeoutMs)
564
+ const config = getConfig()
565
+ const timeoutMs = Number.isFinite(state.webSearchTimeoutSeconds) ? Math.min(600_000, Math.max(30_000, Math.round(state.webSearchTimeoutSeconds * 1000))) : config.webSearchTimeoutMs
566
+ try { definition.timeoutMs = state.enabled && state.webSearch ? timeoutMs : patchedDefinitions.get(definition) } catch {}
1382
567
  }
1383
568
 
1384
- function currentAlphaCapabilityFingerprint(config, model = config.model) {
1385
- return alphaCapabilityFingerprint({
1386
- baseURL: config.baseURL,
1387
- provider: config.provider,
1388
- model,
1389
- profile: config.alphaProfile,
1390
- group: config.alphaGroup,
1391
- schemaFingerprint: ALPHA_SCHEMA_FINGERPRINT,
1392
- })
569
+ function refreshPatchedWebSearchTimeouts(state, getConfig, patchedDefinitions) {
570
+ const config = getConfig()
571
+ const timeoutMs = Number.isFinite(state.webSearchTimeoutSeconds) ? Math.min(600_000, Math.max(30_000, Math.round(state.webSearchTimeoutSeconds * 1000))) : config.webSearchTimeoutMs
572
+ for (const [definition, original] of patchedDefinitions.entries()) { try { definition.timeoutMs = state.enabled && state.webSearch ? timeoutMs : original } catch {} }
1393
573
  }
1394
574
 
1395
- function isHttpReference(value) {
1396
- try {
1397
- return ['http:', 'https:'].includes(new URL(value).protocol)
1398
- } catch {
1399
- return false
1400
- }
575
+ function restoreWebSearchTimeouts(patchedDefinitions) {
576
+ for (const [definition, original] of patchedDefinitions.entries()) { try { if (original === undefined) delete definition.timeoutMs; else definition.timeoutMs = original } catch {} }
577
+ patchedDefinitions.clear()
1401
578
  }
1402
579
 
1403
- function alphaUnavailable(message = 'Alpha Web Search action is unavailable for the verified deployment capability') {
1404
- return webError(message, 'LCX_ALPHA_ACTION_UNAVAILABLE')
1405
- }
1406
-
1407
- function activeAgentRoute(agent) {
1408
- const requestContext = agent?.session?.requestContext?.()
1409
- const requestConfig = agent?.session?.requestHeader?.()?.config
1410
- return {
1411
- provider: requestContext?.provider ?? requestConfig?.provider ?? agent?.options?.provider,
1412
- model: requestContext?.model ?? requestConfig?.model ?? agent?.options?.model,
1413
- }
1414
- }
580
+ function messagesContainNativeCheckpoint(messages, session) { return Boolean(session && (messages ?? []).some((message) => checkpointStateForMessage(session, message))) }
581
+ function messagesContainLegacyCheckpoint(messages) { return (messages ?? []).some((message) => Boolean(legacyCheckpointId(message))) }
1415
582
 
1416
- function searchRouteConfig(ctx, config, agent) {
1417
- const { provider, model } = activeAgentRoute(agent)
1418
- return resolveResponsesRouteConfig(ctx, {
1419
- provider: provider ?? config.provider,
1420
- model: model ?? config.model,
1421
- }, config) ?? configuredResponsesRoute(ctx, config)
583
+ async function* recursiveLlmStream(ctx, options, messages) {
584
+ const llm = ctx?.get?.('llm') ?? ctx?.llm
585
+ if (!llm?.stream) throw Object.assign(new Error('LCX portable replay requires ctx.llm.stream'), { code: 'LCX_CHECKPOINT_REPLAY_UNAVAILABLE' })
586
+ const rewritten = { ...options, messages }
587
+ bypassReplayOptions.add(rewritten)
588
+ const stream = await llm.stream(rewritten)
589
+ for await (const chunk of stream) yield chunk
1422
590
  }
1423
591
 
1424
- async function executeAlphaSearch(ctx, config, capabilityStore, refStore, args, exec) {
1425
- abortIfNeeded(exec?.signal)
1426
- const normalized = normalizeAlphaSearchArgs(args)
1427
- const sessionId = typeof exec?.agent?.id === 'string' ? exec.agent.id : undefined
1428
- if (!sessionId) throw webError('Alpha Web Search requires a live DSH agent session', 'LCX_ALPHA_SESSION_REQUIRED')
1429
- const routeConfig = searchRouteConfig(ctx, config, exec?.agent)
1430
- if (!routeConfig) throw alphaUnavailable('Alpha Web Search requires a configured DSH GPT openai-responses route')
1431
- const fingerprint = currentAlphaCapabilityFingerprint(routeConfig)
1432
- const capability = capabilityStore.get(fingerprint)
1433
- if (!alphaCapabilityUsable(capability) || capability.schemaFingerprint !== ALPHA_SCHEMA_FINGERPRINT || capability.actions?.[normalized.action] !== 'supported') {
1434
- throw alphaUnavailable()
1435
- }
1436
- if (normalized.refId && !isHttpReference(normalized.refId)) refStore.assertUsable(sessionId, fingerprint, normalized.refId)
1437
- const requestId = randomUUID()
1438
- try {
1439
- const response = await fetchJsonWithRetry(
1440
- `${routeConfig.baseURL}/alpha/search`,
1441
- buildAlphaSearchBody(normalized, routeConfig.model, sessionId, true, routeConfig.alphaMaxOutputTokens),
1442
- await authenticatedHeaders(ctx, routeConfig, sessionId, requestId),
1443
- exec?.signal,
1444
- routeConfig.timeoutMs,
1445
- { maxAttempts: routeConfig.maxAttempts, maxResponseBytes: routeConfig.maxResponseBytes },
1446
- )
1447
- const result = parseAlphaSearchResponse(response, {
1448
- action: normalized.action,
1449
- capability: capability.classification,
1450
- requestId,
1451
- })
1452
- if (result.refs.length > 0) {
1453
- refStore.record(sessionId, fingerprint, result.refs.map((refId) => ({
1454
- refId,
1455
- ...(result.sources.find((source) => source.refId === refId)?.url ? { url: result.sources.find((source) => source.refId === refId).url } : {}),
1456
- })))
1457
- }
1458
- return result
1459
- } catch (error) {
1460
- if (exec?.signal?.aborted) throw webError('LCX Alpha Web Search aborted', 'LCX_ALPHA_ABORTED', error)
1461
- if (error?.code === 'LCX_TIMEOUT') throw webError('LCX Alpha Web Search timed out', 'LCX_ALPHA_TIMEOUT', error)
1462
- if (error?.code?.startsWith?.('WEB_') || error?.code?.startsWith?.('LCX_ALPHA_')) throw error
1463
- if ([404, 405].includes(error?.status) || /channel does not support/iu.test(String(error?.message ?? ''))) throw alphaUnavailable()
1464
- throw webError('LCX Alpha Web Search provider request failed', 'LCX_ALPHA_PROVIDER_ERROR', error)
592
+ async function* replayCheckpointStream(options, routeConfig, ctx) {
593
+ const route = currentRoute(options, routeConfig)
594
+ const session = sessionFor(ctx, route.sessionId)
595
+ if (!session) throw Object.assign(new Error('Native checkpoint replay requires the live DSH session'), { code: 'LCX_SESSION_UNAVAILABLE' })
596
+ const incompatibleV4 = (options.messages ?? []).some((message) => { const state = checkpointStateForMessage(session, message); return state && !stateRouteCompatible(state, route, ctx) })
597
+ if (incompatibleV4) {
598
+ const portable = rewriteCheckpointsPortable(options.messages, session, { maxChars: routeConfig.portableReplayMaxChars })
599
+ yield* recursiveLlmStream(ctx, options, portable)
600
+ return
1465
601
  }
602
+ const prepared = await serializeNativeAware(options.messages, route, routeConfig, ctx, { signal: options.signal, system: options.system, tools: options.tools })
603
+ const cacheSessionId = promptCacheSessionId(route, routeConfig)
604
+ const headers = await authenticatedHeaders(ctx, routeConfig, cacheSessionId, cacheSessionId === undefined ? null : undefined)
605
+ yield* requestNativeReplay({ baseURL: routeConfig.baseURL, provider: route.provider, model: route.model, input: prepared.input, tools: prepared.tools ?? options.tools, promptCacheKey: promptCacheKey(route, routeConfig), promptCacheRetention: promptCacheRetention(routeConfig), reasoningEffort: options.reasoningEffort, temperature: options.temperature, maxTokens: options.maxTokens, headers, signal: options.signal, timeoutMs: routeConfig.timeoutMs, maxAttempts: routeConfig.maxAttempts, maxResponseBytes: routeConfig.maxResponseBytes })
1466
606
  }
1467
607
 
1468
- class LcxResponsesSearchProvider {
1469
- constructor(ctxOrConfig, maybeConfig, enabled = () => true) {
1470
- this.ctx = maybeConfig === undefined ? undefined : ctxOrConfig
1471
- this.config = maybeConfig ?? ctxOrConfig
1472
- this.enabled = typeof enabled === 'function' ? enabled : () => true
1473
- this.id = this.config.webSearchProvider
1474
- }
1475
-
1476
- available() {
1477
- const routeConfig = configuredResponsesRoute(this.ctx, this.config)
1478
- return this.enabled() && Boolean(routeConfig) && URL.canParse(`${routeConfig.baseURL}/responses`)
1479
- }
1480
-
1481
- async search(request, signal) {
1482
- abortIfNeeded(signal)
1483
- const routeConfig = configuredResponsesRoute(this.ctx, this.config)
1484
- if (!routeConfig) throw webError('Hosted Web Search requires a configured DSH GPT openai-responses model route', 'LCX_WEB_ROUTE_UNAVAILABLE')
1485
- const result = await executeHostedSearch(this.ctx, routeConfig, { query: request.query }, signal)
1486
- const maxResults = Number.isInteger(request.maxResults) && request.maxResults > 0 ? request.maxResults : routeConfig.webMaxResults
1487
- return {
1488
- content: `【Responses Hosted 搜索 · LCX】${result.content ? `\n${result.content}` : ''}`,
1489
- sources: result.sources.slice(0, maxResults),
1490
- truncated: result.sources.length > maxResults || result.truncated,
608
+ function installInjected(ctx, configInput = {}) {
609
+ const baseConfig = normalizeConfig(configInput)
610
+ let runtimeConfig = baseConfig
611
+ const state = { enabled: false, webSearch: false, advancedHostedSearch: false, alphaSearch: false, remoteCompaction: false, fallbackToBasicCompaction: true, autoCompaction: true, webSearchTimeoutSeconds: baseConfig.webSearchTimeoutMs / 1000, autoCompactionThresholdPercent: baseConfig.autoCompactionThresholdPercent, emergencyPruneThresholdPercent: baseConfig.emergencyPruneThresholdPercent }
612
+ const originalSearchProvider = readWebSearchProvider(ctx)
613
+ let warnedWebSelection = false
614
+ const provider = new LcxResponsesSearchProvider(ctx, () => runtimeConfig, () => state.enabled && state.webSearch)
615
+ ctx.web.registerSearchProvider(provider)
616
+ const tools = ctx?.get?.('tools') ?? ctx?.tools
617
+ let disposeAdvanced, disposeAlpha
618
+ const capabilityStore = new AlphaCapabilityStore(baseConfig.alphaCapabilityPath)
619
+ const refStore = new AlphaRefStore(baseConfig.alphaRefPath)
620
+ const compactionPatchRecords = new Map()
621
+ const patchedWebSearchDefinitions = new Map()
622
+ const requestHeaders = new Map()
623
+ const seedRequestHeader = (session) => updateRequestHeaderCache(requestHeaders, session, { type: 'compaction/start' })
624
+ for (const session of sessionsService(ctx)?.list?.() ?? []) seedRequestHeader(session)
625
+ ctx.on('session/created', (session) => {
626
+ seedRequestHeader(session)
627
+ }, { global: true })
628
+ ctx.on('session/disposed', (session) => {
629
+ requestHeaders.delete(String(session?.id ?? ''))
630
+ }, { global: true })
631
+ ctx.on('session/event', (session, event) => {
632
+ updateRequestHeaderCache(requestHeaders, session, event)
633
+ }, { global: true })
634
+
635
+ const refreshTools = () => {
636
+ if (state.enabled && state.webSearch && state.advancedHostedSearch && !disposeAdvanced && tools?.register) disposeAdvanced = tools.register(createAdvancedHostedTool(ctx, state, () => runtimeConfig))
637
+ if ((!state.enabled || !state.webSearch || !state.advancedHostedSearch) && disposeAdvanced) { disposeAdvanced(); disposeAdvanced = undefined }
638
+ let alphaUsable = false
639
+ if (state.enabled && state.alphaSearch) {
640
+ try { const { record } = alphaCapabilityFor(runtimeConfig, capabilityStore); alphaUsable = alphaCapabilityUsable(record) && record?.schemaFingerprint === ALPHA_SCHEMA_FINGERPRINT } catch (error) { ctx.logger?.warn?.(`[lcx-codex] Alpha capability store unavailable: ${error?.message ?? error}`) }
641
+ }
642
+ if (alphaUsable && !disposeAlpha && tools?.register) disposeAlpha = tools.register(createAlphaTool(ctx, state, () => runtimeConfig, capabilityStore, refStore))
643
+ if (!alphaUsable && disposeAlpha) { disposeAlpha(); disposeAlpha = undefined }
644
+ if (ctx.web) {
645
+ const target = state.enabled && state.webSearch ? runtimeConfig.webSearchProvider : originalSearchProvider
646
+ const selected = writeWebSearchProvider(ctx, target)
647
+ if (!selected && state.enabled && state.webSearch && !warnedWebSelection) {
648
+ warnedWebSelection = true
649
+ ctx.logger?.warn?.('[lcx-codex] DSH web provider selection could not be changed at runtime; websearch_gpt_advanced still works, but DSH web_search may remain on its configured provider. Pin web.searchProvider=lcx-responses in a profile overlay if this DSH version removes the runtime compatibility field.')
650
+ }
1491
651
  }
1492
652
  }
1493
- }
1494
-
1495
- function createWebSearchGptTool(ctx, state, getConfig) {
1496
- return {
1497
- name: WEB_SEARCH_TOOL_NAME,
1498
- description: '通过已配置 GPT Responses 路由执行的一次性 Hosted Web Search,调用该工具的主模型不限。支持 query、域名过滤、用户位置、搜索上下文、外部 Web 访问、返回预算和图片搜索控制,并返回来源、引用和检索时间;不提供基于当前会话 ref_id 的 open、find、click 或 screenshot 页面操作。',
1499
- parameters: HOSTED_SEARCH_PARAMETERS,
1500
- output: {
1501
- schema: HOSTED_SEARCH_OUTPUT,
1502
- render: (_args, value) => renderHostedSearchResult(value),
1503
- presentationMeta: (_args, value) => ({
1504
- mode: value.mode,
1505
- action: value.action,
1506
- emulation: value.emulation,
1507
- answer: value.content,
1508
- sources: value.sources,
1509
- citations: value.citations,
1510
- images: value.images,
1511
- warnings: value.warnings,
1512
- ...(value.outputBlocks ? { outputBlocks: value.outputBlocks } : {}),
1513
- ...(value.domains ? { domains: value.domains } : {}),
1514
- ...(value.lineRange ? { lineRange: value.lineRange } : {}),
1515
- ...(value.requestId ? { requestId: value.requestId } : {}),
1516
- ...(value.responseId ? { responseId: value.responseId } : {}),
1517
- retrievedAt: value.retrievedAt,
1518
- truncated: value.truncated,
1519
- }),
1520
- },
1521
- presentCall: (args) => ({
1522
- card: 'generic',
1523
- title: `Responses Hosted 搜索:${typeof args?.query === 'string' ? args.query : ''}`,
1524
- kind: 'search',
1525
- rawInput: JSON.stringify(args ?? {}),
1526
- }),
1527
- async execute(args, exec) {
1528
- if (!state.enabled || !state.webSearch) throw new Error(`${WEB_SEARCH_TOOL_NAME} 未启用,请先在设置中开启 Hosted Web Search`)
1529
- const config = getConfig()
1530
- const routeConfig = searchRouteConfig(ctx, config, exec?.agent)
1531
- if (!routeConfig) throw webError('Hosted Web Search requires a configured DSH GPT openai-responses route', 'LCX_WEB_ROUTE_UNAVAILABLE')
1532
- return executeHostedSearch(ctx, routeConfig, args, exec?.signal)
1533
- },
1534
- }
1535
- }
1536
653
 
1537
- function createWebSearchAlphaTool(ctx, getConfig, capabilityStore, refStore) {
1538
- return {
1539
- name: ALPHA_SEARCH_TOOL_NAME,
1540
- description: '通过已验证 GPT Responses 路由执行的独立、有状态 Alpha Web Search command 工具,调用该工具的主模型不限。一次只执行一个 search_query、image_query、open、find、click、screenshot、finance、weather、sports 或 time action;open、find、click 和 screenshot 使用当前会话已返回的 ref_id。仅在匹配的 capability probe 已验证时注册。',
1541
- parameters: ALPHA_SEARCH_PARAMETERS,
1542
- output: {
1543
- schema: ALPHA_SEARCH_OUTPUT,
1544
- render: (_args, value) => renderAlphaSearchResult(value),
1545
- presentationMeta: (_args, value) => ({
1546
- mode: value.mode,
1547
- action: value.action,
1548
- capability: value.capability,
1549
- emulation: value.emulation,
1550
- answer: value.content,
1551
- sources: value.sources,
1552
- citations: value.citations,
1553
- refs: value.refs,
1554
- links: value.links,
1555
- pdfRefs: value.pdfRefs,
1556
- ...(value.requestId ? { requestId: value.requestId } : {}),
1557
- ...(value.responseId ? { responseId: value.responseId } : {}),
1558
- retrievedAt: value.retrievedAt,
1559
- warnings: value.warnings,
1560
- }),
1561
- },
1562
- presentCall: (args) => ({
1563
- card: 'generic',
1564
- title: `Alpha Web Search:${typeof args?.action === 'string' ? args.action : ''}`,
1565
- kind: 'search',
1566
- rawInput: JSON.stringify(args ?? {}),
1567
- }),
1568
- execute(args, exec) {
1569
- return executeAlphaSearch(ctx, getConfig(), capabilityStore, refStore, args, exec)
654
+ let source = () => ({ ...baseConfig, ...state })
655
+ installSettingsSection(ctx, SETTINGS_NS, SettingsSchema, {
656
+ enabled: false, webSearch: false, advancedHostedSearch: false, alphaSearch: false, remoteCompaction: false, fallbackToBasicCompaction: true, autoCompaction: true,
657
+ webSearchTimeoutSeconds: baseConfig.webSearchTimeoutMs / 1000,
658
+ autoCompactionThresholdPercent: baseConfig.autoCompactionThresholdPercent,
659
+ emergencyPruneThresholdPercent: baseConfig.emergencyPruneThresholdPercent,
660
+ provider: baseConfig.provider, baseURL: baseConfig.baseURL, apiKeyEnv: baseConfig.apiKeyEnv, model: baseConfig.model,
661
+ }, {
662
+ setSource: (current) => { source = current },
663
+ onChange: () => {
664
+ const value = source()
665
+ state.enabled = Boolean(value.enabled); state.webSearch = Boolean(value.webSearch); state.advancedHostedSearch = Boolean(value.advancedHostedSearch); state.alphaSearch = Boolean(value.alphaSearch); state.remoteCompaction = Boolean(value.remoteCompaction)
666
+ state.fallbackToBasicCompaction = value.fallbackToBasicCompaction !== false; state.autoCompaction = value.autoCompaction !== false
667
+ state.webSearchTimeoutSeconds = Number.isFinite(value.webSearchTimeoutSeconds) ? value.webSearchTimeoutSeconds : baseConfig.webSearchTimeoutMs / 1000
668
+ state.autoCompactionThresholdPercent = clampPercent(value.autoCompactionThresholdPercent, baseConfig.autoCompactionThresholdPercent, 85, 95)
669
+ state.emergencyPruneThresholdPercent = clampPercent(value.emergencyPruneThresholdPercent, baseConfig.emergencyPruneThresholdPercent, 90, 99)
670
+ runtimeConfig = normalizeConfig({ ...baseConfig, provider: value.provider ?? baseConfig.provider, baseURL: value.baseURL ?? baseConfig.baseURL, apiKeyEnv: value.apiKeyEnv ?? baseConfig.apiKeyEnv, model: value.model ?? baseConfig.model })
671
+ refreshTools(); refreshPatchedWebSearchTimeouts(state, () => runtimeConfig, patchedWebSearchDefinitions)
1570
672
  },
1571
- }
1572
- }
1573
-
1574
- function apply(ctx, rawConfig) {
1575
- const config = normalizeConfig(rawConfig)
1576
- const portableStore = new CheckpointV3Store(config.portableCheckpointPath)
1577
- const originalSearchProvider = ctx.web.searchProviderId
1578
- const state = {
1579
- enabled: false,
1580
- webSearch: false,
1581
- alphaSearch: false,
1582
- remoteCompaction: false,
1583
- fallbackToBasicCompaction: true,
1584
- }
1585
- let runtimeConfig = config
1586
- const provider = new LcxResponsesSearchProvider(ctx, runtimeConfig, () => state.enabled && state.webSearch)
1587
- ctx.web.registerSearchProvider(provider)
1588
- const tools = ctx.get?.('tools') ?? ctx.tools
1589
- const systemPrompt = ctx.get?.('systemPrompt') ?? ctx.systemPrompt
1590
- const sessionTracker = createSessionGenerationTracker(ctx)
1591
- let disposeHostedTool
1592
- let disposeAlphaTool
1593
- let disposeHostedPrompt
1594
- let disposeAlphaPrompt
1595
- let alphaCapabilityStore
1596
- let alphaRefStore
1597
- let lastAlphaStoreErrorCode
673
+ })
674
+ try {
675
+ const value = source()
676
+ Object.assign(state, { enabled: Boolean(value.enabled), webSearch: Boolean(value.webSearch), advancedHostedSearch: Boolean(value.advancedHostedSearch), alphaSearch: Boolean(value.alphaSearch), remoteCompaction: Boolean(value.remoteCompaction), fallbackToBasicCompaction: value.fallbackToBasicCompaction !== false, autoCompaction: value.autoCompaction !== false, webSearchTimeoutSeconds: Number.isFinite(value.webSearchTimeoutSeconds) ? value.webSearchTimeoutSeconds : baseConfig.webSearchTimeoutMs / 1000, autoCompactionThresholdPercent: clampPercent(value.autoCompactionThresholdPercent, baseConfig.autoCompactionThresholdPercent, 85, 95), emergencyPruneThresholdPercent: clampPercent(value.emergencyPruneThresholdPercent, baseConfig.emergencyPruneThresholdPercent, 90, 99) })
677
+ runtimeConfig = normalizeConfig({ ...baseConfig, provider: value.provider ?? baseConfig.provider, baseURL: value.baseURL ?? baseConfig.baseURL, apiKeyEnv: value.apiKeyEnv ?? baseConfig.apiKeyEnv, model: value.model ?? baseConfig.model })
678
+ } catch {}
679
+ refreshTools()
680
+ ctx.inject(['compaction'], compactionCtx => {
681
+ patchCompactionPressureService(resolveContextService(compactionCtx, 'compaction'), state, () => runtimeConfig, ctx, compactionPatchRecords, requestHeaders)
682
+ })
1598
683
 
1599
- const alphaStores = () => {
1600
- try {
1601
- alphaCapabilityStore ??= new AlphaCapabilityStore(config.alphaCapabilityPath)
1602
- alphaRefStore ??= new AlphaRefStore(config.alphaRefPath)
1603
- return { capability: alphaCapabilityStore, refs: alphaRefStore }
1604
- } catch (error) {
1605
- const code = String(error?.code ?? 'LCX_ALPHA_STORE_UNAVAILABLE')
1606
- if (code !== lastAlphaStoreErrorCode) ctx.logger?.error?.(`[lcx-codex] alpha-store unavailable code=${code}`)
1607
- lastAlphaStoreErrorCode = code
1608
- alphaCapabilityStore = undefined
1609
- alphaRefStore = undefined
1610
- return undefined
1611
- }
1612
- }
684
+ ctx.on('tools/execute', async (exec, next) => {
685
+ if (!state.enabled || !state.webSearch || exec?.name !== 'web_search') return next()
686
+ const active = activeAgentRoute(exec, runtimeConfig)
687
+ return hostedSearchRouteContext.run(active, () => next())
688
+ })
1613
689
 
1614
- const syncToolRegistration = () => {
1615
- const hostedEnabled = Boolean(state.enabled && state.webSearch)
1616
- const configuredRoute = configuredResponsesRoute(ctx, runtimeConfig)
1617
- const fingerprint = configuredRoute ? currentAlphaCapabilityFingerprint(configuredRoute) : undefined
1618
- const stores = state.enabled && state.alphaSearch ? alphaStores() : undefined
1619
- let alphaCapability
1620
- try {
1621
- alphaCapability = fingerprint ? stores?.capability.get(fingerprint) : undefined
1622
- if (stores) lastAlphaStoreErrorCode = undefined
1623
- } catch (error) {
1624
- const code = String(error?.code ?? 'LCX_ALPHA_STORE_UNAVAILABLE')
1625
- if (code !== lastAlphaStoreErrorCode) ctx.logger?.error?.(`[lcx-codex] alpha-store unavailable code=${code}`)
1626
- lastAlphaStoreErrorCode = code
1627
- }
1628
- const alphaEnabled = Boolean(stores && alphaCapabilityUsable(alphaCapability) && alphaCapability?.schemaFingerprint === ALPHA_SCHEMA_FINGERPRINT)
1629
- if (hostedEnabled && !disposeHostedTool && tools?.register) disposeHostedTool = tools.register(createWebSearchGptTool(ctx, state, () => runtimeConfig))
1630
- if (!hostedEnabled && disposeHostedTool) {
1631
- disposeHostedTool()
1632
- disposeHostedTool = undefined
1633
- }
1634
- if (alphaEnabled && !disposeAlphaTool && tools?.register) disposeAlphaTool = tools.register(createWebSearchAlphaTool(ctx, () => runtimeConfig, stores.capability, stores.refs))
1635
- if (!alphaEnabled && disposeAlphaTool) {
1636
- disposeAlphaTool()
1637
- disposeAlphaTool = undefined
1638
- }
1639
- if (hostedEnabled && !disposeHostedPrompt && systemPrompt?.section) {
1640
- disposeHostedPrompt = systemPrompt.section({
1641
- name: 'tool:websearch_gpt',
1642
- order: 111,
1643
- text: '使用 websearch_gpt 查询当前或需要来源的问题。该工具走 Responses hosted web_search;最终回答必须引用直接 URL,并区分来源发布时间与检索时间。',
1644
- }) ?? (() => {})
1645
- }
1646
- if (!hostedEnabled && disposeHostedPrompt) {
1647
- disposeHostedPrompt()
1648
- disposeHostedPrompt = undefined
1649
- }
1650
- if (alphaEnabled && !disposeAlphaPrompt && systemPrompt?.section) {
1651
- disposeAlphaPrompt = systemPrompt.section({
1652
- name: 'tool:websearch_alpha',
1653
- order: 112,
1654
- text: 'websearch_alpha 是独立 Alpha command API。一次只调用一个 action;只能对当前 session 已返回的 ref_id 执行 open/find/click/screenshot,最终回答引用直接 URL,不暴露内部 ref_id。',
1655
- }) ?? (() => {})
1656
- }
1657
- if (!alphaEnabled && disposeAlphaPrompt) {
1658
- disposeAlphaPrompt()
1659
- disposeAlphaPrompt = undefined
1660
- }
1661
- }
690
+ ctx.on('agent/created', ({ agent }) => {
691
+ if (!agent) return
692
+ const installed = patchCompactionPressureForAgent(agent, state, () => runtimeConfig, ctx, compactionPatchRecords, requestHeaders)
693
+ if (installed) ctx.logger?.info?.('[lcx-codex] pressure coordination installed through AgentPresets service resolver')
694
+ }, { global: true })
1662
695
 
1663
- const sync = (next) => {
1664
- Object.assign(state, next ?? {})
1665
- runtimeConfig = normalizeConfig({ ...config, ...next })
1666
- provider.config = runtimeConfig
1667
- ctx.web.searchProviderId = state.enabled && state.webSearch ? runtimeConfig.webSearchProvider : originalSearchProvider
1668
- syncToolRegistration()
1669
- }
1670
- const settingsBase = {
1671
- enabled: false,
1672
- webSearch: false,
1673
- alphaSearch: false,
1674
- remoteCompaction: false,
1675
- fallbackToBasicCompaction: true,
1676
- provider: config.provider,
1677
- baseURL: config.baseURL,
1678
- apiKeyEnv: config.apiKeyEnv,
1679
- model: config.model,
1680
- compactTransport: config.compactTransport,
1681
- }
1682
- const attachSettings = (settingsContext) => {
1683
- const settings = settingsContext.settings ?? settingsContext
1684
- if (!settings?.register) return
1685
- const scope = settings.register(settingsNamespace(SETTINGS_NAMESPACE), SettingsSchema, { base: settingsBase })
1686
- sync(scope.get())
1687
- scope.watch(() => sync(scope.get()))
1688
- }
1689
- if (typeof ctx.inject === 'function') ctx.inject(['settings'], attachSettings)
1690
- else attachSettings(ctx.get?.('settings') ?? {})
696
+ ctx.on('agent/status', ({ agent, status }) => {
697
+ if (status !== 'running' || !agent) return
698
+ const installed = patchCompactionPressureForAgent(agent, state, () => runtimeConfig, ctx, compactionPatchRecords, requestHeaders)
699
+ if (installed) ctx.logger?.info?.('[lcx-codex] pressure coordination installed through AgentPresets service resolver')
700
+ patchVisibleWebSearchTimeout(agent, state, () => runtimeConfig, patchedWebSearchDefinitions)
701
+ }, { global: true })
1691
702
 
1692
- ctx.effect?.(() => () => {
1693
- disposeHostedTool?.()
1694
- disposeAlphaTool?.()
1695
- disposeHostedPrompt?.()
1696
- disposeAlphaPrompt?.()
1697
- sessionTracker.dispose()
1698
- ctx.web.searchProviderId = originalSearchProvider
1699
- }, 'lcx-codex: restore web provider selection')
1700
703
  ctx.on('llm/stream', (options, next) => {
1701
- if (checkpointReplayOptions.delete(options)) return next()
1702
- if (!state.enabled || !state.remoteCompaction) return next()
1703
- if (options.purpose === 'session-title') return next()
704
+ if (bypassReplayOptions.delete(options)) return next()
705
+ if (!state.enabled || !state.remoteCompaction || options.purpose === 'session-title') return next()
1704
706
  const routeConfig = resolveResponsesRouteConfig(ctx, options, runtimeConfig)
1705
- let portableCheckpoint
1706
- try {
1707
- portableCheckpoint = hasPortableCheckpoint(options.messages)
1708
- } catch (error) {
1709
- return guardedStream(() => { throw error }, options.signal)
1710
- }
1711
- if (portableCheckpoint) {
1712
- if (!routeConfig) return guardedStream(() => { throw nativeRouteError(options) }, options.signal)
1713
- if (options.purpose === 'compaction') {
1714
- const factory = () => remoteCompactionStream(options, routeConfig, portableStore, ctx, sessionTracker)
1715
- return guardedStream(factory, options.signal)
1716
- }
1717
- let replay
1718
- try {
1719
- replay = checkpointReplayRecord(options.messages, portableStore)
1720
- } catch (error) {
1721
- return guardedStream(() => { throw error }, options.signal)
1722
- }
1723
- const route = routeWithSessionAncestry(ctx, currentRoute(options, routeConfig))
1724
- if (options.sessionId && replay?.record.routeFingerprint === routeFingerprint(route)) {
1725
- return guardedStream(
1726
- () => nativeCheckpointReplayStream(options, routeConfig, portableStore, ctx, replay.record, replay.marker),
1727
- options.signal,
1728
- )
1729
- }
1730
- const llm = ctx?.llm ?? ctx?.get?.('llm')
1731
- if (!llm || typeof llm.stream !== 'function') {
1732
- return guardedStream(() => { throw checkpointReplayUnavailableError() }, options.signal)
1733
- }
1734
- return guardedStream(async function* () {
1735
- const markerTail = replay?.marker ? options.messages.slice(replay.marker.index + 1) : []
1736
- const needsImageCapability = Number(replay?.record.portableImageCount ?? 0) > 0 || dshMessagesContainImage(markerTail)
1737
- const imageSupport = needsImageCapability
1738
- ? await resolveModelImageSupport(llm, route, options.signal)
1739
- : 'unknown'
1740
- const rewrittenOptions = {
1741
- ...options,
1742
- messages: buildPortableReplayMessages(options.messages, portableStore, route, { imageSupport }),
1743
- }
1744
- checkpointReplayOptions.add(rewrittenOptions)
1745
- const stream = await llm.stream(rewrittenOptions)
1746
- for await (const chunk of stream) yield chunk
1747
- }, options.signal)
1748
- }
1749
707
  if (options.purpose === 'compaction') {
1750
708
  if (!routeConfig) return next()
1751
- const factory = () => remoteCompactionStream(options, routeConfig, portableStore, ctx, sessionTracker)
1752
- const diagnostic = (error, phase) => logCompactionDiagnostic(ctx, options, routeConfig, error, phase)
1753
- return state.fallbackToBasicCompaction
1754
- ? guardedStream(
1755
- () => parallelCompactionStream(options, routeConfig, portableStore, ctx, sessionTracker, next, diagnostic),
1756
- options.signal,
1757
- )
1758
- : guardedStream(factory, options.signal)
709
+ return remoteCompactionStream(options, routeConfig, state, ctx, next, requestHeaders)
710
+ }
711
+ const session = sessionFor(ctx, String(options.sessionId ?? ''))
712
+ const hasNative = messagesContainNativeCheckpoint(options.messages, session)
713
+ const hasLegacy = messagesContainLegacyCheckpoint(options.messages)
714
+ if (!hasNative && !hasLegacy) return next()
715
+ if (!routeConfig) {
716
+ if (hasNative && session) return recursiveLlmStream(ctx, options, rewriteCheckpointsPortable(options.messages, session, { maxChars: runtimeConfig.portableReplayMaxChars }))
717
+ return next()
1759
718
  }
1760
- return next()
1761
- }, { global: true, prepend: true })
719
+ return replayCheckpointStream(options, routeConfig, ctx)
720
+ })
721
+
722
+ ctx.effect?.(() => async () => {
723
+ disposeAdvanced?.(); disposeAlpha?.()
724
+ await restoreCompactionPressure(compactionPatchRecords)
725
+ restoreWebSearchTimeouts(patchedWebSearchDefinitions)
726
+ writeWebSearchProvider(ctx, originalSearchProvider)
727
+ requestHeaders.clear()
728
+ }, 'lcx-codex cleanup')
1762
729
  }
1763
730
 
1764
- export {
1765
- Config,
1766
- SettingsSchema,
1767
- CheckpointV3Store,
1768
- LcxResponsesSearchProvider,
1769
- apply,
1770
- baseURLFingerprint,
1771
- buildPortableHistory,
1772
- buildPortableReplayMessages,
1773
- buildPortableResponsesInput,
1774
- hasPortableCheckpoint,
1775
- inject,
1776
- name,
1777
- normalizeConfig,
1778
- normalizeCompactionResponse,
1779
- normalizeHostedSearchArgs,
1780
- normalizeAlphaSearchArgs,
1781
- portableResponsesToMessages,
1782
- routeFingerprint,
731
+ export function apply(ctx, configInput = {}) {
732
+ return ctx.inject(['llm', 'web', 'sessions'], injectedCtx => installInjected(injectedCtx, configInput))
1783
733
  }
734
+
735
+ apply.inject = inject
736
+ apply.Config = Config
737
+
738
+ export default apply