@shuind/dsh-codex-harness 0.1.24 → 0.1.26

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
@@ -12,13 +12,15 @@
12
12
  - **旧版 Web 兼容**:在未提供新版模型设置/上下文设置 slot 的 DSH Web 中,Fast 和上下文容量会自动回退到旧版 composer slot;client 注入不依赖纯类型 slot 包。
13
13
  - **Responses 原生 apply_patch**:在 GPT Responses 请求的插件传输边界,把普通 JSON function tool 改写为 `type: "custom"` + OpenAI `lark` grammar;下一轮历史同步改写为 `custom_tool_call`。如果中转站拒绝 custom tool,则自动回退到普通 function tool,不改变 DSH 工具执行器。
14
14
  - **远程搜索与压缩**:OpenAI Responses 请求默认优先使用 hosted `web_search` 和 `/responses/compact`;失败时回退到 DSH 的本地实现。
15
+ - **远程压缩用量**:`/responses/compact` 返回标准 `usage` 时,插件会把 `input_tokens`、缓存读写和 `output_tokens` 原样拆分为 DSH 的用量字段;不再把远程压缩伪造成 `0`。中转站缺失或返回不一致的 usage 时,插件不会编造数字。
15
16
  - **上下文容量**:在 Web 中设置 `1K`–`1000K` tokens,设置值作用于下一次请求。
16
- - **活动状态**:Codex 等待模型响应或进行上下文压缩时,在 `Deep diving...` 右侧显示对应状态和耗时,让长时间 Deep Diving 不再像卡住。
17
+ - **自动压缩阈值**:Codex preset 的自动压缩会读取最新请求持久化的有效上下文容量;例如设置 `400K` 时,默认 `80%` pressure threshold 是 `320K`,不会错误地按适配器默认 `262,144` 在约 `210K` 提前触发。
18
+ - **活动状态**:Codex 请求模型、接收模型回复或进行上下文压缩时,在 `Deep diving...` 右侧显示对应阶段和耗时,让长时间 Deep Diving 不再像卡住。
17
19
 
18
20
  ## 安装
19
21
 
20
22
  ```sh
21
- dsh plugin --profile web add @shuind/dsh-codex-harness@0.1.24
23
+ dsh plugin --profile web add @shuind/dsh-codex-harness@0.1.26
22
24
  ```
23
25
 
24
26
  重启 Web,创建新会话,在模式菜单中选择 **Codex 模式**。
@@ -51,13 +53,13 @@ Fast 设置保存在 `codex` 命名空间,只对 Codex preset 生效。
51
53
 
52
54
  ### 上下文容量
53
55
 
54
- 点击 Web 顶部的上下文使用量指示器,可以设置下一次请求的上下文容量。上方 meter 显示当前请求的实际容量;修改设置后,需要发送下一次请求才会更新。
56
+ 点击 Web 顶部的上下文使用量指示器,可以设置下一次请求的上下文容量。上方 meter 显示当前请求的实际容量;修改设置后,需要发送下一次请求才会更新。Codex preset 的自动压缩也使用这个已经持久化的有效容量。
55
57
 
56
58
  上下文容量使用整数 K tokens,范围为 `1K` 到 `1000K`。实际可用上限仍取决于模型和中转站支持情况。
57
59
 
58
60
  ### 活动状态
59
61
 
60
- Codex 等待模型首个输出时,Web 会在 `Deep diving...` 右侧显示 `正在等待模型响应... · Ns`;开始上下文压缩后会切换为 `正在压缩上下文... · Ns`。收到模型输出、压缩结束或下一轮开始后自动隐藏。该状态由 Host 的 `sessionProjections` 投影驱动,因此本地压缩和远程 `/responses/compact` 都会显示同一种状态。
62
+ Codex 发起模型请求时,Web 会在 `Deep diving...` 右侧显示 `请求模型... · Ns`;收到首个非空模型 token(非流式适配器则收到 `assistant/message`)后切换为 `模型回复... · Ns`。模型回复结束、进入工具调用、压缩结束或下一轮开始后自动隐藏。该状态由 Host 的 `sessionProjections` 投影驱动,因此本地压缩和远程 `/responses/compact` 都会显示同一种状态。
61
63
 
62
64
  如果当前 DSH 组合没有加载 projection 服务,插件会跳过注册,客户端也会自动隐藏这行,不影响普通会话。
63
65
 
@@ -105,7 +107,7 @@ description: 使用 Codex 工具并开启可选协作提示词。
105
107
  order: 10
106
108
  ```
107
109
 
108
- 然后在 Web 的 **Agent 预设** 中选择 `My Codex`,再新建会话。这个 preset 会加载 Codex 工具,并开启协作提示词;网页搜索、远程压缩、后台任务控制和 Skills 等其他功能需要在同一个 `agent.cordis.yml` 中按需添加。
110
+ 然后在 Web 的 **Agent 预设** 中选择 `My Codex`,再新建会话。这个 preset 会加载 Codex 工具,并开启协作提示词;网页搜索、远程压缩、后台任务控制和 Skills 等其他功能需要在同一个 `agent.cordis.yml` 中按需添加。若希望自定义 preset 也修正自动压缩阈值,请在 compaction 隔离组中使用 `@shuind/dsh-codex-harness/compaction` 替代 `@deepseek-ai/dsh-compaction-basic`。
109
111
 
110
112
  ### 可选协作提示词(私货)
111
113
 
package/lib/client.js CHANGED
@@ -21,13 +21,52 @@ window.__ModuleLoader__.load({
21
21
  //#endregion
22
22
  //#region lib/types/client/activity.js
23
23
  /** Client-only inference used while the host activity projection frame is in flight. */
24
- /** Infer the active model phase from the existing conversation snapshot. */
24
+ /** Equality for selector hooks, whose default is reference identity. */
25
+ function sameModelActivity(left, right) {
26
+ return left?.activity === right?.activity && left?.startedAt === right?.startedAt;
27
+ }
28
+ /** Infer the request-phase clock from the existing conversation snapshot. */
25
29
  function awaitingModelStartedAt(snapshot) {
26
30
  if (!snapshot.running || snapshot.runningCalls.length > 0) return void 0;
27
31
  let latest;
28
32
  for (const timing of snapshot.turnTimings.values()) if (timing.endTime === void 0) latest = timing.startTime;
29
33
  return latest;
30
34
  }
35
+ /** Infer the first non-empty model token timestamp from the current snapshot. */
36
+ function modelReplyStartedAt(snapshot) {
37
+ if (!snapshot.running || snapshot.runningCalls.length > 0) return void 0;
38
+ let openTurn;
39
+ for (const [turn, timing] of snapshot.turnTimings) if (timing.endTime === void 0) openTurn = {
40
+ turn,
41
+ startTime: timing.startTime
42
+ };
43
+ if (openTurn === void 0) return void 0;
44
+ if (snapshot.partial !== null && snapshot.partial.turn === openTurn.turn) {
45
+ const partialReplyStartedAt = snapshot.partial.replyStartedAt;
46
+ if (partialReplyStartedAt !== void 0) return partialReplyStartedAt;
47
+ }
48
+ const chat = snapshot.chat;
49
+ const step = (chat?.timeline.turns.get(openTurn.turn))?.steps.findLast((candidate) => candidate.end === void 0);
50
+ if (step === void 0) return void 0;
51
+ for (const node of [...chat?.nodes.values() ?? []].reverse()) {
52
+ if (node.kind !== "assistant-step") continue;
53
+ const data = node.data;
54
+ if (data.turn === openTurn.turn && data.step === step.step && typeof data.replyStartedAt === "number") return data.replyStartedAt;
55
+ }
56
+ }
57
+ /** Infer the two-phase model activity from the current client snapshot. */
58
+ function modelActivity(snapshot) {
59
+ const startedAt = awaitingModelStartedAt(snapshot);
60
+ if (startedAt === void 0) return void 0;
61
+ const replyStartedAt = modelReplyStartedAt(snapshot);
62
+ return replyStartedAt === void 0 ? {
63
+ activity: "requesting-model",
64
+ startedAt
65
+ } : {
66
+ activity: "model-reply",
67
+ startedAt: replyStartedAt
68
+ };
69
+ }
31
70
  //#endregion
32
71
  //#region lib/types/client/index.js
33
72
  /** Browser controls for the Codex request settings mounted by the Host plugin. */
@@ -50,7 +89,8 @@ window.__ModuleLoader__.load({
50
89
  contextSizeDescription: "Next request capacity in K tokens. The meter above is current.",
51
90
  contextRestore: "Restore model default",
52
91
  activityCompacting: "Compacting context...",
53
- activityAwaitingModel: "Waiting for model response..."
92
+ activityRequestingModel: "Requesting model...",
93
+ activityModelReply: "Model replying..."
54
94
  };
55
95
  const zh = {
56
96
  fast: "Fast",
@@ -62,7 +102,8 @@ window.__ModuleLoader__.load({
62
102
  contextSizeDescription: "设置下次请求的上下文容量,单位为 K tokens;上方是当前请求。",
63
103
  contextRestore: "恢复模型默认值",
64
104
  activityCompacting: "正在压缩上下文...",
65
- activityAwaitingModel: "正在等待模型响应..."
105
+ activityRequestingModel: "请求模型...",
106
+ activityModelReply: "模型回复..."
66
107
  };
67
108
  function FastModeButton({ sessionId, useSessions, useSettings, setSetting, t }) {
68
109
  const agentPreset = useSessions((state) => state.byId[sessionId]?.agentPreset);
@@ -283,11 +324,12 @@ window.__ModuleLoader__.load({
283
324
  function ActivityLine({ sessionId, useSession, useSessions, useProjection, t }) {
284
325
  const agentPreset = useSessions((state) => state.byId[sessionId]?.agentPreset);
285
326
  const projectedActivity = useProjection("codexActivity");
286
- const fallbackStartedAt = useSession((snapshot) => awaitingModelStartedAt(snapshot));
287
- const activity = projectedActivity !== void 0 ? projectedActivity : isCodexPresetId(agentPreset) && fallbackStartedAt !== void 0 ? {
288
- activity: "awaiting-model",
289
- startedAt: fallbackStartedAt
290
- } : null;
327
+ const fallbackActivity = useSession(modelActivity, sameModelActivity);
328
+ const normalizedProjectedActivity = projectedActivity?.activity === "awaiting-model" ? {
329
+ activity: fallbackActivity?.activity ?? "requesting-model",
330
+ startedAt: fallbackActivity?.startedAt ?? projectedActivity.startedAt
331
+ } : projectedActivity;
332
+ const activity = projectedActivity !== void 0 ? normalizedProjectedActivity : isCodexPresetId(agentPreset) && fallbackActivity !== void 0 ? fallbackActivity : null;
291
333
  const [now, setNow] = (0, react.useState)(() => Date.now());
292
334
  (0, react.useEffect)(() => {
293
335
  if (!isCodexPresetId(agentPreset) || activity === void 0 || activity === null) return void 0;
@@ -296,9 +338,13 @@ window.__ModuleLoader__.load({
296
338
  setNow(Date.now());
297
339
  }, 1e3);
298
340
  return () => clearInterval(timer);
299
- }, [agentPreset, activity?.startedAt]);
341
+ }, [
342
+ agentPreset,
343
+ activity?.activity,
344
+ activity?.startedAt
345
+ ]);
300
346
  if (!isCodexPresetId(agentPreset) || activity === void 0 || activity === null) return null;
301
- const label = activity.activity === "awaiting-model" ? t("activityAwaitingModel") : t("activityCompacting");
347
+ const label = activity.activity === "compaction" ? t("activityCompacting") : activity.activity === "model-reply" ? t("activityModelReply") : t("activityRequestingModel");
302
348
  return (0, react_jsx_runtime.jsxs)("span", {
303
349
  id: "codex-activity",
304
350
  role: "status",
@@ -0,0 +1,45 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import BasicCompactionEngine from "@deepseek-ai/dsh-compaction-basic";
3
+ //#region lib/types/compaction.js
4
+ /** Codex compaction backend that honors the latest durable request capacity. */
5
+ /** Overlay only the capacity lookup used by pressure compaction. */
6
+ function withDurableContextCapacity(llm, activeSession) {
7
+ return new Proxy(llm, { get(target, property) {
8
+ if (property === "resolveModelInfo") return async (provider, model, signal) => {
9
+ const info = await target.resolveModelInfo(provider, model, signal);
10
+ const requestContext = activeSession.getStore()?.requestContext();
11
+ if (requestContext?.provider !== provider || requestContext.model !== model || requestContext.contextWindow === void 0) return info;
12
+ return {
13
+ ...info,
14
+ context: {
15
+ ...info.context,
16
+ contextWindow: requestContext.contextWindow
17
+ }
18
+ };
19
+ };
20
+ const value = Reflect.get(target, property, target);
21
+ return typeof value === "function" ? value.bind(target) : value;
22
+ } });
23
+ }
24
+ /**
25
+ * Keep the upstream compaction policy, retention, overflow recovery, and
26
+ * transaction implementation intact. The scoped LLM seam changes only the
27
+ * context metadata returned during a pressure check.
28
+ */
29
+ var CodexCompactionEngine = class extends BasicCompactionEngine {
30
+ static inject = BasicCompactionEngine.inject;
31
+ static Config = BasicCompactionEngine.Config;
32
+ activeSession;
33
+ constructor(ctx, config = {}) {
34
+ const activeSession = new AsyncLocalStorage();
35
+ const llm = ctx.get("llm");
36
+ if (llm === void 0) throw new Error("codex compaction requires ctx.llm");
37
+ super(ctx.extend({ llm: withDurableContextCapacity(llm, activeSession) }), config);
38
+ this.activeSession = activeSession;
39
+ }
40
+ compactIfNeeded(agent, trigger, signal) {
41
+ return this.activeSession.run(agent.session, () => super.compactIfNeeded(agent, trigger, signal));
42
+ }
43
+ };
44
+ //#endregion
45
+ export { CodexCompactionEngine as default };
package/lib/index.js CHANGED
@@ -5,6 +5,7 @@ import { randomBytes } from "node:crypto";
5
5
  import { resolve } from "node:path";
6
6
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
7
7
  import { AsyncLocalStorage } from "node:async_hooks";
8
+ import { isTokenDelta } from "@deepseek-ai/dsh-llm/message";
8
9
  import { z as z$1 } from "zod";
9
10
  //#region lib/types/context.js
10
11
  const CODEX_CONTEXT_MAX = 1e6;
@@ -13,9 +14,8 @@ const CODEX_CONTEXT_MAX = 1e6;
13
14
  /** Parser and line-oriented applicator for Codex's `apply_patch` language. */
14
15
  const APPLY_PATCH_DESCRIPTION = "Edits files using Codex patch syntax with Begin/End Patch markers and file update directives. In hunk lines, the first character is the operation marker; repeat a source-leading marker when the source line itself starts with one.";
15
16
  /** The grammar sent to providers that support OpenAI custom grammar tools. */
16
- const APPLY_PATCH_GRAMMAR = String.raw`start: begin_patch environment_id? hunk+ end_patch
17
+ const APPLY_PATCH_GRAMMAR = String.raw`start: begin_patch hunk+ end_patch
17
18
  begin_patch: "*** Begin Patch" LF
18
- environment_id: "*** Environment ID: " filename LF
19
19
  end_patch: "*** End Patch" LF?
20
20
  hunk: add_hunk | delete_hunk | update_hunk
21
21
  add_hunk: "*** Add File: " filename LF add_line+
@@ -37,7 +37,6 @@ function isFileHeader(line) {
37
37
  const trimmed = line.trim();
38
38
  return trimmed.startsWith("*** Add File: ") || trimmed.startsWith("*** Delete File: ") || trimmed.startsWith("*** Update File: ");
39
39
  }
40
- const ENVIRONMENT_ID_MARKER = "*** Environment ID:";
41
40
  function unwrapHeredoc(input) {
42
41
  const normalized = input.replaceAll("\r\n", "\n").trim();
43
42
  const lines = normalized.split("\n");
@@ -66,11 +65,7 @@ function parsePatch(input) {
66
65
  const files = [];
67
66
  let index = 1;
68
67
  const end = lines.length - 1;
69
- if (lines[index]?.trim().startsWith(ENVIRONMENT_ID_MARKER) === true) {
70
- if (lines[index].trim().slice(19).trim().length === 0) invalid("environment id cannot be empty");
71
- index++;
72
- if (lines[index]?.trim().startsWith(ENVIRONMENT_ID_MARKER) === true) invalid("environment id cannot be specified more than once");
73
- }
68
+ if (lines[index]?.trim().startsWith("*** Environment ID:") === true) invalid("environment selection is unavailable in this single-environment DSH plugin");
74
69
  while (index < end) {
75
70
  const header = lines[index++]?.trim();
76
71
  if (header === void 0) invalid("unexpected end of input");
@@ -218,6 +213,7 @@ function applyPatchHunks(original, hunks, label = "file") {
218
213
  /** Codex `exec_command` and `write_stdin` execution over dsh capability seams. */
219
214
  const STATES = /* @__PURE__ */ new WeakMap();
220
215
  const MIN_YIELD_TIME_MS = 250;
216
+ const WINDOWS_INITIAL_EXEC_YIELD_TIME_MS = 1e4;
221
217
  const MIN_EMPTY_YIELD_TIME_MS = 5e3;
222
218
  const MAX_YIELD_TIME_MS = 3e4;
223
219
  const MAX_EMPTY_YIELD_TIME_MS = 3e5;
@@ -236,8 +232,9 @@ function stateFor(agent) {
236
232
  function positiveFinite(name, value) {
237
233
  if (value !== void 0 && (!Number.isFinite(value) || value < 0)) throw new Error(`${name} must be a non-negative finite number`);
238
234
  }
239
- function normalizeWaitMs(value, fallback, empty = false) {
240
- return Math.max(empty ? MIN_EMPTY_YIELD_TIME_MS : MIN_YIELD_TIME_MS, Math.min(empty ? MAX_EMPTY_YIELD_TIME_MS : MAX_YIELD_TIME_MS, Math.trunc(value ?? fallback)));
235
+ function normalizeWaitMs(value, fallback, empty = false, initialExec = false) {
236
+ const lower = empty ? MIN_EMPTY_YIELD_TIME_MS : initialExec && process.platform === "win32" ? WINDOWS_INITIAL_EXEC_YIELD_TIME_MS : MIN_YIELD_TIME_MS;
237
+ return Math.max(lower, Math.min(empty ? MAX_EMPTY_YIELD_TIME_MS : MAX_YIELD_TIME_MS, Math.trunc(value ?? fallback)));
241
238
  }
242
239
  function outputLimit(maxOutputBytes, maxOutputTokens) {
243
240
  const tokens = maxOutputTokens ?? DEFAULT_OUTPUT_TOKENS;
@@ -369,7 +366,7 @@ async function runExecCommand(ctx, args, exec, config) {
369
366
  const sendRequest = {
370
367
  text: commandFor(args),
371
368
  submit: true,
372
- waitMs: normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs),
369
+ waitMs: normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs, false, true),
373
370
  signal: exec.signal
374
371
  };
375
372
  const result = await terminals.startSend(agent, spawned.sessionId, sendRequest).done;
@@ -432,7 +429,7 @@ async function runExecCommand(ctx, args, exec, config) {
432
429
  signal: exec.signal
433
430
  }));
434
431
  try {
435
- await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs), exec.signal);
432
+ await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs, false, true), exec.signal);
436
433
  const output = readShellOutput(process.readOutput(), maxBytes);
437
434
  if (process.status === "running") {
438
435
  if (exec.agent === void 0) {
@@ -899,6 +896,29 @@ function compactText(body) {
899
896
  const encrypted = text.join("\n\n").trim();
900
897
  return encrypted.length === 0 ? "" : `${REMOTE_COMPACTION_OPEN}${encrypted}${REMOTE_COMPACTION_CLOSE}`;
901
898
  }
899
+ function nonNegativeInteger(value) {
900
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
901
+ }
902
+ /** Map the Responses compact usage object without inventing missing counts. */
903
+ function compactUsage(body) {
904
+ if (typeof body.usage !== "object" || body.usage === null) return void 0;
905
+ const raw = body.usage;
906
+ const inputTokens = nonNegativeInteger(raw.input_tokens);
907
+ const outputTokens = nonNegativeInteger(raw.output_tokens);
908
+ if (inputTokens === void 0 || outputTokens === void 0) return void 0;
909
+ const details = typeof raw.input_tokens_details === "object" && raw.input_tokens_details !== null ? raw.input_tokens_details : void 0;
910
+ const cacheReadTokens = nonNegativeInteger(details?.cached_tokens);
911
+ const cacheWriteTokens = nonNegativeInteger(details?.cache_write_tokens);
912
+ const cached = cacheReadTokens ?? 0;
913
+ const writes = cacheWriteTokens ?? 0;
914
+ if (cached + writes > inputTokens) return void 0;
915
+ return {
916
+ inputTokens: inputTokens - cached - writes,
917
+ outputTokens,
918
+ ...cacheReadTokens !== void 0 && cacheReadTokens > 0 ? { cacheReadTokens } : {},
919
+ ...cacheWriteTokens !== void 0 && cacheWriteTokens > 0 ? { cacheWriteTokens } : {}
920
+ };
921
+ }
902
922
  async function remoteCompact(ctx, options) {
903
923
  const profile = profileOf(ctx, options.provider);
904
924
  if (!supportsResponses(profile, options.provider) || profile?.baseURL === void 0) throw new Error("Codex remote compaction requires an OpenAI Responses provider with baseURL");
@@ -911,15 +931,20 @@ async function remoteCompact(ctx, options) {
911
931
  ...options.signal === void 0 ? {} : { signal: options.signal }
912
932
  });
913
933
  if (!response.ok) throw new Error(`remote compaction returned HTTP ${response.status}`);
914
- const text = compactText(await response.json());
934
+ const body = await response.json();
935
+ const text = compactText(body);
915
936
  if (text.length === 0) throw new Error("remote compaction returned no compaction text");
916
- return text;
937
+ const usage = compactUsage(body);
938
+ return {
939
+ text,
940
+ ...usage === void 0 ? {} : { usage }
941
+ };
917
942
  }
918
943
  /** Remote-first compaction waterfall with the existing DSH path as fallback. */
919
944
  function remoteCompactStream(ctx, options, next) {
920
945
  return (async function* () {
921
946
  try {
922
- const text = await remoteCompact(ctx, options);
947
+ const result = await remoteCompact(ctx, options);
923
948
  yield {
924
949
  type: "block-start",
925
950
  index: 0,
@@ -928,22 +953,19 @@ function remoteCompactStream(ctx, options, next) {
928
953
  yield {
929
954
  type: "text-delta",
930
955
  index: 0,
931
- text
956
+ text: result.text
932
957
  };
933
958
  yield {
934
959
  type: "block-end",
935
960
  index: 0,
936
961
  block: {
937
962
  type: "text",
938
- text
963
+ text: result.text
939
964
  }
940
965
  };
941
- yield {
966
+ if (result.usage !== void 0) yield {
942
967
  type: "usage",
943
- usage: {
944
- inputTokens: 0,
945
- outputTokens: 0
946
- }
968
+ usage: result.usage
947
969
  };
948
970
  yield {
949
971
  type: "finish",
@@ -959,13 +981,28 @@ function remoteCompactStream(ctx, options, next) {
959
981
  }
960
982
  //#endregion
961
983
  //#region lib/types/activity.js
962
- /** Host-side activity projection for the Codex compaction status line. */
984
+ /** Host-side activity projection for the Codex status line. */
985
+ /** Only a non-empty model delta starts the reply phase; usage and finish frames do not. */
986
+ function startsModelReply(event) {
987
+ if (event.type === "assistant/message") return true;
988
+ if (event.type !== "assistant/chunk" || typeof event.data !== "object" || event.data === null) return false;
989
+ const chunk = event.data.chunk;
990
+ return chunk !== void 0 && isTokenDelta(chunk);
991
+ }
963
992
  /** Persisted and wire shape of the Codex activity value. */
964
993
  const codexActivitySchema = z$1.union([
965
994
  z$1.object({
966
995
  activity: z$1.literal("compaction"),
967
996
  startedAt: z$1.number().int().nonnegative()
968
997
  }),
998
+ z$1.object({
999
+ activity: z$1.literal("requesting-model"),
1000
+ startedAt: z$1.number().int().nonnegative()
1001
+ }),
1002
+ z$1.object({
1003
+ activity: z$1.literal("model-reply"),
1004
+ startedAt: z$1.number().int().nonnegative()
1005
+ }),
969
1006
  z$1.object({
970
1007
  activity: z$1.literal("awaiting-model"),
971
1008
  startedAt: z$1.number().int().nonnegative()
@@ -979,11 +1016,15 @@ function applyCodexActivity(state, event) {
979
1016
  startedAt: event.time
980
1017
  };
981
1018
  if (event.type === "step/start" || event.type === "llm/retry-started") return {
982
- activity: "awaiting-model",
1019
+ activity: "requesting-model",
1020
+ startedAt: event.time
1021
+ };
1022
+ if (startsModelReply(event) && (state?.activity === "requesting-model" || state?.activity === "awaiting-model")) return {
1023
+ activity: "model-reply",
983
1024
  startedAt: event.time
984
1025
  };
985
1026
  if (event.type === "turn/start" || event.type === "turn/end" || event.type === "compaction/end") return null;
986
- if (state?.activity === "awaiting-model" && (event.type === "llm/retry" || event.type === "tool/call" || event.type === "step/end")) return null;
1027
+ if ((state?.activity === "requesting-model" || state?.activity === "model-reply" || state?.activity === "awaiting-model") && (event.type === "llm/retry" || event.type === "tool/call" || event.type === "step/end")) return null;
987
1028
  return state;
988
1029
  }
989
1030
  const codexActivityProjection = {
@@ -997,7 +1038,7 @@ const codexActivityProjection = {
997
1038
  },
998
1039
  schema: codexActivitySchema,
999
1040
  view: (state) => state,
1000
- stateVersion: 1
1041
+ stateVersion: 2
1001
1042
  };
1002
1043
  /** Register the optional activity unit without requiring the projection seam in every assembly. */
1003
1044
  function registerCodexActivityProjection(ctx) {
@@ -1018,12 +1059,13 @@ const inject = [
1018
1059
  "credentials",
1019
1060
  "settings"
1020
1061
  ];
1062
+ const DEFAULT_MAX_OUTPUT_BYTES = 1048576;
1021
1063
  /** Runtime configuration schema for the Codex tool bridge. */
1022
1064
  const Config = z.object({
1023
1065
  defaultYieldTimeMs: z.number().step(1).min(0).default(1e4),
1024
1066
  pollYieldTimeMs: z.number().step(1).min(0).default(5e3),
1025
1067
  writeYieldTimeMs: z.number().step(1).min(0).default(250),
1026
- maxOutputBytes: z.number().step(1).min(1).default(64e3),
1068
+ maxOutputBytes: z.number().step(1).min(1).default(DEFAULT_MAX_OUTPUT_BYTES),
1027
1069
  hostedWebSearch: z.boolean().default(true),
1028
1070
  remoteCompact: z.boolean().default(true),
1029
1071
  collaborationPrompt: z.boolean().default(false)
@@ -1566,7 +1608,7 @@ function apply(ctx, config = {}) {
1566
1608
  defaultYieldTimeMs: config.defaultYieldTimeMs ?? 1e4,
1567
1609
  pollYieldTimeMs: config.pollYieldTimeMs ?? 5e3,
1568
1610
  writeYieldTimeMs: config.writeYieldTimeMs ?? 250,
1569
- maxOutputBytes: config.maxOutputBytes ?? 64e3,
1611
+ maxOutputBytes: config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
1570
1612
  hostedWebSearch: config.hostedWebSearch ?? true,
1571
1613
  remoteCompact: config.remoteCompact ?? true,
1572
1614
  collaborationPrompt: config.collaborationPrompt ?? false
@@ -1,6 +1,6 @@
1
1
  /** Pure client/host-shared type for the Codex activity projection value. */
2
2
  export type CodexActivity = {
3
- activity: 'compaction' | 'awaiting-model';
3
+ activity: 'compaction' | 'requesting-model' | 'model-reply' | 'awaiting-model';
4
4
  startedAt: number;
5
5
  };
6
6
  //# sourceMappingURL=activity-types.d.ts.map
@@ -1,14 +1,15 @@
1
- /** Host-side activity projection for the Codex compaction status line. */
1
+ /** Host-side activity projection for the Codex status line. */
2
2
  import type { Context } from '@deepseek-ai/cordis';
3
3
  import { z, type ZodType } from 'zod';
4
4
  import type { CodexActivity } from './activity-types.ts';
5
5
  export type { CodexActivity } from './activity-types.ts';
6
- /** The only long-running Codex activity currently surfaced to the client. */
6
+ /** Long-running Codex activities currently surfaced to the client. */
7
7
  export type CodexActivityState = CodexActivity | null;
8
8
  /** Minimal event shape needed by this projection; no compaction package import is required. */
9
9
  export interface CodexActivityEvent {
10
10
  type: string;
11
11
  time: number;
12
+ data?: unknown;
12
13
  }
13
14
  /** Persisted and wire shape of the Codex activity value. */
14
15
  export declare const codexActivitySchema: ZodType<CodexActivityState>;
@@ -25,7 +26,7 @@ export declare const codexActivityProjection: {
25
26
  };
26
27
  readonly schema: z.ZodType<CodexActivityState, unknown, z.core.$ZodTypeInternals<CodexActivityState, unknown>>;
27
28
  readonly view: (state: CodexActivityState) => CodexActivityState;
28
- readonly stateVersion: 1;
29
+ readonly stateVersion: 2;
29
30
  };
30
31
  /** Register the optional activity unit without requiring the projection seam in every assembly. */
31
32
  export declare function registerCodexActivityProjection(ctx: Context): void;
@@ -1,11 +1,31 @@
1
- /** Host-side activity projection for the Codex compaction status line. */
1
+ /** Host-side activity projection for the Codex status line. */
2
+ import { isTokenDelta } from '@deepseek-ai/dsh-llm/message';
2
3
  import { z } from 'zod';
4
+ /** Only a non-empty model delta starts the reply phase; usage and finish frames do not. */
5
+ function startsModelReply(event) {
6
+ if (event.type === 'assistant/message')
7
+ return true;
8
+ if (event.type !== 'assistant/chunk' || typeof event.data !== 'object' || event.data === null)
9
+ return false;
10
+ const chunk = event.data.chunk;
11
+ return chunk !== undefined && isTokenDelta(chunk);
12
+ }
3
13
  /** Persisted and wire shape of the Codex activity value. */
4
14
  export const codexActivitySchema = z.union([
5
15
  z.object({
6
16
  activity: z.literal('compaction'),
7
17
  startedAt: z.number().int().nonnegative(),
8
18
  }),
19
+ z.object({
20
+ activity: z.literal('requesting-model'),
21
+ startedAt: z.number().int().nonnegative(),
22
+ }),
23
+ z.object({
24
+ activity: z.literal('model-reply'),
25
+ startedAt: z.number().int().nonnegative(),
26
+ }),
27
+ // Accepted for rolling upgrades: an older Codex host may still send this
28
+ // ambiguous state until its projection definition is refreshed.
9
29
  z.object({
10
30
  activity: z.literal('awaiting-model'),
11
31
  startedAt: z.number().int().nonnegative(),
@@ -17,14 +37,20 @@ export function applyCodexActivity(state, event) {
17
37
  if (event.type === 'compaction/start')
18
38
  return { activity: 'compaction', startedAt: event.time };
19
39
  if (event.type === 'step/start' || event.type === 'llm/retry-started') {
20
- return { activity: 'awaiting-model', startedAt: event.time };
40
+ return { activity: 'requesting-model', startedAt: event.time };
41
+ }
42
+ if (startsModelReply(event)
43
+ && (state?.activity === 'requesting-model' || state?.activity === 'awaiting-model')) {
44
+ return { activity: 'model-reply', startedAt: event.time };
21
45
  }
22
46
  if (event.type === 'turn/start' || event.type === 'turn/end' || event.type === 'compaction/end')
23
47
  return null;
24
48
  // Keep the line through reasoning and streamed output. It is the only
25
49
  // visible indication that the model is still progressing; tool cards have
26
50
  // their own status, so a tool dispatch retires this ambient line.
27
- if (state?.activity === 'awaiting-model'
51
+ if ((state?.activity === 'requesting-model'
52
+ || state?.activity === 'model-reply'
53
+ || state?.activity === 'awaiting-model')
28
54
  && (event.type === 'llm/retry'
29
55
  || event.type === 'tool/call'
30
56
  || event.type === 'step/end'))
@@ -44,7 +70,7 @@ export const codexActivityProjection = {
44
70
  // the `stateSchema`/`wire` split.
45
71
  schema: codexActivitySchema,
46
72
  view: (state) => state,
47
- stateVersion: 1,
73
+ stateVersion: 2,
48
74
  };
49
75
  /** Register the optional activity unit without requiring the projection seam in every assembly. */
50
76
  export function registerCodexActivityProjection(ctx) {
@@ -1,6 +1,19 @@
1
1
  /** Client-only inference used while the host activity projection frame is in flight. */
2
2
  import type { ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client';
3
- export type ActivitySessionSnapshot = Pick<ConversationSnapshot, 'running' | 'partial' | 'runningCalls' | 'turnTimings'>;
4
- /** Infer the active model phase from the existing conversation snapshot. */
3
+ export type ActivitySessionSnapshot = Pick<ConversationSnapshot, 'running' | 'partial' | 'runningCalls' | 'turnTimings'> & {
4
+ /** Optional for hosts that predate the assembled Chat snapshot. */
5
+ readonly chat?: ConversationSnapshot['chat'];
6
+ };
7
+ export type CodexModelActivity = {
8
+ activity: 'requesting-model' | 'model-reply';
9
+ startedAt: number;
10
+ };
11
+ /** Equality for selector hooks, whose default is reference identity. */
12
+ export declare function sameModelActivity(left: CodexModelActivity | undefined, right: CodexModelActivity | undefined): boolean;
13
+ /** Infer the request-phase clock from the existing conversation snapshot. */
5
14
  export declare function awaitingModelStartedAt(snapshot: ActivitySessionSnapshot): number | undefined;
15
+ /** Infer the first non-empty model token timestamp from the current snapshot. */
16
+ export declare function modelReplyStartedAt(snapshot: ActivitySessionSnapshot): number | undefined;
17
+ /** Infer the two-phase model activity from the current client snapshot. */
18
+ export declare function modelActivity(snapshot: ActivitySessionSnapshot): CodexModelActivity | undefined;
6
19
  //# sourceMappingURL=activity.d.ts.map
@@ -1,5 +1,9 @@
1
1
  /** Client-only inference used while the host activity projection frame is in flight. */
2
- /** Infer the active model phase from the existing conversation snapshot. */
2
+ /** Equality for selector hooks, whose default is reference identity. */
3
+ export function sameModelActivity(left, right) {
4
+ return left?.activity === right?.activity && left?.startedAt === right?.startedAt;
5
+ }
6
+ /** Infer the request-phase clock from the existing conversation snapshot. */
3
7
  export function awaitingModelStartedAt(snapshot) {
4
8
  if (!snapshot.running || snapshot.runningCalls.length > 0)
5
9
  return undefined;
@@ -10,4 +14,48 @@ export function awaitingModelStartedAt(snapshot) {
10
14
  }
11
15
  return latest;
12
16
  }
17
+ /** Infer the first non-empty model token timestamp from the current snapshot. */
18
+ export function modelReplyStartedAt(snapshot) {
19
+ if (!snapshot.running || snapshot.runningCalls.length > 0)
20
+ return undefined;
21
+ let openTurn;
22
+ for (const [turn, timing] of snapshot.turnTimings) {
23
+ if (timing.endTime === undefined)
24
+ openTurn = { turn, startTime: timing.startTime };
25
+ }
26
+ if (openTurn === undefined)
27
+ return undefined;
28
+ if (snapshot.partial !== null && snapshot.partial.turn === openTurn.turn) {
29
+ const partialReplyStartedAt = snapshot.partial.replyStartedAt;
30
+ if (partialReplyStartedAt !== undefined)
31
+ return partialReplyStartedAt;
32
+ }
33
+ // A non-streaming adapter has no partial row. The assembled Chat view still
34
+ // carries the reply boundary, but only accept it for the currently open step
35
+ // so an earlier tool step in the same turn cannot mask a new request.
36
+ const chat = snapshot.chat;
37
+ const turn = chat?.timeline.turns.get(openTurn.turn);
38
+ const step = turn?.steps.findLast(candidate => candidate.end === undefined);
39
+ if (step === undefined)
40
+ return undefined;
41
+ for (const node of [...(chat?.nodes.values() ?? [])].reverse()) {
42
+ if (node.kind !== 'assistant-step')
43
+ continue;
44
+ const data = node.data;
45
+ if (data.turn === openTurn.turn && data.step === step.step && typeof data.replyStartedAt === 'number') {
46
+ return data.replyStartedAt;
47
+ }
48
+ }
49
+ return undefined;
50
+ }
51
+ /** Infer the two-phase model activity from the current client snapshot. */
52
+ export function modelActivity(snapshot) {
53
+ const startedAt = awaitingModelStartedAt(snapshot);
54
+ if (startedAt === undefined)
55
+ return undefined;
56
+ const replyStartedAt = modelReplyStartedAt(snapshot);
57
+ return replyStartedAt === undefined
58
+ ? { activity: 'requesting-model', startedAt }
59
+ : { activity: 'model-reply', startedAt: replyStartedAt };
60
+ }
13
61
  //# sourceMappingURL=activity.js.map
@@ -11,7 +11,8 @@ declare const en: {
11
11
  readonly contextSizeDescription: "Next request capacity in K tokens. The meter above is current.";
12
12
  readonly contextRestore: "Restore model default";
13
13
  readonly activityCompacting: "Compacting context...";
14
- readonly activityAwaitingModel: "Waiting for model response...";
14
+ readonly activityRequestingModel: "Requesting model...";
15
+ readonly activityModelReply: "Model replying...";
15
16
  };
16
17
  type CodexKey = keyof typeof en;
17
18
  declare module '@deepseek-ai/dsh-client-ui-slots' {
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** Browser controls for the Codex request settings mounted by the Host plugin. */
3
3
  import { useEffect, useState } from 'react';
4
4
  import { CODEX_CONTEXT_MAX, CODEX_CONTEXT_UNIT, isCodexPresetId } from "../context.js";
5
- import { awaitingModelStartedAt } from "./activity.js";
5
+ import { modelActivity, sameModelActivity } from "./activity.js";
6
6
  const NS = 'codex';
7
7
  const SETTINGS_NAMESPACE = 'codex';
8
8
  const MODEL_SETTINGS_SLOT = 'conversation.input.model.settings';
@@ -24,7 +24,8 @@ const en = {
24
24
  contextSizeDescription: 'Next request capacity in K tokens. The meter above is current.',
25
25
  contextRestore: 'Restore model default',
26
26
  activityCompacting: 'Compacting context...',
27
- activityAwaitingModel: 'Waiting for model response...',
27
+ activityRequestingModel: 'Requesting model...',
28
+ activityModelReply: 'Model replying...',
28
29
  };
29
30
  const zh = {
30
31
  fast: 'Fast',
@@ -36,7 +37,8 @@ const zh = {
36
37
  contextSizeDescription: '设置下次请求的上下文容量,单位为 K tokens;上方是当前请求。',
37
38
  contextRestore: '恢复模型默认值',
38
39
  activityCompacting: '正在压缩上下文...',
39
- activityAwaitingModel: '正在等待模型响应...',
40
+ activityRequestingModel: '请求模型...',
41
+ activityModelReply: '模型回复...',
40
42
  };
41
43
  function FastModeButton({ sessionId, useSessions, useSettings, setSetting, t }) {
42
44
  const agentPreset = useSessions(state => state.byId[sessionId]?.agentPreset);
@@ -143,14 +145,20 @@ export function ActivityLine({ sessionId, useSession, useSessions, useProjection
143
145
  // Keep the projection seam optional: this package must still load when the
144
146
  // host has no session-projection registry or has not carried this key.
145
147
  const projectedActivity = useProjection('codexActivity');
146
- // The projection is the authoritative clock. The session snapshot fallback
147
- // only covers the short wire gap before the next projection frame arrives;
148
- // running tool calls and streamed output intentionally remain silent.
149
- const fallbackStartedAt = useSession((snapshot) => awaitingModelStartedAt(snapshot));
148
+ // The projection is the authoritative phase and clock. The session snapshot
149
+ // fallback only covers the short wire gap before the next projection frame;
150
+ // tool calls intentionally remain silent because their cards own the status.
151
+ const fallbackActivity = useSession(modelActivity, sameModelActivity);
150
152
  // `undefined` means the key has not arrived yet; `null` is an authoritative
151
153
  // clear and must not be resurrected by the local snapshot fallback.
152
- const activity = projectedActivity !== undefined ? projectedActivity : (isCodexPresetId(agentPreset) && fallbackStartedAt !== undefined
153
- ? { activity: 'awaiting-model', startedAt: fallbackStartedAt }
154
+ const normalizedProjectedActivity = projectedActivity?.activity === 'awaiting-model'
155
+ ? {
156
+ activity: fallbackActivity?.activity ?? 'requesting-model',
157
+ startedAt: fallbackActivity?.startedAt ?? projectedActivity.startedAt,
158
+ }
159
+ : projectedActivity;
160
+ const activity = projectedActivity !== undefined ? normalizedProjectedActivity : (isCodexPresetId(agentPreset) && fallbackActivity !== undefined
161
+ ? fallbackActivity
154
162
  : null);
155
163
  const [now, setNow] = useState(() => Date.now());
156
164
  useEffect(() => {
@@ -159,12 +167,14 @@ export function ActivityLine({ sessionId, useSession, useSessions, useProjection
159
167
  setNow(Date.now());
160
168
  const timer = setInterval(() => { setNow(Date.now()); }, 1_000);
161
169
  return () => clearInterval(timer);
162
- }, [agentPreset, activity?.startedAt]);
170
+ }, [agentPreset, activity?.activity, activity?.startedAt]);
163
171
  if (!isCodexPresetId(agentPreset) || activity === undefined || activity === null)
164
172
  return null;
165
- const label = activity.activity === 'awaiting-model'
166
- ? t('activityAwaitingModel')
167
- : t('activityCompacting');
173
+ const label = activity.activity === 'compaction'
174
+ ? t('activityCompacting')
175
+ : activity.activity === 'model-reply'
176
+ ? t('activityModelReply')
177
+ : t('activityRequestingModel');
168
178
  return (_jsxs("span", { id: "codex-activity", role: "status", "aria-live": "polite", style: {
169
179
  display: 'inline-flex',
170
180
  alignItems: 'center',
@@ -0,0 +1,18 @@
1
+ /** Codex compaction backend that honors the latest durable request capacity. */
2
+ import { Context } from '@deepseek-ai/cordis';
3
+ import BasicCompactionEngine from '@deepseek-ai/dsh-compaction-basic';
4
+ import type { CompactionResult, CompactionTrigger } from '@deepseek-ai/dsh-compaction';
5
+ import type { Agent } from '@deepseek-ai/dsh-agent';
6
+ /**
7
+ * Keep the upstream compaction policy, retention, overflow recovery, and
8
+ * transaction implementation intact. The scoped LLM seam changes only the
9
+ * context metadata returned during a pressure check.
10
+ */
11
+ export default class CodexCompactionEngine extends BasicCompactionEngine {
12
+ static inject: string[];
13
+ static Config: import("@deepseek-ai/schemastery").default<import("@deepseek-ai/dsh-compaction-basic").BasicCompactionConfig>;
14
+ private readonly activeSession;
15
+ constructor(ctx: Context, config?: ConstructorParameters<typeof BasicCompactionEngine>[1]);
16
+ compactIfNeeded(agent: Agent, trigger: CompactionTrigger, signal: AbortSignal): Promise<CompactionResult | null>;
17
+ }
18
+ //# sourceMappingURL=compaction.d.ts.map
@@ -0,0 +1,48 @@
1
+ /** Codex compaction backend that honors the latest durable request capacity. */
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ import BasicCompactionEngine from '@deepseek-ai/dsh-compaction-basic';
4
+ /** Overlay only the capacity lookup used by pressure compaction. */
5
+ function withDurableContextCapacity(llm, activeSession) {
6
+ return new Proxy(llm, {
7
+ get(target, property) {
8
+ if (property === 'resolveModelInfo') {
9
+ return async (provider, model, signal) => {
10
+ const info = await target.resolveModelInfo(provider, model, signal);
11
+ const requestContext = activeSession.getStore()?.requestContext();
12
+ if (requestContext?.provider !== provider
13
+ || requestContext.model !== model
14
+ || requestContext.contextWindow === undefined)
15
+ return info;
16
+ return {
17
+ ...info,
18
+ context: { ...info.context, contextWindow: requestContext.contextWindow },
19
+ };
20
+ };
21
+ }
22
+ const value = Reflect.get(target, property, target);
23
+ return typeof value === 'function' ? value.bind(target) : value;
24
+ },
25
+ });
26
+ }
27
+ /**
28
+ * Keep the upstream compaction policy, retention, overflow recovery, and
29
+ * transaction implementation intact. The scoped LLM seam changes only the
30
+ * context metadata returned during a pressure check.
31
+ */
32
+ export default class CodexCompactionEngine extends BasicCompactionEngine {
33
+ static inject = BasicCompactionEngine.inject;
34
+ static Config = BasicCompactionEngine.Config;
35
+ activeSession;
36
+ constructor(ctx, config = {}) {
37
+ const activeSession = new AsyncLocalStorage();
38
+ const llm = ctx.get('llm');
39
+ if (llm === undefined)
40
+ throw new Error('codex compaction requires ctx.llm');
41
+ super(ctx.extend({ llm: withDurableContextCapacity(llm, activeSession) }), config);
42
+ this.activeSession = activeSession;
43
+ }
44
+ compactIfNeeded(agent, trigger, signal) {
45
+ return this.activeSession.run(agent.session, () => super.compactIfNeeded(agent, trigger, signal));
46
+ }
47
+ }
48
+ //# sourceMappingURL=compaction.js.map
@@ -29,7 +29,7 @@ export interface ExecResult {
29
29
  exit_code?: number;
30
30
  original_token_count?: number;
31
31
  }
32
- export declare function normalizeWaitMs(value: number | undefined, fallback: number, empty?: boolean): number;
32
+ export declare function normalizeWaitMs(value: number | undefined, fallback: number, empty?: boolean, initialExec?: boolean): number;
33
33
  /** Render the ordinary Responses tool result text used by Codex's unified exec tools. */
34
34
  export declare function renderExecResult(result: ExecResult): string;
35
35
  /** Execute one Codex command through the configured pipe or PTY capability. */
package/lib/types/exec.js CHANGED
@@ -3,6 +3,7 @@ import { randomBytes } from 'node:crypto';
3
3
  import { resolve as resolvePath } from 'node:path';
4
4
  const STATES = new WeakMap();
5
5
  const MIN_YIELD_TIME_MS = 250;
6
+ const WINDOWS_INITIAL_EXEC_YIELD_TIME_MS = 10_000;
6
7
  const MIN_EMPTY_YIELD_TIME_MS = 5_000;
7
8
  const MAX_YIELD_TIME_MS = 30_000;
8
9
  const MAX_EMPTY_YIELD_TIME_MS = 300_000;
@@ -21,8 +22,12 @@ function positiveFinite(name, value) {
21
22
  throw new Error(`${name} must be a non-negative finite number`);
22
23
  }
23
24
  }
24
- export function normalizeWaitMs(value, fallback, empty = false) {
25
- const lower = empty ? MIN_EMPTY_YIELD_TIME_MS : MIN_YIELD_TIME_MS;
25
+ export function normalizeWaitMs(value, fallback, empty = false, initialExec = false) {
26
+ const lower = empty
27
+ ? MIN_EMPTY_YIELD_TIME_MS
28
+ : initialExec && process.platform === 'win32'
29
+ ? WINDOWS_INITIAL_EXEC_YIELD_TIME_MS
30
+ : MIN_YIELD_TIME_MS;
26
31
  const upper = empty ? MAX_EMPTY_YIELD_TIME_MS : MAX_YIELD_TIME_MS;
27
32
  return Math.max(lower, Math.min(upper, Math.trunc(value ?? fallback)));
28
33
  }
@@ -178,7 +183,7 @@ export async function runExecCommand(ctx, args, exec, config) {
178
183
  const sendRequest = {
179
184
  text: commandFor(args),
180
185
  submit: true,
181
- waitMs: normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs),
186
+ waitMs: normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs, false, true),
182
187
  signal: exec.signal,
183
188
  };
184
189
  const operation = terminals.startSend(agent, spawned.sessionId, sendRequest);
@@ -250,7 +255,7 @@ export async function runExecCommand(ctx, args, exec, config) {
250
255
  signal: exec.signal,
251
256
  }));
252
257
  try {
253
- await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs), exec.signal);
258
+ await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs, false, true), exec.signal);
254
259
  const output = readShellOutput(process.readOutput(), maxBytes);
255
260
  if (process.status === 'running') {
256
261
  if (exec.agent === undefined) {
@@ -9,12 +9,13 @@ import { hostedWebSearchStream, installHostedWebSearch, remoteCompactStream } fr
9
9
  import { registerCodexActivityProjection } from "./activity.js";
10
10
  export const name = 'codex';
11
11
  export const inject = ['tools', 'systemPrompt', 'shell', 'fs', 'llm', 'credentials', 'settings'];
12
+ const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
12
13
  /** Runtime configuration schema for the Codex tool bridge. */
13
14
  export const Config = z.object({
14
15
  defaultYieldTimeMs: z.number().step(1).min(0).default(10_000),
15
16
  pollYieldTimeMs: z.number().step(1).min(0).default(5_000),
16
17
  writeYieldTimeMs: z.number().step(1).min(0).default(250),
17
- maxOutputBytes: z.number().step(1).min(1).default(64_000),
18
+ maxOutputBytes: z.number().step(1).min(1).default(DEFAULT_MAX_OUTPUT_BYTES),
18
19
  hostedWebSearch: z.boolean().default(true),
19
20
  remoteCompact: z.boolean().default(true),
20
21
  collaborationPrompt: z.boolean().default(false),
@@ -451,7 +452,7 @@ export function apply(ctx, config = {}) {
451
452
  defaultYieldTimeMs: config.defaultYieldTimeMs ?? 10_000,
452
453
  pollYieldTimeMs: config.pollYieldTimeMs ?? 5_000,
453
454
  writeYieldTimeMs: config.writeYieldTimeMs ?? 250,
454
- maxOutputBytes: config.maxOutputBytes ?? 64_000,
455
+ maxOutputBytes: config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
455
456
  hostedWebSearch: config.hostedWebSearch ?? true,
456
457
  remoteCompact: config.remoteCompact ?? true,
457
458
  collaborationPrompt: config.collaborationPrompt ?? false,
@@ -1,9 +1,8 @@
1
1
  /** Parser and line-oriented applicator for Codex's `apply_patch` language. */
2
2
  export const APPLY_PATCH_DESCRIPTION = 'Edits files using Codex patch syntax with Begin/End Patch markers and file update directives. In hunk lines, the first character is the operation marker; repeat a source-leading marker when the source line itself starts with one.';
3
3
  /** The grammar sent to providers that support OpenAI custom grammar tools. */
4
- export const APPLY_PATCH_GRAMMAR = String.raw `start: begin_patch environment_id? hunk+ end_patch
4
+ export const APPLY_PATCH_GRAMMAR = String.raw `start: begin_patch hunk+ end_patch
5
5
  begin_patch: "*** Begin Patch" LF
6
- environment_id: "*** Environment ID: " filename LF
7
6
  end_patch: "*** End Patch" LF?
8
7
  hunk: add_hunk | delete_hunk | update_hunk
9
8
  add_hunk: "*** Add File: " filename LF add_line+
@@ -27,7 +26,6 @@ function isFileHeader(line) {
27
26
  || trimmed.startsWith('*** Delete File: ')
28
27
  || trimmed.startsWith('*** Update File: ');
29
28
  }
30
- const ENVIRONMENT_ID_MARKER = '*** Environment ID:';
31
29
  function unwrapHeredoc(input) {
32
30
  const normalized = input.replaceAll('\r\n', '\n').trim();
33
31
  const lines = normalized.split('\n');
@@ -63,14 +61,8 @@ export function parsePatch(input) {
63
61
  const files = [];
64
62
  let index = 1;
65
63
  const end = lines.length - 1;
66
- if (lines[index]?.trim().startsWith(ENVIRONMENT_ID_MARKER) === true) {
67
- const environmentId = lines[index].trim().slice(ENVIRONMENT_ID_MARKER.length).trim();
68
- if (environmentId.length === 0)
69
- invalid('environment id cannot be empty');
70
- index++;
71
- if (lines[index]?.trim().startsWith(ENVIRONMENT_ID_MARKER) === true) {
72
- invalid('environment id cannot be specified more than once');
73
- }
64
+ if (lines[index]?.trim().startsWith('*** Environment ID:') === true) {
65
+ invalid('environment selection is unavailable in this single-environment DSH plugin');
74
66
  }
75
67
  while (index < end) {
76
68
  const header = lines[index++]?.trim();
@@ -437,6 +437,36 @@ function compactText(body) {
437
437
  const encrypted = text.join('\n\n').trim();
438
438
  return encrypted.length === 0 ? '' : `${REMOTE_COMPACTION_OPEN}${encrypted}${REMOTE_COMPACTION_CLOSE}`;
439
439
  }
440
+ function nonNegativeInteger(value) {
441
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
442
+ }
443
+ /** Map the Responses compact usage object without inventing missing counts. */
444
+ function compactUsage(body) {
445
+ if (typeof body.usage !== 'object' || body.usage === null)
446
+ return undefined;
447
+ const raw = body.usage;
448
+ const inputTokens = nonNegativeInteger(raw.input_tokens);
449
+ const outputTokens = nonNegativeInteger(raw.output_tokens);
450
+ if (inputTokens === undefined || outputTokens === undefined)
451
+ return undefined;
452
+ const details = typeof raw.input_tokens_details === 'object' && raw.input_tokens_details !== null
453
+ ? raw.input_tokens_details
454
+ : undefined;
455
+ const cacheReadTokens = nonNegativeInteger(details?.cached_tokens);
456
+ const cacheWriteTokens = nonNegativeInteger(details?.cache_write_tokens);
457
+ const cached = cacheReadTokens ?? 0;
458
+ const writes = cacheWriteTokens ?? 0;
459
+ // Responses input_tokens includes the cache buckets. Reject an inconsistent
460
+ // response instead of clamping it and reporting numbers that no longer sum.
461
+ if (cached + writes > inputTokens)
462
+ return undefined;
463
+ return {
464
+ inputTokens: inputTokens - cached - writes,
465
+ outputTokens,
466
+ ...cacheReadTokens !== undefined && cacheReadTokens > 0 ? { cacheReadTokens } : {},
467
+ ...cacheWriteTokens !== undefined && cacheWriteTokens > 0 ? { cacheWriteTokens } : {},
468
+ };
469
+ }
440
470
  async function remoteCompact(ctx, options) {
441
471
  const profile = profileOf(ctx, options.provider);
442
472
  if (!supportsResponses(profile, options.provider) || profile?.baseURL === undefined) {
@@ -457,17 +487,19 @@ async function remoteCompact(ctx, options) {
457
487
  const text = compactText(body);
458
488
  if (text.length === 0)
459
489
  throw new Error('remote compaction returned no compaction text');
460
- return text;
490
+ const usage = compactUsage(body);
491
+ return { text, ...usage === undefined ? {} : { usage } };
461
492
  }
462
493
  /** Remote-first compaction waterfall with the existing DSH path as fallback. */
463
494
  export function remoteCompactStream(ctx, options, next) {
464
495
  return (async function* () {
465
496
  try {
466
- const text = await remoteCompact(ctx, options);
497
+ const result = await remoteCompact(ctx, options);
467
498
  yield { type: 'block-start', index: 0, blockType: 'text' };
468
- yield { type: 'text-delta', index: 0, text };
469
- yield { type: 'block-end', index: 0, block: { type: 'text', text } };
470
- yield { type: 'usage', usage: { inputTokens: 0, outputTokens: 0 } };
499
+ yield { type: 'text-delta', index: 0, text: result.text };
500
+ yield { type: 'block-end', index: 0, block: { type: 'text', text: result.text } };
501
+ if (result.usage !== undefined)
502
+ yield { type: 'usage', usage: result.usage };
471
503
  yield { type: 'finish', reason: { kind: 'stop' } };
472
504
  }
473
505
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shuind/dsh-codex-harness",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "A Codex harness with a streamlined system prompt for GPT models that do not fit DSH's native interface, while remaining compatible with the DSH plugin ecosystem.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -18,6 +18,10 @@
18
18
  "types": "./lib/types/installer.d.ts",
19
19
  "default": "./lib/installer.js"
20
20
  },
21
+ "./compaction": {
22
+ "types": "./lib/types/compaction.d.ts",
23
+ "default": "./lib/compaction.js"
24
+ },
21
25
  "./client": {
22
26
  "types": "./lib/types/client/index.d.ts",
23
27
  "default": "./lib/client.js"
@@ -30,6 +34,7 @@
30
34
  "lib/index.js",
31
35
  "lib/invariant.js",
32
36
  "lib/installer.js",
37
+ "lib/compaction.js",
33
38
  "lib/types/**/*.js",
34
39
  "lib/types/**/*.d.ts",
35
40
  "lib/client.js",
@@ -87,6 +92,9 @@
87
92
  "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.0-rc.8",
88
93
  "@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.8",
89
94
  "@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.8",
95
+ "@deepseek-ai/dsh-compaction": ">=0.1.1-rc.2",
96
+ "@deepseek-ai/dsh-compaction-basic": ">=0.1.1-rc.2",
97
+ "@deepseek-ai/dsh-compaction-tool-result-pruner": ">=0.1.1-rc.2",
90
98
  "@deepseek-ai/dsh-credentials": ">=0.1.0-rc.8",
91
99
  "@deepseek-ai/dsh-fs": ">=0.0.1-rc.1",
92
100
  "@deepseek-ai/dsh-invariants": ">=0.0.1-rc.1",
@@ -95,6 +103,7 @@
95
103
  "@deepseek-ai/dsh-settings": ">=0.1.0-rc.8",
96
104
  "@deepseek-ai/dsh-sandbox": ">=0.0.1-rc.1",
97
105
  "@deepseek-ai/dsh-sandbox-policy": ">=0.0.1-rc.1",
106
+ "@deepseek-ai/dsh-session": ">=0.1.1-rc.2",
98
107
  "@deepseek-ai/dsh-shell": ">=0.0.1-rc.5",
99
108
  "@deepseek-ai/dsh-shell-env": ">=0.0.1-rc.3",
100
109
  "@deepseek-ai/dsh-system-prompt": ">=0.0.1-rc.1",
@@ -102,6 +111,7 @@
102
111
  "@deepseek-ai/dsh-tool-todo": ">=0.0.1-rc.1",
103
112
  "@deepseek-ai/dsh-terminal": ">=0.0.1-rc.3",
104
113
  "@deepseek-ai/dsh-tool-jobs": ">=0.1.0-rc.8",
114
+ "@deepseek-ai/dsh-token-meter": ">=0.1.1-rc.2",
105
115
  "@deepseek-ai/dsh-tools": ">=0.0.1-rc.1"
106
116
  },
107
117
  "peerDependenciesMeta": {
@@ -126,6 +136,15 @@
126
136
  "@deepseek-ai/dsh-client-ui-slots": {
127
137
  "optional": true
128
138
  },
139
+ "@deepseek-ai/dsh-compaction": {
140
+ "optional": true
141
+ },
142
+ "@deepseek-ai/dsh-compaction-basic": {
143
+ "optional": true
144
+ },
145
+ "@deepseek-ai/dsh-compaction-tool-result-pruner": {
146
+ "optional": true
147
+ },
129
148
  "@deepseek-ai/dsh-credentials": {
130
149
  "optional": true
131
150
  },
@@ -150,6 +169,9 @@
150
169
  "@deepseek-ai/dsh-sandbox-policy": {
151
170
  "optional": true
152
171
  },
172
+ "@deepseek-ai/dsh-session": {
173
+ "optional": true
174
+ },
153
175
  "@deepseek-ai/dsh-shell": {
154
176
  "optional": true
155
177
  },
@@ -171,6 +193,9 @@
171
193
  "@deepseek-ai/dsh-tool-jobs": {
172
194
  "optional": true
173
195
  },
196
+ "@deepseek-ai/dsh-token-meter": {
197
+ "optional": true
198
+ },
174
199
  "@deepseek-ai/dsh-tools": {
175
200
  "optional": true
176
201
  }
@@ -183,6 +208,9 @@
183
208
  "@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.8",
184
209
  "@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.8",
185
210
  "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.8",
211
+ "@deepseek-ai/dsh-compaction": "0.1.1-rc.2",
212
+ "@deepseek-ai/dsh-compaction-basic": "0.1.1-rc.2",
213
+ "@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.1-rc.2",
186
214
  "@deepseek-ai/dsh-credentials": "0.1.0-rc.8",
187
215
  "@deepseek-ai/dsh-fs": "0.1.0-rc.6",
188
216
  "@deepseek-ai/dsh-invariants": "0.1.0-rc.6",
@@ -191,6 +219,7 @@
191
219
  "@deepseek-ai/dsh-settings": "0.1.0-rc.8",
192
220
  "@deepseek-ai/dsh-sandbox": "0.1.0-rc.6",
193
221
  "@deepseek-ai/dsh-sandbox-policy": "0.1.0-rc.6",
222
+ "@deepseek-ai/dsh-session": "0.1.1-rc.2",
194
223
  "@deepseek-ai/dsh-shell": "0.1.0-rc.6",
195
224
  "@deepseek-ai/dsh-shell-env": "0.1.0-rc.6",
196
225
  "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.6",
@@ -198,6 +227,7 @@
198
227
  "@deepseek-ai/dsh-tool-todo": "0.1.0-rc.6",
199
228
  "@deepseek-ai/dsh-terminal": "0.1.0-rc.6",
200
229
  "@deepseek-ai/dsh-tool-jobs": "0.1.0-rc.8",
230
+ "@deepseek-ai/dsh-token-meter": "0.1.1-rc.2",
201
231
  "@deepseek-ai/dsh-tools": "0.1.0-rc.6",
202
232
  "@types/node": "^22.20.0",
203
233
  "@types/react": "~18.3.1",
@@ -70,7 +70,7 @@
70
70
  toolResultPruner: true
71
71
  config:
72
72
  - id: compaction-basic
73
- name: '@deepseek-ai/dsh-compaction-basic'
73
+ name: '@shuind/dsh-codex-harness/compaction'
74
74
 
75
75
  - id: command-compact
76
76
  name: '@deepseek-ai/dsh-command-compact'
@@ -70,7 +70,7 @@
70
70
  toolResultPruner: true
71
71
  config:
72
72
  - id: compaction-basic
73
- name: '@deepseek-ai/dsh-compaction-basic'
73
+ name: '@shuind/dsh-codex-harness/compaction'
74
74
 
75
75
  - id: command-compact
76
76
  name: '@deepseek-ai/dsh-command-compact'