dsh-lcx-codex 0.4.1 → 0.4.2

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.
@@ -98,6 +98,7 @@ export function readDshPiReplayState(value) {
98
98
  if (!['text', 'reasoning', 'tool-call'].includes(block.type)) throw invalidReplay(`block ${index} has an unknown type`)
99
99
  for (const signature of ['textSignature', 'thinkingSignature', 'thoughtSignature']) if (block[signature] !== undefined && typeof block[signature] !== 'string') throw invalidReplay(`block ${index} ${signature} must be a string`)
100
100
  if (block.redacted !== undefined && typeof block.redacted !== 'boolean') throw invalidReplay(`block ${index} redacted must be boolean`)
101
+ if (block.namespace !== undefined && typeof block.namespace !== 'string') throw invalidReplay(`block ${index} namespace must be a string`)
101
102
  }
102
103
  return { response, blocks: value.blocks }
103
104
  }
@@ -125,7 +126,7 @@ function replayedAssistant(message, source) {
125
126
  if (replayBlockType(block?.type) !== replay?.type) throw invalidReplay(`block ${index} does not match assistant content`)
126
127
  if (block.type === 'text') return { type: 'text', text: String(block.text ?? ''), ...(replay.textSignature === undefined ? {} : { textSignature: replay.textSignature }) }
127
128
  if (block.type === 'reasoning') return { type: 'thinking', thinking: String(block.text ?? ''), ...(replay.thinkingSignature === undefined ? {} : { thinkingSignature: replay.thinkingSignature }), ...(replay.redacted === undefined ? {} : { redacted: replay.redacted }) }
128
- return { type: 'toolCall', id: String(block.id), name: String(block.name), arguments: parseArguments(block.arguments), ...(replay.thoughtSignature === undefined ? {} : { thoughtSignature: replay.thoughtSignature }) }
129
+ return { type: 'toolCall', id: String(block.id), name: String(block.name), arguments: parseArguments(block.arguments), ...(replay.thoughtSignature === undefined ? {} : { thoughtSignature: replay.thoughtSignature }), ...(replay.namespace === undefined ? {} : { namespace: replay.namespace }) }
129
130
  })
130
131
  return { role: 'assistant', content, api: state.response.api, provider: state.response.provider, model: state.response.model, ...(state.response.responseModel === undefined ? {} : { responseModel: state.response.responseModel }), ...(state.response.responseId === undefined ? {} : { responseId: state.response.responseId }), usage: emptyUsage(), stopReason: state.response.stopReason, timestamp: 0 }
131
132
  }
@@ -173,7 +174,7 @@ function builtinResponsesModel(provider, modelId) {
173
174
  catch { return undefined }
174
175
  }
175
176
 
176
- function piModel(options) {
177
+ export function resolvePiResponsesModel(options) {
177
178
  const route = options.route ?? {}
178
179
  const explicit = options.model && typeof options.model === 'object' ? options.model : undefined
179
180
  const provider = String(explicit?.provider ?? route.provider ?? 'dsh-lcx-codex')
@@ -217,19 +218,25 @@ export async function serializeDshMessages(messages, ctx, options = {}) {
217
218
  requestImageMaxBytes: Number.isSafeInteger(options.requestImageMaxBytes) && options.requestImageMaxBytes > 0 ? options.requestImageMaxBytes : DEFAULT_REQUEST_IMAGE_MAX_BYTES,
218
219
  }
219
220
  const projected = offloadRequestImagesWithPolicy(messages ?? [], { representation: 'base64', maxBytes: normalized.maxRequestImageBytes, byteQuantum: 1, byteLength: ref => Math.min(ref.bytes, normalized.requestImageMaxBytes) })
220
- const model = piModel(normalized)
221
+ const model = resolvePiResponsesModel(normalized)
221
222
  const context = { systemPrompt: normalized.systemPrompt, messages: await dshToPiMessages(projected, ctx, normalized, imageMap), tools: options.tools ?? [] }
222
223
  const supportsStrictMode = model.compat?.supportsStrictMode ?? false
223
224
  const supportsOpenAIGrammarTools = model.compat?.supportsOpenAIGrammarTools ?? false
225
+ const supportsAdditionalTools = model.compat?.supportsAdditionalTools ?? false
224
226
  const supportsToolSearch = model.compat?.supportsToolSearch ?? false
227
+ const deferredToolsMode = supportsAdditionalTools ? 'additional-tools' : supportsToolSearch ? 'tool-search' : undefined
225
228
  const grammarToolInputProperties = createGrammarToolInputProperties(context.tools, supportsOpenAIGrammarTools)
226
- const placement = splitDeferredTools(context, supportsToolSearch)
229
+ const placement = splitDeferredTools(context, deferredToolsMode !== undefined)
227
230
  const toolOptions = { supportsStrictMode, supportsOpenAIGrammarTools }
228
231
  const input = convertResponsesMessages(model, context, new Set(['openai', 'openai-codex', 'opencode']), {
229
- includeSystemPrompt: normalized.includeSystemPrompt, grammarToolInputProperties, deferredTools: placement.deferred, toolOptions,
232
+ includeSystemPrompt: normalized.includeSystemPrompt,
233
+ grammarToolInputProperties,
234
+ deferredTools: placement.deferred,
235
+ deferredToolsMode,
236
+ toolOptions,
230
237
  })
231
238
  const tools = options.tools === undefined ? undefined : convertResponsesTools(placement.immediate, toolOptions)
232
- return { input, imageMap, tools }
239
+ return { input, imageMap, tools, model, grammarToolInputProperties, deferredToolsMode }
233
240
  }
234
241
 
235
242
  export function responsesTools(tools) {
package/lib/index.js CHANGED
@@ -49,8 +49,9 @@ import {
49
49
  resolveModelImageSupport,
50
50
  serializeDshMessages,
51
51
  } from './dsh-responses.js'
52
- import { requestNativeCompaction } from './compact-v2.js'
53
- import { requestNativeReplay } from './responses-replay.js'
52
+ import { mergeFeatureHeader, requestNativeCompaction } from './compact-v2.js'
53
+ import { buildResponsesBody } from './responses-request.js'
54
+ import { managedFailureChunk, streamResponsesRequest } from './responses-stream.js'
54
55
  import {
55
56
  checkpointStateForMessage,
56
57
  compactCheckpointId,
@@ -94,7 +95,6 @@ const SETTINGS_NS = settingsNamespace('lcx-codex')
94
95
  const ADVANCED_HOSTED_TOOL = 'websearch_gpt_advanced'
95
96
  const ALPHA_TOOL = 'websearch_alpha'
96
97
  const COMPACTION_DIRECTIVE = 'You are now acting as a compaction engine'
97
- const bypassReplayOptions = new WeakSet()
98
98
  const hostedSearchRouteContext = new AsyncLocalStorage()
99
99
 
100
100
  function dshHome() { return process.env.DSH_HOME ?? join(homedir(), '.dsh') }
@@ -107,6 +107,7 @@ export const Config = z.object({
107
107
  baseURL: z.string().default('https://api.lcxbot.com/v1'),
108
108
  apiKeyEnv: z.string().default('LCX_API_KEY'),
109
109
  model: z.string().default('gpt-5.6-sol'),
110
+ supportsExplicitPromptCacheMode: z.boolean().default(false),
110
111
  legacyCheckpointPath: z.string().default(''),
111
112
  checkpointPath: z.string().default(''),
112
113
  alphaCapabilityPath: z.string().default(''),
@@ -155,6 +156,7 @@ function normalizeConfig(input = {}) {
155
156
  baseURL: String(input.baseURL || 'https://api.lcxbot.com/v1').replace(/\/+$/u, ''),
156
157
  apiKeyEnv: input.apiKeyEnv || 'LCX_API_KEY',
157
158
  model: input.model || 'gpt-5.6-sol',
159
+ supportsExplicitPromptCacheMode: input.supportsExplicitPromptCacheMode === true,
158
160
  headers: input.headers && typeof input.headers === 'object' ? { ...input.headers } : {},
159
161
  legacyCheckpointPath: legacy,
160
162
  alphaCapabilityPath: input.alphaCapabilityPath || defaultAlphaCapabilityPath(),
@@ -370,15 +372,16 @@ function mergeMap(target, source) { for (const [key, value] of source ?? []) tar
370
372
  async function serializeNativeAware(messages, route, routeConfig, ctx, options = {}) {
371
373
  const session = sessionFor(ctx, route.sessionId)
372
374
  const imageSupport = await resolveModelImageSupport(ctx, route, options.signal)
373
- const input = []; const imageMap = new Map(); let nativeTools
375
+ const input = []; const imageMap = new Map(); let nativeTools; let nativeModel; let grammarToolInputProperties
374
376
  let normal = []
375
377
  const serializeOptions = (imageMapOverride, extra = {}) => requestImageOptions(routeConfig, imageSupport, options.signal, imageMapOverride, { route, tools: options.tools, responsesCompat: routeConfig.responsesCompat, ...extra })
376
378
  const prelude = await serializeDshMessages([], ctx, serializeOptions(undefined, { systemPrompt: options.system, includeSystemPrompt: true }))
377
- input.push(...prelude.input); nativeTools = prelude.tools
379
+ const ephemeralPreludeItemCount = prelude.input.length
380
+ input.push(...prelude.input); nativeTools = prelude.tools; nativeModel = prelude.model; grammarToolInputProperties = prelude.grammarToolInputProperties
378
381
  const flush = async () => {
379
382
  if (!normal.length) return
380
383
  const serialized = await serializeDshMessages(normal, ctx, serializeOptions())
381
- nativeTools = serialized.tools ?? nativeTools
384
+ nativeTools = serialized.tools ?? nativeTools; nativeModel = serialized.model ?? nativeModel; grammarToolInputProperties = serialized.grammarToolInputProperties ?? grammarToolInputProperties
382
385
  input.push(...serialized.input); mergeMap(imageMap, serialized.imageMap); normal = []
383
386
  }
384
387
  for (const message of messages ?? []) {
@@ -439,7 +442,8 @@ async function serializeNativeAware(messages, route, routeConfig, ctx, options =
439
442
  normal.push(message)
440
443
  }
441
444
  await flush()
442
- return { input, imageMap, imageSupport, tools: nativeTools }
445
+ if (!nativeModel) throw Object.assign(new Error('LCX could not resolve the Pi Responses model descriptor'), { code: 'LCX_RESPONSES_MODEL_UNAVAILABLE' })
446
+ return { input, ephemeralPreludeItemCount, imageMap, imageSupport, tools: nativeTools, model: nativeModel, grammarToolInputProperties }
443
447
  }
444
448
 
445
449
  function fallbackEligible(error, signal) {
@@ -461,10 +465,12 @@ async function* remoteCompactionStream(options, routeConfig, state, ctx, next, r
461
465
  const result = await requestNativeCompaction({
462
466
  baseURL: routeConfig.baseURL,
463
467
  model: route.model,
468
+ modelDescriptor: prepared.model,
464
469
  input: prepared.input,
465
470
  tools: prepared.tools ?? options.tools,
466
471
  promptCacheKey: promptCacheKey(route, routeConfig),
467
472
  promptCacheRetention: promptCacheRetention(routeConfig),
473
+ cacheRetention: routeConfig.cacheRetention,
468
474
  reasoningEffort: generation.reasoningEffort,
469
475
  temperature: generation.temperature,
470
476
  maxTokens: generation.maxTokens,
@@ -477,7 +483,7 @@ async function* remoteCompactionStream(options, routeConfig, state, ctx, next, r
477
483
  })
478
484
  const session = sessionFor(ctx, route.sessionId)
479
485
  const block = createNativeCheckpointBlock({
480
- session, route, result, input: prepared.input, imageMap: prepared.imageMap,
486
+ session, route, result, input: prepared.input, ephemeralPreludeItemCount: prepared.ephemeralPreludeItemCount, imageMap: prepared.imageMap,
481
487
  retentionOptions: {
482
488
  tokenBudget: routeConfig.nativeRetentionTokenBudget,
483
489
  assistantTokenReserve: routeConfig.assistantRetentionTokenReserve,
@@ -522,6 +528,11 @@ function pressurePolicy(state, config) {
522
528
  return { auto, emergency: Math.min(99, emergency) }
523
529
  }
524
530
 
531
+ export function compactionPressureBand(totalTokens, contextWindow, policy) {
532
+ const ratioPercent = totalTokens / contextWindow * 100
533
+ return { ratioPercent, band: ratioPercent < policy.auto ? 'below' : ratioPercent < policy.emergency ? 'native' : 'emergency' }
534
+ }
535
+
525
536
  function adjustedCompactionConfig(config, target, thresholdRatio) {
526
537
  if (!config || typeof config !== 'object') return config
527
538
  const modelPolicies = Array.isArray(config.modelPolicies)
@@ -549,7 +560,7 @@ function patchCompactionPressureService(compactionValue, state, getConfig, ctx,
549
560
  return original.call(this, agentArg, trigger, activeSignal)
550
561
  }
551
562
  return record.mutex.run(activeSignal, async () => {
552
- if (trigger !== 'pressure' || !state.enabled || !state.remoteCompaction || !state.autoCompaction) return callOriginal()
563
+ if (trigger !== 'pressure' || !state.enabled || !state.autoCompaction) return callOriginal()
553
564
  const config = getConfig()
554
565
  const target = routedTargetForAgent(agentArg, config)
555
566
  if (!target || !resolveResponsesRouteConfig(ctx, target, config)) return callOriginal()
@@ -561,11 +572,12 @@ function patchCompactionPressureService(compactionValue, state, getConfig, ctx,
561
572
  if (!Number.isFinite(contextWindow) || contextWindow <= 0) return callOriginal()
562
573
  const totalTokens = Number(tokenMeter.measure(agentArg.session)?.totalTokens ?? 0)
563
574
  const { auto, emergency } = pressurePolicy(state, config)
564
- const ratioPercent = totalTokens / contextWindow * 100
565
- if (ratioPercent < auto) return null
575
+ const pressure = compactionPressureBand(totalTokens, contextWindow, { auto, emergency })
576
+ const ratioPercent = pressure.ratioPercent
577
+ if (pressure.band === 'below') return null
566
578
  const prunerState = toolResultPrunerState(resolveAgentService(ctx, agentArg, 'toolResultPruner') ?? resolveContextService(this?.ctx, 'toolResultPruner'))
567
579
  const configState = compactionConfigState(this)
568
- const nativeFirst = ratioPercent < emergency
580
+ const nativeFirst = pressure.band === 'native'
569
581
  const noOpPrune = () => ({ pruned: [], charsRemoved: 0 })
570
582
  const prunerPatch = nativeFirst ? patchToolResultPruner(prunerState, noOpPrune) : undefined
571
583
  const configPatch = patchCompactionConfig(configState, originalConfig => adjustedCompactionConfig(originalConfig, target, auto / 100))
@@ -604,29 +616,51 @@ function visibleWebSearchTimeout(state, getConfig) {
604
616
  function messagesContainNativeCheckpoint(messages, session) { return Boolean(session && (messages ?? []).some((message) => checkpointStateForMessage(session, message))) }
605
617
  function messagesContainLegacyCheckpoint(messages) { return (messages ?? []).some((message) => Boolean(legacyCheckpointId(message))) }
606
618
 
607
- async function* recursiveLlmStream(ctx, options, messages) {
608
- const llm = contextService(ctx, 'llm')
609
- if (!llm?.stream) throw Object.assign(new Error('LCX portable replay requires ctx.llm.stream'), { code: 'LCX_CHECKPOINT_REPLAY_UNAVAILABLE' })
610
- const rewritten = { ...options, messages }
611
- bypassReplayOptions.add(rewritten)
612
- const stream = await llm.stream(rewritten)
613
- for await (const chunk of stream) yield chunk
619
+ function inputHasNativeState(input) {
620
+ return (input ?? []).some((item) => item?.type === 'compaction')
614
621
  }
615
622
 
616
- async function* replayCheckpointStream(options, routeConfig, ctx) {
623
+ async function* managedResponsesStream(options, routeConfig, ctx) {
617
624
  const route = currentRoute(options, routeConfig)
618
- const session = sessionFor(ctx, route.sessionId)
619
- if (!session) throw Object.assign(new Error('Native checkpoint replay requires the live DSH session'), { code: 'LCX_SESSION_UNAVAILABLE' })
620
- const incompatibleV4 = (options.messages ?? []).some((message) => { const state = checkpointStateForMessage(session, message); return state && !stateRouteCompatible(state, route, ctx) })
621
- if (incompatibleV4) {
622
- const portable = rewriteCheckpointsPortable(options.messages, session, { maxChars: routeConfig.portableReplayMaxChars })
623
- yield* recursiveLlmStream(ctx, options, portable)
624
- return
625
+ try {
626
+ if (options.stop !== undefined) throw Object.assign(new Error('LCX Responses does not support GenerateOptions.stop'), { code: 'LCX_RESPONSES_UNSUPPORTED_OPTION' })
627
+ const prepared = await serializeNativeAware(options.messages, route, routeConfig, ctx, { signal: options.signal, system: options.system, tools: options.tools })
628
+ const cacheSessionId = promptCacheSessionId(route, routeConfig)
629
+ const headers = await authenticatedHeaders(ctx, routeConfig, cacheSessionId, cacheSessionId === undefined ? null : undefined)
630
+ const body = buildResponsesBody({
631
+ model: prepared.model,
632
+ input: prepared.input,
633
+ tools: prepared.tools ?? options.tools,
634
+ sessionId: cacheSessionId,
635
+ promptCacheKey: promptCacheKey(route, routeConfig),
636
+ promptCacheRetention: promptCacheRetention(routeConfig),
637
+ cacheRetention: routeConfig.cacheRetention,
638
+ reasoningEffort: options.reasoningEffort,
639
+ temperature: options.temperature,
640
+ maxTokens: options.maxTokens,
641
+ })
642
+ const nativeReplay = inputHasNativeState(prepared.input)
643
+ if (nativeReplay) { body.tool_choice = 'auto'; body.parallel_tool_calls = true }
644
+ yield* streamResponsesRequest({
645
+ baseURL: routeConfig.baseURL,
646
+ provider: route.provider,
647
+ model: route.model,
648
+ piModel: prepared.model,
649
+ body,
650
+ grammarToolInputProperties: prepared.grammarToolInputProperties,
651
+ headers: nativeReplay ? mergeFeatureHeader(headers) : headers,
652
+ signal: options.signal,
653
+ timeoutMs: routeConfig.timeoutMs,
654
+ maxAttempts: 1,
655
+ maxResponseBytes: routeConfig.maxResponseBytes,
656
+ })
657
+ } catch (error) {
658
+ yield managedFailureChunk(error, options.signal)
625
659
  }
626
- const prepared = await serializeNativeAware(options.messages, route, routeConfig, ctx, { signal: options.signal, system: options.system, tools: options.tools })
627
- const cacheSessionId = promptCacheSessionId(route, routeConfig)
628
- const headers = await authenticatedHeaders(ctx, routeConfig, cacheSessionId, cacheSessionId === undefined ? null : undefined)
629
- 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 })
660
+ }
661
+
662
+ async function* unavailableManagedRouteStream(options) {
663
+ yield managedFailureChunk(Object.assign(new Error('LCX is enabled but the selected DSH route cannot be resolved as an authenticated OpenAI Responses wire route'), { code: 'LCX_RESPONSES_ROUTE_UNAVAILABLE' }), options.signal)
630
664
  }
631
665
 
632
666
  function installInjected(ctx, configInput = {}) {
@@ -733,22 +767,14 @@ function installInjected(ctx, configInput = {}) {
733
767
  }, { global: true })
734
768
 
735
769
  ctx.on('llm/stream', (options, next) => {
736
- if (bypassReplayOptions.delete(options)) return next()
737
- if (!state.enabled || !state.remoteCompaction || options.purpose === 'session-title') return next()
770
+ if (!state.enabled || options.purpose === 'session-title') return next()
738
771
  const routeConfig = resolveResponsesRouteConfig(ctx, options, runtimeConfig)
739
772
  if (options.purpose === 'compaction') {
740
- if (!routeConfig) return next()
773
+ if (!routeConfig) return unavailableManagedRouteStream(options)
741
774
  return remoteCompactionStream(options, routeConfig, state, ctx, next, requestHeaders)
742
775
  }
743
- const session = sessionFor(ctx, String(options.sessionId ?? ''))
744
- const hasNative = messagesContainNativeCheckpoint(options.messages, session)
745
- const hasLegacy = messagesContainLegacyCheckpoint(options.messages)
746
- if (!hasNative && !hasLegacy) return next()
747
- if (!routeConfig) {
748
- if (hasNative && session) return recursiveLlmStream(ctx, options, rewriteCheckpointsPortable(options.messages, session, { maxChars: runtimeConfig.portableReplayMaxChars }))
749
- return next()
750
- }
751
- return replayCheckpointStream(options, routeConfig, ctx)
776
+ if (!routeConfig) return unavailableManagedRouteStream(options)
777
+ return managedResponsesStream(options, routeConfig, ctx)
752
778
  })
753
779
 
754
780
  ctx.effect?.(() => async () => {
@@ -63,7 +63,7 @@ import { estimateBudgetItem, portableBudgetError, portableTokenCeiling } from '.
63
63
  * @property {unknown} [retainedAssistantCount]
64
64
  */
65
65
  /** @typedef {{ compaction: CompactionItem }} NativeCompactionResult */
66
- /** @typedef {{ session: NativeSession, route: RouteIdentity, result: NativeCompactionResult, input?: unknown[], imageMap?: unknown, retentionOptions?: RetentionOptions }} CreateCheckpointOptions */
66
+ /** @typedef {{ session: NativeSession, route: RouteIdentity, result: NativeCompactionResult, input?: unknown[], ephemeralPreludeItemCount?: number, imageMap?: unknown, retentionOptions?: RetentionOptions }} CreateCheckpointOptions */
67
67
  /** @typedef {Error & { code?: string }} LcxError */
68
68
 
69
69
  export const NATIVE_BLOCK_TYPE = 'lcx-native-compaction-v5'
@@ -99,6 +99,8 @@ function assistantTextParts(item) { if (!isObject(item) || item.type !== 'messag
99
99
  function isRetainedAssistantItem(item) { return assistantTextParts(item).length > 0 }
100
100
  /** @param {unknown} item */
101
101
  function assistantText(item) { return assistantTextParts(item).map((part) => part.text).join('') }
102
+ /** @param {unknown} item */
103
+ function retainedAssistantPhase(item) { const phase = isObject(item) ? item.phase : undefined; return phase === 'commentary' || phase === 'final_answer' ? phase : undefined }
102
104
 
103
105
  /**
104
106
  * @param {unknown} item
@@ -107,16 +109,22 @@ function assistantText(item) { return assistantTextParts(item).map((part) => par
107
109
  */
108
110
  function truncateVisibleAssistantItem(item, maxTokens = ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP) {
109
111
  const text = assistantText(item); if (!text) return undefined
110
- /** @param {string} retained */
111
- const candidate = (retained) => ({ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: retained }] })
112
- const full = candidate(text)
112
+ const phase = retainedAssistantPhase(item)
113
+ const providerId = isObject(item) && typeof item.id === 'string' && item.id ? item.id : undefined
114
+ /** @param {string} retained @param {boolean} preserveProviderId */
115
+ const candidate = (retained, preserveProviderId) => ({
116
+ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: retained }],
117
+ ...(phase ? { phase } : {}),
118
+ ...(preserveProviderId && providerId ? { id: providerId } : {}),
119
+ })
120
+ const full = candidate(text, true)
113
121
  if ((estimatedItemTokens(full) ?? Infinity) <= maxTokens) return full
114
122
  const marker = '\n…[LCX retained answer truncated]…\n'
115
123
  /** @param {number} count */
116
124
  const shortened = (count) => {
117
125
  const available = Math.max(0, count - marker.length)
118
126
  const head = Math.floor(available * 0.72); const tail = available - head
119
- return candidate(`${text.slice(0, head)}${marker}${text.slice(Math.max(head, text.length - tail))}`)
127
+ return candidate(`${text.slice(0, head)}${marker}${text.slice(Math.max(head, text.length - tail))}`, false)
120
128
  }
121
129
  let low = 0; let high = text.length; let best
122
130
  while (low <= high) {
@@ -184,7 +192,7 @@ export function activeCompactionId(session) { if (!session?.events) return undef
184
192
  * @param {CreateCheckpointOptions} options
185
193
  * @returns {NativeCheckpointV5}
186
194
  */
187
- export function createNativeCheckpointBlock({ session, route, result, input = [], imageMap, retentionOptions = {} }) {
195
+ export function createNativeCheckpointBlock({ session, route, result, input = [], ephemeralPreludeItemCount = 0, imageMap, retentionOptions = {} }) {
188
196
  const compactionId = activeCompactionId(session)
189
197
  if (!compactionId) {
190
198
  /** @type {LcxError} */
@@ -192,7 +200,10 @@ export function createNativeCheckpointBlock({ session, route, result, input = []
192
200
  error.code = 'LCX_COMPACTION_ID_UNAVAILABLE'
193
201
  throw error
194
202
  }
195
- const retention = retainedConversationPlan(input, retentionOptions)
203
+ if (!Number.isSafeInteger(ephemeralPreludeItemCount) || ephemeralPreludeItemCount < 0 || ephemeralPreludeItemCount > input.length) {
204
+ throw Object.assign(new Error('Native compaction prelude provenance is invalid'), { code: 'LCX_COMPACTION_PRELUDE_PROVENANCE_INVALID' })
205
+ }
206
+ const retention = retainedConversationPlan(input.slice(ephemeralPreludeItemCount), retentionOptions)
196
207
  /** @type {NativeOutputItem[]} */
197
208
  const nativeOutput = persistNativeImageReferences([...retention.items, structuredClone(result.compaction)], imageMap)
198
209
  return { type: NATIVE_BLOCK_TYPE, version: NATIVE_BLOCK_VERSION, retentionPolicy: 'conversation-fidelity-v1', compactionId, provider: route.provider, model: route.model, baseURLFingerprint: baseURLFingerprint(route.baseURL), sourceSessionId: route.sessionId, nativeOutput, nativeCompaction: structuredClone(result.compaction), retainedInputCount: retention.items.length, retainedClientCount: retention.clientCount, retainedAssistantCount: retention.assistantCount, retainedEstimatedTokens: retention.estimatedTokens, createdAt: Date.now() }