@lyogvce/dsh-codex-subscription 0.2.9 → 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,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
@@ -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",
@@ -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
  }
@@ -708,29 +744,142 @@ function apply(ctx, config = {}) {
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 { 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
+ };
712
849
  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;
850
+ const rootKey = resolveRootSessionKey(sessionId);
851
+ if (sessionFastModes.has(rootKey)) return sessionFastModes.get(rootKey) === true;
716
852
  return getConfigFastMode();
717
853
  };
718
854
  const setSessionFastMode = (sessionId, nextFastMode) => {
719
855
  const codexHome = getCodexHome();
720
- const key = normalizeCodexSessionId(sessionId) ?? DEFAULT_SESSION_KEY;
721
- sessionFastModes.delete(key);
722
- sessionFastModes.set(key, nextFastMode);
856
+ syncAllLiveSessionsFromHost();
857
+ const rootKey = resolveRootSessionKey(sessionId);
858
+ sessionFastModes.delete(rootKey);
859
+ sessionFastModes.set(rootKey, nextFastMode);
860
+ normalizeFastModesToRoots();
723
861
  while (sessionFastModes.size > MAX_PERSISTED_SESSIONS) {
724
862
  const oldest = sessionFastModes.keys().next().value;
725
863
  if (oldest === void 0) break;
726
864
  sessionFastModes.delete(oldest);
727
865
  }
728
- writePersistedSessionFastModes(codexHome, ctx, sessionFastModes);
866
+ writePersistedRuntimeState(codexHome, ctx, sessionFastModes, sessionParents);
729
867
  appendPluginLog(codexHome, ctx, "INFO", "fast_mode.updated", {
730
- sessionId: key,
868
+ sessionId: normalizeCodexSessionId(sessionId) ?? "__default__",
869
+ rootSessionId: rootKey,
731
870
  fastMode: nextFastMode
732
871
  });
733
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
+ };
734
883
  const authManager = createAuthManager({
735
884
  getCodexHome,
736
885
  getClientVersion: () => ensureSnapshot().catalog.clientVersion,
@@ -762,7 +911,9 @@ function apply(ctx, config = {}) {
762
911
  const { catalog } = ensureSnapshot();
763
912
  const modelMeta = catalog.metadataById.get(model?.id);
764
913
  const { temperature: _temperature, sessionId, onPayload, ...rest } = options;
765
- const normalizedSessionId = normalizeCodexSessionId(sessionId);
914
+ const initiatorId = (ctx.get?.("agents"))?.currentInitiator?.()?.id;
915
+ const normalizedSessionId = normalizeCodexSessionId(sessionId) ?? normalizeCodexSessionId(initiatorId);
916
+ resolveRootSessionKey(normalizedSessionId);
766
917
  return {
767
918
  ...rest,
768
919
  ...normalizedSessionId !== void 0 ? { sessionId: normalizedSessionId } : {},
@@ -960,7 +1111,8 @@ function apply(ctx, config = {}) {
960
1111
  return new Response(JSON.stringify({
961
1112
  ok: true,
962
1113
  defaultFastMode: getConfigFastMode(),
963
- sessionFastModes: Object.fromEntries(sessionFastModes)
1114
+ sessionFastModes: buildProjectedSessionFastModes(),
1115
+ sessionParents: Object.fromEntries(sessionParents)
964
1116
  }), {
965
1117
  status: 200,
966
1118
  headers: { "content-type": "application/json" }
@@ -971,7 +1123,8 @@ function apply(ctx, config = {}) {
971
1123
  return new Response(JSON.stringify({
972
1124
  ...result,
973
1125
  defaultFastMode: getConfigFastMode(),
974
- sessionFastModes: Object.fromEntries(sessionFastModes)
1126
+ sessionFastModes: buildProjectedSessionFastModes(),
1127
+ sessionParents: Object.fromEntries(sessionParents)
975
1128
  }), {
976
1129
  status: 200,
977
1130
  headers: { "content-type": "application/json" }
@@ -983,6 +1136,17 @@ function apply(ctx, config = {}) {
983
1136
  };
984
1137
  if (typeof ctx.inject === "function") ctx.inject(["connection"], mountUsageRoute);
985
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
+ });
986
1150
  ctx.on?.("loader/volatile-update", () => {
987
1151
  ensureSnapshot();
988
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.9",
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",