@ycodium-ai/agent-codex 0.2.3102 → 0.2.3112

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.
Files changed (2) hide show
  1. package/dist/index.js +215 -98
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -678,7 +678,7 @@ async function spawnCodexAppServer(input) {
678
678
  }
679
679
 
680
680
  // src/failureCategory.ts
681
- var codexProcessExitCategory = (session) => session !== void 0 && !session.closed && typeof session.nativeThreadId === "string" && session.nativeThreadId.length > 0 ? "transport" : void 0;
681
+ var codexProcessExitCategory = (session) => session !== void 0 && !session.stopRequested && typeof session.nativeThreadId === "string" && session.nativeThreadId.length > 0 ? "transport" : void 0;
682
682
 
683
683
  // src/protocol.ts
684
684
  var CODEX_INITIALIZE_PARAMS = {
@@ -996,7 +996,7 @@ async function createCodexRpcClient(handle, handlers) {
996
996
  let stderrTail = "";
997
997
  let exitReason = null;
998
998
  let exitFailureCategory;
999
- let closed = false;
999
+ let closeTask;
1000
1000
  const attach = (error) => exitFailureCategory === void 0 ? error : Object.assign(error, { failureCategory: exitFailureCategory });
1001
1001
  const failAll = (reason, failureCategory) => {
1002
1002
  if (exitReason === null) exitFailureCategory = failureCategory;
@@ -1132,19 +1132,39 @@ async function createCodexRpcClient(handle, handlers) {
1132
1132
  const respondError = async (id, code, message) => {
1133
1133
  await writeLine({ id, error: { code, message } }, id);
1134
1134
  };
1135
- const close = async () => {
1136
- if (closed) return;
1137
- closed = true;
1138
- try {
1139
- await handle.closeStdin();
1140
- } catch {
1141
- }
1142
- try {
1143
- await handle.kill("SIGTERM");
1144
- } catch {
1145
- }
1146
- failAll("the client was closed");
1147
- await Promise.all([pumpStdout, pumpStderr]);
1135
+ const close = (options) => {
1136
+ if (closeTask) return closeTask;
1137
+ closeTask = Promise.resolve().then(async () => {
1138
+ try {
1139
+ await handle.closeStdin();
1140
+ } catch {
1141
+ }
1142
+ try {
1143
+ await handle.kill("SIGTERM");
1144
+ } finally {
1145
+ failAll("the client was closed");
1146
+ }
1147
+ const drained = Promise.all([pumpStdout, pumpStderr]);
1148
+ if (options === void 0) {
1149
+ await drained;
1150
+ return;
1151
+ }
1152
+ let timer;
1153
+ try {
1154
+ await Promise.race([
1155
+ drained,
1156
+ new Promise((resolve2) => {
1157
+ timer = setTimeout(() => {
1158
+ options.onDrainTimeout();
1159
+ resolve2();
1160
+ }, options.drainTimeoutMs);
1161
+ })
1162
+ ]);
1163
+ } finally {
1164
+ if (timer !== void 0) clearTimeout(timer);
1165
+ }
1166
+ });
1167
+ return closeTask;
1148
1168
  };
1149
1169
  return {
1150
1170
  request,
@@ -3064,6 +3084,60 @@ function openCommandsOf(booked) {
3064
3084
  };
3065
3085
  }
3066
3086
 
3087
+ // src/sessionLifecycle.ts
3088
+ import { isExecutorLifecycleEvent } from "@ycodium-ai/plugin-api/executor";
3089
+ var CodexSessionLifecycle = class {
3090
+ closing = false;
3091
+ closed = false;
3092
+ #stopped = false;
3093
+ #task;
3094
+ #exit;
3095
+ threadId;
3096
+ publish;
3097
+ requestClose;
3098
+ constructor(threadId, publish, requestClose) {
3099
+ this.threadId = threadId;
3100
+ this.publish = publish;
3101
+ this.requestClose = requestClose;
3102
+ }
3103
+ /** 宿主发起的停止(stopSession / dispose)已认领;进程自己退出或协议关闭不算。 */
3104
+ get stopRequested() {
3105
+ return this.#stopped;
3106
+ }
3107
+ get acceptsLifecycle() {
3108
+ return !this.closed && !this.#stopped;
3109
+ }
3110
+ stop() {
3111
+ this.#stopped = true;
3112
+ }
3113
+ emit(event) {
3114
+ if (isExecutorLifecycleEvent(event) && !this.acceptsLifecycle) return;
3115
+ if (event.type === "session.exited") {
3116
+ this.#exit ??= event;
3117
+ this.requestClose();
3118
+ return;
3119
+ }
3120
+ this.publish(event);
3121
+ }
3122
+ /** 停止边界必须同步认领;RPC 的 dispatch 同步发布,认领前的终态已经交给事件流。 */
3123
+ close(reason, cleanup) {
3124
+ if (reason === "stop") this.stop();
3125
+ if (this.#task) return this.#task;
3126
+ this.closing = true;
3127
+ this.#task = (async () => {
3128
+ try {
3129
+ await cleanup();
3130
+ } finally {
3131
+ this.closed = true;
3132
+ }
3133
+ this.publish(
3134
+ this.#exit ?? baseEvent({ type: "session.exited", threadId: this.threadId, payload: {} })
3135
+ );
3136
+ })();
3137
+ return this.#task;
3138
+ }
3139
+ };
3140
+
3067
3141
  // src/sessionCollab.ts
3068
3142
  var COLLAB_META_LOOKUP_TIMEOUT_MS = 5e3;
3069
3143
  function metaValue(value) {
@@ -3530,8 +3604,18 @@ var CodexSessionRuntime = class {
3530
3604
  /** 宿主解好的这一轮的档(座位画的就是它)。缺席 = 宿主没给,按 `runtimeMode` 查表。 */
3531
3605
  nativeModeId;
3532
3606
  interactionMode;
3533
- closed = false;
3534
- closeCompleted = false;
3607
+ get closed() {
3608
+ return this.#lifecycle.closed;
3609
+ }
3610
+ get closing() {
3611
+ return this.#lifecycle.closing;
3612
+ }
3613
+ get stopRequested() {
3614
+ return this.#lifecycle.stopRequested;
3615
+ }
3616
+ markStopped() {
3617
+ this.#lifecycle.stop();
3618
+ }
3535
3619
  turns = [];
3536
3620
  pendingApprovals = /* @__PURE__ */ new Map();
3537
3621
  pendingUserInputs = /* @__PURE__ */ new Map();
@@ -3553,6 +3637,8 @@ var CodexSessionRuntime = class {
3553
3637
  attachments;
3554
3638
  rpc;
3555
3639
  emit;
3640
+ #lifecycle;
3641
+ closeOptions;
3556
3642
  #collab = new CodexSessionCollab(this);
3557
3643
  #requests = new CodexSessionRequests(this);
3558
3644
  /**
@@ -3574,11 +3660,20 @@ var CodexSessionRuntime = class {
3574
3660
  codexNativeModeFor(this.runtimeMode, this.nativeModeId)
3575
3661
  );
3576
3662
  }
3577
- constructor(host, rpc, emit, input, cwd, appServerArgs, grants, declineRelay, attachments) {
3663
+ constructor(host, rpc, emit, input, cwd, appServerArgs, grants, declineRelay, attachments, closeOptions = {}) {
3578
3664
  this.attachments = attachments;
3665
+ this.closeOptions = closeOptions;
3579
3666
  this.host = host;
3580
3667
  this.rpc = rpc;
3581
- this.emit = emit;
3668
+ this.#lifecycle = new CodexSessionLifecycle(input.threadId, emit, () => {
3669
+ void (closeOptions.requestClose?.() ?? this.close("process")).catch((error) => {
3670
+ host.log.warn("closing codex session failed", {
3671
+ threadId: input.threadId,
3672
+ error: String(error)
3673
+ });
3674
+ });
3675
+ });
3676
+ this.emit = (event) => this.#lifecycle.emit(event);
3582
3677
  this.threadId = input.threadId;
3583
3678
  this.cwd = cwd;
3584
3679
  this.runtimeMode = input.runtimeMode;
@@ -3602,6 +3697,7 @@ var CodexSessionRuntime = class {
3602
3697
  };
3603
3698
  }
3604
3699
  registerWriteAck(jsonRpcId) {
3700
+ if (this.closing) return Promise.resolve("closed");
3605
3701
  return new Promise((resolve2) => {
3606
3702
  this.writeAcks.set(jsonRpcId, { resolve: resolve2 });
3607
3703
  });
@@ -3711,17 +3807,18 @@ var CodexSessionRuntime = class {
3711
3807
  })) {
3712
3808
  this.emit(event);
3713
3809
  }
3714
- if (method === "turn/started") {
3810
+ if (method === "turn/started" && this.#lifecycle.acceptsLifecycle) {
3715
3811
  const turnId = turnIdOf(params);
3716
3812
  if (turnId) this.activeTurnId = turnId;
3717
3813
  }
3718
- if (method === "turn/completed") {
3814
+ if (method === "turn/completed" && this.#lifecycle.acceptsLifecycle) {
3719
3815
  this.activeTurnId = void 0;
3720
3816
  const { activeTurnId: _completedTurn, ...idleSession } = this.session;
3721
3817
  this.session = { ...idleSession, status: "idle", updatedAt: nowIso() };
3722
3818
  }
3723
3819
  }
3724
3820
  async handleServerRequest(id, method, params) {
3821
+ if (this.closing) return;
3725
3822
  return this.#requests.handleServerRequest(id, method, params);
3726
3823
  }
3727
3824
  async respondToRequest(requestId, decision, reason, persist) {
@@ -3733,6 +3830,7 @@ var CodexSessionRuntime = class {
3733
3830
  async withActiveWriterRecovery(run, allowUnsubscribe) {
3734
3831
  let lastError;
3735
3832
  for (let attempt = 0; attempt < ACTIVE_WRITER_ATTEMPTS; attempt += 1) {
3833
+ if (this.closing) throw new Error("codex session is closing");
3736
3834
  try {
3737
3835
  return await run();
3738
3836
  } catch (error) {
@@ -3840,6 +3938,7 @@ var CodexSessionRuntime = class {
3840
3938
  return items;
3841
3939
  }
3842
3940
  async sendTurn(input) {
3941
+ if (this.closing) throw new Error("codex session is closing");
3843
3942
  if (!this.nativeThreadId) throw new Error("codex session has no native thread");
3844
3943
  if (this.hasConfiguredMcpServer) {
3845
3944
  await this.rpc.request("config/mcpServer/reload", void 0, 0).catch((error) => {
@@ -3872,6 +3971,7 @@ var CodexSessionRuntime = class {
3872
3971
  })
3873
3972
  };
3874
3973
  const isSteer = input.expectedTurnId !== void 0;
3974
+ if (this.closing) throw new Error("codex session is closing");
3875
3975
  const result = await markProviderRejection(
3876
3976
  () => isSteer ? this.rpc.request(
3877
3977
  "turn/steer",
@@ -3961,31 +4061,59 @@ var CodexSessionRuntime = class {
3961
4061
  settlePendingUserInputs(result) {
3962
4062
  return this.#requests.settlePendingUserInputs(result);
3963
4063
  }
3964
- async close() {
3965
- if (this.closeCompleted) return;
3966
- this.closed = true;
3967
- this.requestKinds.clear();
3968
- this.failPendingNativeWrites("session closed before write confirmation");
3969
- this.settlePendingApprovals();
3970
- this.settlePendingUserInputs({});
3971
- this.declineRelay.clear(this.threadId);
3972
- this.grants.clear();
3973
- try {
3974
- await this.rpc.close();
3975
- } catch {
3976
- }
3977
- this.closeCompleted = true;
3978
- this.emit({
3979
- eventId: nextEventId(),
3980
- provider: CODEX_DRIVER_KIND,
3981
- threadId: this.threadId,
3982
- createdAt: nowIso(),
3983
- type: "session.exited",
3984
- payload: {}
4064
+ close(reason = "stop") {
4065
+ return this.#lifecycle.close(reason, async () => {
4066
+ this.requestKinds.clear();
4067
+ this.failPendingNativeWrites("session closed before write confirmation");
4068
+ this.settlePendingApprovals();
4069
+ this.settlePendingUserInputs({});
4070
+ this.declineRelay.clear(this.threadId);
4071
+ this.grants.clear();
4072
+ await this.rpc.close({
4073
+ drainTimeoutMs: this.closeOptions.drainTimeoutMs ?? 2e3,
4074
+ onDrainTimeout: () => {
4075
+ this.#lifecycle.closed = true;
4076
+ this.host.log.warn("codex output drain timed out", { threadId: this.threadId });
4077
+ }
4078
+ });
3985
4079
  });
3986
4080
  }
3987
4081
  };
3988
4082
 
4083
+ // src/eventBus.ts
4084
+ var EventBus = class {
4085
+ #queued = [];
4086
+ #waiters = [];
4087
+ #closed = false;
4088
+ push(event) {
4089
+ const waiter = this.#waiters.shift();
4090
+ if (waiter) {
4091
+ waiter({ done: false, value: event });
4092
+ return;
4093
+ }
4094
+ this.#queued.push(event);
4095
+ }
4096
+ close() {
4097
+ if (this.#closed) return;
4098
+ this.#closed = true;
4099
+ while (this.#waiters.length > 0) {
4100
+ this.#waiters.shift()?.({ done: true, value: void 0 });
4101
+ }
4102
+ }
4103
+ [Symbol.asyncIterator]() {
4104
+ return {
4105
+ next: () => {
4106
+ const queued = this.#queued.shift();
4107
+ if (queued !== void 0) return Promise.resolve({ done: false, value: queued });
4108
+ if (this.#closed) return Promise.resolve({ done: true, value: void 0 });
4109
+ return new Promise((resolve2) => {
4110
+ this.#waiters.push(resolve2);
4111
+ });
4112
+ }
4113
+ };
4114
+ }
4115
+ };
4116
+
3989
4117
  // src/sessions.ts
3990
4118
  import * as NodeFS3 from "node:fs";
3991
4119
  import * as NodePath4 from "node:path";
@@ -5714,38 +5842,6 @@ function missingBinaryDetail(binaryPath) {
5714
5842
  var yieldTick = () => new Promise((resolve2) => {
5715
5843
  setImmediate(resolve2);
5716
5844
  });
5717
- var EventBus = class {
5718
- #queued = [];
5719
- #waiters = [];
5720
- #closed = false;
5721
- push(event) {
5722
- const waiter = this.#waiters.shift();
5723
- if (waiter) {
5724
- waiter({ done: false, value: event });
5725
- return;
5726
- }
5727
- this.#queued.push(event);
5728
- }
5729
- close() {
5730
- if (this.#closed) return;
5731
- this.#closed = true;
5732
- while (this.#waiters.length > 0) {
5733
- this.#waiters.shift()?.({ done: true, value: void 0 });
5734
- }
5735
- }
5736
- [Symbol.asyncIterator]() {
5737
- return {
5738
- next: () => {
5739
- const queued = this.#queued.shift();
5740
- if (queued !== void 0) return Promise.resolve({ done: false, value: queued });
5741
- if (this.#closed) return Promise.resolve({ done: true, value: void 0 });
5742
- return new Promise((resolve2) => {
5743
- this.#waiters.push(resolve2);
5744
- });
5745
- }
5746
- };
5747
- }
5748
- };
5749
5845
  function accountToAuth2(account) {
5750
5846
  switch (account.state) {
5751
5847
  case "signed-in":
@@ -5775,9 +5871,11 @@ var createCodexInstance = (config, host, attachments, cache) => {
5775
5871
  const signInOperations = /* @__PURE__ */ new Map();
5776
5872
  const live = /* @__PURE__ */ new Map();
5777
5873
  let disposed = false;
5874
+ let disposeTask;
5778
5875
  const requireThread = (threadId) => {
5779
5876
  const booked = threads.get(threadId);
5780
5877
  if (!booked) throw new Error(`unknown thread ${threadId}`);
5878
+ if (booked.closing) throw new Error(`thread ${threadId} is closing`);
5781
5879
  return booked;
5782
5880
  };
5783
5881
  const probeAuth = () => accountToAuth2(readCodexAccount(accountHomeFromLayout(layoutFor(recipe))));
@@ -5831,7 +5929,7 @@ var createCodexInstance = (config, host, attachments, cache) => {
5831
5929
  ({ handle } = await spawnCodexAppServer({ host, recipe, layout }));
5832
5930
  } catch (error) {
5833
5931
  if (error instanceof ExecutorSpawnRefusedError) {
5834
- const borrowed = [...threads.values()][0];
5932
+ const borrowed = [...threads.values()].find((entry) => !entry.closing);
5835
5933
  if (borrowed !== void 0) return await run(borrowed.rpc);
5836
5934
  }
5837
5935
  throw error;
@@ -5868,8 +5966,12 @@ var createCodexInstance = (config, host, attachments, cache) => {
5868
5966
  live.set(handle.pid, { command, ...threadId === void 0 ? {} : { threadId } });
5869
5967
  void handle.exited.finally(() => live.delete(handle.pid));
5870
5968
  };
5871
- const stopBooked = async (booked) => {
5872
- await booked.runtime.close().catch(() => void 0);
5969
+ const stopBooked = (threadId, booked, reason = "stop") => {
5970
+ if (reason === "stop") booked.runtime.markStopped();
5971
+ booked.closing ??= booked.runtime.close(reason).then(() => {
5972
+ if (threads.get(threadId) === booked) threads.delete(threadId);
5973
+ });
5974
+ return booked.closing;
5873
5975
  };
5874
5976
  const instance = {
5875
5977
  probe: async () => {
@@ -5930,8 +6032,8 @@ var createCodexInstance = (config, host, attachments, cache) => {
5930
6032
  if (cwd.trim() === "") throw new Error("workspace path missing");
5931
6033
  const existing = threads.get(input.threadId);
5932
6034
  if (existing !== void 0) {
5933
- await stopBooked(existing);
5934
- threads.delete(input.threadId);
6035
+ if (existing.closing) throw new Error(`thread ${input.threadId} is closing`);
6036
+ await stopBooked(input.threadId, existing);
5935
6037
  }
5936
6038
  const layout = layoutFor(recipe);
5937
6039
  const { handle, args } = await spawnCodexAppServer({
@@ -5942,6 +6044,7 @@ var createCodexInstance = (config, host, attachments, cache) => {
5942
6044
  });
5943
6045
  trackHandle(handle, [recipe.binaryPath, ...args].join(" "), input.threadId);
5944
6046
  let runtime;
6047
+ let booked;
5945
6048
  const rpc = await createCodexRpcClient(handle, {
5946
6049
  onNotification: (notification) => {
5947
6050
  runtime?.handleNotification(notification.method, notification.params);
@@ -5962,7 +6065,13 @@ var createCodexInstance = (config, host, attachments, cache) => {
5962
6065
  runtime?.settleNativeWrite(id, status);
5963
6066
  },
5964
6067
  onExit: () => {
5965
- if (runtime && !runtime.closed) void runtime.close().catch(() => void 0);
6068
+ if (booked)
6069
+ void stopBooked(input.threadId, booked, "process").catch((error) => {
6070
+ host.log.warn("closing codex session failed", {
6071
+ threadId: input.threadId,
6072
+ error: String(error)
6073
+ });
6074
+ });
5966
6075
  },
5967
6076
  failureCategoryForProcessExit: () => codexProcessExitCategory(runtime)
5968
6077
  });
@@ -5975,22 +6084,25 @@ var createCodexInstance = (config, host, attachments, cache) => {
5975
6084
  args,
5976
6085
  void 0,
5977
6086
  void 0,
5978
- attachments
6087
+ attachments,
6088
+ { requestClose: () => stopBooked(input.threadId, booked, "process") }
5979
6089
  );
6090
+ booked = { runtime, rpc, handle };
6091
+ threads.set(input.threadId, booked);
5980
6092
  try {
5981
6093
  await rpc.request("initialize", CODEX_INITIALIZE_PARAMS);
5982
6094
  await rpc.notify("initialized");
5983
6095
  await setCodexSessionSkillRoots(rpc, input.skills, host.log, input.threadId);
5984
6096
  const session = await runtime.openThread(input);
5985
6097
  await yieldTick();
5986
- threads.set(input.threadId, { runtime, rpc, handle });
6098
+ if (booked.closing) throw new Error("codex session closed during startup");
5987
6099
  host.log.info("session started", {
5988
6100
  threadId: input.threadId,
5989
6101
  nativeThreadId: runtime.nativeThreadId
5990
6102
  });
5991
6103
  return session;
5992
6104
  } catch (error) {
5993
- await runtime.close().catch(() => void 0);
6105
+ await stopBooked(input.threadId, booked).catch(() => void 0);
5994
6106
  throw error;
5995
6107
  }
5996
6108
  },
@@ -6009,8 +6121,8 @@ var createCodexInstance = (config, host, attachments, cache) => {
6009
6121
  }
6010
6122
  },
6011
6123
  interruptTurn: async (threadId, turnId) => {
6012
- const booked = threads.get(threadId);
6013
- if (booked === void 0) return;
6124
+ if (!threads.has(threadId)) return;
6125
+ const booked = requireThread(threadId);
6014
6126
  host.log.info("interrupt turn", { threadId, turnId });
6015
6127
  await booked.runtime.interruptTurn(turnId);
6016
6128
  },
@@ -6038,26 +6150,31 @@ var createCodexInstance = (config, host, attachments, cache) => {
6038
6150
  stopSession: async (threadId) => {
6039
6151
  const booked = threads.get(threadId);
6040
6152
  if (booked === void 0) return;
6041
- threads.delete(threadId);
6042
- await stopBooked(booked);
6153
+ await stopBooked(threadId, booked);
6043
6154
  },
6044
6155
  readThread: async (threadId) => {
6045
6156
  const booked = requireThread(threadId);
6046
6157
  return await booked.runtime.readThread();
6047
6158
  },
6048
6159
  events,
6049
- dispose: async () => {
6050
- if (disposed) return;
6160
+ dispose: () => {
6161
+ if (disposeTask) return disposeTask;
6051
6162
  disposed = true;
6052
- const operations = [...signInOperations.values()];
6053
- signInOperations.clear();
6054
- for (const operation of operations) {
6055
- await operation.close().catch(() => void 0);
6056
- }
6057
- const booked = [...threads.values()];
6058
- threads.clear();
6059
- await Promise.all(booked.map((entry) => stopBooked(entry)));
6060
- events.close();
6163
+ const closing = Promise.allSettled(
6164
+ [...threads.entries()].map(([threadId, booked]) => stopBooked(threadId, booked))
6165
+ );
6166
+ disposeTask = (async () => {
6167
+ const operations = [...signInOperations.values()];
6168
+ signInOperations.clear();
6169
+ for (const operation of operations) {
6170
+ await operation.close().catch(() => void 0);
6171
+ }
6172
+ for (const result of await closing) {
6173
+ if (result.status === "rejected") throw result.reason;
6174
+ }
6175
+ events.close();
6176
+ })();
6177
+ return disposeTask;
6061
6178
  },
6062
6179
  // 项目里的 repo 级技能:探测的 app-server 没有工作区,只报得出用户与系统级;这里按目录问
6063
6180
  // 一次性 app-server(与读额度同一条进程通道),宿主缓存结果。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-codex",
3
- "version": "0.2.3102",
3
+ "version": "0.2.3112",
4
4
  "description": "Codex executor: Codex CLI over the app-server JSON-RPC protocol.",
5
5
  "keywords": [
6
6
  "ycodium",
@@ -29,7 +29,7 @@
29
29
  "tailwindcss": "^4.0.0",
30
30
  "typescript": "~6.0.3",
31
31
  "vite-plus": "0.2.2",
32
- "@ycodium-ai/plugin-api": "0.2.3102",
32
+ "@ycodium-ai/plugin-api": "0.2.3112",
33
33
  "ycodium-executor-testing": "0.0.0"
34
34
  },
35
35
  "ycodium": {