minecodex 1.2.1 → 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
 
@@ -103,6 +104,8 @@ export class CodexRuntime {
103
104
  this.scriptIds = new Map();
104
105
  this.bindingTokens = new Map();
105
106
  this.hostActionChains = new Map();
107
+ this.sessionStatusUpdates = new Map();
108
+ this.sessionStatusRevision = 0;
106
109
  this.refreshPromise = null;
107
110
  this.stopping = false;
108
111
  this.monitorPromise = null;
@@ -290,7 +293,9 @@ export class CodexRuntime {
290
293
  }
291
294
 
292
295
  async startTargetDiscovery() {
293
- 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
+ });
294
299
  if (!response?.ok) {
295
300
  throw new Error(`Codex CDP version returned HTTP ${response?.status ?? "error"}`);
296
301
  }
@@ -355,7 +360,9 @@ export class CodexRuntime {
355
360
 
356
361
  async isAvailable() {
357
362
  try {
358
- 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
+ });
359
366
  if (!response?.ok) return false;
360
367
  requireCdpWebSocketUrl(await response.json(), this.cdpPort);
361
368
  return true;
@@ -518,6 +525,12 @@ export class CodexRuntime {
518
525
  }
519
526
  if (request.token !== this.bindingTokens.get(targetId) || typeof request.requestId !== "string") return;
520
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
+ }
521
534
  if (feature && request.action === SURFACE_LOAD_ACTION) {
522
535
  try {
523
536
  const result = await this.loadSurfaceIntoFrame(client, feature, request.payload);
@@ -541,6 +554,18 @@ export class CodexRuntime {
541
554
  await this.importGeneratedImage(feature, request.payload);
542
555
  return;
543
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
+ }
544
569
  try {
545
570
  const result = request.action === "create-image-edit-thread"
546
571
  ? await this.createImageEditThread(client, feature, request.payload, request.requestId)
@@ -568,12 +593,15 @@ export class CodexRuntime {
568
593
  if (!response?.ok) {
569
594
  throw new Error(`Generated image import returned HTTP ${response?.status ?? "error"}`);
570
595
  }
596
+ return response.json();
571
597
  }
572
598
 
573
599
  async refreshTargets() {
574
600
  let response;
575
601
  try {
576
- 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
+ });
577
605
  } catch (error) {
578
606
  throw this.markCodexUnavailable(error);
579
607
  }
@@ -604,7 +632,8 @@ export class CodexRuntime {
604
632
  ...this.targetFailures.keys(),
605
633
  ]);
606
634
  for (const id of trackedTargetIds) {
607
- if (liveRendererIds.has(id)) continue;
635
+ const socketState = this.clients.get(id)?.socket.readyState;
636
+ if (liveRendererIds.has(id) && (socketState == null || socketState === 1)) continue;
608
637
  removedTarget = true;
609
638
  this.clients.get(id)?.socket.close();
610
639
  this.clients.delete(id);
@@ -640,7 +669,13 @@ export class CodexRuntime {
640
669
  const bindingToken = randomBytes(24).toString("hex");
641
670
  this.bindingTokens.set(target.id, bindingToken);
642
671
  client.on?.("Runtime.bindingCalled", (params) => {
643
- 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
644
679
  .catch((error) => this.logger.warn?.("Codex Host action failed", error.message));
645
680
  });
646
681
  const source = createInjectionSource(this.features, {
@@ -693,22 +728,43 @@ export class CodexRuntime {
693
728
  this.notifyStatusChange();
694
729
  }
695
730
 
696
- async publishSessionStatus() {
731
+ async publishSessionStatus(onlyTargetId = null) {
697
732
  for (const [targetId, client] of this.clients) {
733
+ if (onlyTargetId && targetId !== onlyTargetId) continue;
698
734
  if (!this.injectedTargets.has(targetId)) continue;
699
- try {
700
- const response = await client.send("Runtime.evaluate", {
701
- expression: SESSION_STATUS_THREAD_ID_EXPRESSION,
702
- returnByValue: true,
703
- });
704
- const threadId = threadIdFromLocation(response.result?.value);
705
- const snapshot = threadId ? await this.sessionStatusReader.read(threadId) : null;
706
- await client.send("Runtime.evaluate", {
707
- expression: `window.__minecodexSessionStatus?.update(${JSON.stringify(snapshot)})`,
708
- returnByValue: true,
709
- });
710
- } catch (error) {
711
- 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);
712
768
  }
713
769
  }
714
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); }