@ycodium-ai/agent-codex 0.2.2163 → 0.2.2488

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
@@ -17,7 +17,8 @@ Facts:
17
17
  - Manifest permission is `{ kind: "executor" }`.
18
18
  - `runtimeModeSwitch` is `next-turn` (same table the host adapter reads).
19
19
  - `sessionModelSwitch` is `in-session`.
20
- - Native ChatGPT device-code login is supported (`supportsNativeSubscription`).
20
+ - Native ChatGPT device-code login is supported: `deviceCodeSignInArgs` runs
21
+ `codex login --device-auth`.
21
22
 
22
23
  Develop with `vp run --filter @ycodium-ai/agent-codex typecheck`, `build`, and `test`.
23
24
 
package/dist/index.js CHANGED
@@ -11,6 +11,13 @@ var CODEX_CARD = {
11
11
  commandName: "codex",
12
12
  launchArgs: CODEX_DEFAULT_LAUNCH_ARGS,
13
13
  homeEnvVar: "CODEX_HOME",
14
+ // 影子目录只分开凭据,会话文件仍写在共用 home 下(`resolveCodexHomeLayout`
15
+ // 的 authOverlay:auth.json 是真文件,sessions 是指回共用 home 的软链)。
16
+ homeLayout: {
17
+ accountConfigKey: "shadowHomePath",
18
+ credentialFile: "auth.json",
19
+ transcriptSubdir: "sessions"
20
+ },
14
21
  environment: [],
15
22
  install: { kind: "npm" },
16
23
  signIn: [{ kind: "command", command: "codex login" }],
@@ -31,9 +38,6 @@ var CODEX_CARD = {
31
38
  "5.3-spark": "gpt-5.3-codex-spark",
32
39
  "gpt-5.3-spark": "gpt-5.3-codex-spark"
33
40
  },
34
- modelsDevProviderIds: ["openai"],
35
- modelsDevSlugPrefixed: false,
36
- modelSlugPrefix: "",
37
41
  mcpTransports: ["stdio", "http"],
38
42
  pluginManaged: true,
39
43
  verifiedSkills: true,
@@ -45,20 +49,53 @@ var CODEX_CARD = {
45
49
  runtimeModeSwitch: "next-turn",
46
50
  // 三字段组合在 openThread 时定,切档要重开会话(与 runtimeModeSwitch 同为 next-turn)。
47
51
  nativeModeSwitch: true,
48
- contextCompaction: "native"
52
+ contextCompaction: "native",
53
+ // 子代理经 collab 通知报成 task.*(events.ts 的 mapCollabAgentEvent)。
54
+ reportsSpawnedAgents: true
49
55
  },
50
56
  credentialSources: [],
51
57
  diagnostics: [],
58
+ // 设置表单照这份画:带 x-ycodium-form 的属性才进表单(enabled、customModels 等另有入口,不在此列),
59
+ // 顺序看 x-ycodium-form-order;文案键在本插件 locales/<locale>.json 里解析,宿主不兜底。
52
60
  configSchema: {
53
61
  type: "object",
62
+ "x-ycodium-form-order": ["binaryPath", "homePath", "shadowHomePath", "launchArgs"],
54
63
  properties: {
55
64
  authMode: { type: "string", enum: ["default", "chatgpt", "apiKey"] },
56
65
  apiBaseUrl: { type: "string" },
57
66
  enabled: { type: "boolean" },
58
- binaryPath: { type: "string" },
59
- homePath: { type: "string" },
60
- shadowHomePath: { type: "string" },
61
- launchArgs: { type: "string" },
67
+ binaryPath: {
68
+ type: "string",
69
+ "x-ycodium-form": {
70
+ titleKey: "form.binaryPath.title",
71
+ descriptionKey: "form.binaryPath.description",
72
+ placeholderKey: "form.binaryPath.placeholder",
73
+ control: "executable-path"
74
+ }
75
+ },
76
+ homePath: {
77
+ type: "string",
78
+ "x-ycodium-form": {
79
+ titleKey: "form.homePath.title",
80
+ descriptionKey: "form.homePath.description",
81
+ placeholderKey: "form.homePath.placeholder"
82
+ }
83
+ },
84
+ shadowHomePath: {
85
+ type: "string",
86
+ "x-ycodium-form": {
87
+ titleKey: "form.shadowHomePath.title",
88
+ descriptionKey: "form.shadowHomePath.description",
89
+ placeholderKey: "form.shadowHomePath.placeholder"
90
+ }
91
+ },
92
+ launchArgs: {
93
+ type: "string",
94
+ "x-ycodium-form": {
95
+ titleKey: "form.launchArgs.title",
96
+ descriptionKey: "form.launchArgs.description"
97
+ }
98
+ },
62
99
  customModels: { type: "array" },
63
100
  driver: { type: "string" },
64
101
  config: { type: "object" },
@@ -72,12 +109,12 @@ var CODEX_CARD = {
72
109
  nativePathSuffix: "/responses",
73
110
  credentialEnvKey: "CODEX_API_KEY",
74
111
  supportsNativeSubscription: true,
75
- // codex 不认「地址一个变量」:端点写在 `-c model_providers.<id>.base_url` 上,
76
- // 凭据只能经环境变量、变量名还得告诉它。`$providerId` / `$credentialEnvKey`
77
- // 由宿主按 binding 生成,同一台机器上两份绑定因此不抢同一个变量名。
112
+ // codex 不认「地址一个变量」:地址写在 `-c model_providers.<id>.base_url` 上,
113
+ // 凭据只能经环境变量、变量名还得告诉它。这只手的「API URL and key」账户连接
114
+ // (`connection.ts`)就按这个形状拼;这一栏只是说明,宿主不读。
78
115
  //
79
- // `forced_login_method=api` 要写在最后:chatgpt 实例自己先注了
80
- // `forced_login_method=chatgpt`,不盖掉的话手仍走订阅登录,代理地址白注。
116
+ // `forced_login_method=api` 要写在最后:chatgpt 实例自己先加了
117
+ // `forced_login_method=chatgpt`,不盖掉的话手仍走订阅登录,填的地址白填。
81
118
  extraLaunchArgs: [
82
119
  { key: "model_provider", value: "$providerId" },
83
120
  { key: "model_providers.$providerId.name", value: "Ycodium API" },
@@ -90,13 +127,196 @@ var CODEX_CARD = {
90
127
  }
91
128
  };
92
129
 
93
- // src/codexInstance.ts
94
- import * as NodeProcess from "node:process";
95
- import { ExecutorSpawnRefusedError } from "@ycodium-ai/plugin-api/executor";
96
-
97
130
  // src/codexAccount.ts
98
131
  import * as NodeFS from "node:fs";
132
+ import * as NodePath3 from "node:path";
133
+
134
+ // src/homeLayout.ts
135
+ import * as NodeFSP from "node:fs/promises";
136
+ import * as NodeOS2 from "node:os";
137
+ import * as NodePath2 from "node:path";
138
+
139
+ // src/paths.ts
140
+ import * as NodeOS from "node:os";
99
141
  import * as NodePath from "node:path";
142
+ function expandHomePath(value) {
143
+ if (value === "~") return NodeOS.homedir();
144
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
145
+ return NodePath.join(NodeOS.homedir(), value.slice(2));
146
+ }
147
+ return value;
148
+ }
149
+ function nowIso() {
150
+ return (/* @__PURE__ */ new Date()).toISOString();
151
+ }
152
+
153
+ // src/homeLayout.ts
154
+ var KNOWN_SHARED_DIRECTORIES = [
155
+ "sessions",
156
+ "archived_sessions",
157
+ "sqlite",
158
+ "shell_snapshots",
159
+ "worktrees",
160
+ "skills",
161
+ "plugins",
162
+ "cache",
163
+ "logs",
164
+ "mcp-oauth-locks"
165
+ ];
166
+ var PRIVATE_ENTRY_NAMES = /* @__PURE__ */ new Set(["auth.json", "models_cache.json"]);
167
+ var SHADOW_LOCAL_ENTRY_NAMES = /* @__PURE__ */ new Set(["log", "memories", "tmp"]);
168
+ var REPLACEABLE_SHARED_RUNTIME_DIRECTORIES = /* @__PURE__ */ new Set(["mcp-oauth-locks"]);
169
+ function resolveCodexHomeDirectory(value) {
170
+ const expanded = value && value.trim().length > 0 ? expandHomePath(value) : NodePath2.join(NodeOS2.homedir(), ".codex");
171
+ return NodePath2.resolve(expanded);
172
+ }
173
+ function resolveCodexHomeLayout(input) {
174
+ const configuredHome = input.homePath.trim() || (input.environmentHome?.trim() ?? "");
175
+ const sharedHomePath = resolveCodexHomeDirectory(configuredHome || void 0);
176
+ const shadowHomePath = input.shadowHomePath.trim();
177
+ if (shadowHomePath.length === 0) {
178
+ return {
179
+ mode: "direct",
180
+ sharedHomePath,
181
+ effectiveHomePath: configuredHome.length > 0 ? sharedHomePath : void 0,
182
+ continuationKey: `codex:home:${sharedHomePath}`
183
+ };
184
+ }
185
+ return {
186
+ mode: "authOverlay",
187
+ sharedHomePath,
188
+ effectiveHomePath: NodePath2.resolve(expandHomePath(shadowHomePath)),
189
+ continuationKey: `codex:home:${sharedHomePath}`
190
+ };
191
+ }
192
+ function codexContinuationKey(input) {
193
+ return resolveCodexHomeLayout({
194
+ homePath: input.homePath,
195
+ shadowHomePath: "",
196
+ environmentHome: input.instanceEnvironment["CODEX_HOME"] ?? input.inheritedEnvironment["CODEX_HOME"]
197
+ }).continuationKey;
198
+ }
199
+ async function readLinkState(linkPath) {
200
+ try {
201
+ const target = await NodeFSP.readlink(linkPath);
202
+ return { _tag: "Symlink", target };
203
+ } catch (error) {
204
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
205
+ if (code === "ENOENT") return { _tag: "Missing" };
206
+ if (code === "EINVAL") return { _tag: "NotSymlink" };
207
+ throw error;
208
+ }
209
+ }
210
+ async function createEntryLink(target, link) {
211
+ const type = await NodeFSP.stat(target).then(
212
+ (info) => info.isDirectory() ? "junction" : "file",
213
+ () => "file"
214
+ );
215
+ await NodeFSP.symlink(target, link, type);
216
+ }
217
+ async function removePrivateSymlink(effectiveHomePath, entryName) {
218
+ const privatePath = NodePath2.join(effectiveHomePath, entryName);
219
+ const state = await readLinkState(privatePath);
220
+ if (state._tag === "Symlink") await NodeFSP.rm(privatePath, { force: true });
221
+ }
222
+ async function ensureSymlink(input) {
223
+ const target = NodePath2.join(input.sharedHomePath, input.entryName);
224
+ const link = NodePath2.join(input.effectiveHomePath, input.entryName);
225
+ const state = await readLinkState(link);
226
+ if (state._tag === "NotSymlink") {
227
+ if (!REPLACEABLE_SHARED_RUNTIME_DIRECTORIES.has(input.entryName)) {
228
+ throw new Error(
229
+ `Cannot create Codex shadow home entry '${input.entryName}' because '${link}' already exists and is not a symlink.`
230
+ );
231
+ }
232
+ await NodeFSP.rm(link, { recursive: true, force: true });
233
+ await createEntryLink(target, link);
234
+ return;
235
+ }
236
+ if (state._tag === "Missing") {
237
+ await createEntryLink(target, link);
238
+ return;
239
+ }
240
+ const resolvedExisting = NodePath2.resolve(NodePath2.dirname(link), state.target);
241
+ if (resolvedExisting !== target) {
242
+ await NodeFSP.rm(link, { force: true });
243
+ await createEntryLink(target, link);
244
+ }
245
+ }
246
+ async function makeDirectory(directoryPath) {
247
+ try {
248
+ await NodeFSP.mkdir(directoryPath, { recursive: true });
249
+ } catch (error) {
250
+ throw new Error(
251
+ `Codex shadow home filesystem operation 'makeDirectory' failed for '${directoryPath}': ${String(error)}`,
252
+ { cause: error }
253
+ );
254
+ }
255
+ }
256
+ async function materializeCodexShadowHome(layout, log) {
257
+ if (layout.mode !== "authOverlay") return;
258
+ const effectiveHomePath = layout.effectiveHomePath;
259
+ if (!effectiveHomePath) return;
260
+ if (layout.sharedHomePath === effectiveHomePath) {
261
+ throw new Error(
262
+ `Codex shadow home path '${effectiveHomePath}' must be different from the shared home path '${layout.sharedHomePath}'.`
263
+ );
264
+ }
265
+ await makeDirectory(layout.sharedHomePath);
266
+ await makeDirectory(effectiveHomePath);
267
+ await Promise.all(
268
+ KNOWN_SHARED_DIRECTORIES.map(
269
+ (directory) => makeDirectory(NodePath2.join(layout.sharedHomePath, directory))
270
+ )
271
+ );
272
+ let sharedEntryNames = [];
273
+ try {
274
+ sharedEntryNames = await NodeFSP.readdir(layout.sharedHomePath);
275
+ } catch (error) {
276
+ throw new Error(
277
+ `Codex shadow home filesystem operation 'readDirectory' failed for '${layout.sharedHomePath}': ${String(error)}`,
278
+ { cause: error }
279
+ );
280
+ }
281
+ const entries = new Set(KNOWN_SHARED_DIRECTORIES);
282
+ for (const entryName of sharedEntryNames) {
283
+ if (!PRIVATE_ENTRY_NAMES.has(entryName) && !SHADOW_LOCAL_ENTRY_NAMES.has(entryName)) {
284
+ entries.add(entryName);
285
+ }
286
+ }
287
+ for (const entryName of PRIVATE_ENTRY_NAMES) {
288
+ if (entryName === "auth.json") continue;
289
+ await removePrivateSymlink(effectiveHomePath, entryName);
290
+ }
291
+ for (const entryName of entries) {
292
+ if (PRIVATE_ENTRY_NAMES.has(entryName)) continue;
293
+ try {
294
+ await ensureSymlink({
295
+ sharedHomePath: layout.sharedHomePath,
296
+ effectiveHomePath,
297
+ entryName
298
+ });
299
+ } catch (error) {
300
+ log?.warn("Codex shadow home entry left unlinked.", {
301
+ entryName,
302
+ effectiveHomePath,
303
+ reason: error instanceof Error ? error.message : String(error)
304
+ });
305
+ }
306
+ }
307
+ const authPath = NodePath2.join(effectiveHomePath, "auth.json");
308
+ const authState = await readLinkState(authPath);
309
+ if (authState._tag === "Symlink") {
310
+ throw new Error(
311
+ `Codex shadow home private entry 'auth.json' at '${authPath}' must be a real file, not a symlink.`
312
+ );
313
+ }
314
+ }
315
+ function accountHomeFromLayout(layout) {
316
+ return layout.effectiveHomePath ?? layout.sharedHomePath;
317
+ }
318
+
319
+ // src/codexAccount.ts
100
320
  var AUTH_FILE_NAME = "auth.json";
101
321
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
102
322
  var readString = (record, key) => {
@@ -137,14 +357,16 @@ var signedOutProbe = (exists) => ({
137
357
  directoryExists: exists,
138
358
  email: void 0,
139
359
  name: void 0,
140
- expiresAt: void 0
360
+ expiresAt: void 0,
361
+ authType: void 0
141
362
  });
142
363
  var unreadableProbe = (exists) => ({
143
364
  state: "unreadable",
144
365
  directoryExists: exists,
145
366
  email: void 0,
146
367
  name: void 0,
147
- expiresAt: void 0
368
+ expiresAt: void 0,
369
+ authType: void 0
148
370
  });
149
371
  var epochMillisToIso = (ms) => {
150
372
  if (ms === void 0) return void 0;
@@ -164,7 +386,7 @@ function decodeJwtClaims(token) {
164
386
  }
165
387
  var readCodexAccount = (homePath) => {
166
388
  const exists = directoryExists(homePath);
167
- const raw = readCredentialFile(NodePath.join(homePath, AUTH_FILE_NAME));
389
+ const raw = readCredentialFile(NodePath3.join(homePath, AUTH_FILE_NAME));
168
390
  if (raw === void 0) return signedOutProbe(exists);
169
391
  const parsed = parseJsonRecord(raw);
170
392
  if (parsed === void 0 || !("OPENAI_API_KEY" in parsed || "tokens" in parsed)) {
@@ -185,10 +407,26 @@ var readCodexAccount = (homePath) => {
185
407
  directoryExists: exists,
186
408
  email: claims === void 0 ? void 0 : readString(claims, "email"),
187
409
  name: void 0,
188
- expiresAt
410
+ expiresAt,
411
+ authType: accessToken !== void 0 ? "chatgpt" : "apiKey"
412
+ };
413
+ };
414
+ var probeCodexAccount = (accountPath) => {
415
+ const resolved = resolveCodexHomeDirectory(accountPath || process.env.CODEX_HOME);
416
+ const account = readCodexAccount(resolved);
417
+ return {
418
+ accountPath: resolved,
419
+ state: account.state,
420
+ ...account.email === void 0 ? {} : { email: account.email },
421
+ ...account.expiresAt === void 0 ? {} : { expiresAt: account.expiresAt },
422
+ ...account.authType === void 0 ? {} : { authType: account.authType }
189
423
  };
190
424
  };
191
425
 
426
+ // src/codexInstance.ts
427
+ import * as NodeProcess from "node:process";
428
+ import { ExecutorSpawnRefusedError } from "@ycodium-ai/plugin-api/executor";
429
+
192
430
  // src/config.ts
193
431
  import { modelSelectionStringOption } from "@ycodium-ai/plugin-api/executor";
194
432
  var CODEX_INSTANCE_API_KEY_ENV = "YCODIUM_CODEX_API_KEY";
@@ -349,12 +587,6 @@ function codexMcpLaunchConfig(servers) {
349
587
  }
350
588
  return { args, environment };
351
589
  }
352
- function codexOverrideArgs(overrides) {
353
- return overrides.flatMap(({ key, value }) => ["-c", `${key}=${JSON.stringify(value)}`]);
354
- }
355
- function encodeCodexConfigOverrides(overrides) {
356
- return overrides.map(({ key, value }) => `-c ${JSON.stringify(`${key}=${JSON.stringify(value)}`)}`).join(" ");
357
- }
358
590
  function modelIdFromSelection(selection) {
359
591
  if (typeof selection !== "object" || selection === null || Array.isArray(selection)) {
360
592
  return void 0;
@@ -364,7 +596,7 @@ function modelIdFromSelection(selection) {
364
596
  }
365
597
 
366
598
  // src/connection.ts
367
- import { mergeLaunchEnv, brainSlotEnvKeys } from "@ycodium-ai/plugin-api/executor";
599
+ import { mergeLaunchEnv } from "@ycodium-ai/plugin-api/executor";
368
600
  function codexApiConnectionError(recipe, environment) {
369
601
  if (recipe.authMode !== "apiKey") return null;
370
602
  try {
@@ -404,16 +636,7 @@ function connectionArgv(recipe, layout) {
404
636
  }
405
637
  return args;
406
638
  }
407
- function codexSpawnArgs(recipe, layout, nativeSubscription = false) {
408
- if (nativeSubscription) {
409
- return [
410
- ...tokenizeLaunchArgs(recipe.launchArgs),
411
- "-c",
412
- `model_provider=${JSON.stringify("openai")}`,
413
- "-c",
414
- `forced_login_method=${JSON.stringify("chatgpt")}`
415
- ];
416
- }
639
+ function codexSpawnArgs(recipe, layout) {
417
640
  const needsOverrides = layout.mode === "authOverlay" || recipe.authMode !== "default";
418
641
  if (!needsOverrides) return tokenizeLaunchArgs(recipe.launchArgs);
419
642
  return connectionArgv(recipe, layout);
@@ -425,7 +648,6 @@ var CRUSHED_API_KEYS = {
425
648
  function buildCodexSpawnEnv(input) {
426
649
  const { HOME: _instanceHome, ...instanceTable } = input.recipe.environment;
427
650
  return mergeLaunchEnv({
428
- brainSlots: brainSlotEnvKeys(CODEX_CARD),
429
651
  accountHome: input.layout.effectiveHomePath !== void 0 ? { CODEX_HOME: input.layout.effectiveHomePath } : {},
430
652
  instanceTable,
431
653
  handOverrides: input.crushApiKeys ? CRUSHED_API_KEYS : {},
@@ -434,7 +656,6 @@ function buildCodexSpawnEnv(input) {
434
656
  }
435
657
  function nativeLoginSpawnEnv(processEnv, isolatedHome) {
436
658
  return mergeLaunchEnv({
437
- brainSlots: brainSlotEnvKeys(CODEX_CARD),
438
659
  inherited: processEnv,
439
660
  handOverrides: { ...CRUSHED_API_KEYS, CODEX_HOME: isolatedHome }
440
661
  });
@@ -443,191 +664,6 @@ function shouldCrushApiKeys(authMode) {
443
664
  return authMode === "chatgpt";
444
665
  }
445
666
 
446
- // src/homeLayout.ts
447
- import * as NodeFSP from "node:fs/promises";
448
- import * as NodeOS2 from "node:os";
449
- import * as NodePath3 from "node:path";
450
-
451
- // src/paths.ts
452
- import * as NodeOS from "node:os";
453
- import * as NodePath2 from "node:path";
454
- function expandHomePath(value) {
455
- if (value === "~") return NodeOS.homedir();
456
- if (value.startsWith("~/") || value.startsWith("~\\")) {
457
- return NodePath2.join(NodeOS.homedir(), value.slice(2));
458
- }
459
- return value;
460
- }
461
- function nowIso() {
462
- return (/* @__PURE__ */ new Date()).toISOString();
463
- }
464
-
465
- // src/homeLayout.ts
466
- var KNOWN_SHARED_DIRECTORIES = [
467
- "sessions",
468
- "archived_sessions",
469
- "sqlite",
470
- "shell_snapshots",
471
- "worktrees",
472
- "skills",
473
- "plugins",
474
- "cache",
475
- "logs",
476
- "mcp-oauth-locks"
477
- ];
478
- var PRIVATE_ENTRY_NAMES = /* @__PURE__ */ new Set(["auth.json", "models_cache.json"]);
479
- var SHADOW_LOCAL_ENTRY_NAMES = /* @__PURE__ */ new Set(["log", "memories", "tmp"]);
480
- var REPLACEABLE_SHARED_RUNTIME_DIRECTORIES = /* @__PURE__ */ new Set(["mcp-oauth-locks"]);
481
- function resolveCodexHomeDirectory(value) {
482
- const expanded = value && value.trim().length > 0 ? expandHomePath(value) : NodePath3.join(NodeOS2.homedir(), ".codex");
483
- return NodePath3.resolve(expanded);
484
- }
485
- function resolveCodexHomeLayout(input) {
486
- const configuredHome = input.homePath.trim() || (input.environmentHome?.trim() ?? "");
487
- const sharedHomePath = resolveCodexHomeDirectory(configuredHome || void 0);
488
- const shadowHomePath = input.shadowHomePath.trim();
489
- if (shadowHomePath.length === 0) {
490
- return {
491
- mode: "direct",
492
- sharedHomePath,
493
- effectiveHomePath: configuredHome.length > 0 ? sharedHomePath : void 0,
494
- continuationKey: `codex:home:${sharedHomePath}`
495
- };
496
- }
497
- return {
498
- mode: "authOverlay",
499
- sharedHomePath,
500
- effectiveHomePath: NodePath3.resolve(expandHomePath(shadowHomePath)),
501
- continuationKey: `codex:home:${sharedHomePath}`
502
- };
503
- }
504
- function codexContinuationKey(input) {
505
- return resolveCodexHomeLayout({
506
- homePath: input.homePath,
507
- shadowHomePath: "",
508
- environmentHome: input.instanceEnvironment["CODEX_HOME"] ?? input.inheritedEnvironment["CODEX_HOME"]
509
- }).continuationKey;
510
- }
511
- async function readLinkState(linkPath) {
512
- try {
513
- const target = await NodeFSP.readlink(linkPath);
514
- return { _tag: "Symlink", target };
515
- } catch (error) {
516
- const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
517
- if (code === "ENOENT") return { _tag: "Missing" };
518
- if (code === "EINVAL") return { _tag: "NotSymlink" };
519
- throw error;
520
- }
521
- }
522
- async function createEntryLink(target, link) {
523
- const type = await NodeFSP.stat(target).then(
524
- (info) => info.isDirectory() ? "junction" : "file",
525
- () => "file"
526
- );
527
- await NodeFSP.symlink(target, link, type);
528
- }
529
- async function removePrivateSymlink(effectiveHomePath, entryName) {
530
- const privatePath = NodePath3.join(effectiveHomePath, entryName);
531
- const state = await readLinkState(privatePath);
532
- if (state._tag === "Symlink") await NodeFSP.rm(privatePath, { force: true });
533
- }
534
- async function ensureSymlink(input) {
535
- const target = NodePath3.join(input.sharedHomePath, input.entryName);
536
- const link = NodePath3.join(input.effectiveHomePath, input.entryName);
537
- const state = await readLinkState(link);
538
- if (state._tag === "NotSymlink") {
539
- if (!REPLACEABLE_SHARED_RUNTIME_DIRECTORIES.has(input.entryName)) {
540
- throw new Error(
541
- `Cannot create Codex shadow home entry '${input.entryName}' because '${link}' already exists and is not a symlink.`
542
- );
543
- }
544
- await NodeFSP.rm(link, { recursive: true, force: true });
545
- await createEntryLink(target, link);
546
- return;
547
- }
548
- if (state._tag === "Missing") {
549
- await createEntryLink(target, link);
550
- return;
551
- }
552
- const resolvedExisting = NodePath3.resolve(NodePath3.dirname(link), state.target);
553
- if (resolvedExisting !== target) {
554
- await NodeFSP.rm(link, { force: true });
555
- await createEntryLink(target, link);
556
- }
557
- }
558
- async function makeDirectory(directoryPath) {
559
- try {
560
- await NodeFSP.mkdir(directoryPath, { recursive: true });
561
- } catch (error) {
562
- throw new Error(
563
- `Codex shadow home filesystem operation 'makeDirectory' failed for '${directoryPath}': ${String(error)}`,
564
- { cause: error }
565
- );
566
- }
567
- }
568
- async function materializeCodexShadowHome(layout, log) {
569
- if (layout.mode !== "authOverlay") return;
570
- const effectiveHomePath = layout.effectiveHomePath;
571
- if (!effectiveHomePath) return;
572
- if (layout.sharedHomePath === effectiveHomePath) {
573
- throw new Error(
574
- `Codex shadow home path '${effectiveHomePath}' must be different from the shared home path '${layout.sharedHomePath}'.`
575
- );
576
- }
577
- await makeDirectory(layout.sharedHomePath);
578
- await makeDirectory(effectiveHomePath);
579
- await Promise.all(
580
- KNOWN_SHARED_DIRECTORIES.map(
581
- (directory) => makeDirectory(NodePath3.join(layout.sharedHomePath, directory))
582
- )
583
- );
584
- let sharedEntryNames = [];
585
- try {
586
- sharedEntryNames = await NodeFSP.readdir(layout.sharedHomePath);
587
- } catch (error) {
588
- throw new Error(
589
- `Codex shadow home filesystem operation 'readDirectory' failed for '${layout.sharedHomePath}': ${String(error)}`,
590
- { cause: error }
591
- );
592
- }
593
- const entries = new Set(KNOWN_SHARED_DIRECTORIES);
594
- for (const entryName of sharedEntryNames) {
595
- if (!PRIVATE_ENTRY_NAMES.has(entryName) && !SHADOW_LOCAL_ENTRY_NAMES.has(entryName)) {
596
- entries.add(entryName);
597
- }
598
- }
599
- for (const entryName of PRIVATE_ENTRY_NAMES) {
600
- if (entryName === "auth.json") continue;
601
- await removePrivateSymlink(effectiveHomePath, entryName);
602
- }
603
- for (const entryName of entries) {
604
- if (PRIVATE_ENTRY_NAMES.has(entryName)) continue;
605
- try {
606
- await ensureSymlink({
607
- sharedHomePath: layout.sharedHomePath,
608
- effectiveHomePath,
609
- entryName
610
- });
611
- } catch (error) {
612
- log?.warn("Codex shadow home entry left unlinked.", {
613
- entryName,
614
- effectiveHomePath,
615
- reason: error instanceof Error ? error.message : String(error)
616
- });
617
- }
618
- }
619
- const authPath = NodePath3.join(effectiveHomePath, "auth.json");
620
- const authState = await readLinkState(authPath);
621
- if (authState._tag === "Symlink") {
622
- throw new Error(
623
- `Codex shadow home private entry 'auth.json' at '${authPath}' must be a real file, not a symlink.`
624
- );
625
- }
626
- }
627
- function accountHomeFromLayout(layout) {
628
- return layout.effectiveHomePath ?? layout.sharedHomePath;
629
- }
630
-
631
667
  // src/protocol.ts
632
668
  var CODEX_INITIALIZE_PARAMS = {
633
669
  clientInfo: { name: "ycodium", title: "Ycodium", version: "0" },
@@ -1057,6 +1093,9 @@ async function createCodexRpcClient(handle, handlers) {
1057
1093
  };
1058
1094
  }
1059
1095
 
1096
+ // src/sessionRuntime.ts
1097
+ import * as NodeCrypto2 from "node:crypto";
1098
+
1060
1099
  // src/approvalGrant.ts
1061
1100
  function codexApprovalGrantKey(kind, identity) {
1062
1101
  if (kind === "mcp-elicitation") return `codex:mcp:${identity || "unknown"}`;
@@ -1111,7 +1150,7 @@ You are running inside Ycodium. The \`ycodium\` MCP server is the product-native
1111
1150
 
1112
1151
  For browser work, first call \`preview_status\`. If no automation-capable preview is attached, call \`preview_open\` before concluding that the browser is unavailable. Then use \`preview_navigate\`, \`preview_snapshot\`, and the focused interaction tools. Prefer snapshot-provided locators over coordinates.
1113
1152
 
1114
- Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the Ycodium preview tools are absent, the user explicitly requests another browser, or \`preview_open\` returns an explicit unsupported/unavailable error. A failed Ycodium preview tool call should be inspected and retried with corrected arguments when the error is actionable.
1153
+ Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the Ycodium preview tools are absent, the user explicitly requests another browser, or \`preview_open\` returns an explicit unsupported/unavailable error. A failed Ycodium preview tool call should be inspected and retried with corrected arguments when the error is actionable. The exception is a tool result saying the user has not allowed agents to use a tab's browser profile: that is the user's decision, not a fault. Do not retry on that tab or work around it with another browser; follow the message (open a new tab, or ask the user).
1115
1154
  `;
1116
1155
  var YCODIUM_THREAD_TOOL_INSTRUCTIONS = `
1117
1156
 
@@ -1279,11 +1318,13 @@ function buildCodexDeveloperInstructions(interactionMode, runtime, browserToolsA
1279
1318
  }
1280
1319
 
1281
1320
  // src/events.ts
1321
+ import * as NodeCrypto from "node:crypto";
1282
1322
  var CODEX_PROVIDER = "codex";
1323
+ var EVENT_ID_PREFIX = `codex-evt-${NodeCrypto.randomUUID()}`;
1283
1324
  var eventSeq = 0;
1284
1325
  function nextEventId() {
1285
1326
  eventSeq += 1;
1286
- return `codex-evt-${eventSeq}`;
1327
+ return `${EVENT_ID_PREFIX}-${eventSeq}`;
1287
1328
  }
1288
1329
  function trimText(value) {
1289
1330
  if (typeof value !== "string") return void 0;
@@ -1562,7 +1603,8 @@ function mapCollabAgentEvent(input) {
1562
1603
  role,
1563
1604
  ...knownName ? { title: knownName } : {},
1564
1605
  ...agentPath ? { agentPath } : {},
1565
- timelineBypass: true
1606
+ timelineBypass: true,
1607
+ rosterKind: "subagent"
1566
1608
  };
1567
1609
  const threadId = input.rootThreadId;
1568
1610
  const method = input.method;
@@ -1639,16 +1681,17 @@ function mapCollabAgentEvent(input) {
1639
1681
  const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
1640
1682
  const totalTokens = total ? count(total["totalTokens"]) : void 0;
1641
1683
  if (total === void 0 || totalTokens === void 0) return [];
1642
- const typedUsage = { totalTokens };
1643
- for (const key of [
1644
- "inputTokens",
1645
- "cachedInputTokens",
1646
- "outputTokens",
1647
- "reasoningOutputTokens"
1648
- ]) {
1649
- const value = count(total[key]);
1650
- if (value !== void 0) typedUsage[key] = value;
1651
- }
1684
+ const inputTokens = count(total["inputTokens"]);
1685
+ const cachedInputTokens = count(total["cachedInputTokens"]);
1686
+ const outputTokens = count(total["outputTokens"]);
1687
+ const reasoningOutputTokens = count(total["reasoningOutputTokens"]);
1688
+ const typedUsage = {
1689
+ totalTokens,
1690
+ ...inputTokens !== void 0 ? { inputTokens } : {},
1691
+ ...cachedInputTokens !== void 0 ? { cachedInputTokens } : {},
1692
+ ...outputTokens !== void 0 ? { outputTokens } : {},
1693
+ ...reasoningOutputTokens !== void 0 ? { reasoningOutputTokens } : {}
1694
+ };
1652
1695
  return [
1653
1696
  baseEvent({
1654
1697
  type: "task.progress",
@@ -2316,7 +2359,7 @@ function elicitationOptions(params) {
2316
2359
  ];
2317
2360
  }
2318
2361
  function mapServerRequestToEvents(input) {
2319
- const requestId = String(input.id);
2362
+ const requestId = input.requestId;
2320
2363
  if (input.method === "item/tool/requestUserInput") {
2321
2364
  const questions = toUserInputQuestions(input.params);
2322
2365
  if (!questions) return [];
@@ -2378,6 +2421,9 @@ function mapServerRequestToEvents(input) {
2378
2421
  }
2379
2422
 
2380
2423
  // src/sessionRuntime.ts
2424
+ function cancelWireFor(pending) {
2425
+ return pending.kind === "mcp-elicitation" ? toMcpElicitationResponse("cancel", pending.params) : { decision: "cancel" };
2426
+ }
2381
2427
  var sleep = (ms) => new Promise((resolve2) => {
2382
2428
  setTimeout(resolve2, ms);
2383
2429
  });
@@ -2430,8 +2476,18 @@ var CodexSessionRuntime = class {
2430
2476
  memoryThreadIds = /* @__PURE__ */ new Set();
2431
2477
  appServerArgs;
2432
2478
  host;
2479
+ attachments;
2433
2480
  rpc;
2434
2481
  emit;
2482
+ /**
2483
+ * 对外 requestId 的前缀,每个会话(即每个 app-server 进程)一份。JSON-RPC id 每个进程
2484
+ * 都从 0 数起,宿主按 requestId 记「已接受」,不加前缀的话重启后的新请求会撞上旧请求。
2485
+ */
2486
+ #requestIdPrefix = `codex-${NodeCrypto2.randomUUID()}:`;
2487
+ /** JSON-RPC id → 对外 requestId。`serverRequest/resolved` 里的 id 可能是数字也可能是字符串。 */
2488
+ requestIdFor(jsonRpcId) {
2489
+ return `${this.#requestIdPrefix}${String(jsonRpcId)}`;
2490
+ }
2435
2491
  /** 这一轮该用哪套三字段组合:只由这一轮的档决定(见 `codexNativeModeFor`)。 */
2436
2492
  #threadConfig() {
2437
2493
  return codexThreadConfigForNativeMode(codexNativeModeFor(this.runtimeMode, this.nativeModeId));
@@ -2451,7 +2507,8 @@ var CodexSessionRuntime = class {
2451
2507
  error: error instanceof Error ? error.message : String(error)
2452
2508
  });
2453
2509
  }
2454
- constructor(host, rpc, emit, input, cwd, appServerArgs, grants, declineRelay) {
2510
+ constructor(host, rpc, emit, input, cwd, appServerArgs, grants, declineRelay, attachments) {
2511
+ this.attachments = attachments;
2455
2512
  this.host = host;
2456
2513
  this.rpc = rpc;
2457
2514
  this.emit = emit;
@@ -2510,7 +2567,7 @@ var CodexSessionRuntime = class {
2510
2567
  if (kind === "drop") return;
2511
2568
  if (isFatalStderr(line)) {
2512
2569
  this.emit({
2513
- eventId: `codex-stderr-${Date.now()}`,
2570
+ eventId: nextEventId(),
2514
2571
  provider: CODEX_DRIVER_KIND,
2515
2572
  threadId: this.threadId,
2516
2573
  createdAt: nowIso(),
@@ -2553,21 +2610,24 @@ var CodexSessionRuntime = class {
2553
2610
  }
2554
2611
  dispatchMapped(method, params) {
2555
2612
  if (method === "serverRequest/resolved") {
2556
- const resolvedId = (isRecord3(params) ? asString(params["requestId"]) : null) ?? void 0;
2557
- if (resolvedId !== void 0) {
2558
- this.emit({
2559
- eventId: `codex-req-resolved-${resolvedId}`,
2560
- provider: CODEX_DRIVER_KIND,
2561
- threadId: this.threadId,
2562
- createdAt: nowIso(),
2563
- type: "request.resolved",
2564
- requestId: resolvedId,
2565
- payload: {
2566
- requestType: toRequestTypeFromKind(this.requestKinds.get(resolvedId)),
2567
- resolution: params
2568
- }
2569
- });
2570
- }
2613
+ const rawId = isRecord3(params) ? params["requestId"] : void 0;
2614
+ if (typeof rawId !== "number" && typeof rawId !== "string") return;
2615
+ const resolvedId = this.requestIdFor(rawId);
2616
+ const pending = this.pendingApprovals.get(resolvedId);
2617
+ if (pending === void 0) return;
2618
+ pending.release();
2619
+ this.emit({
2620
+ eventId: nextEventId(),
2621
+ provider: CODEX_DRIVER_KIND,
2622
+ threadId: this.threadId,
2623
+ createdAt: nowIso(),
2624
+ type: "request.resolved",
2625
+ requestId: resolvedId,
2626
+ payload: {
2627
+ requestType: toRequestTypeFromKind(this.requestKinds.get(resolvedId)),
2628
+ resolution: params
2629
+ }
2630
+ });
2571
2631
  return;
2572
2632
  }
2573
2633
  for (const event of mapNotificationToEvents({
@@ -2746,9 +2806,9 @@ var CodexSessionRuntime = class {
2746
2806
  return;
2747
2807
  }
2748
2808
  if (method === "item/tool/requestUserInput") {
2749
- const requestId = String(id);
2809
+ const requestId = this.requestIdFor(id);
2750
2810
  const events = mapServerRequestToEvents({
2751
- id,
2811
+ requestId,
2752
2812
  method,
2753
2813
  params,
2754
2814
  threadId: this.threadId,
@@ -2779,9 +2839,9 @@ var CodexSessionRuntime = class {
2779
2839
  await this.rpc.respond(id, { decision });
2780
2840
  return;
2781
2841
  }
2782
- const requestId = String(id);
2842
+ const requestId = this.requestIdFor(id);
2783
2843
  const events = mapServerRequestToEvents({
2784
- id,
2844
+ requestId,
2785
2845
  method,
2786
2846
  params,
2787
2847
  threadId: this.threadId,
@@ -2798,6 +2858,10 @@ var CodexSessionRuntime = class {
2798
2858
  settle: (decision) => {
2799
2859
  this.pendingApprovals.delete(requestId);
2800
2860
  void this.rpc.respond(id, decision).catch((error) => this.#warnAnswerWriteFailed(method, id, error)).finally(resolve2);
2861
+ },
2862
+ release: () => {
2863
+ this.pendingApprovals.delete(requestId);
2864
+ resolve2();
2801
2865
  }
2802
2866
  });
2803
2867
  for (const event of events) this.emit(event);
@@ -2805,7 +2869,7 @@ var CodexSessionRuntime = class {
2805
2869
  }
2806
2870
  async respondToRequest(requestId, decision, reason, persist) {
2807
2871
  const pending = this.pendingApprovals.get(requestId);
2808
- if (!pending) throw new Error(`unknown request ${requestId}`);
2872
+ if (!pending) throw new Error(`Unknown pending Codex approval request: ${requestId}`);
2809
2873
  if (decision === "deny" && reason) this.declineRelay.queue(this.threadId, reason);
2810
2874
  let wire = { decision: decision === "allow" ? "accept" : "decline" };
2811
2875
  if (decision === "allow" && pending.kind === "mcp-elicitation") {
@@ -2821,7 +2885,7 @@ var CodexSessionRuntime = class {
2821
2885
  pending.settle(wire);
2822
2886
  const delivery = await ack.catch(() => "closed");
2823
2887
  this.emit({
2824
- eventId: `codex-req-${requestId}`,
2888
+ eventId: nextEventId(),
2825
2889
  provider: CODEX_DRIVER_KIND,
2826
2890
  threadId: this.threadId,
2827
2891
  createdAt: nowIso(),
@@ -2838,7 +2902,7 @@ var CodexSessionRuntime = class {
2838
2902
  }
2839
2903
  async respondToUserInput(requestId, answers) {
2840
2904
  const pending = this.pendingUserInputs.get(requestId);
2841
- if (!pending) throw new Error(`unknown user-input ${requestId}`);
2905
+ if (!pending) throw new Error(`Unknown pending Codex user input request: ${requestId}`);
2842
2906
  const mapped = toCodexUserInputAnswers(answers);
2843
2907
  if (mapped === void 0) {
2844
2908
  throw new Error(`invalid user input answers for request ${requestId}`);
@@ -2847,7 +2911,7 @@ var CodexSessionRuntime = class {
2847
2911
  pending.settle({ answers: mapped });
2848
2912
  const delivery = await ack.catch(() => "closed");
2849
2913
  this.emit({
2850
- eventId: `codex-ui-${requestId}`,
2914
+ eventId: nextEventId(),
2851
2915
  provider: CODEX_DRIVER_KIND,
2852
2916
  threadId: this.threadId,
2853
2917
  createdAt: nowIso(),
@@ -2899,7 +2963,7 @@ var CodexSessionRuntime = class {
2899
2963
  }
2900
2964
  async openThread(input) {
2901
2965
  const resume = input.discardResumeCursor === true ? void 0 : parseCodexResumeCursor(input.resumeCursor);
2902
- const requestedModel = input.launch.requestedModelId?.trim() || modelIdFromSelection(input.modelSelection);
2966
+ const requestedModel = modelIdFromSelection(input.modelSelection);
2903
2967
  const startParams = {
2904
2968
  ...this.threadConfigParams(),
2905
2969
  ...requestedModel ? { model: requestedModel } : {}
@@ -2935,7 +2999,7 @@ var CodexSessionRuntime = class {
2935
2999
  ...this.model ? { model: this.model } : {}
2936
3000
  };
2937
3001
  this.emit({
2938
- eventId: `codex-session-${this.threadId}`,
3002
+ eventId: nextEventId(),
2939
3003
  provider: CODEX_DRIVER_KIND,
2940
3004
  threadId: this.threadId,
2941
3005
  createdAt: nowIso(),
@@ -2953,24 +3017,45 @@ var CodexSessionRuntime = class {
2953
3017
  const effort = modelSelectionStringOption(input.modelSelection, "reasoningEffort") ?? DEFAULT_TEXT_GENERATION_REASONING_EFFORT;
2954
3018
  return buildCodexDeveloperInstructions(mode, { model, reasoningEffort: effort }, tools, tools);
2955
3019
  }
2956
- turnInput(input) {
3020
+ async turnInput(input) {
3021
+ const images = await this.#imageInputs(input.attachments);
2957
3022
  const items = [];
2958
3023
  const declined = this.declineRelay.take(this.threadId);
2959
3024
  const text = [declined, input.input ?? ""].filter((part) => part && part.length > 0).join("\n\n");
2960
3025
  if (text.length > 0) items.push({ type: "text", text });
2961
- if (Array.isArray(input.attachments)) {
2962
- for (const attachment of input.attachments) {
2963
- if (!isRecord3(attachment)) continue;
2964
- const url = asString(attachment["dataUrl"]) ?? asString(attachment["url"]);
2965
- if (url) items.push({ type: "image", url });
3026
+ items.push(...images);
3027
+ return items;
3028
+ }
3029
+ /** 宿主只给附件 id:字节经附件接口读回,拼成 data URL 交给 codex。读不到就报错,不悄悄丢图。 */
3030
+ async #imageInputs(attachments) {
3031
+ const items = [];
3032
+ for (const attachment of attachments ?? []) {
3033
+ if (!isRecord3(attachment) || attachment["type"] !== "image") continue;
3034
+ const id = asString(attachment["id"]);
3035
+ const mimeType = asString(attachment["mimeType"]);
3036
+ if (!id || !mimeType) {
3037
+ throw new Error("codex image attachment is missing its id or mime type");
2966
3038
  }
3039
+ if (!this.attachments) {
3040
+ throw new Error(`codex cannot read image attachment ${id}: no attachment access`);
3041
+ }
3042
+ const bytes = await this.attachments.read(id);
3043
+ items.push({
3044
+ type: "image",
3045
+ url: `data:${mimeType};base64,${Buffer.from(bytes).toString("base64")}`
3046
+ });
2967
3047
  }
2968
3048
  return items;
2969
3049
  }
2970
3050
  async sendTurn(input) {
2971
3051
  if (!this.nativeThreadId) throw new Error("codex session has no native thread");
2972
3052
  if (hasConfiguredMcpServer(this.appServerArgs)) {
2973
- await this.rpc.request("mcpServer/reload", {}, 0).catch(() => void 0);
3053
+ await this.rpc.request("config/mcpServer/reload", void 0, 0).catch((error) => {
3054
+ this.host.log.warn("refreshing the codex MCP tool list before a turn failed", {
3055
+ threadId: this.threadId,
3056
+ error: error instanceof Error ? error.message : String(error)
3057
+ });
3058
+ });
2974
3059
  }
2975
3060
  const sandboxPolicy = this.#turnSandboxPolicy();
2976
3061
  const config = this.#threadConfig();
@@ -2979,7 +3064,7 @@ var CodexSessionRuntime = class {
2979
3064
  const serviceTier = modelSelectionStringOption(input.modelSelection, "serviceTier");
2980
3065
  const params = {
2981
3066
  threadId: this.nativeThreadId,
2982
- input: this.turnInput(input),
3067
+ input: await this.turnInput(input),
2983
3068
  approvalPolicy: config.approvalPolicy,
2984
3069
  approvalsReviewer: config.approvalsReviewer,
2985
3070
  sandboxPolicy,
@@ -3039,24 +3124,29 @@ var CodexSessionRuntime = class {
3039
3124
  );
3040
3125
  if (!this.nativeThreadId) return;
3041
3126
  const target = turnId ?? this.activeTurnId;
3042
- if (!target) return;
3043
- await this.rpc.request("turn/interrupt", { threadId: this.nativeThreadId, turnId: target }, 0).catch(() => void 0);
3127
+ if (target) {
3128
+ await this.rpc.request("turn/interrupt", { threadId: this.nativeThreadId, turnId: target }, 0).catch(() => void 0);
3129
+ }
3130
+ this.cancelPendingApprovals();
3131
+ }
3132
+ /** 把还挂着的审批答成「取消」,逐条发 request.resolved。 */
3133
+ cancelPendingApprovals() {
3134
+ for (const [requestId, pending] of this.pendingApprovals) {
3135
+ pending.settle(cancelWireFor(pending));
3136
+ this.emit({
3137
+ eventId: nextEventId(),
3138
+ provider: CODEX_DRIVER_KIND,
3139
+ threadId: this.threadId,
3140
+ createdAt: nowIso(),
3141
+ type: "request.resolved",
3142
+ requestId,
3143
+ payload: { requestType: toRequestTypeFromKind(pending.kind), decision: "cancel" }
3144
+ });
3145
+ }
3044
3146
  }
3045
3147
  async compactContext() {
3046
3148
  if (!this.nativeThreadId) return;
3047
- await this.rpc.request("thread/compact", { threadId: this.nativeThreadId }, 0).catch(() => void 0);
3048
- }
3049
- async uploadFeedback(reason) {
3050
- const result = await this.rpc.request(
3051
- "feedback/upload",
3052
- {
3053
- threadId: this.nativeThreadId,
3054
- ...reason ? { reason } : {}
3055
- },
3056
- 0
3057
- );
3058
- const id = (isRecord3(result) ? asString(result["feedbackId"]) : null) ?? `codex-feedback-${Date.now()}`;
3059
- return { feedbackId: id };
3149
+ await this.rpc.request("thread/compact/start", { threadId: this.nativeThreadId }, 0);
3060
3150
  }
3061
3151
  async readThread() {
3062
3152
  if (!this.nativeThreadId) return { threadId: this.threadId, turns: this.turns };
@@ -3111,7 +3201,7 @@ var CodexSessionRuntime = class {
3111
3201
  }
3112
3202
  this.closeCompleted = true;
3113
3203
  this.emit({
3114
- eventId: `codex-close-${this.threadId}`,
3204
+ eventId: nextEventId(),
3115
3205
  provider: CODEX_DRIVER_KIND,
3116
3206
  threadId: this.threadId,
3117
3207
  createdAt: nowIso(),
@@ -3466,10 +3556,6 @@ function serviceTierDescriptor(row) {
3466
3556
  currentValue: defaultTier
3467
3557
  };
3468
3558
  }
3469
- function isSupersededModel(row) {
3470
- if (isRecord3(row["upgradeInfo"])) return true;
3471
- return (asString(row["upgrade"]) ?? "").length > 0;
3472
- }
3473
3559
  function codexProbeModel(row) {
3474
3560
  if (!isRecord3(row)) return null;
3475
3561
  const slug = asString(row["model"]);
@@ -3482,7 +3568,6 @@ function codexProbeModel(row) {
3482
3568
  slug,
3483
3569
  ...displayName ? { name: codexModelDisplayName(displayName) } : {},
3484
3570
  ...row["isDefault"] === true ? { isDefault: true } : {},
3485
- ...isSupersededModel(row) ? { isLegacy: true } : {},
3486
3571
  ...row["hidden"] === true ? { hidden: true } : {},
3487
3572
  // 空表也给 `capabilities`:`{}` 与 `null` 对宿主是一回事(都不画那一格),
3488
3573
  // 但给对象保住「这只手确实答过这一问」这层意思。
@@ -3515,6 +3600,47 @@ async function listCodexModels(rpc) {
3515
3600
  return models;
3516
3601
  }
3517
3602
 
3603
+ // src/skills.ts
3604
+ var SKILLS_LIST_TIMEOUT_MS = 15e3;
3605
+ function codexProbeSkill(raw) {
3606
+ if (!isRecord3(raw)) return null;
3607
+ const name = asString(raw["name"]);
3608
+ const path = asString(raw["path"]);
3609
+ const scope = asString(raw["scope"]);
3610
+ if (!name || !path || scope === "repo") return null;
3611
+ const face = isRecord3(raw["interface"]) ? raw["interface"] : void 0;
3612
+ const description = asString(raw["description"]);
3613
+ const displayName = asString(face?.["displayName"]);
3614
+ const shortDescription = asString(face?.["shortDescription"]) || asString(raw["shortDescription"]);
3615
+ return {
3616
+ name,
3617
+ path,
3618
+ enabled: raw["enabled"] !== false,
3619
+ ...scope ? { scope } : {},
3620
+ ...description ? { description } : {},
3621
+ ...displayName ? { displayName } : {},
3622
+ ...shortDescription ? { shortDescription } : {}
3623
+ };
3624
+ }
3625
+ async function listCodexSkills(rpc) {
3626
+ let response;
3627
+ try {
3628
+ response = await rpc.request("skills/list", {}, SKILLS_LIST_TIMEOUT_MS);
3629
+ } catch {
3630
+ return void 0;
3631
+ }
3632
+ if (!isRecord3(response) || !Array.isArray(response["data"])) return void 0;
3633
+ const skills = /* @__PURE__ */ new Map();
3634
+ for (const entry of response["data"]) {
3635
+ if (!isRecord3(entry) || !Array.isArray(entry["skills"])) continue;
3636
+ for (const raw of entry["skills"]) {
3637
+ const skill = codexProbeSkill(raw);
3638
+ if (skill !== null) skills.set(skill.path, skill);
3639
+ }
3640
+ }
3641
+ return [...skills.values()].toSorted((left, right) => left.name.localeCompare(right.name));
3642
+ }
3643
+
3518
3644
  // src/signIn.ts
3519
3645
  var DEVICE_CODE_URL = /https?:\/\/\S+/;
3520
3646
  var DEVICE_CODE_VALUE = /\b([A-Z0-9]{4,}(?:-[A-Z0-9]{4,})+)\b/;
@@ -3704,8 +3830,7 @@ import * as NodeFSP2 from "node:fs/promises";
3704
3830
  import * as NodeOS3 from "node:os";
3705
3831
  import * as NodePath5 from "node:path";
3706
3832
  import {
3707
- mergeLaunchEnv as mergeLaunchEnv2,
3708
- brainSlotEnvKeys as brainSlotEnvKeys2
3833
+ mergeLaunchEnv as mergeLaunchEnv2
3709
3834
  } from "@ycodium-ai/plugin-api/executor";
3710
3835
  var CODEX_TIMEOUT_MS = 18e4;
3711
3836
  var EARLIER_CONTENT_TRUNCATION_MARKER = "[Earlier content truncated]\n\n";
@@ -3852,7 +3977,7 @@ async function collectStream(stream) {
3852
3977
  return out;
3853
3978
  }
3854
3979
  function createCodexTextGeneration(input) {
3855
- const run = async (operation, cwd, prompt, schema, modelSelection, launch, imagePaths, signal) => {
3980
+ const run = async (operation, cwd, prompt, schema, modelSelection, imagePaths, signal) => {
3856
3981
  if (input.recipe.binaryPath.length === 0) {
3857
3982
  throw new Error("Codex CLI is required but not available on PATH.");
3858
3983
  }
@@ -3861,33 +3986,16 @@ function createCodexTextGeneration(input) {
3861
3986
  const outputPath = NodePath5.join(tmp, "output.json");
3862
3987
  await NodeFSP2.writeFile(schemaPath, JSON.stringify(schema));
3863
3988
  await NodeFSP2.writeFile(outputPath, "");
3864
- const launchRecord = isRecord3(launch) ? launch : {};
3865
- const processEnv = isRecord3(launchRecord.processEnv) ? Object.fromEntries(
3866
- Object.entries(launchRecord.processEnv).filter(
3867
- (entry) => typeof entry[1] === "string"
3868
- )
3869
- ) : {};
3870
- const overlay = Array.isArray(launchRecord.launchArgOverrides) && launchRecord.launchArgOverrides.length > 0 ? encodeCodexConfigOverrides(
3871
- launchRecord.launchArgOverrides.flatMap((entry) => {
3872
- if (!isRecord3(entry) || typeof entry.key !== "string") return [];
3873
- const value = entry.value;
3874
- if (typeof value !== "string" && typeof value !== "boolean") return [];
3875
- return [{ key: entry.key, value }];
3876
- })
3877
- ) : "";
3878
- const launchArgs = [input.recipe.launchArgs, overlay].filter((value) => value.length > 0).join(" ");
3879
3989
  const reasoningEffort = modelSelectionStringOption(modelSelection, "reasoningEffort") ?? DEFAULT_TEXT_GENERATION_REASONING_EFFORT;
3880
3990
  const serviceTier = modelSelectionStringOption(modelSelection, "serviceTier");
3881
- const model = asString(launchRecord.requestedModelId) ?? modelIdFromSelection(modelSelection) ?? "gpt-5.6-luna";
3991
+ const model = modelIdFromSelection(modelSelection) ?? "gpt-5.6-luna";
3882
3992
  const env = mergeLaunchEnv2({
3883
- brainSlots: brainSlotEnvKeys2(CODEX_CARD),
3884
3993
  accountHome: input.layout.effectiveHomePath ? { CODEX_HOME: input.layout.effectiveHomePath } : input.recipe.homePath.trim() ? { CODEX_HOME: expandHomePath(input.recipe.homePath) } : {},
3885
- instanceTable: input.recipe.environment,
3886
- launch: processEnv
3994
+ instanceTable: input.recipe.environment
3887
3995
  });
3888
3996
  const args = [
3889
3997
  "exec",
3890
- ...codexExecLaunchArgs(launchArgs),
3998
+ ...codexExecLaunchArgs(input.recipe.launchArgs),
3891
3999
  "--ephemeral",
3892
4000
  "--skip-git-repo-check",
3893
4001
  "-s",
@@ -3985,7 +4093,6 @@ function createCodexTextGeneration(input) {
3985
4093
  prompt,
3986
4094
  includeBranch ? COMMIT_WITH_BRANCH_SCHEMA : COMMIT_SCHEMA,
3987
4095
  inputRecord.modelSelection,
3988
- inputRecord.launch,
3989
4096
  [],
3990
4097
  signal
3991
4098
  );
@@ -4036,7 +4143,6 @@ function createCodexTextGeneration(input) {
4036
4143
  prompt,
4037
4144
  PR_SCHEMA,
4038
4145
  inputRecord.modelSelection,
4039
- inputRecord.launch,
4040
4146
  [],
4041
4147
  signal
4042
4148
  );
@@ -4076,7 +4182,6 @@ function createCodexTextGeneration(input) {
4076
4182
  prompt,
4077
4183
  BRANCH_SCHEMA,
4078
4184
  inputRecord.modelSelection,
4079
- inputRecord.launch,
4080
4185
  absoluteImagePaths(attachments),
4081
4186
  signal
4082
4187
  );
@@ -4113,7 +4218,6 @@ ${extra}` : ""}`;
4113
4218
  prompt,
4114
4219
  TITLE_SCHEMA,
4115
4220
  inputRecord.modelSelection,
4116
- inputRecord.launch,
4117
4221
  absoluteImagePaths(attachments),
4118
4222
  signal
4119
4223
  );
@@ -4142,7 +4246,6 @@ ${extra}` : ""}`;
4142
4246
  prompt,
4143
4247
  HANDOVER_SCHEMA,
4144
4248
  inputRecord.modelSelection,
4145
- inputRecord.launch,
4146
4249
  [],
4147
4250
  signal
4148
4251
  );
@@ -4201,18 +4304,15 @@ function accountToAuth2(account) {
4201
4304
  return { status: "unknown" };
4202
4305
  }
4203
4306
  }
4204
- function layoutFor(recipe, launchHome, nativeSubscription = false) {
4307
+ function layoutFor(recipe) {
4205
4308
  const environmentHome = recipe.environment["CODEX_HOME"];
4206
- if (nativeSubscription && launchHome?.trim()) {
4207
- return resolveCodexHomeLayout({ homePath: launchHome.trim(), shadowHomePath: "" });
4208
- }
4209
4309
  return resolveCodexHomeLayout({
4210
- homePath: launchHome?.trim() || recipe.homePath,
4310
+ homePath: recipe.homePath,
4211
4311
  shadowHomePath: recipe.shadowHomePath,
4212
4312
  ...environmentHome === void 0 ? {} : { environmentHome }
4213
4313
  });
4214
4314
  }
4215
- var createCodexInstance = (config, host) => {
4315
+ var createCodexInstance = (config, host, attachments) => {
4216
4316
  const recipe = parseCodexInstanceConfig(config);
4217
4317
  const events = new EventBus();
4218
4318
  const threads = /* @__PURE__ */ new Map();
@@ -4252,10 +4352,11 @@ var createCodexInstance = (config, host) => {
4252
4352
  await rpc.request("initialize", CODEX_INITIALIZE_PARAMS)
4253
4353
  );
4254
4354
  await rpc.notify("initialized");
4255
- return { version, models: input.listModels ? await listCodexModels(rpc) : void 0 };
4355
+ const models = input.listModels ? await listCodexModels(rpc) : void 0;
4356
+ return { version, models, skills: await listCodexSkills(rpc) };
4256
4357
  } catch (error) {
4257
4358
  if (error instanceof ExecutorSpawnRefusedError) throw error;
4258
- return { version, models: void 0 };
4359
+ return { version, models: void 0, skills: void 0 };
4259
4360
  } finally {
4260
4361
  await rpc?.close().catch(() => void 0);
4261
4362
  await handle?.kill("SIGTERM").catch(() => void 0);
@@ -4282,7 +4383,9 @@ var createCodexInstance = (config, host) => {
4282
4383
  };
4283
4384
  }
4284
4385
  const connectionError = codexApiConnectionError(recipe, recipe.environment);
4285
- const { version, models } = await probeAppServer({ listModels: connectionError === null });
4386
+ const { version, models, skills } = await probeAppServer({
4387
+ listModels: connectionError === null
4388
+ });
4286
4389
  if (connectionError) {
4287
4390
  return {
4288
4391
  installed: true,
@@ -4290,6 +4393,7 @@ var createCodexInstance = (config, host) => {
4290
4393
  readiness: "error",
4291
4394
  auth: probeAuth(),
4292
4395
  detail: connectionError,
4396
+ ...skills === void 0 ? {} : { skills },
4293
4397
  ...CODEX_NATIVE_MODE_REPORT
4294
4398
  };
4295
4399
  }
@@ -4299,6 +4403,7 @@ var createCodexInstance = (config, host) => {
4299
4403
  readiness: models === void 0 ? "unchecked" : "ready",
4300
4404
  auth: probeAuth(),
4301
4405
  ...models === void 0 ? {} : { models },
4406
+ ...skills === void 0 ? {} : { skills },
4302
4407
  // 宿主拿的是这一份。登录核验那份(`verifyCodexSignIn`)报了档、这里没报,
4303
4408
  // 界面就一直画宿主四档——2026-09-20 真发生过,浏览器里才看得见。
4304
4409
  ...CODEX_NATIVE_MODE_REPORT
@@ -4317,22 +4422,17 @@ var createCodexInstance = (config, host) => {
4317
4422
  await stopBooked(existing);
4318
4423
  threads.delete(input.threadId);
4319
4424
  }
4320
- const nativeSubscription = input.launch.nativeSubscription === true;
4321
- const layout = layoutFor(recipe, input.launch.homePath, nativeSubscription);
4425
+ const layout = layoutFor(recipe);
4322
4426
  await materializeCodexShadowHome(layout, host.log);
4323
4427
  const mcp = codexMcpLaunchConfig(input.mcpServers ?? []);
4324
4428
  const env = buildCodexSpawnEnv({
4325
4429
  recipe,
4326
4430
  layout,
4327
- // MCP 那几个变量是宿主挑好的 server 带来的,与绑定同一层:实例表盖不得。
4328
- launchEnv: { ...input.launch.processEnv, ...mcp.environment },
4329
- crushApiKeys: nativeSubscription || shouldCrushApiKeys(recipe.authMode)
4431
+ // MCP 那几个变量是宿主挑好的 server 带来的,铺在最终层:实例表盖不得。
4432
+ launchEnv: mcp.environment,
4433
+ crushApiKeys: shouldCrushApiKeys(recipe.authMode)
4330
4434
  });
4331
- const args = [
4332
- ...codexSpawnArgs(recipe, layout, nativeSubscription),
4333
- ...codexOverrideArgs(input.launch.launchArgOverrides),
4334
- ...mcp.args
4335
- ];
4435
+ const args = [...codexSpawnArgs(recipe, layout), ...mcp.args];
4336
4436
  const handle = await host.process.spawn({
4337
4437
  program: recipe.binaryPath,
4338
4438
  args,
@@ -4367,7 +4467,17 @@ var createCodexInstance = (config, host) => {
4367
4467
  }
4368
4468
  }
4369
4469
  });
4370
- runtime = new CodexSessionRuntime(host, rpc, (event) => events.push(event), input, cwd, args);
4470
+ runtime = new CodexSessionRuntime(
4471
+ host,
4472
+ rpc,
4473
+ (event) => events.push(event),
4474
+ input,
4475
+ cwd,
4476
+ args,
4477
+ void 0,
4478
+ void 0,
4479
+ attachments
4480
+ );
4371
4481
  try {
4372
4482
  await rpc.request("initialize", CODEX_INITIALIZE_PARAMS);
4373
4483
  await rpc.notify("initialized");
@@ -4435,10 +4545,6 @@ var createCodexInstance = (config, host) => {
4435
4545
  const booked = requireThread(threadId);
4436
4546
  return await booked.runtime.readThread();
4437
4547
  },
4438
- uploadFeedback: async (input) => {
4439
- const booked = requireThread(input.threadId);
4440
- return await booked.runtime.uploadFeedback(input.reason);
4441
- },
4442
4548
  events,
4443
4549
  dispose: async () => {
4444
4550
  if (disposed) return;
@@ -4624,13 +4730,15 @@ var createCodexInstance = (config, host) => {
4624
4730
  };
4625
4731
 
4626
4732
  // src/index.ts
4733
+ var capturedAttachments;
4627
4734
  var executors = [
4628
4735
  {
4629
4736
  card: CODEX_CARD,
4630
4737
  createInstance: async (config, host) => {
4631
4738
  host.log.info("createInstance");
4632
- return createCodexInstance(config, host);
4633
- }
4739
+ return createCodexInstance(config, host, capturedAttachments);
4740
+ },
4741
+ probeAccount: async ({ accountPath }) => probeCodexAccount(accountPath)
4634
4742
  }
4635
4743
  ];
4636
4744
  var index_default = definePlugin({
@@ -4638,6 +4746,7 @@ var index_default = definePlugin({
4638
4746
  apiVersion: 2,
4639
4747
  executors,
4640
4748
  activate(ctx) {
4749
+ capturedAttachments = ctx.attachments;
4641
4750
  ctx.log.info("codex executor ready");
4642
4751
  }
4643
4752
  });
@@ -0,0 +1,23 @@
1
+ {
2
+ "form": {
3
+ "binaryPath": {
4
+ "title": "Program path",
5
+ "description": "Path to the Codex program this provider runs.",
6
+ "placeholder": "codex"
7
+ },
8
+ "homePath": {
9
+ "title": "CODEX_HOME path",
10
+ "description": "Custom Codex home and config directory.",
11
+ "placeholder": "~/.codex"
12
+ },
13
+ "shadowHomePath": {
14
+ "title": "Shadow home path",
15
+ "description": "Account-specific Codex home. Keeps auth.json separate while sharing state from CODEX_HOME.",
16
+ "placeholder": "~/.codex-personal"
17
+ },
18
+ "launchArgs": {
19
+ "title": "Launch arguments",
20
+ "description": "Additional CLI arguments passed to codex app-server on session start."
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "form": {
3
+ "binaryPath": {
4
+ "title": "程序路径",
5
+ "description": "此 provider 运行的 Codex 程序的路径。",
6
+ "placeholder": "codex"
7
+ },
8
+ "homePath": {
9
+ "title": "CODEX_HOME 路径",
10
+ "description": "自定义的 Codex 主目录与配置目录。",
11
+ "placeholder": "~/.codex"
12
+ },
13
+ "shadowHomePath": {
14
+ "title": "影子目录路径",
15
+ "description": "账户专用的 Codex 目录。auth.json 单独保存,其余状态仍与 CODEX_HOME 共用。",
16
+ "placeholder": "~/.codex-personal"
17
+ },
18
+ "launchArgs": {
19
+ "title": "启动参数",
20
+ "description": "会话启动时传给 codex app-server 的额外命令行参数。"
21
+ }
22
+ }
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-codex",
3
- "version": "0.2.2163",
3
+ "version": "0.2.2488",
4
4
  "description": "Codex executor: Codex CLI over the app-server JSON-RPC protocol.",
5
5
  "keywords": [
6
6
  "ycodium",
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "dist",
12
12
  "assets",
13
+ "locales",
13
14
  "README.md"
14
15
  ],
15
16
  "type": "module",
@@ -20,7 +21,8 @@
20
21
  "@types/node": "24.12.4",
21
22
  "esbuild": "0.28.1",
22
23
  "vite-plus": "0.2.2",
23
- "@ycodium-ai/plugin-api": "0.2.0"
24
+ "@ycodium-ai/plugin-api": "0.2.2488",
25
+ "ycodium-executor-testing": "0.0.0"
24
26
  },
25
27
  "ycodium": {
26
28
  "plugin": "./dist/index.js",