@lyogvce/dsh-codex-subscription 0.2.8 → 0.2.10

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
@@ -22,7 +22,7 @@
22
22
  - 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、规范化纯 UUID `session-id` / `x-client-request-id` 缓存头、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载)。
23
23
  5. **Codex 订阅限额内联展示与 ⚡ Fast 极速模式一键切换(`conversation.input.right`)**
24
24
  - 当会话选中 `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"`。
25
+ - **⚡ 会话树级 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
26
 
27
27
  ---
28
28
 
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,38 +176,108 @@ 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
  };
183
+ let confirmedSessionFastModes = {};
184
+ let knownSessionParents = {};
185
+ const latestSeqByNode = new Map();
186
+ const pendingBySeq = new Map();
160
187
  let inflight = null;
161
188
  let inflightForce = false;
162
189
  let requestSeq = 0;
163
190
  let fastModeSeq = 0;
191
+ let fastMutationEpoch = 0;
164
192
  const listeners = new Set();
165
193
  function setState(next) {
166
194
  state = next;
167
195
  for (const listener of listeners) listener();
168
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
+ }
232
+ function deriveSessionFastModes() {
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
+ }
254
+ return merged;
255
+ }
256
+ function parseServerSessionFastModes(raw) {
257
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
258
+ const nextMap = {};
259
+ for (const [k, v] of Object.entries(raw)) if (typeof k === "string" && k !== "" && typeof v === "boolean") nextMap[k] = v;
260
+ return nextMap;
261
+ }
169
262
  function mergeFastStateFromPayload(payload, allowSessionReplace) {
170
263
  const defaultFastMode = typeof payload?.defaultFastMode === "boolean" ? payload.defaultFastMode : state.defaultFastMode;
171
- if (allowSessionReplace && payload?.sessionFastModes && typeof payload.sessionFastModes === "object" && !Array.isArray(payload.sessionFastModes)) {
172
- const nextMap = {};
173
- for (const [k, v] of Object.entries(payload.sessionFastModes)) if (typeof k === "string" && k !== "" && typeof v === "boolean") nextMap[k] = v;
174
- return {
175
- defaultFastMode,
176
- sessionFastModes: nextMap
177
- };
264
+ mergeServerSessionParents(payload?.sessionParents);
265
+ if (allowSessionReplace) {
266
+ const parsedMap = parseServerSessionFastModes(payload?.sessionFastModes);
267
+ if (parsedMap !== void 0) confirmedSessionFastModes = parsedMap;
178
268
  }
179
269
  return {
180
270
  defaultFastMode,
181
- sessionFastModes: state.sessionFastModes
271
+ sessionFastModes: deriveSessionFastModes(),
272
+ sessionParents: knownSessionParents
182
273
  };
183
274
  }
184
275
  async function refresh(force = false) {
185
276
  if (!force && state.quota !== null && nowFn() - state.quota.fetchedAt < ttlMs) return;
186
277
  if (inflight !== null && (!force || inflightForce)) return inflight;
187
278
  const seq = ++requestSeq;
188
- const fastSeqAtStart = fastModeSeq;
279
+ const canReplaceFastAtStart = pendingBySeq.size === 0;
280
+ const fastEpochAtStart = fastMutationEpoch;
189
281
  inflightForce = force;
190
282
  setState({
191
283
  ...state,
@@ -210,7 +302,7 @@ window.__ModuleLoader__.load({
210
302
  }
211
303
  const payload = await response.json();
212
304
  if (seq !== requestSeq) return;
213
- const fastState = mergeFastStateFromPayload(payload, fastSeqAtStart === fastModeSeq);
305
+ const fastState = mergeFastStateFromPayload(payload, canReplaceFastAtStart && pendingBySeq.size === 0 && fastEpochAtStart === fastMutationEpoch);
214
306
  if (payload && payload.ok === true && payload.quota) setState({
215
307
  quota: payload.quota,
216
308
  ...fastState,
@@ -244,16 +336,39 @@ window.__ModuleLoader__.load({
244
336
  inflight = task;
245
337
  return task;
246
338
  }
247
- async function setFastMode(sessionId, enabled) {
339
+ async function setFastMode(sessionId, enabled, parentSessionId) {
248
340
  const nextFastMode = Boolean(enabled);
249
- 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);
250
344
  const seq = ++fastModeSeq;
345
+ fastMutationEpoch += 1;
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, {
351
+ seq,
352
+ selfKey,
353
+ parentKey,
354
+ enabled: nextFastMode
355
+ });
356
+ const rollback = (errorMsg) => {
357
+ pendingBySeq.delete(seq);
358
+ if (isTreeSeqSuperseded(seq, selfKey, parentKey)) return;
359
+ fastMutationEpoch += 1;
360
+ setState({
361
+ ...state,
362
+ sessionFastModes: deriveSessionFastModes(),
363
+ sessionParents: knownSessionParents,
364
+ error: errorMsg
365
+ });
366
+ };
251
367
  setState({
252
368
  ...state,
253
- sessionFastModes: {
254
- ...state.sessionFastModes,
255
- [key]: nextFastMode
256
- }
369
+ sessionFastModes: deriveSessionFastModes(),
370
+ sessionParents: knownSessionParents,
371
+ error: null
257
372
  });
258
373
  try {
259
374
  const response = await fetchFn(USAGE_ROUTE_PATH, {
@@ -263,17 +378,56 @@ window.__ModuleLoader__.load({
263
378
  Accept: "application/json"
264
379
  },
265
380
  body: JSON.stringify({
266
- sessionId: key,
381
+ sessionId: selfKey,
267
382
  fastMode: nextFastMode
268
383
  })
269
384
  });
270
- if (seq !== fastModeSeq || !response.ok) return;
385
+ if (!response.ok) {
386
+ const errPayload = await response.json().catch(() => null);
387
+ mergeServerSessionParents(errPayload?.sessionParents);
388
+ rollback(typeof errPayload?.error === "string" && errPayload.error !== "" ? errPayload.error : `HTTP ${response.status}`);
389
+ return;
390
+ }
271
391
  const payload = await response.json();
272
- if (seq === fastModeSeq) setState({
392
+ mergeServerSessionParents(payload?.sessionParents);
393
+ if (isTreeSeqSuperseded(seq, selfKey, parentKey)) {
394
+ pendingBySeq.delete(seq);
395
+ return;
396
+ }
397
+ if (!payload || payload.ok === false) {
398
+ rollback(typeof payload?.error === "string" && payload.error !== "" ? payload.error : "Invalid response");
399
+ return;
400
+ }
401
+ fastMutationEpoch += 1;
402
+ pendingBySeq.delete(seq);
403
+ const actualRootKey = resolveRootSessionId(selfKey, knownSessionParents, parentKey);
404
+ latestSeqByNode.set(actualRootKey, Math.max(latestSeqByNode.get(actualRootKey) ?? 0, seq));
405
+ const parsedMap = parseServerSessionFastModes(payload.sessionFastModes);
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;
418
+ const defaultFastMode = typeof payload.defaultFastMode === "boolean" ? payload.defaultFastMode : state.defaultFastMode;
419
+ setState({
273
420
  ...state,
274
- ...mergeFastStateFromPayload(payload, true)
421
+ defaultFastMode,
422
+ sessionFastModes: deriveSessionFastModes(),
423
+ sessionParents: knownSessionParents,
424
+ error: null
275
425
  });
276
- } catch {}
426
+ } catch (error) {
427
+ rollback(safeErrorMessage(error));
428
+ } finally {
429
+ pendingBySeq.delete(seq);
430
+ }
277
431
  }
278
432
  return {
279
433
  getSnapshot: () => state,
@@ -359,12 +513,26 @@ window.__ModuleLoader__.load({
359
513
  getSnapshot: () => null,
360
514
  subscribe: () => () => {}
361
515
  };
516
+ function renderFastBolt(size) {
517
+ return h("svg", {
518
+ className: "dsh-codex-quota-fast-bolt",
519
+ viewBox: "0 0 12 12",
520
+ width: size,
521
+ height: size,
522
+ "aria-hidden": true
523
+ }, h("path", {
524
+ d: "M6.8 1L2.5 6.8h3.1L4.8 11l4.7-5.8H6.4L6.8 1z",
525
+ fill: "currentColor"
526
+ }));
527
+ }
362
528
  function CodexQuotaPill(props) {
363
529
  const { sessionId, quotaStore, getModelStore, useProjection, useSession, t, getLocale } = props;
364
530
  const modelStore = getModelStore(sessionId);
365
531
  const directorySnap = React.useSyncExternalStore(modelStore.subscribe, modelStore.getSnapshot);
366
532
  const projectedSelection = typeof useProjection === "function" ? useProjection("modelSelection") : void 0;
367
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;
368
536
  const isCodex = isCodexProvider(resolveActiveProvider(directorySnap, projectedSelection));
369
537
  const quotaSnap = React.useSyncExternalStore(quotaStore.subscribe, quotaStore.getSnapshot);
370
538
  const [open, setOpen] = React.useState(false);
@@ -424,7 +592,7 @@ window.__ModuleLoader__.load({
424
592
  })), h("div", { className: "dsh-codex-quota-meta" }, h("span", null, `${t("quota.remainPrefix")}${remainPercent}%`), countdown !== "" ? h("span", null, countdown) : null));
425
593
  };
426
594
  const quota = quotaSnap.quota;
427
- const isFastMode = resolveSessionFastMode(quotaSnap, sessionId);
595
+ const isFastMode = resolveSessionFastMode(quotaSnap, sessionId, parentSessionId);
428
596
  const secondaryReached = Boolean(quota?.secondary && quota.secondary.usedPercent >= 100);
429
597
  const primaryReached = Boolean(quota?.limitReached && !secondaryReached);
430
598
  return h("span", {
@@ -462,16 +630,7 @@ window.__ModuleLoader__.load({
462
630
  r: 5.5,
463
631
  strokeDasharray: ringDash,
464
632
  transform: "rotate(-90 7 7)"
465
- })), isFastMode ? h("svg", {
466
- className: "dsh-codex-quota-fast-bolt",
467
- viewBox: "0 0 12 12",
468
- width: 11,
469
- height: 11,
470
- "aria-hidden": true
471
- }, h("path", {
472
- d: "M6.8 1L2.5 6.8h3.1L4.8 11l4.7-5.8H6.4L6.8 1z",
473
- fill: "currentColor"
474
- })) : null, h("span", { className: "dsh-codex-quota-text" }, pillText)), open ? h("div", {
633
+ })), isFastMode ? renderFastBolt(11) : null, h("span", { className: "dsh-codex-quota-text" }, pillText)), open ? h("div", {
475
634
  className: "dsh-codex-quota-popover",
476
635
  role: "dialog",
477
636
  "aria-label": t("quota.title"),
@@ -485,16 +644,7 @@ window.__ModuleLoader__.load({
485
644
  onClick: () => {
486
645
  quotaStore.refresh(true);
487
646
  }
488
- }, quotaSnap.loading ? t("quota.refreshing") : t("quota.refresh"))), quota !== null ? renderWindowSection(formatWindowTitle(quota.primary.windowSeconds, locale, DEFAULT_PRIMARY_WINDOW_SECONDS), quota.primary, primaryReached) : quotaSnap.loading || quotaSnap.error === null ? h("div", { className: "dsh-codex-quota-meta" }, t("quota.loading")) : null, quota?.secondary !== null && quota?.secondary !== void 0 ? renderWindowSection(formatWindowTitle(quota.secondary.windowSeconds, locale, DEFAULT_SECONDARY_WINDOW_SECONDS), quota.secondary, secondaryReached) : null, h("div", { className: "dsh-codex-fast-row" }, h("div", { className: "dsh-codex-fast-info" }, h("div", { className: "dsh-codex-fast-title" }, h("svg", {
489
- className: "dsh-codex-quota-fast-bolt",
490
- viewBox: "0 0 12 12",
491
- width: 12,
492
- height: 12,
493
- "aria-hidden": true
494
- }, h("path", {
495
- d: "M6.8 1L2.5 6.8h3.1L4.8 11l4.7-5.8H6.4L6.8 1z",
496
- fill: "currentColor"
497
- })), h("span", null, t("quota.fastTitle"))), h("div", { className: "dsh-codex-fast-desc" }, t("quota.fastDesc"))), h("button", {
647
+ }, quotaSnap.loading ? t("quota.refreshing") : t("quota.refresh"))), quota !== null ? renderWindowSection(formatWindowTitle(quota.primary.windowSeconds, locale, DEFAULT_PRIMARY_WINDOW_SECONDS), quota.primary, primaryReached) : quotaSnap.loading || quotaSnap.error === null ? h("div", { className: "dsh-codex-quota-meta" }, t("quota.loading")) : null, quota?.secondary !== null && quota?.secondary !== void 0 ? renderWindowSection(formatWindowTitle(quota.secondary.windowSeconds, locale, DEFAULT_SECONDARY_WINDOW_SECONDS), quota.secondary, secondaryReached) : null, h("div", { className: "dsh-codex-fast-row" }, h("div", { className: "dsh-codex-fast-info" }, h("div", { className: "dsh-codex-fast-title" }, renderFastBolt(12), h("span", null, t("quota.fastTitle"))), h("div", { className: "dsh-codex-fast-desc" }, t("quota.fastDesc"))), h("button", {
498
648
  type: "button",
499
649
  role: "switch",
500
650
  className: "dsh-codex-fast-switch",
@@ -502,7 +652,7 @@ window.__ModuleLoader__.load({
502
652
  "aria-label": t("quota.fastTitle"),
503
653
  "data-checked": isFastMode ? "true" : "false",
504
654
  onClick: () => {
505
- quotaStore.setFastMode(sessionId, !isFastMode);
655
+ quotaStore.setFastMode(sessionId, !isFastMode, parentSessionId);
506
656
  }
507
657
  }, h("span", { className: "dsh-codex-fast-switch-thumb" }))), quotaSnap.error !== null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error) : null) : null);
508
658
  }
package/lib/index.d.ts CHANGED
@@ -17,7 +17,7 @@ interface PluginContextLike {
17
17
  llm: LlmServiceLike;
18
18
  logger?: LoggerLike | undefined;
19
19
  get?: (name: string) => unknown;
20
- on?: (event: string, listener: () => void) => () => void;
20
+ on?: (event: string, listener: (...args: unknown[]) => void) => () => void;
21
21
  effect?: (factory: () => void | (() => void), label?: string) => () => void;
22
22
  inject?: (services: readonly string[], callback: (ctx: PluginContextLike) => void) => () => void;
23
23
  }
@@ -134,12 +134,14 @@ type CodexQuotaResponse = {
134
134
  readonly quota: CodexQuotaSnapshot;
135
135
  readonly defaultFastMode?: boolean | undefined;
136
136
  readonly sessionFastModes?: Readonly<Record<string, boolean>> | undefined;
137
+ readonly sessionParents?: Readonly<Record<string, string>> | undefined;
137
138
  } | {
138
139
  readonly ok: false;
139
140
  readonly error: string;
140
141
  readonly quota?: CodexQuotaSnapshot | undefined;
141
142
  readonly defaultFastMode?: boolean | undefined;
142
143
  readonly sessionFastModes?: Readonly<Record<string, boolean>> | undefined;
144
+ readonly sessionParents?: Readonly<Record<string, string>> | undefined;
143
145
  };
144
146
  interface UsageFetcherOptions {
145
147
  readonly resolveCredentials: () => Promise<CodexResolvedCredentials>;
package/lib/index.js CHANGED
@@ -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",
@@ -35,6 +36,8 @@ const DEFAULT_DISABLED_MODELS = Object.freeze([
35
36
  function parseRawModelsCache(rawText) {
36
37
  const parsed = JSON.parse(rawText);
37
38
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new SyntaxError("models_cache.json root must be a JSON object");
39
+ const models = parsed.models;
40
+ if (!Array.isArray(models) || !models.some((entry) => entry !== null && typeof entry === "object" && typeof entry.slug === "string" && entry.slug.trim() !== "" && entry.visibility === "list")) throw new SyntaxError("models_cache.json must contain at least one visible model entry");
38
41
  return parsed;
39
42
  }
40
43
  function normalizeCodexSessionId(sessionId) {
@@ -42,6 +45,26 @@ function normalizeCodexSessionId(sessionId) {
42
45
  const stripped = sessionId.trim().replace(/^session-/iu, "").trim();
43
46
  return stripped.length > 0 ? stripped : void 0;
44
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
+ }
45
68
  function isSameModelsCatalogPayload(a, b) {
46
69
  if (!a || !b) return false;
47
70
  const { fetched_at: _fa, ...restA } = a;
@@ -94,7 +117,7 @@ function resolveFastServiceTier(entry) {
94
117
  if (entry.service_tiers.length === 0 && Array.isArray(entry.additional_speed_tiers)) return entry.additional_speed_tiers.some((t) => typeof t === "string" && t.trim().toLowerCase() === "fast") ? "priority" : void 0;
95
118
  return;
96
119
  }
97
- if (Array.isArray(entry.additional_speed_tiers) && entry.additional_speed_tiers.some((t) => typeof t === "string" && t.trim().toLowerCase() === "fast")) return "priority";
120
+ if (Array.isArray(entry.additional_speed_tiers)) return entry.additional_speed_tiers.some((t) => typeof t === "string" && t.trim().toLowerCase() === "fast") ? "priority" : void 0;
98
121
  return "priority";
99
122
  }
100
123
  function parseModelsCache(rawCache, baseModels, disabledModels = [], contextWindows = {}) {
@@ -218,7 +241,7 @@ function sanitizeCodexPayload(payload, modelMeta, fastMode = false) {
218
241
  const tier = modelMeta === void 0 ? "priority" : modelMeta.fastServiceTier;
219
242
  if (typeof tier === "string" && tier !== "") cleaned.service_tier = tier;
220
243
  else delete cleaned.service_tier;
221
- } else if (cleaned.service_tier !== "priority" && cleaned.service_tier !== "ultrafast") delete cleaned.service_tier;
244
+ } else delete cleaned.service_tier;
222
245
  const currentText = cleaned.text && typeof cleaned.text === "object" && !Array.isArray(cleaned.text) ? { ...cleaned.text } : void 0;
223
246
  if (modelMeta?.supportVerbosity === false) {
224
247
  if (currentText !== void 0) {
@@ -587,10 +610,16 @@ const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
587
610
  const REQUEST_IMAGE_MAX_BYTES = 1048576;
588
611
  const MAX_LOG_FILE_BYTES = 524288;
589
612
  const MAX_PERSISTED_SESSIONS = 200;
590
- const DEFAULT_SESSION_KEY = "__default__";
613
+ const MAX_PERSISTED_PARENTS = 1e3;
591
614
  function resolvePluginCacheDir(codexHome) {
592
615
  return join(codexHome, PLUGIN_CACHE_DIR_NAME);
593
616
  }
617
+ function sliceUtf8Prefix(buf, maxBytes) {
618
+ if (buf.length <= maxBytes) return buf;
619
+ let end = maxBytes;
620
+ while (end > 0 && (buf[end] & 192) === 128) end -= 1;
621
+ return buf.subarray(0, end);
622
+ }
594
623
  function appendPluginLog(codexHome, ctx, level, event, detail) {
595
624
  const line = `[${new Date().toISOString()}] [${level}] ${event} ${JSON.stringify(detail)}\n`;
596
625
  if (level === "ERROR") {
@@ -598,6 +627,7 @@ function appendPluginLog(codexHome, ctx, level, event, detail) {
598
627
  else console.error(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
599
628
  } else if (level === "WARN") {
600
629
  if (typeof ctx.logger?.warn === "function") ctx.logger.warn(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
630
+ else console.warn(`[dsh-codex-subscription] ${event}: ${JSON.stringify(detail)}`);
601
631
  }
602
632
  try {
603
633
  const cacheDir = resolvePluginCacheDir(codexHome);
@@ -606,18 +636,24 @@ function appendPluginLog(codexHome, ctx, level, event, detail) {
606
636
  mode: 448
607
637
  });
608
638
  const logPath = join(cacheDir, LOG_FILE_NAME);
639
+ const rawLineBuf = Buffer.from(line, "utf8");
640
+ const lineBuf = rawLineBuf.length <= MAX_LOG_FILE_BYTES ? rawLineBuf : Buffer.concat([sliceUtf8Prefix(rawLineBuf, 524287), Buffer.from("\n", "utf8")]);
641
+ let st;
609
642
  try {
610
- if (statSync(logPath).size > MAX_LOG_FILE_BYTES) {
611
- const buf = readFileSync(logPath);
612
- const halfOffset = Math.max(0, buf.length - Math.floor(MAX_LOG_FILE_BYTES / 2));
613
- const newlineIdx = buf.indexOf(10, halfOffset);
614
- writeFileAtomicSync(logPath, buf.subarray(newlineIdx >= 0 && newlineIdx + 1 < buf.length ? newlineIdx + 1 : halfOffset));
615
- }
616
- } catch {}
617
- appendFileSync(logPath, line, {
618
- encoding: "utf8",
619
- mode: 384
620
- });
643
+ st = statSync(logPath);
644
+ } catch {
645
+ st = void 0;
646
+ }
647
+ if (st !== void 0 && st.size + lineBuf.length > MAX_LOG_FILE_BYTES) {
648
+ const buf = readFileSync(logPath);
649
+ const keepBudget = Math.max(0, Math.floor(MAX_LOG_FILE_BYTES / 2) - lineBuf.length);
650
+ const startOffset = Math.max(0, buf.length - keepBudget);
651
+ const newlineIdx = keepBudget > 0 ? buf.indexOf(10, startOffset) : -1;
652
+ const tail = newlineIdx >= 0 && newlineIdx + 1 < buf.length ? buf.subarray(newlineIdx + 1) : Buffer.alloc(0);
653
+ writeFileAtomicSync(logPath, Buffer.concat([tail, lineBuf]));
654
+ return;
655
+ }
656
+ appendFileSync(logPath, lineBuf, { mode: 384 });
621
657
  } catch {}
622
658
  }
623
659
  function readBackupCache(codexHome) {
@@ -627,25 +663,44 @@ function readBackupCache(codexHome) {
627
663
  return;
628
664
  }
629
665
  }
630
- function writeBackupCache(codexHome, raw) {
666
+ function writeBackupCache(codexHome, ctx, raw) {
631
667
  try {
632
668
  writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), BACKUP_CACHE_FILE_NAME), `${JSON.stringify(raw, null, 2)}\n`);
633
- } catch {}
669
+ } catch (error) {
670
+ appendPluginLog(codexHome, ctx, "WARN", "catalog.backup_write_error", { error: safeErrorMessage(error) });
671
+ }
634
672
  }
635
- function readPersistedSessionFastModes(codexHome) {
636
- const result = new Map();
673
+ function readPersistedRuntimeState(codexHome) {
674
+ const sessionFastModes = new Map();
675
+ const sessionParents = new Map();
637
676
  try {
638
- const entries = JSON.parse(readFileSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), "utf8"))?.sessionFastModes;
639
- if (entries && typeof entries === "object" && !Array.isArray(entries)) {
640
- 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);
641
688
  }
642
689
  } catch {}
643
- return result;
690
+ return {
691
+ sessionFastModes,
692
+ sessionParents
693
+ };
644
694
  }
645
- function writePersistedSessionFastModes(codexHome, sessionFastModes) {
695
+ function writePersistedRuntimeState(codexHome, ctx, sessionFastModes, sessionParents) {
646
696
  try {
647
- writeFileAtomicSync(join(resolvePluginCacheDir(codexHome), RUNTIME_STATE_FILE_NAME), `${JSON.stringify({ sessionFastModes: Object.fromEntries(sessionFastModes) }, null, 2)}\n`);
648
- } catch {}
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`);
701
+ } catch (error) {
702
+ appendPluginLog(codexHome, ctx, "WARN", "fast_mode.persist_error", { error: safeErrorMessage(error) });
703
+ }
649
704
  }
650
705
  const Config = z.object({
651
706
  fastMode: z.boolean().default(false).description("新会话默认是否开启 Fast 极速模式(未填时默认 false;单会话可在右下角限额弹窗内独立切换)").volatile(),
@@ -689,29 +744,142 @@ function apply(ctx, config = {}) {
689
744
  const getConfigFastMode = () => readVolatile(config.fastMode, false) === true;
690
745
  const getDisabledModels = () => readVolatile(config.disabledModels, DEFAULT_DISABLED_MODELS);
691
746
  const getContextWindows = () => readVolatile(config.contextWindows, {});
692
- const sessionFastModes = readPersistedSessionFastModes(getCodexHome());
747
+ const { sessionFastModes, sessionParents } = readPersistedRuntimeState(getCodexHome());
748
+ const recordSessionParent = (childKey, parentKey) => {
749
+ if (childKey === "" || parentKey === "" || childKey === parentKey) return false;
750
+ let cursor = parentKey;
751
+ let depth = 0;
752
+ while (cursor !== void 0 && depth < 32) {
753
+ if (cursor === childKey) return false;
754
+ cursor = sessionParents.get(cursor);
755
+ depth += 1;
756
+ }
757
+ if (sessionParents.get(childKey) === parentKey) return false;
758
+ sessionParents.delete(childKey);
759
+ sessionParents.set(childKey, parentKey);
760
+ while (sessionParents.size > MAX_PERSISTED_PARENTS) {
761
+ let victim;
762
+ for (const candidate of sessionParents.keys()) {
763
+ const root = resolveRootSessionId(candidate, sessionParents);
764
+ if (!sessionFastModes.has(root)) {
765
+ victim = candidate;
766
+ break;
767
+ }
768
+ }
769
+ const keyToDelete = victim ?? sessionParents.keys().next().value;
770
+ if (keyToDelete === void 0) break;
771
+ sessionParents.delete(keyToDelete);
772
+ }
773
+ return true;
774
+ };
775
+ const findLiveSession = (key) => {
776
+ const sessions = ctx.get?.("sessions");
777
+ if (typeof sessions?.get !== "function") return void 0;
778
+ return sessions.get(key) ?? sessions.get(`session-${key}`);
779
+ };
780
+ const findLiveAgent = (key) => {
781
+ const agents = ctx.get?.("agents");
782
+ if (typeof agents?.get !== "function") return void 0;
783
+ return agents.get(key) ?? agents.get(`session-${key}`);
784
+ };
785
+ const inspectSessionParentFromHost = (sessionLike, agentLike) => {
786
+ const childKey = normalizeCodexSessionId(sessionLike?.id ?? sessionLike?.header?.id ?? agentLike?.id ?? agentLike?.session?.id);
787
+ if (childKey === void 0) return false;
788
+ const header = sessionLike?.header ?? agentLike?.session?.header;
789
+ if (header?.origin === "subagent" && typeof header.parentSession === "string") {
790
+ const parentKey = normalizeCodexSessionId(header.parentSession);
791
+ if (parentKey !== void 0) return recordSessionParent(childKey, parentKey);
792
+ }
793
+ const agentTeams = ctx.get?.("agentTeams");
794
+ if (typeof agentTeams?.tryMembership === "function") {
795
+ const agent = agentLike ?? findLiveAgent(childKey);
796
+ if (agent !== void 0) {
797
+ const membership = agentTeams.tryMembership(agent);
798
+ if (membership?.role === "teammate" && typeof membership.root?.id === "string") {
799
+ const rootKey = normalizeCodexSessionId(membership.root.id);
800
+ if (rootKey !== void 0) return recordSessionParent(childKey, rootKey);
801
+ }
802
+ }
803
+ }
804
+ return false;
805
+ };
806
+ const normalizeFastModesToRoots = () => {
807
+ let migrated = false;
808
+ for (const [k, v] of [...sessionFastModes.entries()]) {
809
+ const rootKey = resolveRootSessionId(k, sessionParents);
810
+ if (rootKey !== k) {
811
+ if (!sessionFastModes.has(rootKey)) sessionFastModes.set(rootKey, v);
812
+ sessionFastModes.delete(k);
813
+ migrated = true;
814
+ }
815
+ }
816
+ return migrated;
817
+ };
818
+ const syncAllLiveSessionsFromHost = () => {
819
+ let changed = false;
820
+ const sessions = ctx.get?.("sessions");
821
+ if (typeof sessions?.list === "function") {
822
+ for (const s of sessions.list()) if (inspectSessionParentFromHost(s)) changed = true;
823
+ }
824
+ const agents = ctx.get?.("agents");
825
+ if (typeof agents?.list === "function") {
826
+ for (const a of agents.list()) if (inspectSessionParentFromHost(a?.session, a)) changed = true;
827
+ }
828
+ if (normalizeFastModesToRoots()) changed = true;
829
+ return changed;
830
+ };
831
+ const resolveRootSessionKey = (sessionId) => {
832
+ const initiatorId = (ctx.get?.("agents"))?.currentInitiator?.()?.id;
833
+ const startKey = normalizeCodexSessionId(sessionId) ?? normalizeCodexSessionId(initiatorId);
834
+ if (startKey === void 0) return DEFAULT_SESSION_KEY;
835
+ let current = startKey;
836
+ const visited = new Set([current]);
837
+ while (visited.size <= 32) {
838
+ const liveSession = findLiveSession(current);
839
+ const liveAgent = findLiveAgent(current);
840
+ if (liveSession !== void 0 || liveAgent !== void 0) inspectSessionParentFromHost(liveSession, liveAgent);
841
+ const parent = sessionParents.get(current);
842
+ if (parent === void 0 || visited.has(parent)) break;
843
+ visited.add(parent);
844
+ current = parent;
845
+ }
846
+ normalizeFastModesToRoots();
847
+ return current;
848
+ };
693
849
  const getEffectiveFastMode = (sessionId) => {
694
- const key = normalizeCodexSessionId(sessionId);
695
- if (key !== void 0 && sessionFastModes.has(key)) return sessionFastModes.get(key) === true;
696
- if (key === void 0 && sessionFastModes.has(DEFAULT_SESSION_KEY)) return sessionFastModes.get(DEFAULT_SESSION_KEY) === true;
850
+ const rootKey = resolveRootSessionKey(sessionId);
851
+ if (sessionFastModes.has(rootKey)) return sessionFastModes.get(rootKey) === true;
697
852
  return getConfigFastMode();
698
853
  };
699
854
  const setSessionFastMode = (sessionId, nextFastMode) => {
700
855
  const codexHome = getCodexHome();
701
- const key = normalizeCodexSessionId(sessionId) ?? DEFAULT_SESSION_KEY;
702
- sessionFastModes.delete(key);
703
- sessionFastModes.set(key, nextFastMode);
856
+ syncAllLiveSessionsFromHost();
857
+ const rootKey = resolveRootSessionKey(sessionId);
858
+ sessionFastModes.delete(rootKey);
859
+ sessionFastModes.set(rootKey, nextFastMode);
860
+ normalizeFastModesToRoots();
704
861
  while (sessionFastModes.size > MAX_PERSISTED_SESSIONS) {
705
862
  const oldest = sessionFastModes.keys().next().value;
706
863
  if (oldest === void 0) break;
707
864
  sessionFastModes.delete(oldest);
708
865
  }
709
- writePersistedSessionFastModes(codexHome, sessionFastModes);
866
+ writePersistedRuntimeState(codexHome, ctx, sessionFastModes, sessionParents);
710
867
  appendPluginLog(codexHome, ctx, "INFO", "fast_mode.updated", {
711
- sessionId: key,
868
+ sessionId: normalizeCodexSessionId(sessionId) ?? "__default__",
869
+ rootSessionId: rootKey,
712
870
  fastMode: nextFastMode
713
871
  });
714
872
  };
873
+ const buildProjectedSessionFastModes = () => {
874
+ syncAllLiveSessionsFromHost();
875
+ const projected = {};
876
+ for (const [rootKey, v] of sessionFastModes) projected[rootKey] = v;
877
+ for (const childKey of [...sessionParents.keys()]) {
878
+ const rootKey = resolveRootSessionId(childKey, sessionParents);
879
+ if (sessionFastModes.has(rootKey)) projected[childKey] = sessionFastModes.get(rootKey) === true;
880
+ }
881
+ return projected;
882
+ };
715
883
  const authManager = createAuthManager({
716
884
  getCodexHome,
717
885
  getClientVersion: () => ensureSnapshot().catalog.clientVersion,
@@ -719,6 +887,7 @@ function apply(ctx, config = {}) {
719
887
  });
720
888
  let fileSignature;
721
889
  let cachedRaw;
890
+ let lastRejectedRaw;
722
891
  let rawRevision = 0;
723
892
  let snapshotSignature;
724
893
  let currentCatalog = {
@@ -742,7 +911,9 @@ function apply(ctx, config = {}) {
742
911
  const { catalog } = ensureSnapshot();
743
912
  const modelMeta = catalog.metadataById.get(model?.id);
744
913
  const { temperature: _temperature, sessionId, onPayload, ...rest } = options;
745
- const normalizedSessionId = normalizeCodexSessionId(sessionId);
914
+ const initiatorId = (ctx.get?.("agents"))?.currentInitiator?.()?.id;
915
+ const normalizedSessionId = normalizeCodexSessionId(sessionId) ?? normalizeCodexSessionId(initiatorId);
916
+ resolveRootSessionKey(normalizedSessionId);
746
917
  return {
747
918
  ...rest,
748
919
  ...normalizedSessionId !== void 0 ? { sessionId: normalizedSessionId } : {},
@@ -782,6 +953,7 @@ function apply(ctx, config = {}) {
782
953
  const contextWindows = getContextWindows();
783
954
  const setActiveRaw = (next) => {
784
955
  cachedRaw = next;
956
+ lastRejectedRaw = void 0;
785
957
  rawRevision += 1;
786
958
  };
787
959
  const tryRestoreFromBackup = (extra, minVersion) => {
@@ -820,16 +992,20 @@ function apply(ctx, config = {}) {
820
992
  fileSignature = nextFileSig;
821
993
  try {
822
994
  const incomingRaw = parseRawModelsCache(readFileSync(cachePath, "utf8"));
823
- if (cachedRaw !== void 0 && compareClientVersions(incomingRaw.client_version, cachedRaw.client_version) < 0) appendPluginLog(codexHome, ctx, "WARN", "catalog.downgrade_rejected", {
824
- cachePath,
825
- keptVersion: cachedRaw.client_version ?? "0.163.0",
826
- rejectedVersion: incomingRaw.client_version ?? "unknown",
827
- incomingFetchedAt: incomingRaw.fetched_at ?? null
828
- });
829
- else if (cachedRaw !== void 0 || !tryRestoreFromBackup({ diskVersion: incomingRaw.client_version ?? "unknown" }, incomingRaw.client_version)) {
995
+ if (cachedRaw !== void 0 && compareClientVersions(incomingRaw.client_version, cachedRaw.client_version) < 0) {
996
+ if (!isSameModelsCatalogPayload(lastRejectedRaw, incomingRaw)) {
997
+ lastRejectedRaw = incomingRaw;
998
+ appendPluginLog(codexHome, ctx, "WARN", "catalog.downgrade_rejected", {
999
+ cachePath,
1000
+ keptVersion: cachedRaw.client_version ?? "0.163.0",
1001
+ rejectedVersion: incomingRaw.client_version ?? "unknown",
1002
+ incomingFetchedAt: incomingRaw.fetched_at ?? null
1003
+ });
1004
+ }
1005
+ } else if (cachedRaw !== void 0 || !tryRestoreFromBackup({ diskVersion: incomingRaw.client_version ?? "unknown" }, incomingRaw.client_version)) {
830
1006
  if (!isSameModelsCatalogPayload(cachedRaw, incomingRaw)) {
831
1007
  setActiveRaw(incomingRaw);
832
- writeBackupCache(codexHome, incomingRaw);
1008
+ writeBackupCache(codexHome, ctx, incomingRaw);
833
1009
  appendPluginLog(codexHome, ctx, "INFO", "catalog.load", {
834
1010
  cachePath,
835
1011
  clientVersion: incomingRaw.client_version ?? "0.163.0",
@@ -917,12 +1093,26 @@ function apply(ctx, config = {}) {
917
1093
  requestBody: "buffered",
918
1094
  fetch: async (request) => {
919
1095
  if (request.method === "POST") {
920
- const body = await request.json().catch(() => null);
921
- if (typeof body?.fastMode === "boolean") setSessionFastMode(typeof body.sessionId === "string" ? body.sessionId : void 0, body.fastMode);
1096
+ let body;
1097
+ try {
1098
+ body = await request.json();
1099
+ } catch {
1100
+ body = null;
1101
+ }
1102
+ const record = body !== null && typeof body === "object" && !Array.isArray(body) ? body : null;
1103
+ if (record === null || typeof record.fastMode !== "boolean" || record.sessionId !== void 0 && typeof record.sessionId !== "string") return new Response(JSON.stringify({
1104
+ ok: false,
1105
+ error: "Invalid request body: boolean fastMode is required"
1106
+ }), {
1107
+ status: 400,
1108
+ headers: { "content-type": "application/json" }
1109
+ });
1110
+ setSessionFastMode(record.sessionId, record.fastMode);
922
1111
  return new Response(JSON.stringify({
923
1112
  ok: true,
924
1113
  defaultFastMode: getConfigFastMode(),
925
- sessionFastModes: Object.fromEntries(sessionFastModes)
1114
+ sessionFastModes: buildProjectedSessionFastModes(),
1115
+ sessionParents: Object.fromEntries(sessionParents)
926
1116
  }), {
927
1117
  status: 200,
928
1118
  headers: { "content-type": "application/json" }
@@ -933,7 +1123,8 @@ function apply(ctx, config = {}) {
933
1123
  return new Response(JSON.stringify({
934
1124
  ...result,
935
1125
  defaultFastMode: getConfigFastMode(),
936
- sessionFastModes: Object.fromEntries(sessionFastModes)
1126
+ sessionFastModes: buildProjectedSessionFastModes(),
1127
+ sessionParents: Object.fromEntries(sessionParents)
937
1128
  }), {
938
1129
  status: 200,
939
1130
  headers: { "content-type": "application/json" }
@@ -945,6 +1136,17 @@ function apply(ctx, config = {}) {
945
1136
  };
946
1137
  if (typeof ctx.inject === "function") ctx.inject(["connection"], mountUsageRoute);
947
1138
  else mountUsageRoute(ctx);
1139
+ ctx.on?.("session/created", (session) => {
1140
+ if (session && typeof session === "object") {
1141
+ if (inspectSessionParentFromHost(session)) writePersistedRuntimeState(getCodexHome(), ctx, sessionFastModes, sessionParents);
1142
+ }
1143
+ });
1144
+ ctx.on?.("agent/created", (payload) => {
1145
+ if (payload && typeof payload === "object") {
1146
+ const agent = payload.agent;
1147
+ if (inspectSessionParentFromHost(agent?.session, agent)) writePersistedRuntimeState(getCodexHome(), ctx, sessionFastModes, sessionParents);
1148
+ }
1149
+ });
948
1150
  ctx.on?.("loader/volatile-update", () => {
949
1151
  ensureSnapshot();
950
1152
  ctx.llm.emitAdaptersUpdated?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyogvce/dsh-codex-subscription",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "复用本机 ~/.codex 订阅凭证与 models_cache.json 模型缓存,在 DSH 中通过 SSE 直连调用 Codex 模型。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",