dsh-lcx-codex 0.4.3-pre.12 → 0.4.3-pre.3

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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  **为 DeepSeek Harness 中的 GPT 提供 Responses 生命周期能力,并为 Grok 接入原生 Web / X Search。**
8
8
 
9
9
  [![npm prerelease](https://img.shields.io/npm/v/dsh-lcx-codex/prelatest?label=prelatest)](https://www.npmjs.com/package/dsh-lcx-codex)
10
- [![DSH](https://img.shields.io/badge/DSH-0.1.5--alpha.1-16803c)](#安装)
10
+ [![DSH](https://img.shields.io/badge/DSH-0.1.3--alpha.2-16803c)](#安装)
11
11
  [![License](https://img.shields.io/badge/license-MIT-555)](LICENSE)
12
12
 
13
13
  **简体中文** · [English](README_EN.md) · [更新日志](CHANGELOG.md) · [问题反馈](https://github.com/kk3ya03-star/dsh-lcx-codex/issues)
@@ -20,20 +20,18 @@ LCX Codex 是 [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness
20
20
 
21
21
  ## 安装
22
22
 
23
- 本页对应 **`0.4.3-pre.12` 预发布版**,只支持 **DSH `0.1.5-alpha.1` + Pi `0.85.1`**。稳定版仍为 `0.4.2`;本预发布版不为旧 DSH / 旧 Session 格式保留兼容层。
23
+ 本页对应 **`0.4.3-pre.3` 预发布版**,适配 **DSH `0.1.3-alpha.2`**。稳定版仍为 `0.4.2`,使用旧版 DSH `0.1.1-rc.2` 的用户请看[稳定版说明](https://github.com/kk3ya03-star/dsh-lcx-codex/blob/v0.4.2/README.md)。
24
24
 
25
- 安装前,请确认 DSH `0.1.5-alpha.1` Web 能正常启动,并已配置需要使用的 GPT Responses 或 Grok Responses API 路由。Node.js 要求 `^22.19.0 || >=24.0.0`。
25
+ 安装前,请确认 DSH Web 能正常启动,并已配置需要使用的 GPT Responses 或 Grok Responses API 路由。Node.js 要求 `^22.19.0 || >=24.0.0`;Windows 启动出现 `fs-ext` 报错时,先看下方[故障排查](#故障排查)。
26
26
 
27
27
  ```sh
28
- dsh plugin --profile web add dsh-lcx-codex@0.4.3-pre.12
28
+ dsh plugin --profile web add dsh-lcx-codex@0.4.3-pre.3
29
29
  dsh web
30
30
  ```
31
31
 
32
32
  以后升级预发布版,可执行 `dsh plugin --profile web add dsh-lcx-codex@prelatest`。不带版本或标签安装会得到稳定版,**不是本页介绍的新版本**。
33
33
 
34
- **升级到本版:** 只支持 DSH `0.1.5-alpha.1` Session V3。旧插件配置、旧 DSH session、旧 LCX checkpoint / Grok replay envelope 都不迁移到活动目录。需要保留旧历史时,请先将旧 `sessions` 目录改名或归档,再让 DSH `0.1.5-alpha.1` 从新的空 `sessions` 目录开始;旧数据只作为离线备份保留。
35
-
36
- DSH 持久化层重新打开同一个已保存 session 时,LCX 可以恢复该 session 内保存的 v5 checkpoint;这只是 DSH session persistence 上的继续对话,**不是 Codex rollout/checkpoint 的进程级 restart/resume 机制**。
34
+ **从 `0.4.2` 升级:** 旧插件配置和 v3/v4 压缩检查点不再兼容,请重新配置插件并新建会话。新版 v5 GPT 检查点仍支持重启续聊。
37
35
 
38
36
  ## 启用
39
37
 
@@ -77,7 +75,7 @@ Grok 使用 xAI Responses 的服务端工具,而不是把搜索包装成 DSH f
77
75
 
78
76
  开启任意 Grok 原生搜索后,Grok 请求会移除 DSH `web_search`,避免重复搜索语义;`web_fetch`、`read` 及其他 DSH/MCP function tools 仍保持可用。原生搜索结果可以继续进入本地工具调用,再回到同一 Grok agentic workflow;插件会保存 xAI 必需的 provider-native replay state,但不会把服务端搜索伪装成 DSH 本地工具调用。
79
77
 
80
- 当前支持 **API Key / API Gateway** 路由;OAuth / SuperGrok / X Premium 订阅登录不在本版范围内。LCX 不再为 Grok 自动追加可见的 synthetic `Sources:` 列表,并会清理已知的 `render_inline_citation` / `stateless_invoke` /私有区 renderer 标记、尾部 `<|eos|>` 等上游渲染协议残留;模型正文中主动写出的普通链接仍保持可见。完整 provider-native 搜索输出只保存在 same-session replay state 中。
78
+ 当前支持 **API Key / API Gateway** 路由;OAuth / SuperGrok / X Premium 订阅登录不在本版范围内。搜索来源 URL 会尽量保留,但具体 citation 展示形式仍受上游兼容网关返回格式影响。
81
79
 
82
80
  ### GPT 搜索图片并显示
83
81
 
@@ -105,9 +103,9 @@ Alpha 需要单独开启,且接口通过能力探测后才会出现。目前
105
103
  | `90%` 至 `95%` | 优先尝试远程原生压缩。 |
106
104
  | 达到 `95%` | 允许 DSH 紧急裁剪工具结果,再按流程压缩。 |
107
105
 
108
- 也可以输入 `/compact` 手动压缩。这些阈值是固定策略,不需要额外设置;实际能否压缩取决于是否有可压缩的历史以及接口支持情况。Grok 不走 GPT Native V2,而继续使用 DSH `0.1.5-alpha.1` 自己的 compaction;Session V3 的 leading `system/message` 由 DSH 保持在摘要之外。
106
+ 也可以输入 `/compact` 手动压缩。这些阈值是固定策略,不需要额外设置;实际能否压缩取决于是否有可压缩的历史以及接口支持情况。
109
107
 
110
- 压缩后可以在同一存活 session 内继续对话;如果 DSH 自身重新打开同一个已持久化 session,LCX 也会校验并恢复其中的 v5 checkpoint。遇到不兼容的模型、route 或 session identity 时,插件改用可迁移的历史,不会强行复用原生状态。首次建立检查点时,部分可恢复失败可以回退到 DSH 基础压缩;并非所有失败都会回退。这里不引入 Codex 式 rollout/checkpoint restart-resume。
108
+ 压缩后可以继续对话、重启恢复或切换 GPT 模型。遇到不兼容的模型或接口配置时,插件改用可迁移的历史,不会强行复用原生状态。首次建立检查点时,部分可恢复失败可以回退到 DSH 基础压缩;并非所有失败都会回退。
111
109
 
112
110
  ## 缓存
113
111
 
@@ -120,9 +118,11 @@ Pi `0.85.1` 的显式缓存模式支持 `30m` 长保留参数;`cacheRetention=
120
118
 
121
119
  ## 故障排查
122
120
 
123
- **从旧版 Windows DSH 升级后还需要 `fs-ext` 本地补丁吗?**
121
+ **DSH Windows 启动时报 `fs-ext` 错误?**
122
+
123
+ DSH `0.1.3-alpha.2` 会在启动时加载 `fs-ext`,即使 Windows 实际不使用它。当它的原生模块不可用时,DSH 会在启动阶段退出。这是 **DSH 的依赖加载问题,不是插件压缩或会话锁损坏**。
124
124
 
125
- 不需要把 `0.1.3-alpha.2` `fs-ext` 本地 workaround 带到 DSH `0.1.5-alpha.1`。本版已经在未修改的 Windows DSH `0.1.5-alpha.1` 上完成 Web 与 headless 启动验证。若旧安装曾手工修改 DSH 文件,请通过正常安装得到干净的 `0.1.5-alpha.1` 文件,不要把旧补丁复制到新版本。
125
+ 本版 Windows 实测使用了一个最小 DSH 修正:仅在非 Windows 平台加载 `fs-ext`,保留原有 Win32 会话锁。插件不会自动修改 DSH;未经该修正的 Windows 环境不保证能启动。不要用关闭会话锁来绕过问题。
126
126
 
127
127
  **安装后没有变化?**
128
128
 
@@ -134,20 +134,18 @@ Pi `0.85.1` 的显式缓存模式支持 `30m` 长保留参数;`cacheRetention=
134
134
 
135
135
  ## 版本与验证范围
136
136
 
137
- 本版使用 DSH `0.1.5-alpha.1`、DSH host Pi `0.85.1` 和插件 Pi `0.85.1`。插件单独声明 Pi 依赖,不替换 DSH 的依赖。
138
-
139
- 发布候选已通过 **246/246 PASS**、strict host/client typecheck 与 DSH schema 校验。真实 DSH `0.1.5-alpha.1` 隔离运行已验证:Web 启动、headless GPT 普通请求、Session V3 `system/message`、Native V2 `/compact`、compact 后同 session continuation,以及 v5 checkpoint 的 session identity 绑定。
137
+ 本版使用 DSH `0.1.3-alpha.2`、DSH host Pi `0.85.1` 和插件 Pi `0.85.1`。插件单独声明 Pi 依赖,不替换 DSH 的依赖。
140
138
 
141
- Grok 另做了真实长会话验证:原生 Web Search 足够长的上下文 DSH 实际 `/compact` compact 后再次原生搜索/续聊。marker 与关键事实保持,`system/message` 只出现一次,Grok replay 为当前 v3 envelope,最终可见正文 `sourceLeak=false`。
139
+ 发布前完整测试为 **237/237 PASS**,strict host/client typecheck 4 DSH schema 均通过。真实运行覆盖 GPT Hosted/Native V2 回归、DeepSeek 原生 DSH 搜索回归,以及 Grok 4.5/4.6 原生 Web/X Search、Web/X → 本地 `read` continuation、provider-native replay、完整 DSH 重启后的续聊、Grok 父/子代理 session/cache 隔离。GPT 已验证的父 cache-sharing 策略保持不变。
142
140
 
143
- 仍需保留的预发布限制:Alpha 引用及 screenshot 展示、代理/NO_PROXY、更广的并发/取消和后台 continuable subagent 矩阵仍未全面覆盖;Grok OAuth 不支持;未知的新型上游 renderer 协议仍可能需要额外适配。DSH `0.1.5-alpha.1` 本身仍是 alpha/developer-preview 级上游。
141
+ 仍需保留的预发布限制:Windows DSH `0.1.3-alpha.2` 的 `fs-ext` 启动问题需要文档中的最小 host loading 修正;Alpha 引用及 screenshot 展示、代理/NO_PROXY、更广的并发/取消和后台 continuable subagent 矩阵仍未全面覆盖;Grok OAuth 不支持,citation 渲染仍可能受上游网关格式影响。详细变更见 [v0.4.3-pre.3 发布说明](https://github.com/kk3ya03-star/dsh-lcx-codex/releases/tag/v0.4.3-pre.3)。
144
142
 
145
143
  ## 开发与反馈
146
144
 
147
145
  ```sh
148
146
  npm ci --ignore-scripts
149
- npm ci --prefix scripts/runtime-dsh015 --ignore-scripts
150
- node scripts/link-dsh-runtime.mjs scripts/runtime-dsh015
147
+ npm ci --prefix scripts/runtime-alpha2 --ignore-scripts
148
+ node scripts/link-dsh-runtime.mjs scripts/runtime-alpha2
151
149
  node scripts/check-generated.mjs
152
150
  npm run typecheck
153
151
  npm test
@@ -93,7 +93,7 @@ function attachmentResolver(ctx, options) {
93
93
  const attachments = attachmentServices(ctx?.attachments);
94
94
  return async (block, signal) => {
95
95
  if (attachments === undefined)
96
- throw error("LCX requires the DSH request-image attachment API", "LCX_COMPACT_IMAGE_API_UNAVAILABLE");
96
+ throw error("LCX requires the DSH 0.1.3 request-image attachment API", "LCX_COMPACT_IMAGE_API_UNAVAILABLE");
97
97
  const request = await attachments.store.readImageRequest(block.attachment, { maxPixels: options.requestImagePixelBudget, maxBytes: options.requestImageMaxBytes }, signal);
98
98
  const data = request.data;
99
99
  const mediaType = request.mediaType ?? block.attachment.mediaType;
@@ -280,18 +280,11 @@ function projectFiles(messages, ctx) {
280
280
  content: projectFileContent(message.content, fileRequestText),
281
281
  }));
282
282
  }
283
- function flattenMessageText(message) {
284
- return message.content
285
- .map((block) => (block.type === "text" ? block.text : ""))
286
- .join("");
287
- }
288
283
  async function dshToPiMessages(messages, ctx, options, imageMap) {
289
284
  const result = [];
290
285
  for (const message of messages) {
291
- if (message.role === "system") {
292
- result.push({ role: "user", content: flattenMessageText(message), timestamp: 0 });
286
+ if (message.role === "system")
293
287
  continue;
294
- }
295
288
  if (message.role === "assistant") {
296
289
  result.push(toPiAssistant(message, options.onReplayDegrade));
297
290
  continue;
@@ -75,10 +75,37 @@ function validVisibleItem(item) {
75
75
  typeof item.name === "string" && typeof item.input === "string";
76
76
  return false;
77
77
  }
78
+ function textSha256(text) {
79
+ return createHash("sha256").update(text, "utf8").digest("hex");
80
+ }
81
+ function lcxCitationAddition(item) {
82
+ if (item.type !== "message" ||
83
+ typeof item.id !== "string" ||
84
+ !item.id.startsWith("msg_lcx_sources_"))
85
+ return undefined;
86
+ const text = itemText(item);
87
+ return text.startsWith("\n\nSources:\n")
88
+ ? { kind: "citation-sources", textSha256: textSha256(text) }
89
+ : undefined;
90
+ }
91
+ function validVisibleAdditions(value) {
92
+ if (!Array.isArray(value) || value.length > GROK_NATIVE_REPLAY_MAX_ITEMS)
93
+ return undefined;
94
+ const additions = [];
95
+ for (const candidate of value) {
96
+ if (!isRecord(candidate) ||
97
+ candidate.kind !== "citation-sources" ||
98
+ typeof candidate.textSha256 !== "string" ||
99
+ !/^[0-9a-f]{64}$/u.test(candidate.textSha256))
100
+ return undefined;
101
+ additions.push({ kind: "citation-sources", textSha256: candidate.textSha256 });
102
+ }
103
+ return additions;
104
+ }
78
105
  function readGrokNativeReplayEnvelope(value, route) {
79
106
  if (!isRecord(value) ||
80
107
  value.kind !== "xai-responses-native-search" ||
81
- value.version !== 3)
108
+ (value.version !== 1 && value.version !== 2))
82
109
  return undefined;
83
110
  if (value.provider !== route.provider ||
84
111
  value.model !== route.model ||
@@ -100,10 +127,16 @@ function readGrokNativeReplayEnvelope(value, route) {
100
127
  const output = [];
101
128
  const itemKinds = new Map();
102
129
  const callIds = new Set();
130
+ const migratedAdditions = [];
103
131
  for (const candidate of value.output) {
104
132
  if (!isRecord(candidate) || typeof candidate.type !== "string")
105
133
  return undefined;
106
134
  const item = candidate;
135
+ const legacyAddition = value.version === 1 ? lcxCitationAddition(item) : undefined;
136
+ if (legacyAddition) {
137
+ migratedAdditions.push(legacyAddition);
138
+ continue;
139
+ }
107
140
  if (GROK_NATIVE_SERVER_TOOL_TYPES.has(item.type)) {
108
141
  if (typeof item.id !== "string" || item.id.length === 0)
109
142
  return undefined;
@@ -130,27 +163,39 @@ function readGrokNativeReplayEnvelope(value, route) {
130
163
  }
131
164
  if (output.length === 0)
132
165
  return undefined;
166
+ const visibleAdditions = value.version === 1
167
+ ? migratedAdditions
168
+ : validVisibleAdditions(value.visibleAdditions);
169
+ if (!visibleAdditions)
170
+ return undefined;
133
171
  return {
134
172
  kind: "xai-responses-native-search",
135
- version: 3,
173
+ version: 2,
136
174
  provider: route.provider,
137
175
  model: route.model,
138
176
  sourceSessionId: route.sessionId,
139
177
  routeAuthorityFingerprint: grokNativeReplayRouteFingerprint(route),
140
178
  output,
179
+ visibleAdditions,
141
180
  };
142
181
  }
143
- export function createGrokNativeReplayEnvelope(output, route) {
182
+ export function createGrokNativeReplayEnvelope(output, route, visibleAdditions = []) {
144
183
  if (!route || !Array.isArray(output))
145
184
  return undefined;
185
+ const additions = visibleAdditions.map((candidate) => isRecord(candidate) && typeof candidate.type === "string"
186
+ ? lcxCitationAddition(candidate)
187
+ : undefined);
188
+ if (additions.some((addition) => addition === undefined))
189
+ return undefined;
146
190
  return readGrokNativeReplayEnvelope({
147
191
  kind: "xai-responses-native-search",
148
- version: 3,
192
+ version: 2,
149
193
  provider: route.provider,
150
194
  model: route.model,
151
195
  sourceSessionId: route.sessionId,
152
196
  routeAuthorityFingerprint: grokNativeReplayRouteFingerprint(route),
153
197
  output,
198
+ visibleAdditions: additions,
154
199
  }, route);
155
200
  }
156
201
  function itemText(item) {
@@ -188,6 +233,19 @@ function visibleItemMatchesInput(item, candidate) {
188
233
  return candidate.call_id === item.call_id;
189
234
  return true;
190
235
  }
236
+ function additionMatchesBlock(addition, candidate) {
237
+ return isRecord(candidate) && candidate.type === "text" &&
238
+ typeof candidate.text === "string" &&
239
+ candidate.text.startsWith("\n\nSources:\n") &&
240
+ textSha256(candidate.text) === addition.textSha256;
241
+ }
242
+ function additionMatchesInput(addition, candidate) {
243
+ if (!isRecord(candidate) || candidate.type !== "message")
244
+ return false;
245
+ if (typeof candidate.id !== "string" || !candidate.id.startsWith("msg_lcx_sources_"))
246
+ return false;
247
+ return textSha256(itemText(candidate)) === addition.textSha256;
248
+ }
191
249
  function replayForMessage(message, route) {
192
250
  if (message.role !== "assistant" || message.source.kind !== "model")
193
251
  return undefined;
@@ -214,18 +272,22 @@ function replayForMessage(message, route) {
214
272
  anchors.push(item);
215
273
  blockIndex += 1;
216
274
  }
217
- else if (!(item.type === "reasoning" &&
218
- (!itemText(item).trim() || (reasoningCounts.get(String(item.id)) ?? 0) > 1))) {
275
+ else if (!(item.type === "reasoning" && (reasoningCounts.get(String(item.id)) ?? 0) > 1)) {
219
276
  return undefined;
220
277
  }
221
278
  }
222
- // Empty Grok reasoning has no UI block. Pi can coalesce repeated reasoning IDs
223
- // in terminal-only gateway responses; every actual DSH-visible block must still match.
279
+ // Pi can coalesce repeated reasoning IDs in terminal-only gateway responses.
280
+ // Match every provider-visible DSH block and only the hashed LCX additions.
224
281
  if (anchors.length === 0)
225
282
  return undefined;
283
+ for (const addition of envelope.visibleAdditions) {
284
+ if (!additionMatchesBlock(addition, message.content[blockIndex]))
285
+ return undefined;
286
+ blockIndex += 1;
287
+ }
226
288
  if (blockIndex !== message.content.length)
227
289
  return undefined;
228
- return { envelope, visible: anchors };
290
+ return { envelope, visible: anchors, additions: envelope.visibleAdditions };
229
291
  }
230
292
  export function restoreGrokNativeReplay(input, messages, route) {
231
293
  const restored = structuredClone(input);
@@ -236,11 +298,12 @@ export function restoreGrokNativeReplay(input, messages, route) {
236
298
  const replay = replayForMessage(message, route);
237
299
  if (!replay)
238
300
  continue;
239
- const { envelope, visible } = replay;
240
- const replacedLength = visible.length;
301
+ const { envelope, visible, additions } = replay;
302
+ const replacedLength = visible.length + additions.length;
241
303
  let start = -1;
242
304
  for (let index = cursor; index <= restored.length - replacedLength; index += 1) {
243
- if (visible.every((item, offset) => visibleItemMatchesInput(item, restored[index + offset]))) {
305
+ if (visible.every((item, offset) => visibleItemMatchesInput(item, restored[index + offset])) &&
306
+ additions.every((addition, offset) => additionMatchesInput(addition, restored[index + visible.length + offset]))) {
244
307
  start = index;
245
308
  break;
246
309
  }
package/lib/index.js CHANGED
@@ -522,15 +522,8 @@ async function serializeNativeAware(messages, route, routeConfig, ctx, options)
522
522
  responsesCompat: routeConfig.responsesCompat,
523
523
  ...extra,
524
524
  });
525
- const firstMessage = messages[0];
526
- const systemHead = firstMessage?.role === "system" ? firstMessage : undefined;
527
- const surfaceMessages = systemHead ? messages.slice(1) : messages;
528
- const systemPrompt = systemHead?.content
529
- .filter((block) => block.type === "text")
530
- .map((block) => block.text)
531
- .join("") || undefined;
532
525
  const prelude = await serializeDshMessages([], ctx, serializeOptions(undefined, {
533
- systemPrompt,
526
+ systemPrompt: options.system,
534
527
  includeSystemPrompt: true,
535
528
  }));
536
529
  const ephemeralPreludeItemCount = prelude.input.length;
@@ -551,7 +544,7 @@ async function serializeNativeAware(messages, route, routeConfig, ctx, options)
551
544
  input.push(...restoreGrokNativeReplay(serialized.input, batch, options.grokNativeReplayRoute));
552
545
  mergeMap(imageMap, serialized.imageMap);
553
546
  };
554
- for (const message of surfaceMessages ?? []) {
547
+ for (const message of messages ?? []) {
555
548
  assertSupportedCheckpointMessage(message);
556
549
  const state = session
557
550
  ? checkpointStateForMessage(session, message)
@@ -607,7 +600,7 @@ async function* remoteCompactionStream(options, routeConfig, ctx, next) {
607
600
  const history = stripCompactionDirective(options.messages);
608
601
  const route = currentRoute(options, routeConfig);
609
602
  try {
610
- const prepared = await serializeNativeAware(history, route, routeConfig, ctx, { signal: options.signal, tools: options.tools });
603
+ const prepared = await serializeNativeAware(history, route, routeConfig, ctx, { signal: options.signal, system: options.system, tools: options.tools });
611
604
  const cacheSessionId = promptCacheSessionId(route, routeConfig, ctx);
612
605
  const headers = await authenticatedHeaders(ctx, routeConfig, cacheSessionId, cacheSessionId === undefined ? null : route.sessionId);
613
606
  const generation = generationControlsFromSession(sessionFor(ctx, route.sessionId), route);
@@ -799,7 +792,7 @@ async function* managedResponsesStream(options, routeConfig, ctx) {
799
792
  try {
800
793
  if (options.stop !== undefined)
801
794
  throw Object.assign(new Error("LCX Responses does not support GenerateOptions.stop"), { code: "LCX_RESPONSES_UNSUPPORTED_OPTION" });
802
- const prepared = await serializeNativeAware(options.messages, route, routeConfig, ctx, { signal: options.signal, tools: options.tools });
795
+ const prepared = await serializeNativeAware(options.messages, route, routeConfig, ctx, { signal: options.signal, system: options.system, tools: options.tools });
803
796
  const cacheSessionId = promptCacheSessionId(route, routeConfig, ctx);
804
797
  const headers = await authenticatedHeaders(ctx, routeConfig, cacheSessionId, cacheSessionId === undefined ? null : route.sessionId);
805
798
  const body = buildResponsesBody({
@@ -846,6 +839,7 @@ async function* managedGrokNativeSearchStream(options, routeConfig, nativeSearch
846
839
  const visibleTools = grokVisibleFunctionTools(options.tools, nativeSearch);
847
840
  const prepared = await serializeNativeAware(options.messages, route, routeConfig, ctx, {
848
841
  signal: options.signal,
842
+ system: options.system,
849
843
  tools: visibleTools,
850
844
  grokNativeReplayRoute: nativeReplayRoute,
851
845
  });
@@ -484,40 +484,6 @@ function itemText(item) {
484
484
  return String(item.input ?? "");
485
485
  return "";
486
486
  }
487
- const GROK_RENDER_MARKER = /(?:\bstateless_invoke\s+)?\{?render_inline_citation(?:\s*\(\s*citation_id\s*=\s*\d+\s*\)|\s+with\s+citation_id\s+is\s+\d+)\}?/giu;
488
- const GROK_UI_CITATION_MARKER = /render_inline_citation[^\r\n]*/giu;
489
- const GROK_PRIVATE_RENDER_MARKER = /[\uE000-\uF8FF]render_inline_citation[^\uE000-\uF8FF\r\n]*[\uE000-\uF8FF]/giu;
490
- const GROK_NUMBERED_CITATION = /\[\[\d+\]\]\(https?:\/\/[^)\s]+\)/giu;
491
- const GROK_EOS_MARKER = /<\|eos\|>/giu;
492
- const GROK_SOURCE_TAIL = /(?:^|[ \t\r\n]+)Sources:\s*(?=(?:\r?\n)?\s*[-*])/iu;
493
- function sanitizeGrokVisibleText(value) {
494
- let text = value
495
- .replace(GROK_RENDER_MARKER, "")
496
- .replace(GROK_UI_CITATION_MARKER, "")
497
- .replace(GROK_PRIVATE_RENDER_MARKER, "")
498
- .replace(GROK_NUMBERED_CITATION, "")
499
- .replace(GROK_EOS_MARKER, "");
500
- const sourceTail = text.search(GROK_SOURCE_TAIL);
501
- if (sourceTail >= 0)
502
- text = text.slice(0, sourceTail);
503
- return text.replace(/[ \t]{2,}/gu, " ").replace(/[ \t]+([,.;:!?])/gu, "$1").replace(/[ \t]+\n/gu, "\n").trimEnd();
504
- }
505
- function sanitizeGrokTerminalItem(item, index) {
506
- const normalized = normalizedTerminalItem(item, index);
507
- if (normalized.type !== "message" || !Array.isArray(normalized.content))
508
- return normalized;
509
- return {
510
- ...normalized,
511
- content: normalized.content.map((part) => {
512
- if (!isObject(part) || (part.type !== "output_text" && part.type !== "refusal"))
513
- return structuredClone(part);
514
- const text = sanitizeGrokVisibleText(String(part.text ?? part.refusal ?? ""));
515
- return part.type === "output_text"
516
- ? { ...structuredClone(part), text }
517
- : { ...structuredClone(part), refusal: text };
518
- }),
519
- };
520
- }
521
487
  /** @param {UnknownRecord} item @param {number} index */
522
488
  function normalizedTerminalItem(item, index) {
523
489
  if (item.type === "message")
@@ -608,52 +574,43 @@ function recordMatches(record, item) {
608
574
  */
609
575
  async function* normalizedResponseEvents(source, meta = {}, options = {}) {
610
576
  const open = new Map();
611
- const grokTextProjection = new Map();
612
- const hiddenPrefixTokens = ["sources:", "render_inline_citation", "{render_inline_citation", "render_inline_citation", "stateless_invoke", "[[", "<|eos|>"];
613
- const safeGrokRawPrefix = (raw, final) => {
614
- if (final || raw.search(GROK_SOURCE_TAIL) >= 0)
615
- return raw;
616
- const lower = raw.toLowerCase();
617
- let holdStart = raw.length;
618
- const pendingSource = lower.lastIndexOf("sources:");
619
- if (pendingSource >= 0 && (pendingSource === 0 || /\s/u.test(raw[pendingSource - 1] ?? "")))
620
- holdStart = Math.min(holdStart, pendingSource);
621
- for (const token of hiddenPrefixTokens) {
622
- const complete = lower.lastIndexOf(token);
623
- if (complete >= 0) {
624
- const tail = raw.slice(complete);
625
- if (sanitizeGrokVisibleText(tail).toLowerCase().includes(token.replace(/^\{/u, "")))
626
- holdStart = Math.min(holdStart, complete);
627
- }
628
- const max = Math.min(token.length - 1, lower.length);
629
- for (let size = max; size > 0; size -= 1) {
630
- if (lower.endsWith(token.slice(0, size))) {
631
- holdStart = Math.min(holdStart, raw.length - size);
632
- break;
633
- }
634
- }
577
+ const completed = new Set();
578
+ const completedWireItems = new Map();
579
+ const serverToolIds = new Map();
580
+ const citations = new Map();
581
+ const observeCitation = (candidate) => {
582
+ if (!isObject(candidate))
583
+ return;
584
+ try {
585
+ const url = new URL(String(candidate.url ?? ""));
586
+ if (url.protocol !== "http:" && url.protocol !== "https:")
587
+ return;
588
+ const title = typeof candidate.title === "string"
589
+ ? candidate.title.replace(/[\r\n]+/gu, " ").trim()
590
+ : "";
591
+ if (!citations.has(url.href))
592
+ citations.set(url.href, title);
635
593
  }
636
- return raw.slice(0, holdStart);
594
+ catch { }
637
595
  };
638
- const grokProjectionDelta = (index, rawText, final = false) => {
639
- let state = grokTextProjection.get(index);
640
- if (!state) {
641
- state = { raw: "", emitted: "" };
642
- grokTextProjection.set(index, state);
596
+ const observeCitations = (item) => {
597
+ if (!isObject(item))
598
+ return;
599
+ if (item.type === "message" && Array.isArray(item.content)) {
600
+ for (const part of item.content) {
601
+ if (!isObject(part) || part.type !== "output_text" || !Array.isArray(part.annotations))
602
+ continue;
603
+ for (const annotation of part.annotations)
604
+ if (isObject(annotation) && annotation.type === "url_citation")
605
+ observeCitation(annotation);
606
+ }
643
607
  }
644
- state.raw = rawText;
645
- const target = sanitizeGrokVisibleText(safeGrokRawPrefix(rawText, final));
646
- if (!target.startsWith(state.emitted))
647
- return "";
648
- const delta = target.slice(state.emitted.length);
649
- state.emitted = target;
650
- if (final)
651
- grokTextProjection.delete(index);
652
- return delta;
608
+ if (item.type === "web_search_call" &&
609
+ isObject(item.action) &&
610
+ Array.isArray(item.action.sources))
611
+ for (const source of item.action.sources)
612
+ observeCitation(source);
653
613
  };
654
- const completed = new Set();
655
- const completedWireItems = new Map();
656
- const serverToolIds = new Map();
657
614
  const isServerToolItem = (value) => isObject(value) && options.serverToolTypes?.has(String(value.type ?? "")) === true;
658
615
  const observeServerTool = (item) => {
659
616
  const type = String(item.type);
@@ -686,6 +643,10 @@ async function* normalizedResponseEvents(source, meta = {}, options = {}) {
686
643
  const index = Number.isInteger(event.output_index)
687
644
  ? Number(event.output_index)
688
645
  : 0;
646
+ if ((event.type === "response.output_item.added" ||
647
+ event.type === "response.output_item.done") &&
648
+ isObject(event.item))
649
+ observeCitations(event.item);
689
650
  if (event.type === "response.output_item.done" && isObject(event.item))
690
651
  completedWireItems.set(index, structuredClone(event.item));
691
652
  if ((event.type === "response.output_item.added" ||
@@ -694,18 +655,10 @@ async function* normalizedResponseEvents(source, meta = {}, options = {}) {
694
655
  observeServerTool(event.item);
695
656
  continue;
696
657
  }
697
- // Grok may send many encrypted reasoning items with no visible summary.
698
- // Keep them in completedWireItems/nativeOutput, but do not open empty UI blocks.
699
- if (options.serverToolTypes &&
700
- (event.type === "response.output_item.added" || event.type === "response.output_item.done") &&
701
- isObject(event.item) && event.item.type === "reasoning" &&
702
- !itemText(event.item).trim() && !open.get(index)?.text.trim())
703
- continue;
704
658
  if (event.type === "response.output_item.added" && isObject(event.item)) {
705
- const rawItem = normalizedTerminalItem(event.item, index);
706
- const item = options.serverToolTypes ? sanitizeGrokTerminalItem(rawItem, index) : rawItem;
707
- open.set(index, { kind: itemKind(item), item: rawItem, text: itemText(rawItem) });
708
- yield { ...event, item: options.serverToolTypes ? addedShell(item, index) : item };
659
+ const item = normalizedTerminalItem(event.item, index);
660
+ open.set(index, { kind: itemKind(item), item, text: itemText(item) });
661
+ yield { ...event, item };
709
662
  continue;
710
663
  }
711
664
  if (event.type === "response.output_text.delta") {
@@ -726,19 +679,11 @@ async function* normalizedResponseEvents(source, meta = {}, options = {}) {
726
679
  record = next.value;
727
680
  }
728
681
  record.text = String(record.text ?? "") + String(event.delta ?? "");
729
- if (!options.serverToolTypes)
730
- yield event;
731
- else {
732
- const delta = grokProjectionDelta(index, record.text);
733
- if (delta)
734
- yield { ...event, delta };
735
- }
682
+ yield event;
736
683
  continue;
737
684
  }
738
685
  if (event.type === "response.reasoning_summary_text.delta" ||
739
686
  event.type === "response.reasoning_text.delta") {
740
- if (options.serverToolTypes && !open.has(index) && !String(event.delta ?? "").trim())
741
- continue;
742
687
  let record = open.get(index);
743
688
  if (!record) {
744
689
  const item = {
@@ -810,13 +755,7 @@ async function* normalizedResponseEvents(source, meta = {}, options = {}) {
810
755
  continue;
811
756
  }
812
757
  if (event.type === "response.output_item.done" && isObject(event.item)) {
813
- const rawItem = normalizedTerminalItem(event.item, index);
814
- const item = options.serverToolTypes ? sanitizeGrokTerminalItem(rawItem, index) : rawItem;
815
- if (options.serverToolTypes && item.type === "message") {
816
- const delta = grokProjectionDelta(index, itemText(rawItem), true);
817
- if (delta)
818
- yield { type: "response.output_text.delta", output_index: index, content_index: 0, item_id: item.id, delta };
819
- }
758
+ const item = normalizedTerminalItem(event.item, index);
820
759
  const identity = itemIdentity(item);
821
760
  if (identity)
822
761
  completed.add(identity);
@@ -836,23 +775,50 @@ async function* normalizedResponseEvents(source, meta = {}, options = {}) {
836
775
  .sort(([left], [right]) => left - right)
837
776
  .map(([, item]) => item);
838
777
  const nativeOutput = terminalOutput.map((item) => structuredClone(item));
778
+ const nativeVisibleAdditions = [];
839
779
  for (const item of terminalOutput) {
780
+ observeCitations(item);
840
781
  if (isServerToolItem(item))
841
782
  observeServerTool(item);
842
783
  }
843
784
  const output = terminalOutput
844
785
  .filter((item) => !isServerToolItem(item))
845
- .filter((item) => !(options.serverToolTypes && isObject(item) &&
846
- item.type === "reasoning" && !itemText(item).trim()))
847
786
  .map((item, terminalIndex) => isObject(item)
848
- ? (options.serverToolTypes
849
- ? sanitizeGrokTerminalItem(item, terminalIndex)
850
- : normalizedTerminalItem(item, terminalIndex))
787
+ ? normalizedTerminalItem(item, terminalIndex)
851
788
  : item);
789
+ if (options.serverToolTypes && citations.size > 0) {
790
+ const visibleText = output
791
+ .filter(isObject)
792
+ .map((item) => itemText(item))
793
+ .join("\n");
794
+ const missing = [...citations].filter(([url]) => !visibleText.includes(url));
795
+ if (missing.length > 0) {
796
+ const suffix = missing
797
+ .map(([url, title]) => `- ${title ? `${title}: ` : ""}${url}`)
798
+ .join("\n");
799
+ const responseId = String(response.id ?? "native")
800
+ .replace(/[^A-Za-z0-9_-]/gu, "_")
801
+ .slice(0, 40);
802
+ const fallbackItem = {
803
+ type: "message",
804
+ id: `msg_lcx_sources_${responseId}`.slice(0, 64),
805
+ role: "assistant",
806
+ status: "completed",
807
+ content: [{
808
+ type: "output_text",
809
+ text: `\n\nSources:\n${suffix}`,
810
+ annotations: [],
811
+ }],
812
+ };
813
+ output.push(fallbackItem);
814
+ nativeVisibleAdditions.push(structuredClone(fallbackItem));
815
+ }
816
+ }
852
817
  if (event.type === "response.completed" &&
853
818
  response.status === "completed" &&
854
819
  options.serverToolTypes) {
855
820
  meta.nativeOutput = nativeOutput;
821
+ meta.nativeVisibleAdditions = nativeVisibleAdditions;
856
822
  }
857
823
  const used = new Set();
858
824
  for (const [streamIndex, record] of open) {
@@ -869,11 +835,6 @@ async function* normalizedResponseEvents(source, meta = {}, options = {}) {
869
835
  : normalizedTerminalItem(record.item, streamIndex);
870
836
  if (terminalIndex >= 0)
871
837
  used.add(terminalIndex);
872
- if (options.serverToolTypes && item.type === "message") {
873
- const delta = grokProjectionDelta(streamIndex, itemText(item), true);
874
- if (delta)
875
- yield { type: "response.output_text.delta", output_index: streamIndex, content_index: 0, item_id: item.id, delta };
876
- }
877
838
  yield {
878
839
  type: "response.output_item.done",
879
840
  output_index: streamIndex,
@@ -884,7 +845,6 @@ async function* normalizedResponseEvents(source, meta = {}, options = {}) {
884
845
  completed.add(identity);
885
846
  }
886
847
  open.clear();
887
- grokTextProjection.clear();
888
848
  for (const [terminalIndex, candidate] of output.entries()) {
889
849
  if (!isObject(candidate) ||
890
850
  !itemKind(candidate) ||
@@ -1000,7 +960,7 @@ function rawArguments(value) {
1000
960
  }
1001
961
  }
1002
962
  /** @param {unknown} message */
1003
- function replayState(message, nativeOutput, nativeReplayRoute) {
963
+ function replayState(message, nativeOutput, nativeVisibleAdditions, nativeReplayRoute) {
1004
964
  if (!isObject(message))
1005
965
  return undefined;
1006
966
  const content = Array.isArray(message.content)
@@ -1011,7 +971,7 @@ function replayState(message, nativeOutput, nativeReplayRoute) {
1011
971
  const api = typeof message.api === "string" ? message.api : undefined;
1012
972
  if (!provider || !model || !api)
1013
973
  return undefined;
1014
- const grokNative = createGrokNativeReplayEnvelope(nativeOutput, nativeReplayRoute);
974
+ const grokNative = createGrokNativeReplayEnvelope(nativeOutput, nativeReplayRoute, nativeVisibleAdditions);
1015
975
  return {
1016
976
  response: {
1017
977
  kind: "pi-ai",
@@ -1189,7 +1149,7 @@ async function* toDshChunks(events, signal, wireMeta, nativeReplayRoute) {
1189
1149
  const reason = successfulFinish(message);
1190
1150
  const replay = reason.kind === "error"
1191
1151
  ? undefined
1192
- : replayState(message, wireMeta?.nativeOutput, nativeReplayRoute);
1152
+ : replayState(message, wireMeta?.nativeOutput, wireMeta?.nativeVisibleAdditions, nativeReplayRoute);
1193
1153
  yield {
1194
1154
  type: "finish",
1195
1155
  reason,
@@ -16,19 +16,24 @@ type WireTool = Record<string, unknown>;
16
16
  type WireItem = Record<string, unknown> & {
17
17
  type: string;
18
18
  };
19
+ type GrokNativeVisibleAddition = {
20
+ kind: "citation-sources";
21
+ textSha256: string;
22
+ };
19
23
  type GrokNativeReplayEnvelope = {
20
24
  kind: "xai-responses-native-search";
21
- version: 3;
25
+ version: 2;
22
26
  provider: string;
23
27
  model: string;
24
28
  sourceSessionId: string;
25
29
  routeAuthorityFingerprint: string;
26
30
  output: WireItem[];
31
+ visibleAdditions: GrokNativeVisibleAddition[];
27
32
  };
28
33
  export declare function grokNativeReplayRouteFingerprint(route: GrokNativeReplayRoute): string;
29
34
  export declare function grokNativeSearchEnabled(state: GrokNativeSearchState): boolean;
30
35
  export declare function grokVisibleFunctionTools(tools: readonly ToolSchema[] | undefined, state?: GrokNativeSearchState): ToolSchema[] | undefined;
31
36
  export declare function grokWireTools(tools: readonly unknown[] | undefined, state: GrokNativeSearchState): WireTool[];
32
- export declare function createGrokNativeReplayEnvelope(output: readonly unknown[] | undefined, route: GrokNativeReplayRoute | undefined): GrokNativeReplayEnvelope | undefined;
37
+ export declare function createGrokNativeReplayEnvelope(output: readonly unknown[] | undefined, route: GrokNativeReplayRoute | undefined, visibleAdditions?: readonly unknown[]): GrokNativeReplayEnvelope | undefined;
33
38
  export declare function restoreGrokNativeReplay(input: readonly unknown[], messages: readonly Message[], route: GrokNativeReplayRoute | undefined): unknown[];
34
39
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-lcx-codex",
3
- "version": "0.4.3-pre.12",
4
- "description": "Model-scoped DSH plugin: GPT Responses lifecycle, Native V2 compaction and Hosted/Alpha Search; Grok native Web/X Search",
3
+ "version": "0.4.3-pre.3",
4
+ "description": "DSH GPT Responses lifecycle owner with Native V2 compaction/replay and Hosted/Alpha Search capabilities",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -11,10 +11,7 @@
11
11
  "openai-responses",
12
12
  "sub2api",
13
13
  "newapi",
14
- "compaction",
15
- "grok",
16
- "xai",
17
- "x-search"
14
+ "compaction"
18
15
  ],
19
16
  "repository": {
20
17
  "type": "git",
@@ -59,7 +56,6 @@
59
56
  "@earendil-works/pi-ai": "0.85.1"
60
57
  },
61
58
  "devDependencies": {
62
- "@deepseek-ai/dsh": "0.1.5-alpha.1",
63
59
  "@deepseek-ai/schemastery": "^3.18.2",
64
60
  "@types/node": "^22.20.0",
65
61
  "@types/react": "~18.3.1",
@@ -68,21 +64,21 @@
68
64
  },
69
65
  "peerDependencies": {
70
66
  "@deepseek-ai/cordis": "^4.0.2",
71
- "@deepseek-ai/dsh-agent": "0.1.5-alpha.1",
72
- "@deepseek-ai/dsh-attachment": "0.1.5-alpha.1",
73
- "@deepseek-ai/dsh-client-locale": "0.1.5-alpha.1",
74
- "@deepseek-ai/dsh-client-modules": "0.1.5-alpha.1",
75
- "@deepseek-ai/dsh-client-store": "0.1.5-alpha.1",
76
- "@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.5-alpha.1",
77
- "@deepseek-ai/dsh-compaction": "0.1.5-alpha.1",
78
- "@deepseek-ai/dsh-credentials": "0.1.5-alpha.1",
79
- "@deepseek-ai/dsh-fs": "0.1.5-alpha.1",
80
- "@deepseek-ai/dsh-llm": "0.1.5-alpha.1",
81
- "@deepseek-ai/dsh-session": "0.1.5-alpha.1",
82
- "@deepseek-ai/dsh-settings": "0.1.5-alpha.1",
83
- "@deepseek-ai/dsh-tool-web": "0.1.5-alpha.1",
84
- "@deepseek-ai/dsh-tools": "0.1.5-alpha.1",
85
- "@deepseek-ai/dsh-web": "0.1.5-alpha.1",
67
+ "@deepseek-ai/dsh-agent": "0.1.3-alpha.2",
68
+ "@deepseek-ai/dsh-attachment": "0.1.3-alpha.2",
69
+ "@deepseek-ai/dsh-client-locale": "0.1.3-alpha.2",
70
+ "@deepseek-ai/dsh-client-modules": "0.1.3-alpha.2",
71
+ "@deepseek-ai/dsh-client-store": "0.1.3-alpha.2",
72
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.3-alpha.2",
73
+ "@deepseek-ai/dsh-compaction": "0.1.3-alpha.2",
74
+ "@deepseek-ai/dsh-credentials": "0.1.3-alpha.2",
75
+ "@deepseek-ai/dsh-fs": "0.1.3-alpha.2",
76
+ "@deepseek-ai/dsh-llm": "0.1.3-alpha.2",
77
+ "@deepseek-ai/dsh-session": "0.1.3-alpha.2",
78
+ "@deepseek-ai/dsh-settings": "0.1.3-alpha.2",
79
+ "@deepseek-ai/dsh-tool-web": "0.1.3-alpha.2",
80
+ "@deepseek-ai/dsh-tools": "0.1.3-alpha.2",
81
+ "@deepseek-ai/dsh-web": "0.1.3-alpha.2",
86
82
  "@deepseek-ai/schemastery": "^3.18.2",
87
83
  "react": "^18.2.0"
88
84
  },