@lyogvce/dsh-codex-subscription 0.2.9 → 0.2.11

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
@@ -15,14 +15,15 @@
15
15
  - 直接读取 `~/.codex/models_cache.json`(每次按文件 `mtime` 与大小做微秒级 `stat` 校验,仅在文件变化时重读解析),提取 `visibility === "list"` 的模型及其 `context_window`、`supported_reasoning_levels`(自动将 `ultra` 映射为 DSH 的 `max` 档位)与 `default_verbosity`。
16
16
  - **防旧版客户端降级污染**:自动将高版本 `models_cache.json` 备份至 `~/.codex/cache/dsh-codex-subscription/models-cache-backup.json`;当外部旧版进程(如 `dsh-subagent-codex` 内置的 `0.153.4`)将 `models_cache.json` 降级覆写为不含新模型的旧缓存时,自动拦截降级并保留高版本模型目录。
17
17
  - **可观测性日志**:自动在 `~/.codex/cache/dsh-codex-subscription/plugin.log`(带 512KB 自动滚动截断,忽略纯 `fetched_at` 心跳刷新)记录目录加载(`catalog.load` / `catalog.snapshot`)、降级拦截(`catalog.downgrade_rejected`)、备份恢复(`catalog.backup_restored`)与凭证续期(`auth.refreshed` / `auth.refresh_failed`)事件。
18
- 3. **支持按配置屏蔽模型(`disabledModels`)与自定义上下文窗口(`contextWindows`)**
18
+ 3. **支持按配置屏蔽模型(`disabledModels`)、自定义上下文窗口(`contextWindows`)与原生重试策略(`retryPolicy`)**
19
19
  - 在插件配置中通过 `disabledModels` 屏蔽指定模型 `slug`,保持模型选择器干净清爽。
20
20
  - 通过 `contextWindows` 按模型 `slug` 自定义向 DSH 会话与自动压缩器暴露的预算窗口(例如设为 `400000`);未配置时默认使用模型物理上限 `max_context_window`(`872000`)。同时底层 `PiCodexModel.contextWindow` 始终保持物理上限 `872000`,彻底防止 `dsh-llm-pi-ai` 在预算窗口与物理上限之间误判 `CONTEXT_WINDOW_EXCEEDED`。
21
+ - 通过 `retryPolicy` 暴露 DSH 原生 `RetryPolicySchema`(支持 `normal` / `always` 模式、`maxRetries`、`retryableCodes` 与 `backoff`,并随 `registration.replace` 热更新生效)。未配置时保持宿主默认策略(`mode: "normal"`、`maxRetries: 5`、默认 5 类错误码 `EMPTY_RESPONSE / RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT` 及 `500ms..10000ms` 退避)。**注意:`retryableCodes` 为完整覆盖集合而非追加列表**,显式配置时需同时列出默认错误码与新增错误码。
21
22
  4. **原生多轮思考链回放与工具调用**
22
23
  - 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、规范化纯 UUID `session-id` / `x-client-request-id` 缓存头、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载)。
23
24
  5. **Codex 订阅限额内联展示与 ⚡ Fast 极速模式一键切换(`conversation.input.right`)**
24
25
  - 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示限额胶囊(根据 `wham/usage` 返回的 `limit_window_seconds` 自适应展示 Plus 的 `5h X% · 7d Y%` 双窗口或 Pro 的 `7d X%` 单窗口,内置 60 秒缓存、单飞合并与每轮回复结束自动刷新),点击可展开查看对应窗口进度条、重置倒计时与手动刷新按钮;切换至其他 Provider 模型时自动隐藏。
25
- - **⚡ 会话级 Fast 极速模式(`service_tier: "priority"`,最高 2x 推理速度)**:每个会话可在限额弹窗底部独立开关 Fast 模式(互不影响,新会话默认继承插件设置中的 `fastMode`,未配置时默认 `false` 关闭),状态自动持久化至 `~/.codex/cache/dsh-codex-subscription/runtime-state.json`。开启后当前会话胶囊显示闪电标识 `⚡`,并在该会话请求体中自动注入 `"service_tier": "priority"`。
26
+ - **⚡ 会话树级 Fast 极速模式(`service_tier: "priority"`,最高 2x 推理速度,主会话与 Subagent / 智能体团队同开同关)**:每个主会话可在限额弹窗底部独立开关 Fast 模式(不同主会话互不影响,新会话默认继承插件设置中的 `fastMode`,未配置时默认 `false` 关闭),该主会话派生的所有 `subagent`、`subagent_fork`、`workflow` 子任务以及智能体团队成员(`spawn_teammate`)均自动绑定至同一主会话开关,实时联动、同开同关;状态自动持久化至 `~/.codex/cache/dsh-codex-subscription/runtime-state.json`。开启后胶囊显示闪电标识 `⚡`,并在请求体中自动注入 `"service_tier": "priority"`。
26
27
 
27
28
  ---
28
29
 
@@ -36,9 +37,11 @@
36
37
  dsh plugin --profile desktop add @lyogvce/dsh-codex-subscription
37
38
  ```
38
39
 
39
- ### 2. 配置屏蔽模型(`disabledModels`)与上下文窗口(`contextWindows`)
40
+ ### 2. 配置屏蔽模型(`disabledModels`)、上下文窗口(`contextWindows`)与重试策略(`retryPolicy`)
40
41
 
41
- 默认配置(见 [cordis.patch.yml](cordis.patch.yml))已屏蔽旧模型与备用模型,仅保留主力模型(如 `gpt-6.1-sol`、`gpt-6-astra`)。未配置 `contextWindows` 时,模型默认使用 `872000` 上下文窗口;若希望主动限制长上下文预算(例如统一设为 400K),可在 Profile 的 `cordis.patch.yml` 中配置:
42
+ 默认配置(见 [cordis.patch.yml](cordis.patch.yml))已屏蔽旧模型与备用模型,仅保留主力模型(如 `gpt-6.1-sol`、`gpt-6-astra`)。未配置 `contextWindows` 时,模型默认使用 `872000` 上下文窗口;未配置 `retryPolicy` 时默认使用宿主 `normal` 重试策略(最多额外重试 5 次,覆盖 `EMPTY_RESPONSE`、`RATE_LIMIT`、`SERVER`、`TIMEOUT`、`TRANSPORT`)。
43
+
44
+ 若希望自定义上下文预算窗口或扩展可重试错误码白名单(**注意:`retryableCodes` 是完整集合而非追加列表,显式填写时需同时保留默认 5 类错误码**),可在 Profile 的 `cordis.patch.yml` 中配置:
42
45
 
43
46
  ```yaml
44
47
  - id: codex-subscription
@@ -53,9 +56,13 @@ dsh plugin --profile desktop add @lyogvce/dsh-codex-subscription
53
56
  contextWindows:
54
57
  gpt-6.1-sol: 400000
55
58
  gpt-6-astra: 400000
59
+ retryPolicy:
60
+ mode: normal
61
+ maxRetries: 5
62
+ retryableCodes: [ EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT, QUOTA, ACCOUNT_QUOTA, AUTH, INVALID_REQUEST, PI_AI_ERROR, STREAM_CLOSED, MALFORMED_RESPONSE ]
56
63
  ```
57
64
 
58
- 可在 DSH 的 **设置 → 插件 → Codex 订阅直连** 页面或 `cordis.patch.yml` 中随时修改 `disabledModels` 与 `contextWindows`,实时生效无需重启。
65
+ 可在 DSH 的 **设置 → 插件 → Codex 订阅直连** 页面或 `cordis.patch.yml` 中随时修改 `disabledModels`、`contextWindows` 与 `retryPolicy`,实时热更新生效无需重启。
59
66
 
60
67
  ---
61
68
 
package/lib/client.js CHANGED
@@ -18,6 +18,26 @@ window.__ModuleLoader__.load({
18
18
  const stripped = sessionId.trim().replace(/^session-/iu, "").trim();
19
19
  return stripped.length > 0 ? stripped : void 0;
20
20
  }
21
+ function resolveRootSessionId(sessionId, sessionParents, directParentSessionId) {
22
+ const startKey = normalizeCodexSessionId(sessionId);
23
+ const directParentKey = normalizeCodexSessionId(directParentSessionId);
24
+ if (startKey === void 0) return directParentKey ?? "__default__";
25
+ const readParent = (key) => {
26
+ if (key === startKey && directParentKey !== void 0 && directParentKey !== startKey) return directParentKey;
27
+ if (!sessionParents) return void 0;
28
+ const raw = sessionParents instanceof Map ? sessionParents.get(key) : sessionParents[key];
29
+ return typeof raw === "string" ? normalizeCodexSessionId(raw) : void 0;
30
+ };
31
+ let current = startKey;
32
+ const visited = new Set([current]);
33
+ while (visited.size <= 32) {
34
+ const parent = readParent(current);
35
+ if (parent === void 0 || visited.has(parent)) break;
36
+ visited.add(parent);
37
+ current = parent;
38
+ }
39
+ return current;
40
+ }
21
41
  //#endregion
22
42
  //#region src/usage.ts
23
43
  function safeErrorMessage(error) {
@@ -30,14 +50,16 @@ window.__ModuleLoader__.load({
30
50
  //#endregion
31
51
  //#region src/client/quota.ts
32
52
  const RING_CIRCUMFERENCE = 2 * Math.PI * 5.5;
33
- const DEFAULT_SESSION_KEY = "__default__";
34
53
  const DEFAULT_PRIMARY_WINDOW_SECONDS = 18e3;
35
54
  const DEFAULT_SECONDARY_WINDOW_SECONDS = 604800;
36
- function resolveSessionFastMode(snapshot, sessionId) {
55
+ function resolveSessionFastMode(snapshot, sessionId, parentSessionId) {
37
56
  if (!snapshot) return false;
38
- const key = normalizeCodexSessionId(sessionId) ?? DEFAULT_SESSION_KEY;
39
- const sessionVal = snapshot.sessionFastModes?.[key];
40
- if (typeof sessionVal === "boolean") return sessionVal;
57
+ const rootKey = resolveRootSessionId(sessionId, snapshot.sessionParents, parentSessionId);
58
+ const rootVal = snapshot.sessionFastModes?.[rootKey];
59
+ if (typeof rootVal === "boolean") return rootVal;
60
+ const selfKey = normalizeCodexSessionId(sessionId) ?? "__default__";
61
+ const selfVal = snapshot.sessionFastModes?.[selfKey];
62
+ if (typeof selfVal === "boolean") return selfVal;
41
63
  return snapshot.defaultFastMode === true;
42
64
  }
43
65
  function resolveActiveProvider(directorySnapshot, projectedSelection) {
@@ -154,13 +176,14 @@ window.__ModuleLoader__.load({
154
176
  quota: null,
155
177
  defaultFastMode: false,
156
178
  sessionFastModes: {},
179
+ sessionParents: {},
157
180
  loading: false,
158
181
  error: null
159
182
  };
160
183
  let confirmedSessionFastModes = {};
161
- let lastConfirmedFastSeq = 0;
162
- const latestSeqBySession = new Map();
163
- const pendingBySession = new Map();
184
+ let knownSessionParents = {};
185
+ const latestSeqByNode = new Map();
186
+ const pendingBySeq = new Map();
164
187
  let inflight = null;
165
188
  let inflightForce = false;
166
189
  let requestSeq = 0;
@@ -171,10 +194,63 @@ window.__ModuleLoader__.load({
171
194
  state = next;
172
195
  for (const listener of listeners) listener();
173
196
  }
197
+ function recordClientSessionParent(childKey, parentKey) {
198
+ if (childKey === "" || parentKey === "" || childKey === parentKey) return;
199
+ let cursor = parentKey;
200
+ let depth = 0;
201
+ while (cursor !== void 0 && depth < 32) {
202
+ if (cursor === childKey) return;
203
+ cursor = knownSessionParents[cursor];
204
+ depth += 1;
205
+ }
206
+ if (knownSessionParents[childKey] === parentKey) return;
207
+ knownSessionParents = {
208
+ ...knownSessionParents,
209
+ [childKey]: parentKey
210
+ };
211
+ }
212
+ function mergeServerSessionParents(raw) {
213
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
214
+ for (const [child, parent] of Object.entries(raw)) {
215
+ const childKey = normalizeCodexSessionId(child);
216
+ const parentKey = typeof parent === "string" ? normalizeCodexSessionId(parent) : void 0;
217
+ if (childKey !== void 0 && parentKey !== void 0) recordClientSessionParent(childKey, parentKey);
218
+ }
219
+ }
220
+ function isTreeSeqSuperseded(seq, selfKey, parentKey) {
221
+ const myRoot = resolveRootSessionId(selfKey, knownSessionParents, parentKey);
222
+ for (const [nodeKey, nodeSeq] of latestSeqByNode.entries()) if (nodeSeq > seq && resolveRootSessionId(nodeKey, knownSessionParents) === myRoot) return true;
223
+ return false;
224
+ }
225
+ function collectTreeKeysForRoot(rootKey, extraKeys = []) {
226
+ const keys = new Set([rootKey, ...extraKeys]);
227
+ for (const k of Object.keys(knownSessionParents)) if (resolveRootSessionId(k, knownSessionParents) === rootKey) keys.add(k);
228
+ for (const k of Object.keys(confirmedSessionFastModes)) if (resolveRootSessionId(k, knownSessionParents) === rootKey) keys.add(k);
229
+ for (const k of latestSeqByNode.keys()) if (resolveRootSessionId(k, knownSessionParents) === rootKey) keys.add(k);
230
+ return [...keys];
231
+ }
174
232
  function deriveSessionFastModes() {
175
- if (pendingBySession.size === 0) return confirmedSessionFastModes;
176
- const merged = { ...confirmedSessionFastModes };
177
- for (const [k, pending] of pendingBySession) merged[k] = pending.enabled;
233
+ const merged = {};
234
+ for (const [k, v] of Object.entries(confirmedSessionFastModes)) {
235
+ const rootKey = resolveRootSessionId(k, knownSessionParents);
236
+ const rootVal = confirmedSessionFastModes[rootKey] ?? v;
237
+ merged[rootKey] = rootVal;
238
+ merged[k] = rootVal;
239
+ }
240
+ for (const childKey of Object.keys(knownSessionParents)) {
241
+ const rootKey = resolveRootSessionId(childKey, knownSessionParents);
242
+ if (typeof merged[rootKey] === "boolean") merged[childKey] = merged[rootKey];
243
+ }
244
+ for (const pending of pendingBySeq.values()) {
245
+ if (isTreeSeqSuperseded(pending.seq, pending.selfKey, pending.parentKey)) continue;
246
+ const rootKey = resolveRootSessionId(pending.selfKey, knownSessionParents, pending.parentKey);
247
+ const extra = pending.parentKey !== void 0 ? [
248
+ rootKey,
249
+ pending.parentKey,
250
+ pending.selfKey
251
+ ] : [rootKey, pending.selfKey];
252
+ for (const k of collectTreeKeysForRoot(rootKey, extra)) merged[k] = pending.enabled;
253
+ }
178
254
  return merged;
179
255
  }
180
256
  function parseServerSessionFastModes(raw) {
@@ -185,20 +261,22 @@ window.__ModuleLoader__.load({
185
261
  }
186
262
  function mergeFastStateFromPayload(payload, allowSessionReplace) {
187
263
  const defaultFastMode = typeof payload?.defaultFastMode === "boolean" ? payload.defaultFastMode : state.defaultFastMode;
264
+ mergeServerSessionParents(payload?.sessionParents);
188
265
  if (allowSessionReplace) {
189
266
  const parsedMap = parseServerSessionFastModes(payload?.sessionFastModes);
190
267
  if (parsedMap !== void 0) confirmedSessionFastModes = parsedMap;
191
268
  }
192
269
  return {
193
270
  defaultFastMode,
194
- sessionFastModes: deriveSessionFastModes()
271
+ sessionFastModes: deriveSessionFastModes(),
272
+ sessionParents: knownSessionParents
195
273
  };
196
274
  }
197
275
  async function refresh(force = false) {
198
276
  if (!force && state.quota !== null && nowFn() - state.quota.fetchedAt < ttlMs) return;
199
277
  if (inflight !== null && (!force || inflightForce)) return inflight;
200
278
  const seq = ++requestSeq;
201
- const canReplaceFastAtStart = pendingBySession.size === 0;
279
+ const canReplaceFastAtStart = pendingBySeq.size === 0;
202
280
  const fastEpochAtStart = fastMutationEpoch;
203
281
  inflightForce = force;
204
282
  setState({
@@ -224,7 +302,7 @@ window.__ModuleLoader__.load({
224
302
  }
225
303
  const payload = await response.json();
226
304
  if (seq !== requestSeq) return;
227
- const fastState = mergeFastStateFromPayload(payload, canReplaceFastAtStart && pendingBySession.size === 0 && fastEpochAtStart === fastMutationEpoch);
305
+ const fastState = mergeFastStateFromPayload(payload, canReplaceFastAtStart && pendingBySeq.size === 0 && fastEpochAtStart === fastMutationEpoch);
228
306
  if (payload && payload.ok === true && payload.quota) setState({
229
307
  quota: payload.quota,
230
308
  ...fastState,
@@ -258,29 +336,38 @@ window.__ModuleLoader__.load({
258
336
  inflight = task;
259
337
  return task;
260
338
  }
261
- async function setFastMode(sessionId, enabled) {
339
+ async function setFastMode(sessionId, enabled, parentSessionId) {
262
340
  const nextFastMode = Boolean(enabled);
263
- const key = normalizeCodexSessionId(sessionId) ?? DEFAULT_SESSION_KEY;
341
+ const selfKey = normalizeCodexSessionId(sessionId) ?? "__default__";
342
+ const parentKey = normalizeCodexSessionId(parentSessionId);
343
+ if (parentKey !== void 0 && parentKey !== selfKey) recordClientSessionParent(selfKey, parentKey);
264
344
  const seq = ++fastModeSeq;
265
345
  fastMutationEpoch += 1;
266
- latestSeqBySession.set(key, seq);
267
- pendingBySession.set(key, {
346
+ latestSeqByNode.set(selfKey, seq);
347
+ if (parentKey !== void 0) latestSeqByNode.set(parentKey, seq);
348
+ const initialRoot = resolveRootSessionId(selfKey, knownSessionParents, parentKey);
349
+ latestSeqByNode.set(initialRoot, seq);
350
+ pendingBySeq.set(seq, {
268
351
  seq,
352
+ selfKey,
353
+ parentKey,
269
354
  enabled: nextFastMode
270
355
  });
271
356
  const rollback = (errorMsg) => {
272
- if (latestSeqBySession.get(key) !== seq) return;
357
+ pendingBySeq.delete(seq);
358
+ if (isTreeSeqSuperseded(seq, selfKey, parentKey)) return;
273
359
  fastMutationEpoch += 1;
274
- pendingBySession.delete(key);
275
360
  setState({
276
361
  ...state,
277
362
  sessionFastModes: deriveSessionFastModes(),
363
+ sessionParents: knownSessionParents,
278
364
  error: errorMsg
279
365
  });
280
366
  };
281
367
  setState({
282
368
  ...state,
283
369
  sessionFastModes: deriveSessionFastModes(),
370
+ sessionParents: knownSessionParents,
284
371
  error: null
285
372
  });
286
373
  try {
@@ -291,41 +378,55 @@ window.__ModuleLoader__.load({
291
378
  Accept: "application/json"
292
379
  },
293
380
  body: JSON.stringify({
294
- sessionId: key,
381
+ sessionId: selfKey,
295
382
  fastMode: nextFastMode
296
383
  })
297
384
  });
298
- if (latestSeqBySession.get(key) !== seq) return;
299
385
  if (!response.ok) {
300
386
  const errPayload = await response.json().catch(() => null);
387
+ mergeServerSessionParents(errPayload?.sessionParents);
301
388
  rollback(typeof errPayload?.error === "string" && errPayload.error !== "" ? errPayload.error : `HTTP ${response.status}`);
302
389
  return;
303
390
  }
304
391
  const payload = await response.json();
305
- if (latestSeqBySession.get(key) !== seq) return;
392
+ mergeServerSessionParents(payload?.sessionParents);
393
+ if (isTreeSeqSuperseded(seq, selfKey, parentKey)) {
394
+ pendingBySeq.delete(seq);
395
+ return;
396
+ }
306
397
  if (!payload || payload.ok === false) {
307
398
  rollback(typeof payload?.error === "string" && payload.error !== "" ? payload.error : "Invalid response");
308
399
  return;
309
400
  }
310
401
  fastMutationEpoch += 1;
311
- pendingBySession.delete(key);
402
+ pendingBySeq.delete(seq);
403
+ const actualRootKey = resolveRootSessionId(selfKey, knownSessionParents, parentKey);
404
+ latestSeqByNode.set(actualRootKey, Math.max(latestSeqByNode.get(actualRootKey) ?? 0, seq));
312
405
  const parsedMap = parseServerSessionFastModes(payload.sessionFastModes);
313
- if (seq > lastConfirmedFastSeq && parsedMap !== void 0) {
314
- lastConfirmedFastSeq = seq;
315
- confirmedSessionFastModes = parsedMap;
316
- } else confirmedSessionFastModes = {
317
- ...confirmedSessionFastModes,
318
- [key]: parsedMap?.[key] ?? nextFastMode
319
- };
406
+ const nextConfirmed = { ...confirmedSessionFastModes };
407
+ if (parsedMap !== void 0) {
408
+ for (const [k, v] of Object.entries(parsedMap)) if (!isTreeSeqSuperseded(seq, k, void 0)) nextConfirmed[k] = v;
409
+ }
410
+ const effectiveVal = parsedMap?.[selfKey] ?? parsedMap?.[actualRootKey] ?? nextFastMode;
411
+ const extraKeys = parentKey !== void 0 && parentKey !== selfKey ? [
412
+ actualRootKey,
413
+ parentKey,
414
+ selfKey
415
+ ] : [actualRootKey, selfKey];
416
+ for (const k of collectTreeKeysForRoot(actualRootKey, extraKeys)) nextConfirmed[k] = effectiveVal;
417
+ confirmedSessionFastModes = nextConfirmed;
320
418
  const defaultFastMode = typeof payload.defaultFastMode === "boolean" ? payload.defaultFastMode : state.defaultFastMode;
321
419
  setState({
322
420
  ...state,
323
421
  defaultFastMode,
324
422
  sessionFastModes: deriveSessionFastModes(),
423
+ sessionParents: knownSessionParents,
325
424
  error: null
326
425
  });
327
426
  } catch (error) {
328
427
  rollback(safeErrorMessage(error));
428
+ } finally {
429
+ pendingBySeq.delete(seq);
329
430
  }
330
431
  }
331
432
  return {
@@ -430,6 +531,8 @@ window.__ModuleLoader__.load({
430
531
  const directorySnap = React.useSyncExternalStore(modelStore.subscribe, modelStore.getSnapshot);
431
532
  const projectedSelection = typeof useProjection === "function" ? useProjection("modelSelection") : void 0;
432
533
  const running = typeof useSession === "function" ? Boolean(useSession((s) => s?.running)) : false;
534
+ const rawParentSessionId = typeof useSession === "function" ? useSession((s) => s?.subagent?.address?.parentSessionId) : void 0;
535
+ const parentSessionId = typeof rawParentSessionId === "string" ? rawParentSessionId : void 0;
433
536
  const isCodex = isCodexProvider(resolveActiveProvider(directorySnap, projectedSelection));
434
537
  const quotaSnap = React.useSyncExternalStore(quotaStore.subscribe, quotaStore.getSnapshot);
435
538
  const [open, setOpen] = React.useState(false);
@@ -489,7 +592,7 @@ window.__ModuleLoader__.load({
489
592
  })), h("div", { className: "dsh-codex-quota-meta" }, h("span", null, `${t("quota.remainPrefix")}${remainPercent}%`), countdown !== "" ? h("span", null, countdown) : null));
490
593
  };
491
594
  const quota = quotaSnap.quota;
492
- const isFastMode = resolveSessionFastMode(quotaSnap, sessionId);
595
+ const isFastMode = resolveSessionFastMode(quotaSnap, sessionId, parentSessionId);
493
596
  const secondaryReached = Boolean(quota?.secondary && quota.secondary.usedPercent >= 100);
494
597
  const primaryReached = Boolean(quota?.limitReached && !secondaryReached);
495
598
  return h("span", {
@@ -549,7 +652,7 @@ window.__ModuleLoader__.load({
549
652
  "aria-label": t("quota.fastTitle"),
550
653
  "data-checked": isFastMode ? "true" : "false",
551
654
  onClick: () => {
552
- quotaStore.setFastMode(sessionId, !isFastMode);
655
+ quotaStore.setFastMode(sessionId, !isFastMode, parentSessionId);
553
656
  }
554
657
  }, h("span", { className: "dsh-codex-fast-switch-thumb" }))), quotaSnap.error !== null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error) : null) : null);
555
658
  }
package/lib/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { PiCodexModel } from "@earendil-works/pi-ai/providers/openai-codex";
2
2
  //#region src/host-types.d.ts
3
+ interface AdapterRegistrationHandle {
4
+ (): void;
5
+ replace: (next: string[]) => void;
6
+ }
3
7
  interface LlmServiceLike {
4
- registerAdapter: (providers: string[], adapter: unknown) => (() => void) & {
5
- replace?: (next: string[]) => void;
6
- };
8
+ registerAdapter: (providers: string[], adapter: unknown) => AdapterRegistrationHandle;
7
9
  emitAdaptersUpdated?: () => void;
8
10
  }
9
11
  type PluginLogLevel = 'INFO' | 'WARN' | 'ERROR';
@@ -15,9 +17,10 @@ interface LoggerLike {
15
17
  }
16
18
  interface PluginContextLike {
17
19
  llm: LlmServiceLike;
20
+ fiber?: unknown;
18
21
  logger?: LoggerLike | undefined;
19
22
  get?: (name: string) => unknown;
20
- on?: (event: string, listener: () => void) => () => void;
23
+ on?: (event: string, listener: (...args: unknown[]) => unknown) => () => void;
21
24
  effect?: (factory: () => void | (() => void), label?: string) => () => void;
22
25
  inject?: (services: readonly string[], callback: (ctx: PluginContextLike) => void) => () => void;
23
26
  }
@@ -134,12 +137,14 @@ type CodexQuotaResponse = {
134
137
  readonly quota: CodexQuotaSnapshot;
135
138
  readonly defaultFastMode?: boolean | undefined;
136
139
  readonly sessionFastModes?: Readonly<Record<string, boolean>> | undefined;
140
+ readonly sessionParents?: Readonly<Record<string, string>> | undefined;
137
141
  } | {
138
142
  readonly ok: false;
139
143
  readonly error: string;
140
144
  readonly quota?: CodexQuotaSnapshot | undefined;
141
145
  readonly defaultFastMode?: boolean | undefined;
142
146
  readonly sessionFastModes?: Readonly<Record<string, boolean>> | undefined;
147
+ readonly sessionParents?: Readonly<Record<string, string>> | undefined;
143
148
  };
144
149
  interface UsageFetcherOptions {
145
150
  readonly resolveCredentials: () => Promise<CodexResolvedCredentials>;
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { appendFileSync, chmodSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { basename, dirname, join, resolve } from "node:path";
3
3
  import z from "@deepseek-ai/schemastery";
4
- import { LlmError, resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
4
+ import { LlmError, RetryPolicySchema, resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
5
5
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
6
6
  import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
7
7
  import { randomUUID } from "node:crypto";
@@ -16,6 +16,7 @@ const DEFAULT_CONTEXT_WINDOW = 872e3;
16
16
  const PLUGIN_CACHE_DIR_NAME = "cache/dsh-codex-subscription";
17
17
  const BACKUP_CACHE_FILE_NAME = "models-cache-backup.json";
18
18
  const RUNTIME_STATE_FILE_NAME = "runtime-state.json";
19
+ const DEFAULT_SESSION_KEY = "__default__";
19
20
  const THINKING_LEVELS = [
20
21
  "off",
21
22
  "minimal",
@@ -44,6 +45,26 @@ function normalizeCodexSessionId(sessionId) {
44
45
  const stripped = sessionId.trim().replace(/^session-/iu, "").trim();
45
46
  return stripped.length > 0 ? stripped : void 0;
46
47
  }
48
+ function resolveRootSessionId(sessionId, sessionParents, directParentSessionId) {
49
+ const startKey = normalizeCodexSessionId(sessionId);
50
+ const directParentKey = normalizeCodexSessionId(directParentSessionId);
51
+ if (startKey === void 0) return directParentKey ?? "__default__";
52
+ const readParent = (key) => {
53
+ if (key === startKey && directParentKey !== void 0 && directParentKey !== startKey) return directParentKey;
54
+ if (!sessionParents) return void 0;
55
+ const raw = sessionParents instanceof Map ? sessionParents.get(key) : sessionParents[key];
56
+ return typeof raw === "string" ? normalizeCodexSessionId(raw) : void 0;
57
+ };
58
+ let current = startKey;
59
+ const visited = new Set([current]);
60
+ while (visited.size <= 32) {
61
+ const parent = readParent(current);
62
+ if (parent === void 0 || visited.has(parent)) break;
63
+ visited.add(parent);
64
+ current = parent;
65
+ }
66
+ return current;
67
+ }
47
68
  function isSameModelsCatalogPayload(a, b) {
48
69
  if (!a || !b) return false;
49
70
  const { fetched_at: _fa, ...restA } = a;
@@ -589,7 +610,7 @@ const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
589
610
  const REQUEST_IMAGE_MAX_BYTES = 1048576;
590
611
  const MAX_LOG_FILE_BYTES = 524288;
591
612
  const MAX_PERSISTED_SESSIONS = 200;
592
- const DEFAULT_SESSION_KEY = "__default__";
613
+ const MAX_PERSISTED_PARENTS = 1e3;
593
614
  function resolvePluginCacheDir(codexHome) {
594
615
  return join(codexHome, PLUGIN_CACHE_DIR_NAME);
595
616
  }
@@ -649,19 +670,34 @@ function writeBackupCache(codexHome, ctx, raw) {
649
670
  appendPluginLog(codexHome, ctx, "WARN", "catalog.backup_write_error", { error: safeErrorMessage(error) });
650
671
  }
651
672
  }
652
- function readPersistedSessionFastModes(codexHome) {
653
- const result = new Map();
673
+ function readPersistedRuntimeState(codexHome) {
674
+ const sessionFastModes = new Map();
675
+ const sessionParents = new Map();
654
676
  try {
655
- const entries = JSON.parse(readFileSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), "utf8"))?.sessionFastModes;
656
- if (entries && typeof entries === "object" && !Array.isArray(entries)) {
657
- for (const [k, v] of Object.entries(entries)) if (typeof k === "string" && k !== "" && typeof v === "boolean") result.set(k, v);
677
+ const raw = JSON.parse(readFileSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), "utf8"));
678
+ const entries = raw?.sessionFastModes;
679
+ if (entries && typeof entries === "object" && !Array.isArray(entries)) for (const [k, v] of Object.entries(entries)) {
680
+ const normKey = normalizeCodexSessionId(k) ?? (k === "__default__" ? "__default__" : void 0);
681
+ if (normKey !== void 0 && typeof v === "boolean") sessionFastModes.set(normKey, v);
682
+ }
683
+ const parents = raw?.sessionParents;
684
+ if (parents && typeof parents === "object" && !Array.isArray(parents)) for (const [child, parent] of Object.entries(parents)) {
685
+ const childKey = normalizeCodexSessionId(child);
686
+ const parentKey = typeof parent === "string" ? normalizeCodexSessionId(parent) : void 0;
687
+ if (childKey !== void 0 && parentKey !== void 0 && childKey !== parentKey) sessionParents.set(childKey, parentKey);
658
688
  }
659
689
  } catch {}
660
- return result;
690
+ return {
691
+ sessionFastModes,
692
+ sessionParents
693
+ };
661
694
  }
662
- function writePersistedSessionFastModes(codexHome, ctx, sessionFastModes) {
695
+ function writePersistedRuntimeState(codexHome, ctx, sessionFastModes, sessionParents) {
663
696
  try {
664
- writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), `${JSON.stringify({ sessionFastModes: Object.fromEntries(sessionFastModes) }, null, 2)}\n`);
697
+ writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), `${JSON.stringify({
698
+ sessionFastModes: Object.fromEntries(sessionFastModes),
699
+ sessionParents: Object.fromEntries(sessionParents)
700
+ }, null, 2)}\n`);
665
701
  } catch (error) {
666
702
  appendPluginLog(codexHome, ctx, "WARN", "fast_mode.persist_error", { error: safeErrorMessage(error) });
667
703
  }
@@ -669,7 +705,8 @@ function writePersistedSessionFastModes(codexHome, ctx, sessionFastModes) {
669
705
  const Config = z.object({
670
706
  fastMode: z.boolean().default(false).description("新会话默认是否开启 Fast 极速模式(未填时默认 false;单会话可在右下角限额弹窗内独立切换)").volatile(),
671
707
  disabledModels: z.array(z.string()).default([...DEFAULT_DISABLED_MODELS]).description("在模型选择器中屏蔽的模型 slug 列表").volatile(),
672
- contextWindows: z.dict(z.number()).default({}).description("按模型 slug 覆盖上下文窗口大小(tokens),未配置时默认使用 872000").volatile()
708
+ contextWindows: z.dict(z.number()).default({}).description("按模型 slug 覆盖上下文窗口大小(tokens),未配置时默认使用 872000").volatile(),
709
+ retryPolicy: RetryPolicySchema.description("原生 Agent 步骤重试策略(retryableCodes 为完整覆盖集合,显式配置时需同时包含默认错误码与新增错误码)").volatile()
673
710
  });
674
711
  function readVolatile(field, fallback) {
675
712
  const value = typeof field?.get === "function" ? field.get() : field;
@@ -683,7 +720,7 @@ var CodexPiAiAdapter = class extends PiAiAdapter {
683
720
  }
684
721
  modelInfo(snapshot, provider, model) {
685
722
  const info = super.modelInfo(snapshot, provider, model);
686
- const meta = this.#getModelMetadata(model);
723
+ const meta = this.#getModelMetadata(snapshot, model);
687
724
  if (meta === void 0) return info;
688
725
  const defaultLevel = meta.defaultReasoningLevel;
689
726
  const hasDefaultLevel = defaultLevel !== void 0 && info.reasoning !== void 0 && info.reasoning.efforts.some((e) => e.id === defaultLevel);
@@ -703,34 +740,148 @@ var CodexPiAiAdapter = class extends PiAiAdapter {
703
740
  function apply(ctx, config = {}) {
704
741
  const baseProvider = openaiCodexProvider();
705
742
  const baseModels = baseProvider.getModels();
706
- const retryPolicy = resolveRetryPolicy(void 0, "codex-subscription retryPolicy");
707
743
  const getCodexHome = () => resolveCodexHome();
708
744
  const getConfigFastMode = () => readVolatile(config.fastMode, false) === true;
709
745
  const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
710
746
  const getContextWindows = () => readVolatile(config.contextWindows, {});
711
- const sessionFastModes = readPersistedSessionFastModes(getCodexHome());
747
+ const getRawRetryPolicy = () => readVolatile(config.retryPolicy, void 0);
748
+ const resolveConfiguredRetryPolicy = () => resolveRetryPolicy(getRawRetryPolicy(), "codex-subscription retryPolicy");
749
+ const { sessionFastModes, sessionParents } = readPersistedRuntimeState(getCodexHome());
750
+ const recordSessionParent = (childKey, parentKey) => {
751
+ if (childKey === "" || parentKey === "" || childKey === parentKey) return false;
752
+ let cursor = parentKey;
753
+ let depth = 0;
754
+ while (cursor !== void 0 && depth < 32) {
755
+ if (cursor === childKey) return false;
756
+ cursor = sessionParents.get(cursor);
757
+ depth += 1;
758
+ }
759
+ if (sessionParents.get(childKey) === parentKey) return false;
760
+ sessionParents.delete(childKey);
761
+ sessionParents.set(childKey, parentKey);
762
+ while (sessionParents.size > MAX_PERSISTED_PARENTS) {
763
+ let victim;
764
+ for (const candidate of sessionParents.keys()) {
765
+ const root = resolveRootSessionId(candidate, sessionParents);
766
+ if (!sessionFastModes.has(root)) {
767
+ victim = candidate;
768
+ break;
769
+ }
770
+ }
771
+ const keyToDelete = victim ?? sessionParents.keys().next().value;
772
+ if (keyToDelete === void 0) break;
773
+ sessionParents.delete(keyToDelete);
774
+ }
775
+ return true;
776
+ };
777
+ const findLiveSession = (key) => {
778
+ const sessions = ctx.get?.("sessions");
779
+ if (typeof sessions?.get !== "function") return void 0;
780
+ return sessions.get(key) ?? sessions.get(`session-${key}`);
781
+ };
782
+ const findLiveAgent = (key) => {
783
+ const agents = ctx.get?.("agents");
784
+ if (typeof agents?.get !== "function") return void 0;
785
+ return agents.get(key) ?? agents.get(`session-${key}`);
786
+ };
787
+ const inspectSessionParentFromHost = (sessionLike, agentLike) => {
788
+ const childKey = normalizeCodexSessionId(sessionLike?.id ?? sessionLike?.header?.id ?? agentLike?.id ?? agentLike?.session?.id);
789
+ if (childKey === void 0) return false;
790
+ const header = sessionLike?.header ?? agentLike?.session?.header;
791
+ if (header?.origin === "subagent" && typeof header.parentSession === "string") {
792
+ const parentKey = normalizeCodexSessionId(header.parentSession);
793
+ if (parentKey !== void 0) return recordSessionParent(childKey, parentKey);
794
+ }
795
+ const agentTeams = ctx.get?.("agentTeams");
796
+ if (typeof agentTeams?.tryMembership === "function") {
797
+ const agent = agentLike ?? findLiveAgent(childKey);
798
+ if (agent !== void 0) {
799
+ const membership = agentTeams.tryMembership(agent);
800
+ if (membership?.role === "teammate" && typeof membership.root?.id === "string") {
801
+ const rootKey = normalizeCodexSessionId(membership.root.id);
802
+ if (rootKey !== void 0) return recordSessionParent(childKey, rootKey);
803
+ }
804
+ }
805
+ }
806
+ return false;
807
+ };
808
+ const normalizeFastModesToRoots = () => {
809
+ let migrated = false;
810
+ for (const [k, v] of [...sessionFastModes.entries()]) {
811
+ const rootKey = resolveRootSessionId(k, sessionParents);
812
+ if (rootKey !== k) {
813
+ if (!sessionFastModes.has(rootKey)) sessionFastModes.set(rootKey, v);
814
+ sessionFastModes.delete(k);
815
+ migrated = true;
816
+ }
817
+ }
818
+ return migrated;
819
+ };
820
+ const syncAllLiveSessionsFromHost = () => {
821
+ let changed = false;
822
+ const sessions = ctx.get?.("sessions");
823
+ if (typeof sessions?.list === "function") {
824
+ for (const s of sessions.list()) if (inspectSessionParentFromHost(s)) changed = true;
825
+ }
826
+ const agents = ctx.get?.("agents");
827
+ if (typeof agents?.list === "function") {
828
+ for (const a of agents.list()) if (inspectSessionParentFromHost(a?.session, a)) changed = true;
829
+ }
830
+ if (normalizeFastModesToRoots()) changed = true;
831
+ return changed;
832
+ };
833
+ const resolveRootSessionKey = (sessionId) => {
834
+ const initiatorId = (ctx.get?.("agents"))?.currentInitiator?.()?.id;
835
+ const startKey = normalizeCodexSessionId(sessionId) ?? normalizeCodexSessionId(initiatorId);
836
+ if (startKey === void 0) return DEFAULT_SESSION_KEY;
837
+ let current = startKey;
838
+ const visited = new Set([current]);
839
+ while (visited.size <= 32) {
840
+ const liveSession = findLiveSession(current);
841
+ const liveAgent = findLiveAgent(current);
842
+ if (liveSession !== void 0 || liveAgent !== void 0) inspectSessionParentFromHost(liveSession, liveAgent);
843
+ const parent = sessionParents.get(current);
844
+ if (parent === void 0 || visited.has(parent)) break;
845
+ visited.add(parent);
846
+ current = parent;
847
+ }
848
+ normalizeFastModesToRoots();
849
+ return current;
850
+ };
712
851
  const getEffectiveFastMode = (sessionId) => {
713
- const key = normalizeCodexSessionId(sessionId);
714
- if (key !== void 0 && sessionFastModes.has(key)) return sessionFastModes.get(key) === true;
715
- if (key === void 0 && sessionFastModes.has(DEFAULT_SESSION_KEY)) return sessionFastModes.get(DEFAULT_SESSION_KEY) === true;
852
+ const rootKey = resolveRootSessionKey(sessionId);
853
+ if (sessionFastModes.has(rootKey)) return sessionFastModes.get(rootKey) === true;
716
854
  return getConfigFastMode();
717
855
  };
718
856
  const setSessionFastMode = (sessionId, nextFastMode) => {
719
857
  const codexHome = getCodexHome();
720
- const key = normalizeCodexSessionId(sessionId) ?? DEFAULT_SESSION_KEY;
721
- sessionFastModes.delete(key);
722
- sessionFastModes.set(key, nextFastMode);
858
+ syncAllLiveSessionsFromHost();
859
+ const rootKey = resolveRootSessionKey(sessionId);
860
+ sessionFastModes.delete(rootKey);
861
+ sessionFastModes.set(rootKey, nextFastMode);
862
+ normalizeFastModesToRoots();
723
863
  while (sessionFastModes.size > MAX_PERSISTED_SESSIONS) {
724
864
  const oldest = sessionFastModes.keys().next().value;
725
865
  if (oldest === void 0) break;
726
866
  sessionFastModes.delete(oldest);
727
867
  }
728
- writePersistedSessionFastModes(codexHome, ctx, sessionFastModes);
868
+ writePersistedRuntimeState(codexHome, ctx, sessionFastModes, sessionParents);
729
869
  appendPluginLog(codexHome, ctx, "INFO", "fast_mode.updated", {
730
- sessionId: key,
870
+ sessionId: normalizeCodexSessionId(sessionId) ?? "__default__",
871
+ rootSessionId: rootKey,
731
872
  fastMode: nextFastMode
732
873
  });
733
874
  };
875
+ const buildProjectedSessionFastModes = () => {
876
+ syncAllLiveSessionsFromHost();
877
+ const projected = {};
878
+ for (const [rootKey, v] of sessionFastModes) projected[rootKey] = v;
879
+ for (const childKey of [...sessionParents.keys()]) {
880
+ const rootKey = resolveRootSessionId(childKey, sessionParents);
881
+ if (sessionFastModes.has(rootKey)) projected[childKey] = sessionFastModes.get(rootKey) === true;
882
+ }
883
+ return projected;
884
+ };
734
885
  const authManager = createAuthManager({
735
886
  getCodexHome,
736
887
  getClientVersion: () => ensureSnapshot().catalog.clientVersion,
@@ -740,13 +891,9 @@ function apply(ctx, config = {}) {
740
891
  let cachedRaw;
741
892
  let lastRejectedRaw;
742
893
  let rawRevision = 0;
743
- let snapshotSignature;
744
- let currentCatalog = {
745
- clientVersion: DEFAULT_CLIENT_VERSION,
746
- models: [],
747
- metadataById: new Map()
748
- };
749
- let currentProfilesMap;
894
+ let activeRetryPolicy = resolveConfiguredRetryPolicy();
895
+ let currentState;
896
+ const catalogByProfiles = new WeakMap();
750
897
  const requestTokenAuth = Object.freeze({
751
898
  name: "Codex CLI auth.json token",
752
899
  async resolve({ credential }) {
@@ -758,11 +905,12 @@ function apply(ctx, config = {}) {
758
905
  };
759
906
  }
760
907
  });
761
- function wrapOptions(model, options = {}) {
762
- const { catalog } = ensureSnapshot();
908
+ function wrapOptions(catalog, model, options = {}) {
763
909
  const modelMeta = catalog.metadataById.get(model?.id);
764
910
  const { temperature: _temperature, sessionId, onPayload, ...rest } = options;
765
- const normalizedSessionId = normalizeCodexSessionId(sessionId);
911
+ const initiatorId = (ctx.get?.("agents"))?.currentInitiator?.()?.id;
912
+ const normalizedSessionId = normalizeCodexSessionId(sessionId) ?? normalizeCodexSessionId(initiatorId);
913
+ resolveRootSessionKey(normalizedSessionId);
766
914
  return {
767
915
  ...rest,
768
916
  ...normalizedSessionId !== void 0 ? { sessionId: normalizedSessionId } : {},
@@ -777,25 +925,28 @@ function apply(ctx, config = {}) {
777
925
  resolveCredentials: () => authManager.resolveCredentials(),
778
926
  buildHeaders: (authHeaders) => buildCodexHeaders(authHeaders, ensureSnapshot().catalog.clientVersion)
779
927
  });
780
- const piProvider = Object.freeze({
781
- ...baseProvider,
782
- id: PROVIDER,
783
- name: DISPLAY_NAME,
784
- auth: Object.freeze({
785
- ...baseProvider.auth,
786
- apiKey: requestTokenAuth
787
- }),
788
- getModels: () => ensureSnapshot().catalog.models,
789
- stream: (model, context, options) => {
790
- usageFetcher.invalidate();
791
- return baseProvider.stream(model, context, wrapOptions(model, options));
792
- },
793
- streamSimple: (model, context, options) => {
794
- usageFetcher.invalidate();
795
- return baseProvider.streamSimple(model, context, wrapOptions(model, options));
796
- }
797
- });
928
+ function createSnapshotPiProvider(snapshotCatalog) {
929
+ return Object.freeze({
930
+ ...baseProvider,
931
+ id: PROVIDER,
932
+ name: DISPLAY_NAME,
933
+ auth: Object.freeze({
934
+ ...baseProvider.auth,
935
+ apiKey: requestTokenAuth
936
+ }),
937
+ getModels: () => snapshotCatalog.models,
938
+ stream: (model, context, options) => {
939
+ usageFetcher.invalidate();
940
+ return baseProvider.stream(model, context, wrapOptions(snapshotCatalog, model, options));
941
+ },
942
+ streamSimple: (model, context, options) => {
943
+ usageFetcher.invalidate();
944
+ return baseProvider.streamSimple(model, context, wrapOptions(snapshotCatalog, model, options));
945
+ }
946
+ });
947
+ }
798
948
  function ensureSnapshot() {
949
+ const retryPolicy = activeRetryPolicy;
799
950
  const codexHome = getCodexHome();
800
951
  const cachePath = join(codexHome, "models_cache.json");
801
952
  const disabled = getDisabledModels();
@@ -868,24 +1019,30 @@ function apply(ctx, config = {}) {
868
1019
  }
869
1020
  }
870
1021
  }
871
- const nextSnapshotSig = JSON.stringify([
1022
+ const nextCatalogSig = JSON.stringify([
872
1023
  rawRevision,
873
1024
  disabled,
874
1025
  contextWindows
875
1026
  ]);
876
- if (nextSnapshotSig !== snapshotSignature || currentProfilesMap === void 0) {
877
- currentCatalog = cachedRaw !== void 0 ? parseModelsCache(cachedRaw, baseModels, disabled, contextWindows) : {
1027
+ let nextCatalog = currentState?.catalog;
1028
+ if (nextCatalog === void 0 || nextCatalogSig !== currentState?.catalogSig) {
1029
+ nextCatalog = cachedRaw !== void 0 ? parseModelsCache(cachedRaw, baseModels, disabled, contextWindows) : {
878
1030
  clientVersion: DEFAULT_CLIENT_VERSION,
879
1031
  models: [],
880
1032
  metadataById: new Map()
881
1033
  };
882
- appendPluginLog(codexHome, ctx, currentCatalog.models.length === 0 ? "WARN" : "INFO", "catalog.snapshot", {
883
- clientVersion: currentCatalog.clientVersion,
884
- models: currentCatalog.models.map((m) => m.id),
1034
+ appendPluginLog(codexHome, ctx, nextCatalog.models.length === 0 ? "WARN" : "INFO", "catalog.snapshot", {
1035
+ clientVersion: nextCatalog.clientVersion,
1036
+ models: nextCatalog.models.map((m) => m.id),
885
1037
  disabledModels: disabled,
886
1038
  contextWindows
887
1039
  });
888
- currentProfilesMap = new Map([[PROVIDER, Object.freeze({
1040
+ }
1041
+ const retryPolicySig = JSON.stringify(retryPolicy);
1042
+ const nextSnapshotSig = JSON.stringify([nextCatalogSig, retryPolicySig]);
1043
+ if (currentState === void 0 || nextSnapshotSig !== currentState.snapshotSig) {
1044
+ const piProvider = createSnapshotPiProvider(nextCatalog);
1045
+ const profileEntry = Object.freeze({
889
1046
  provider: PROVIDER,
890
1047
  displayName: DISPLAY_NAME,
891
1048
  piProvider,
@@ -898,13 +1055,19 @@ function apply(ctx, config = {}) {
898
1055
  streamIdleTimeoutMs: STREAM_IDLE_TIMEOUT_MS,
899
1056
  cacheRetention: "short",
900
1057
  transport: "sse"
901
- })]]);
902
- snapshotSignature = nextSnapshotSig;
1058
+ });
1059
+ const profiles = new Map([[PROVIDER, profileEntry]]);
1060
+ catalogByProfiles.set(profiles, nextCatalog);
1061
+ currentState = Object.freeze({
1062
+ catalog: nextCatalog,
1063
+ catalogSig: nextCatalogSig,
1064
+ profiles,
1065
+ snapshotSig: nextSnapshotSig,
1066
+ retryPolicy,
1067
+ retryPolicySig
1068
+ });
903
1069
  }
904
- return {
905
- catalog: currentCatalog,
906
- profiles: currentProfilesMap
907
- };
1070
+ return currentState;
908
1071
  }
909
1072
  const adapter = new CodexPiAiAdapter({
910
1073
  profiles: () => ensureSnapshot().profiles,
@@ -930,8 +1093,12 @@ function apply(ctx, config = {}) {
930
1093
  onReplayDegrade: (provider, model, reason) => {
931
1094
  console.error(`[dsh-codex-subscription] replay state for "${provider}/${model}" degraded (${reason})`);
932
1095
  }
933
- }, (modelId) => ensureSnapshot().catalog.metadataById.get(modelId));
934
- ctx.llm.registerAdapter([PROVIDER], adapter);
1096
+ }, (snapshot, modelId) => {
1097
+ const snapProfiles = snapshot?.profiles;
1098
+ return ((snapProfiles !== void 0 ? catalogByProfiles.get(snapProfiles) : void 0) ?? ensureSnapshot().catalog).metadataById.get(modelId);
1099
+ });
1100
+ let committedState = ensureSnapshot();
1101
+ const registration = ctx.llm.registerAdapter([PROVIDER], adapter);
935
1102
  const mountUsageRoute = (connCtx) => {
936
1103
  const connection = connCtx.get?.("connection");
937
1104
  const registerFetch = connection?.fetch?.register;
@@ -960,7 +1127,8 @@ function apply(ctx, config = {}) {
960
1127
  return new Response(JSON.stringify({
961
1128
  ok: true,
962
1129
  defaultFastMode: getConfigFastMode(),
963
- sessionFastModes: Object.fromEntries(sessionFastModes)
1130
+ sessionFastModes: buildProjectedSessionFastModes(),
1131
+ sessionParents: Object.fromEntries(sessionParents)
964
1132
  }), {
965
1133
  status: 200,
966
1134
  headers: { "content-type": "application/json" }
@@ -971,7 +1139,8 @@ function apply(ctx, config = {}) {
971
1139
  return new Response(JSON.stringify({
972
1140
  ...result,
973
1141
  defaultFastMode: getConfigFastMode(),
974
- sessionFastModes: Object.fromEntries(sessionFastModes)
1142
+ sessionFastModes: buildProjectedSessionFastModes(),
1143
+ sessionParents: Object.fromEntries(sessionParents)
975
1144
  }), {
976
1145
  status: 200,
977
1146
  headers: { "content-type": "application/json" }
@@ -983,8 +1152,45 @@ function apply(ctx, config = {}) {
983
1152
  };
984
1153
  if (typeof ctx.inject === "function") ctx.inject(["connection"], mountUsageRoute);
985
1154
  else mountUsageRoute(ctx);
1155
+ ctx.on?.("session/created", (session) => {
1156
+ if (session && typeof session === "object") {
1157
+ if (inspectSessionParentFromHost(session)) writePersistedRuntimeState(getCodexHome(), ctx, sessionFastModes, sessionParents);
1158
+ }
1159
+ });
1160
+ ctx.on?.("agent/created", (payload) => {
1161
+ if (payload && typeof payload === "object") {
1162
+ const agent = payload.agent;
1163
+ if (inspectSessionParentFromHost(agent?.session, agent)) writePersistedRuntimeState(getCodexHome(), ctx, sessionFastModes, sessionParents);
1164
+ }
1165
+ });
1166
+ ctx.on?.("internal/config", function(rawInput, next) {
1167
+ const raw = typeof next === "function" ? next() : rawInput;
1168
+ if (ctx.fiber !== void 0 && this !== ctx.fiber) return raw;
1169
+ if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
1170
+ const candidateRetry = readVolatile(raw.retryPolicy, void 0);
1171
+ resolveRetryPolicy(candidateRetry, "codex-subscription retryPolicy");
1172
+ }
1173
+ return raw;
1174
+ });
986
1175
  ctx.on?.("loader/volatile-update", () => {
987
- ensureSnapshot();
1176
+ const candidateRetryPolicy = resolveConfiguredRetryPolicy();
1177
+ const prevRetryPolicy = activeRetryPolicy;
1178
+ const prevCommitted = committedState;
1179
+ activeRetryPolicy = candidateRetryPolicy;
1180
+ let nextState;
1181
+ try {
1182
+ nextState = ensureSnapshot();
1183
+ if (nextState.retryPolicySig !== prevCommitted.retryPolicySig) {
1184
+ registration.replace([PROVIDER]);
1185
+ committedState = nextState;
1186
+ return;
1187
+ }
1188
+ } catch (error) {
1189
+ activeRetryPolicy = prevRetryPolicy;
1190
+ currentState = prevCommitted;
1191
+ throw error;
1192
+ }
1193
+ committedState = nextState;
988
1194
  ctx.llm.emitAdaptersUpdated?.();
989
1195
  });
990
1196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyogvce/dsh-codex-subscription",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",