minecodex 1.2.0 → 1.2.2

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.
@@ -1,3 +1,4 @@
1
+ import { cloudImagesAction } from "./cloud-images.mjs";
1
2
  // CodexRuntime:Codex Renderer 生命周期与 host action 执行。
2
3
  // 注入源(createInjectionSource)与 CDP 客户端分别在 codex-injection.mjs / codex-cdp.mjs。
3
4
 
@@ -44,6 +45,19 @@ export {
44
45
  responsiveSummaryDisplayMode,
45
46
  responsiveSummaryVisibleSurface,
46
47
  } from "./codex-injection.mjs";
48
+
49
+ export const SESSION_STATUS_THREAD_ID_EXPRESSION = `(() => {
50
+ const composer = document.querySelector('[data-codex-composer="true"][contenteditable="true"]')
51
+ ?? document.querySelector('[contenteditable="true"][role="textbox"]');
52
+ const card = composer?.closest('[data-codex-composer-root]');
53
+ const portal = card?.querySelector('[data-above-composer-portal]');
54
+ if (portal) return portal.getAttribute('data-above-composer-conversation-id');
55
+ if (card?.getAttribute('data-composer-placement') === 'home') return null;
56
+ const selected = document.querySelector('[data-app-action-sidebar-thread-selected="true"]');
57
+ return selected?.getAttribute('data-app-action-sidebar-thread-id')
58
+ ?? location.pathname + location.search + location.hash;
59
+ })()`;
60
+
47
61
  export class CodexRuntime {
48
62
  constructor({
49
63
  features,
@@ -90,6 +104,8 @@ export class CodexRuntime {
90
104
  this.scriptIds = new Map();
91
105
  this.bindingTokens = new Map();
92
106
  this.hostActionChains = new Map();
107
+ this.sessionStatusUpdates = new Map();
108
+ this.sessionStatusRevision = 0;
93
109
  this.refreshPromise = null;
94
110
  this.stopping = false;
95
111
  this.monitorPromise = null;
@@ -277,7 +293,9 @@ export class CodexRuntime {
277
293
  }
278
294
 
279
295
  async startTargetDiscovery() {
280
- const response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/version`);
296
+ const response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/version`, {
297
+ signal: AbortSignal.timeout(3_000),
298
+ });
281
299
  if (!response?.ok) {
282
300
  throw new Error(`Codex CDP version returned HTTP ${response?.status ?? "error"}`);
283
301
  }
@@ -342,7 +360,9 @@ export class CodexRuntime {
342
360
 
343
361
  async isAvailable() {
344
362
  try {
345
- const response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/version`);
363
+ const response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/version`, {
364
+ signal: AbortSignal.timeout(3_000),
365
+ });
346
366
  if (!response?.ok) return false;
347
367
  requireCdpWebSocketUrl(await response.json(), this.cdpPort);
348
368
  return true;
@@ -505,6 +525,12 @@ export class CodexRuntime {
505
525
  }
506
526
  if (request.token !== this.bindingTokens.get(targetId) || typeof request.requestId !== "string") return;
507
527
  const feature = this.features.find((candidate) => candidate.id === request.featureId);
528
+ if (feature?.id === "session-status" && request.action === "refresh-session-status") {
529
+ if (this.sessionStatusReader && this.clients.get(targetId) === client && !this.stopping) {
530
+ await this.publishSessionStatus(targetId);
531
+ }
532
+ return;
533
+ }
508
534
  if (feature && request.action === SURFACE_LOAD_ACTION) {
509
535
  try {
510
536
  const result = await this.loadSurfaceIntoFrame(client, feature, request.payload);
@@ -528,6 +554,18 @@ export class CodexRuntime {
528
554
  await this.importGeneratedImage(feature, request.payload);
529
555
  return;
530
556
  }
557
+ if (["list-cloud-images", "import-cloud-image"].includes(request.action)) {
558
+ try {
559
+ let result = await cloudImagesAction(client, request.action === "list-cloud-images" ? "list" : "import", request.payload);
560
+ if (request.action === "import-cloud-image" && !result.skipped) {
561
+ result = await this.importGeneratedImage(feature, result);
562
+ }
563
+ await this.resolveHostAction(client, request.requestId, { ok: true, result });
564
+ } catch (error) {
565
+ await this.resolveHostAction(client, request.requestId, { ok: false, error: { code: "CLOUD_IMAGES_FAILED", message: error.message } });
566
+ }
567
+ return;
568
+ }
531
569
  try {
532
570
  const result = request.action === "create-image-edit-thread"
533
571
  ? await this.createImageEditThread(client, feature, request.payload, request.requestId)
@@ -555,12 +593,15 @@ export class CodexRuntime {
555
593
  if (!response?.ok) {
556
594
  throw new Error(`Generated image import returned HTTP ${response?.status ?? "error"}`);
557
595
  }
596
+ return response.json();
558
597
  }
559
598
 
560
599
  async refreshTargets() {
561
600
  let response;
562
601
  try {
563
- response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/list`);
602
+ response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/list`, {
603
+ signal: AbortSignal.timeout(3_000),
604
+ });
564
605
  } catch (error) {
565
606
  throw this.markCodexUnavailable(error);
566
607
  }
@@ -591,7 +632,8 @@ export class CodexRuntime {
591
632
  ...this.targetFailures.keys(),
592
633
  ]);
593
634
  for (const id of trackedTargetIds) {
594
- if (liveRendererIds.has(id)) continue;
635
+ const socketState = this.clients.get(id)?.socket.readyState;
636
+ if (liveRendererIds.has(id) && (socketState == null || socketState === 1)) continue;
595
637
  removedTarget = true;
596
638
  this.clients.get(id)?.socket.close();
597
639
  this.clients.delete(id);
@@ -627,7 +669,13 @@ export class CodexRuntime {
627
669
  const bindingToken = randomBytes(24).toString("hex");
628
670
  this.bindingTokens.set(target.id, bindingToken);
629
671
  client.on?.("Runtime.bindingCalled", (params) => {
630
- this.enqueueHostAction(target.id, () => this.handleBindingCalled(target.id, client, params))
672
+ let action;
673
+ try { action = JSON.parse(params.payload)?.action; } catch { return; }
674
+ // 统计读取独立合并,避免排在图片等用户操作后面。
675
+ const work = action === "refresh-session-status"
676
+ ? this.handleBindingCalled(target.id, client, params)
677
+ : this.enqueueHostAction(target.id, () => this.handleBindingCalled(target.id, client, params));
678
+ work
631
679
  .catch((error) => this.logger.warn?.("Codex Host action failed", error.message));
632
680
  });
633
681
  const source = createInjectionSource(this.features, {
@@ -680,26 +728,43 @@ export class CodexRuntime {
680
728
  this.notifyStatusChange();
681
729
  }
682
730
 
683
- async publishSessionStatus() {
731
+ async publishSessionStatus(onlyTargetId = null) {
684
732
  for (const [targetId, client] of this.clients) {
733
+ if (onlyTargetId && targetId !== onlyTargetId) continue;
685
734
  if (!this.injectedTargets.has(targetId)) continue;
686
- try {
687
- const response = await client.send("Runtime.evaluate", {
688
- expression: `(() => {
689
- const selected = document.querySelector('[data-app-action-sidebar-thread-selected="true"]');
690
- return selected?.getAttribute('data-app-action-sidebar-thread-id')
691
- ?? location.pathname + location.search;
692
- })()`,
693
- returnByValue: true,
694
- });
695
- const threadId = threadIdFromLocation(response.result?.value);
696
- const snapshot = threadId ? await this.sessionStatusReader.read(threadId) : null;
697
- await client.send("Runtime.evaluate", {
698
- expression: `window.__minecodexSessionStatus?.update(${JSON.stringify(snapshot)})`,
699
- returnByValue: true,
700
- });
701
- } catch (error) {
702
- this.logger.warn?.("Session Status update failed", error.message);
735
+ const pending = this.sessionStatusUpdates.get(targetId);
736
+ if (pending) { await pending; continue; }
737
+ const revision = ++this.sessionStatusRevision;
738
+ const update = (async () => {
739
+ try {
740
+ const response = await client.send("Runtime.evaluate", {
741
+ expression: SESSION_STATUS_THREAD_ID_EXPRESSION,
742
+ returnByValue: true,
743
+ }, { timeoutMs: 3_000 });
744
+ const threadId = threadIdFromLocation(response.result?.value);
745
+ const data = threadId ? await this.sessionStatusReader.read(threadId) : null;
746
+ const snapshot = data ?? (threadId ? {
747
+ threadId,
748
+ retryAfterMs: this.sessionStatusReader.retryAfterMs?.(threadId) ?? 500,
749
+ } : null);
750
+ if (this.stopping || this.clients.get(targetId) !== client) return;
751
+ await client.send("Runtime.evaluate", {
752
+ expression: `window.__minecodexSessionStatus?.update(${JSON.stringify(snapshot)}, ${revision})`,
753
+ returnByValue: true,
754
+ }, { timeoutMs: 3_000 });
755
+ } catch (error) {
756
+ this.logger.warn?.("Session Status update failed", error.message);
757
+ if (this.clients.get(targetId) === client) {
758
+ client.socket.close();
759
+ this.injectedTargets.delete(targetId);
760
+ this.notifyStatusChange();
761
+ }
762
+ }
763
+ })();
764
+ this.sessionStatusUpdates.set(targetId, update);
765
+ try { await update; }
766
+ finally {
767
+ if (this.sessionStatusUpdates.get(targetId) === update) this.sessionStatusUpdates.delete(targetId);
703
768
  }
704
769
  }
705
770
  }
@@ -13,6 +13,8 @@ const HOST_ACTIONS = new Set([
13
13
  "open-sidebar",
14
14
  "resolve-file-paths",
15
15
  "import-generated-image",
16
+ "list-cloud-images",
17
+ "import-cloud-image",
16
18
  "create-image-edit-thread",
17
19
  ]);
18
20
 
@@ -62,6 +62,7 @@ function emptyState(filePath) {
62
62
  offset: 0,
63
63
  pending: "",
64
64
  turns: new Set(),
65
+ runningTurns: new Set(),
65
66
  responses: new Set(),
66
67
  hasResponseUsage: false,
67
68
  total: null,
@@ -134,7 +135,14 @@ export function applySessionStatusEvent(state, event) {
134
135
  }
135
136
  if (event?.type === "event_msg") {
136
137
  if (payload.type === "context_compacted") state.compactionEvents += 1;
137
- if (payload.type === "task_started" && payload.turn_id) state.turns.add(payload.turn_id);
138
+ if (payload.type === "task_started" && payload.turn_id) {
139
+ state.turns.add(payload.turn_id);
140
+ state.runningTurns.add(payload.turn_id);
141
+ }
142
+ if (["task_complete", "task_interrupted", "turn_aborted"].includes(payload.type)) {
143
+ if (payload.turn_id) state.runningTurns.delete(payload.turn_id);
144
+ else state.runningTurns.clear();
145
+ }
138
146
  if (payload.type === "task_complete" && Number.isFinite(payload.time_to_first_token_ms)) {
139
147
  state.ttftTotalMs += payload.time_to_first_token_ms;
140
148
  state.ttftSamples += 1;
@@ -200,6 +208,7 @@ export function sessionStatusSnapshot(state, threadId) {
200
208
  const contextWindow = state.contextWindow > 0 ? state.contextWindow : null;
201
209
  return {
202
210
  threadId,
211
+ isRunning: state.runningTurns.size > 0,
203
212
  turns: state.turns.size,
204
213
  steps: state.responses.size,
205
214
  totalTokens: total?.total_tokens ?? null,
@@ -248,24 +257,32 @@ async function findLog(directory, threadId) {
248
257
  }
249
258
 
250
259
  export class SessionStatusReader {
251
- constructor({ sessionsDir = path.join(process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"), "sessions") } = {}) {
260
+ constructor({ sessionsDir = path.join(process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"), "sessions"), now = Date.now } = {}) {
252
261
  this.sessionsDir = sessionsDir;
262
+ this.now = now;
253
263
  this.states = new Map();
254
- this.missingCheckedAt = new Map();
264
+ this.missingChecks = new Map();
265
+ }
266
+
267
+ retryAfterMs(threadId) {
268
+ return Math.max(0, (this.missingChecks.get(threadId)?.nextCheckAt ?? 0) - this.now());
255
269
  }
256
270
 
257
271
  async read(threadId) {
258
272
  if (!THREAD_ID.test(threadId ?? "")) return null;
259
273
  let state = this.states.get(threadId);
260
274
  if (!state) {
261
- const lastCheck = this.missingCheckedAt.get(threadId) ?? 0;
262
- if (Date.now() - lastCheck < 10_000) return null;
263
- this.missingCheckedAt.set(threadId, Date.now());
275
+ if (this.retryAfterMs(threadId) > 0) return null;
264
276
  const filePath = await findLog(this.sessionsDir, threadId);
265
- if (!filePath) return null;
277
+ if (!filePath) {
278
+ const attempts = this.missingChecks.get(threadId)?.attempts ?? 0;
279
+ const delay = [500, 1000, 2000][attempts] ?? 10_000;
280
+ this.missingChecks.set(threadId, { attempts: attempts + 1, nextCheckAt: this.now() + delay });
281
+ return null;
282
+ }
266
283
  state = emptyState(filePath);
267
284
  this.states.set(threadId, state);
268
- this.missingCheckedAt.delete(threadId);
285
+ this.missingChecks.delete(threadId);
269
286
  }
270
287
  let info;
271
288
  try { info = await stat(state.filePath); }