dsh-lcx-codex 0.4.1 → 0.4.2-pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ARCHITECTURE.md CHANGED
@@ -1,3 +1,27 @@
1
+ # GPT Responses full-lifecycle ownership — 0.4.2-pre.1
2
+
3
+ ## Product contract
4
+
5
+ `LCX OFF` leaves every LLM request on the native DSH adapter path. `LCX ON` is the ownership switch for the selected GPT Responses conversation: LCX owns the final Responses wire from the first ordinary turn through tool loops, Native V2 compaction, native replay, portable model migration, and restart/resume. Users turn LCX off before selecting a non-GPT model; DSH model switching itself remains hot and requires no restart.
6
+
7
+ ## Responsibility boundary
8
+
9
+ - **DSH** remains the Agent, Session, request assembly, model/settings/credential, tool-execution, attachment, pressure-policy and compaction-transaction owner.
10
+ - **The rc.2 compatibility seam** projects DSH `GenerateOptions` and durable replay content into Pi's provider-neutral `Context`. It exists only because DSH 0.1.1-rc.2 does not publish `toPiContext()` / `toStreamChunks()` as stable package APIs.
11
+ - **Plugin Pi 0.84.3** owns canonical Responses message/tool serialization and stream semantics: reasoning, IDs, custom tools, strict/grammar tools, `additional_tools`, `tool_search`, namespace, cache semantics and event parsing.
12
+ - **LCX** owns the final body, HTTP/SSE wire, safe error normalization, ordinary/compact/replay orchestration, Remote V2 opaque state and checkpoint compatibility.
13
+
14
+ ## Unified request path
15
+
16
+ `serializeNativeAware()` is the shared history compiler. With no checkpoint it serializes ordinary history; with a compatible checkpoint it injects retained native history plus the opaque compaction item; with an incompatible GPT model/route it reconstructs portable DSH history. All three representations continue through one standard LCX request builder and one LCX transport/parser bridge. Compact is the standard request plus `compaction_trigger` and the Remote V2 feature header; native replay is the standard request plus opaque history. Portable migration never recursively bypasses LCX back to `PiAiAdapter`.
17
+
18
+ Ordinary and replay requests perform one provider attempt and emit DSH terminal failure chunks using the host taxonomy, leaving visible retries to DSH's `agent/request-error` policy. Native compaction retains its bounded idempotent retry and first-checkpoint Basic fallback. Genuine upstream tool namespace is stored only in adapter-private replay metadata because DSH rc.2's visible ToolCall block has no namespace field.
19
+
20
+ ## Gateway continuity
21
+
22
+ The stable DSH session id drives Pi-compatible prompt-cache/session-affinity fields. Sub2API v0.1.183 accepts `session_id`, `session-id`, and `x-client-request-id` for Codex account stickiness and repairs recovered custom-tool/tool-search item ID prefixes. Runtime acceptance therefore verifies both LCX wire continuity and gateway account continuity; release prose is not treated as the wire contract.
23
+
24
+ ---
1
25
 
2
26
  ## rc.6 pressure coordination
3
27
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2-pre.1 - candidate under runtime acceptance
4
+
5
+ ### Architecture
6
+
7
+ - Make the LCX master switch the GPT Responses lifecycle ownership switch: when enabled, the first ordinary Agent turn, tools, Native V2 compaction, native replay and portable GPT-model migration share one LCX final-wire path.
8
+ - Upgrade only the plugin's direct Pi dependency to `0.84.3`; DSH 0.1.1-rc.2 and its host Pi 0.82.x remain isolated and unchanged.
9
+ - Reuse Pi 0.84 canonical Responses serializers and stream semantics for strict/grammar/custom tools, deferred `additional_tools` / `tool_search`, namespace, reasoning, IDs and cache behavior.
10
+ - Add one standard Responses request builder and shared LCX HTTP/SSE transport/parser bridge; remove the old portable recursive bypass back to the DSH adapter.
11
+ - Preserve checkpoint v5, DSH compaction transactions, Native-first pressure coordination, first-checkpoint Basic fallback, restart/resume and Search/#20 architecture.
12
+ - Normalize ordinary/replay failures into DSH terminal taxonomy and keep DSH as the visible retry owner.
13
+
14
+ ### Validation state
15
+
16
+ - Engineering gates pass on the private work branch; exact commit-bound DSH + Sub2API runtime acceptance remains mandatory before integration, publication or a `VERIFIED` compatibility claim.
17
+
3
18
  ## 0.4.1 - 2026-08-25
4
19
 
5
20
  ### Stable promotion
package/cordis.patch.yml CHANGED
@@ -10,6 +10,7 @@
10
10
  baseURL: https://api.lcxbot.com/v1
11
11
  apiKeyEnv: LCX_API_KEY
12
12
  model: gpt-5.6-sol
13
+ supportsExplicitPromptCacheMode: true
13
14
  legacyCheckpointPath: !!js dshHomePath('storages/lcx-codex/checkpoints-v3.json')
14
15
  alphaCapabilityPath: !!js dshHomePath('storages/lcx-codex/web-alpha-capabilities.json')
15
16
  alphaRefPath: !!js dshHomePath('storages/lcx-codex/web-alpha-refs.json')
package/lib/client.js CHANGED
@@ -32,8 +32,9 @@ window.__ModuleLoader__.load({
32
32
  const copy = {
33
33
  zh: {
34
34
  title: 'Responses / Codex 能力',
35
- desc: '复用现有 GPT Responses 路由;普通搜索走 DSH web_search,Native V2 压缩采用 90% 主动压缩 + 95% 紧急保护。',
36
- enabled: '启用插件',
35
+ desc: 'LCX 开启后从第一轮普通请求开始接管当前 GPT Responses 会话,并统一管理普通请求、工具、Native V2 压缩和续接。',
36
+ enabled: '启用 LCX(接管当前 GPT Responses 会话)',
37
+ enabledHelp: '切换 Claude、Gemini、DeepSeek 等非 GPT 模型前先关闭 LCX;模型切换本身不需要重启 DSH。',
37
38
  web: '使用 GPT Hosted Search 作为 DSH web_search 后端',
38
39
  webHelp: '不会新增第二个普通搜索工具;普通 web_search 自动跟随当前 Agent 的 GPT Responses 模型。',
39
40
  searchTimeout: 'web_search 超时(秒)',
@@ -42,8 +43,6 @@ window.__ModuleLoader__.load({
42
43
  advancedHelp: '只在需要域名过滤、位置、search context、图片等原生 Hosted 参数时使用;默认关闭以保持工具 schema 稳定。',
43
44
  alpha: '启用 Alpha command(websearch_alpha)',
44
45
  alphaHelp: '仅 capability probe 对当前 endpoint/provider/model/schema 验证通过后才真正注册。',
45
- compact: '启用 Native V2 远程压缩',
46
- compactHelp: 'DSH 仍负责范围、事务和 /compact;Native state 保存在 DSH session log。',
47
46
  autoCompact: '启用 Native-first 自动压缩',
48
47
  autoCompactHelp: '到主动阈值前不让 DSH 的 80% tool-result prune 改写历史;到主动阈值后优先 Native V2。',
49
48
  autoThreshold: 'Native 自动压缩阈值(%)',
@@ -52,14 +51,15 @@ window.__ModuleLoader__.load({
52
51
  emergencyThresholdHelp: '建议 95%。达到这里才允许 DSH tool-result-pruner 先救场;必须高于 Native 阈值。',
53
52
  fallback: 'Native 失败后回退 DSH basic compaction',
54
53
  fallbackHelp: 'Remote-first:只有 Native 请求失败才调用 basic summary,不并行双跑。',
55
- endpoint: '回退 Responses 地址',
56
- model: '回退 GPT 模型',
54
+ endpoint: 'Responses 地址',
55
+ model: 'GPT 模型',
57
56
  save: '保存', discard: '放弃修改', saving: '保存中…',
58
57
  },
59
58
  en: {
60
59
  title: 'Responses / Codex capabilities',
61
- desc: 'Reuse existing GPT Responses routes; Native V2 uses a 90% proactive threshold with 95% emergency protection.',
62
- enabled: 'Enable plugin',
60
+ desc: 'When enabled, LCX owns the selected GPT Responses conversation from the first ordinary turn through tools, Native V2 compaction and continuation.',
61
+ enabled: 'Enable LCX (own current GPT Responses conversation)',
62
+ enabledHelp: 'Turn LCX off before switching to Claude, Gemini, DeepSeek, or another non-GPT model. Model switching itself does not require a DSH restart.',
63
63
  web: 'Use GPT Hosted Search as DSH web_search backend',
64
64
  webHelp: 'Keeps DSH web_search as the single ordinary search tool and follows the active Agent GPT Responses model.',
65
65
  searchTimeout: 'web_search timeout (seconds)',
@@ -68,8 +68,6 @@ window.__ModuleLoader__.load({
68
68
  advancedHelp: 'Only for native Hosted controls such as domains, location, context size and image search; off by default for stable tool schemas.',
69
69
  alpha: 'Enable Alpha command (websearch_alpha)',
70
70
  alphaHelp: 'Registered only after a matching capability probe.',
71
- compact: 'Enable Native V2 remote compaction',
72
- compactHelp: 'DSH still owns range/transactions and /compact; native state is persisted in the DSH session log.',
73
71
  autoCompact: 'Enable Native-first automatic compaction',
74
72
  autoCompactHelp: 'Suppresses the stock 80% tool-result prune before the proactive threshold, then prefers Native V2.',
75
73
  autoThreshold: 'Native auto-compaction threshold (%)',
@@ -78,7 +76,7 @@ window.__ModuleLoader__.load({
78
76
  emergencyThresholdHelp: 'Default 95%. DSH tool-result pruning is allowed only in this emergency zone; must exceed the Native threshold.',
79
77
  fallback: 'Fall back to DSH basic compaction on Native failure',
80
78
  fallbackHelp: 'Remote-first: basic summary runs only after Native failure, never in parallel.',
81
- endpoint: 'Fallback Responses endpoint', model: 'Fallback GPT model',
79
+ endpoint: 'Responses endpoint', model: 'GPT model',
82
80
  save: 'Save', discard: 'Discard', saving: 'Saving…',
83
81
  },
84
82
  }
@@ -96,7 +94,7 @@ window.__ModuleLoader__.load({
96
94
  field(name) { return this.draftValue()[name] }
97
95
  projection() {
98
96
  const s = this.snapshot()
99
- const bools = ['enabled','webSearch','advancedHostedSearch','alphaSearch','remoteCompaction','fallbackToBasicCompaction','autoCompaction']
97
+ const bools = ['enabled','webSearch','advancedHostedSearch','alphaSearch','fallbackToBasicCompaction','autoCompaction']
100
98
  return {
101
99
  available: s.status === 'ready', writable: s.writable, dirty: this.dirty, saving: this.saving,
102
100
  ...Object.fromEntries(bools.map((f) => [f, { value: f === 'fallbackToBasicCompaction' || f === 'autoCompaction' ? this.field(f) !== false : Boolean(this.field(f)) }])),
@@ -139,18 +137,17 @@ window.__ModuleLoader__.load({
139
137
  React.createElement('button', { className:'lcx-head', type:'button', onClick:()=>setOpen(!open) }, React.createElement('strong',null,t.title), React.createElement('span',null,open?'⌃':'⌄')),
140
138
  open ? React.createElement('div', { className:'lcx-body' },
141
139
  React.createElement('p', { className:'lcx-help' }, t.desc),
142
- React.createElement(Row, { id:'lcx-enabled', label:t.enabled, checked:s.enabled.value, disabled, onChange:v=>props.edit('enabled',v) }),
140
+ React.createElement(Row, { id:'lcx-enabled', label:t.enabled, help:t.enabledHelp, checked:s.enabled.value, disabled, onChange:v=>props.edit('enabled',v) }),
143
141
  React.createElement(Row, { id:'lcx-web', label:t.web, help:t.webHelp, checked:s.webSearch.value, disabled:disabled||!s.enabled.value, onChange:v=>props.edit('webSearch',v) }),
144
142
  React.createElement('div', { className:'lcx-fields' },
145
143
  React.createElement(Field, { label:t.searchTimeout, help:t.searchTimeoutHelp, type:'number', min:30, max:600, step:30, value:s.webSearchTimeoutSeconds, disabled:disabled||!s.enabled.value||!s.webSearch.value, onChange:numeric('webSearchTimeoutSeconds') })),
146
144
  React.createElement(Row, { id:'lcx-advanced', label:t.advanced, help:t.advancedHelp, checked:s.advancedHostedSearch.value, disabled:disabled||!s.enabled.value||!s.webSearch.value, onChange:v=>props.edit('advancedHostedSearch',v) }),
147
145
  React.createElement(Row, { id:'lcx-alpha', label:t.alpha, help:t.alphaHelp, checked:s.alphaSearch.value, disabled:disabled||!s.enabled.value, onChange:v=>props.edit('alphaSearch',v) }),
148
- React.createElement(Row, { id:'lcx-compact', label:t.compact, help:t.compactHelp, checked:s.remoteCompaction.value, disabled:disabled||!s.enabled.value, onChange:v=>props.edit('remoteCompaction',v) }),
149
- React.createElement(Row, { id:'lcx-auto-compact', label:t.autoCompact, help:t.autoCompactHelp, checked:s.autoCompaction.value, disabled:disabled||!s.enabled.value||!s.remoteCompaction.value, onChange:v=>props.edit('autoCompaction',v) }),
146
+ React.createElement(Row, { id:'lcx-auto-compact', label:t.autoCompact, help:t.autoCompactHelp, checked:s.autoCompaction.value, disabled:disabled||!s.enabled.value, onChange:v=>props.edit('autoCompaction',v) }),
150
147
  React.createElement('div', { className:'lcx-fields' },
151
- React.createElement(Field, { label:t.autoThreshold, help:t.autoThresholdHelp, type:'number', min:85, max:95, step:1, value:s.autoCompactionThresholdPercent, disabled:disabled||!s.enabled.value||!s.remoteCompaction.value||!s.autoCompaction.value, onChange:numeric('autoCompactionThresholdPercent') }),
152
- React.createElement(Field, { label:t.emergencyThreshold, help:t.emergencyThresholdHelp, type:'number', min:90, max:99, step:1, value:s.emergencyPruneThresholdPercent, disabled:disabled||!s.enabled.value||!s.remoteCompaction.value||!s.autoCompaction.value, onChange:numeric('emergencyPruneThresholdPercent') })),
153
- React.createElement(Row, { id:'lcx-fallback', label:t.fallback, help:t.fallbackHelp, checked:s.fallbackToBasicCompaction.value, disabled:disabled||!s.enabled.value||!s.remoteCompaction.value, onChange:v=>props.edit('fallbackToBasicCompaction',v) }),
148
+ React.createElement(Field, { label:t.autoThreshold, help:t.autoThresholdHelp, type:'number', min:85, max:95, step:1, value:s.autoCompactionThresholdPercent, disabled:disabled||!s.enabled.value||!s.autoCompaction.value, onChange:numeric('autoCompactionThresholdPercent') }),
149
+ React.createElement(Field, { label:t.emergencyThreshold, help:t.emergencyThresholdHelp, type:'number', min:90, max:99, step:1, value:s.emergencyPruneThresholdPercent, disabled:disabled||!s.enabled.value||!s.autoCompaction.value, onChange:numeric('emergencyPruneThresholdPercent') })),
150
+ React.createElement(Row, { id:'lcx-fallback', label:t.fallback, help:t.fallbackHelp, checked:s.fallbackToBasicCompaction.value, disabled:disabled||!s.enabled.value, onChange:v=>props.edit('fallbackToBasicCompaction',v) }),
154
151
  React.createElement('div', { className:'lcx-fields' },
155
152
  React.createElement(Field, { label:t.endpoint, value:s.baseURL, disabled, onChange:e=>props.edit('baseURL',e.target.value) }),
156
153
  React.createElement(Field, { label:t.model, value:s.model, disabled, onChange:e=>props.edit('model',e.target.value) })),
package/lib/compact-v2.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import { consumeSse, fetchSseWithRetry } from './transport.js'
4
- import { responsesTools } from './dsh-responses.js'
4
+ import { buildCompactionResponsesBody, responsesGenerationEnvelope as standardGenerationEnvelope } from './responses-request.js'
5
5
 
6
6
  /** @typedef {Record<string, unknown>} UnknownRecord */
7
7
  /** @typedef {Record<string, string>} HeaderMap */
@@ -16,11 +16,13 @@ import { responsesTools } from './dsh-responses.js'
16
16
  /**
17
17
  * @typedef {object} NativeCompactionBodyOptions
18
18
  * @property {string} model
19
+ * @property {unknown} [modelDescriptor]
19
20
  * @property {unknown[]} input
20
21
  * @property {string} [instructions]
21
22
  * @property {unknown} [tools]
22
23
  * @property {string} [promptCacheKey]
23
24
  * @property {string} [promptCacheRetention]
25
+ * @property {'none' | 'short' | 'long'} [cacheRetention]
24
26
  * @property {unknown} [reasoningEffort]
25
27
  * @property {unknown} [temperature]
26
28
  * @property {unknown} [maxTokens]
@@ -37,6 +39,7 @@ import { responsesTools } from './dsh-responses.js'
37
39
  * @property {unknown[]} [tools]
38
40
  * @property {string} [prompt_cache_key]
39
41
  * @property {string} [prompt_cache_retention]
42
+ * @property {{ mode?: 'explicit', ttl?: '30m' }} [prompt_cache_options]
40
43
  * @property {{ effort: string, summary: 'auto' }} [reasoning]
41
44
  * @property {string[]} [include]
42
45
  * @property {number} [temperature]
@@ -97,46 +100,27 @@ export function mergeFeatureHeader(headers = {}) {
97
100
  * @param {GenerationControls} [controls]
98
101
  * @returns {GenerationEnvelope}
99
102
  */
100
- export function responsesGenerationEnvelope({ reasoningEffort, temperature, maxTokens } = {}) {
101
- /** @type {GenerationEnvelope} */
102
- const result = {}
103
- if (reasoningEffort !== undefined && reasoningEffort !== 'off') {
104
- result.reasoning = { effort: String(reasoningEffort), summary: 'auto' }
105
- result.include = ['reasoning.encrypted_content']
106
- }
107
- if (temperature !== undefined) {
108
- if (!Number.isFinite(temperature)) throw fail('Responses temperature must be finite', 'LCX_COMPACT_INVALID_INPUT')
109
- result.temperature = Number(temperature)
110
- }
111
- if (maxTokens !== undefined) {
112
- if (!Number.isSafeInteger(maxTokens) || /** @type {number} */ (maxTokens) <= 0) throw fail('Responses maxTokens must be a positive safe integer', 'LCX_COMPACT_INVALID_INPUT')
113
- result.max_output_tokens = Math.max(16, /** @type {number} */ (maxTokens))
114
- }
115
- return result
103
+ export function responsesGenerationEnvelope(controls = {}) {
104
+ return standardGenerationEnvelope(controls)
116
105
  }
117
106
 
118
107
  /**
119
108
  * @param {NativeCompactionBodyOptions} options
120
109
  * @returns {NativeCompactionBody}
121
110
  */
122
- export function buildNativeCompactionBody({ model, input, instructions, tools, promptCacheKey, promptCacheRetention, reasoningEffort, temperature, maxTokens }) {
123
- if (!Array.isArray(input)) throw fail('native compaction input must be an array', 'LCX_COMPACT_INVALID_INPUT')
124
- if (input.some((item) => /** @type {UnknownRecord | undefined} */ (item)?.type === 'compaction_trigger')) throw fail('native compaction input already contains compaction_trigger', 'LCX_COMPACT_DUPLICATE_TRIGGER')
125
- /** @type {unknown[] | undefined} */
126
- const nativeTools = responsesTools(tools)
127
- return {
128
- model,
129
- input: [...structuredClone(input), { type: 'compaction_trigger' }],
130
- stream: true,
131
- store: false,
132
- tool_choice: 'auto',
133
- parallel_tool_calls: true,
134
- ...(instructions !== undefined ? { instructions } : {}),
135
- ...(nativeTools !== undefined ? { tools: nativeTools } : {}),
136
- ...(promptCacheKey ? { prompt_cache_key: String(promptCacheKey) } : {}),
137
- ...(promptCacheRetention ? { prompt_cache_retention: String(promptCacheRetention) } : {}),
138
- ...responsesGenerationEnvelope({ reasoningEffort, temperature, maxTokens }),
139
- }
111
+ export function buildNativeCompactionBody({ model, modelDescriptor, input, instructions, tools, promptCacheKey, promptCacheRetention, cacheRetention, reasoningEffort, temperature, maxTokens }) {
112
+ return /** @type {NativeCompactionBody} */ (/** @type {unknown} */ (buildCompactionResponsesBody({
113
+ model: /** @type {any} */ (modelDescriptor ?? model),
114
+ input,
115
+ instructions,
116
+ tools,
117
+ promptCacheKey,
118
+ promptCacheRetention,
119
+ cacheRetention: cacheRetention ?? (promptCacheKey ? (promptCacheRetention ? 'long' : 'short') : 'none'),
120
+ reasoningEffort,
121
+ temperature,
122
+ maxTokens,
123
+ })))
140
124
  }
141
125
 
142
126
  /**
@@ -260,8 +244,8 @@ export async function parseNativeCompactionSse(response, options = {}) {
260
244
  /**
261
245
  * @param {NativeCompactionRequestOptions} options
262
246
  */
263
- export async function requestNativeCompaction({ baseURL, model, input, instructions, tools, promptCacheKey, promptCacheRetention, reasoningEffort, temperature, maxTokens, idempotencyKey, headers, signal, timeoutMs, maxAttempts, maxResponseBytes }) {
264
- const body = buildNativeCompactionBody({ model, input, instructions, tools, promptCacheKey, promptCacheRetention, reasoningEffort, temperature, maxTokens })
247
+ export async function requestNativeCompaction({ baseURL, model, modelDescriptor, input, instructions, tools, promptCacheKey, promptCacheRetention, cacheRetention, reasoningEffort, temperature, maxTokens, idempotencyKey, headers, signal, timeoutMs, maxAttempts, maxResponseBytes }) {
248
+ const body = buildNativeCompactionBody({ model, modelDescriptor, input, instructions, tools, promptCacheKey, promptCacheRetention, cacheRetention, reasoningEffort, temperature, maxTokens })
265
249
  const requestHeaders = mergeFeatureHeader({ ...headers, ...(idempotencyKey ? { 'idempotency-key': String(idempotencyKey) } : {}) })
266
250
  return fetchSseWithRetry(`${String(baseURL).replace(/\/+$/u, '')}/responses`, body, requestHeaders, signal, timeoutMs, {
267
251
  maxAttempts,
@@ -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() }