@hellolynk-ai/codex-plugin 0.2.100 → 0.2.102

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.
@@ -61,8 +61,8 @@ try {
61
61
  "--expected-manifest-sha256", sourceRelease.outerManifestSha256,
62
62
  ]);
63
63
  } else {
64
- const originalLocations = resolveLocations();
65
64
  const accountModule = await importStableCore(PACKAGE_ROOT, "hellolynk-client-account.mjs", "hellolynk_npm_account_runtime_invalid");
65
+ const originalLocations = resolveLocations(accountModule.clientAccountBaseRoot);
66
66
  const baseRoot = accountModule.clientAccountBaseRoot(originalLocations.hostRoot);
67
67
  const packageLocations = { ...originalLocations, hostRoot: baseRoot, finalCurrentRoot: path.dirname(baseRoot) };
68
68
  const accounts = accountModule.createClientAccountActions({ client: metadata.client, storeRoot: baseRoot });
@@ -84,6 +84,13 @@ try {
84
84
  if (mutating) {
85
85
  await prepareFinalClientRoot(locations);
86
86
  installed = await installStablePackage({ packageClosure, sourceRelease, locations: packageLocations });
87
+ if (metadata.client === "codex") {
88
+ // The invoking updater can still be B255 code. Stop its old cleanup
89
+ // before switching the binding/cache to a release with process records.
90
+ const cleanup = await importStableCore(installed.packageRoot, "hellolynk-release-retention.mjs", "hellolynk_release_retention_unavailable");
91
+ await cleanup.protectRetentionReceipt({ releasesRoot: path.dirname(installed.packageRoot),
92
+ client: "codex", currentRelease: installed.packageRoot });
93
+ }
87
94
  }
88
95
  const activation = await inspectActivation(locations.hostRoot, metadata.profileId);
89
96
  // Both public install commands use the actual account state. Native recovery
@@ -831,7 +838,7 @@ async function verifyRelease(releaseRoot, packageRoot, expectedMetadata = metada
831
838
  return Object.freeze({ manifestSha256, outerManifestSha256 });
832
839
  }
833
840
 
834
- function resolveLocations() {
841
+ function resolveLocations(getDefaultHostRoot) {
835
842
  if (process.platform !== "win32") fail("hellolynk_npm_windows_only");
836
843
  const home = path.resolve(process.env.USERPROFILE || os.homedir());
837
844
  const localAppData = path.resolve(process.env.LOCALAPPDATA || path.join(home, "AppData", "Local"));
@@ -841,7 +848,7 @@ function resolveLocations() {
841
848
  const codexHome = path.resolve(configuredCodexHome || path.join(home, ".codex"));
842
849
  const configuredHostRoot = String(process.env.HELLOLYNK_NEUTRAL_STORE_ROOT || "").trim();
843
850
  if (configuredHostRoot && !path.isAbsolute(configuredHostRoot)) fail("hellolynk_npm_store_root_invalid");
844
- const hostRoot = path.resolve(configuredHostRoot || path.join(localAppData, "HelloLynk", "host"));
851
+ const hostRoot = path.resolve(configuredHostRoot || getDefaultHostRoot());
845
852
  if (configuredHostRoot && path.basename(hostRoot).toLowerCase() !== "host") {
846
853
  fail("hellolynk_npm_store_root_invalid");
847
854
  }
@@ -997,6 +1004,12 @@ async function startHostWhenAbsent(runtime, binding) {
997
1004
  }
998
1005
 
999
1006
  async function secureStableReleaseTree(locations, releaseId, options = {}) {
1007
+ const releasePath = path.resolve(locations.finalCurrentRoot, "releases", releaseId);
1008
+ // An existing AppData root can be shared while a new release is redirected
1009
+ // into one desktop app's private cache. Other clients cannot use that release.
1010
+ if (process.platform === "win32" && realpathSync.native(releasePath).toLowerCase() !== releasePath.toLowerCase()) {
1011
+ fail("client_root_alias_rejected");
1012
+ }
1000
1013
  const module = await importCore(
1001
1014
  "hellolynk-platform-credential-store.mjs",
1002
1015
  "hellolynk_npm_client_root_security_unavailable",
@@ -2,7 +2,7 @@
2
2
  "contract": "hellolynk-client-npm-pack-manifest-v1",
3
3
  "client": "codex",
4
4
  "packageName": "@hellolynk-ai/codex-plugin",
5
- "version": "0.2.100",
5
+ "version": "0.2.102",
6
6
  "workbench": {
7
7
  "path": "plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json",
8
8
  "contract": "hellolynk-shared-workbench-build-v1",
@@ -17,8 +17,8 @@
17
17
  },
18
18
  {
19
19
  "path": "bin/hellolynk-client-setup.mjs",
20
- "bytes": 141259,
21
- "sha256": "ab6ce9491d9b88856fdc0207b0df3230ed27642d9d206f52341a4f9791622a4e"
20
+ "bytes": 142207,
21
+ "sha256": "95386fcd9092f31abb3b0441033b3b385b3c355506613da24b4557d5f9cac6ef"
22
22
  },
23
23
  {
24
24
  "path": "bin/hellolynk-mcp-config-shape.mjs",
@@ -33,7 +33,7 @@
33
33
  {
34
34
  "path": "package.json",
35
35
  "bytes": 1713,
36
- "sha256": "70f9207e339885e7ee3f170f5d06f78a64066c1d5ceaba5692b75bc404149e35"
36
+ "sha256": "a1dffd73cef661cd0f019f5a532f949693fccfa72222d346bd867470977cdc99"
37
37
  },
38
38
  {
39
39
  "path": "plugins/hellolynk-codex/.app.json",
@@ -43,7 +43,7 @@
43
43
  {
44
44
  "path": "plugins/hellolynk-codex/.codex-plugin/plugin.json",
45
45
  "bytes": 1669,
46
- "sha256": "f61ede8bedf0120a53b381a7cdc9cdbcc49242360d8d1e4271bec69f85b9b91f"
46
+ "sha256": "38d2a5204b66ec57368494e69ed71de655f798f9d37ab2af966b59c3ab07d0f2"
47
47
  },
48
48
  {
49
49
  "path": "plugins/hellolynk-codex/.mcp.json",
@@ -58,7 +58,7 @@
58
58
  {
59
59
  "path": "plugins/hellolynk-codex/package.json",
60
60
  "bytes": 1030,
61
- "sha256": "bd762c92db97e96c4b9a1e1bef99a13d96cdd0e06bd58f4cf875fbcd00186cf9"
61
+ "sha256": "befbca064334e8ff7f9d192f095ac1bd786a3882385a450fc2c2d2a5c9b2b7dd"
62
62
  },
63
63
  {
64
64
  "path": "plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs",
@@ -72,8 +72,8 @@
72
72
  },
73
73
  {
74
74
  "path": "plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs",
75
- "bytes": 8282,
76
- "sha256": "4b45a78b4d6c4794a1f494bd95777579c32c56a68321a1d3b484048942ae4742"
75
+ "bytes": 9359,
76
+ "sha256": "8be37831c96610e3d0a64848ada86488970cc6079e49764b47d8a8b2ba852269"
77
77
  },
78
78
  {
79
79
  "path": "plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs",
@@ -92,8 +92,8 @@
92
92
  },
93
93
  {
94
94
  "path": "plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs",
95
- "bytes": 33525,
96
- "sha256": "b5f6288732af6f9b9c0c750f6002eae657dde9513fc97674f6e59200f3cc9dc6"
95
+ "bytes": 33183,
96
+ "sha256": "dfb6bba4c627082cd6fca8cb38dc786f905e38f3e83ccf91dbfe57e55639e8a2"
97
97
  },
98
98
  {
99
99
  "path": "plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs",
@@ -132,8 +132,8 @@
132
132
  },
133
133
  {
134
134
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs",
135
- "bytes": 8282,
136
- "sha256": "4b45a78b4d6c4794a1f494bd95777579c32c56a68321a1d3b484048942ae4742"
135
+ "bytes": 9359,
136
+ "sha256": "8be37831c96610e3d0a64848ada86488970cc6079e49764b47d8a8b2ba852269"
137
137
  },
138
138
  {
139
139
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs",
@@ -267,8 +267,8 @@
267
267
  },
268
268
  {
269
269
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs",
270
- "bytes": 19350,
271
- "sha256": "252251de855c3fe9c5ebdd54a97ba976a8009f4ac57ec44f492bed8c7fb903de"
270
+ "bytes": 19443,
271
+ "sha256": "f4bd81b3d03277e28955a62409383a0c4072c6212567f71425df4e18ece75d63"
272
272
  },
273
273
  {
274
274
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs",
@@ -307,8 +307,8 @@
307
307
  },
308
308
  {
309
309
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs",
310
- "bytes": 33525,
311
- "sha256": "b5f6288732af6f9b9c0c750f6002eae657dde9513fc97674f6e59200f3cc9dc6"
310
+ "bytes": 33183,
311
+ "sha256": "dfb6bba4c627082cd6fca8cb38dc786f905e38f3e83ccf91dbfe57e55639e8a2"
312
312
  },
313
313
  {
314
314
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs",
@@ -318,7 +318,7 @@
318
318
  {
319
319
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs",
320
320
  "bytes": 225161,
321
- "sha256": "48f9bd6451adafdf3b43591a3b1469e36174c30fe04214cd2a84fb0aeebea023"
321
+ "sha256": "e744bacf65f7b86cbcf7d654189c0e07e09401083e2b6f9053b17c1c3bacd1b4"
322
322
  },
323
323
  {
324
324
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs",
@@ -342,8 +342,8 @@
342
342
  },
343
343
  {
344
344
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs",
345
- "bytes": 18822,
346
- "sha256": "4ba5607e92f98a9a33b9ffd9ad166e559bc3197fcb1a6d54cef96b2b91ddc38e"
345
+ "bytes": 20624,
346
+ "sha256": "9b02a42a510fa5ef4a251b6e675f9ccc60f20b718d17c1617027cd048c5634dd"
347
347
  },
348
348
  {
349
349
  "path": "plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs",
@@ -378,12 +378,12 @@
378
378
  {
379
379
  "path": "plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json",
380
380
  "bytes": 13440,
381
- "sha256": "98a0a761956020462c08bc6c02000b06fd37e0c2a5f44266ca5d4657c3ea28ce"
381
+ "sha256": "3f8ad933949136ed1ebf5172c9e49ead0b0e2829650bc960eef1bdddd27c6a8f"
382
382
  },
383
383
  {
384
384
  "path": "plugins/hellolynk-codex/runtime/neutral-host-release-pin.json",
385
385
  "bytes": 146,
386
- "sha256": "4bee28c2ad8c0bfc5f1021127fa793e43797ff08fda49945622827502a5f26a9"
386
+ "sha256": "6c9832284a05105014d72fe52f15e7d36c3c3fbbcb20aaf24f84a10196a7fad8"
387
387
  },
388
388
  {
389
389
  "path": "plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1",
@@ -552,8 +552,8 @@
552
552
  },
553
553
  {
554
554
  "path": "plugins/hellolynk-codex/server/codex-neutral-production.mjs",
555
- "bytes": 26794,
556
- "sha256": "34b865d80d7eb3c8e6986635b158c8583ddddb40bb528c4a75f0a498bdb3e1fb"
555
+ "bytes": 26960,
556
+ "sha256": "7b3249f1b35c331cf2b00ac2873a54d12fc6f61cf341bb82cec98e49ce221b92"
557
557
  },
558
558
  {
559
559
  "path": "plugins/hellolynk-codex/server/mcp-proxy.mjs",
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "contract": "hellolynk-client-npm-pack-pin-v1",
3
- "manifestSha256": "75821d529dc23bf90c905bbfbfd9ada66bdff198413c003305e744499d68ad3d"
3
+ "manifestSha256": "f750254b705c93ec80f1e297d7a03f2c8e3b0a3cdf73bb0073a0527ca64fdf46"
4
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellolynk-ai/codex-plugin",
3
- "version": "0.2.100",
3
+ "version": "0.2.102",
4
4
  "description": "HelloLynk Messaging for Codex: open conversations and let your AI message other people's AI assistants.",
5
5
  "homepage": "https://hellolynk.com",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hellolynk-codex",
3
- "version": "0.2.100",
3
+ "version": "0.2.102",
4
4
  "description": "HelloLynk 通讯、本地未读提醒与安全文字收发的 Codex 插件。",
5
5
  "author": {
6
6
  "name": "HelloLynk local project"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellolynk-ai/codex-plugin",
3
- "version": "0.2.100",
3
+ "version": "0.2.102",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Project-local HelloLynk Messaging, unread notification, and secure messaging plugin for Codex.",
@@ -1,4 +1,5 @@
1
1
  import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
2
+ import { readdirSync } from "node:fs";
2
3
  import { homedir } from "node:os";
3
4
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
5
  import { randomUUID } from "node:crypto";
@@ -19,12 +20,34 @@ const SAFE_SCOPE = /^[a-z][a-z0-9_.:-]{0,127}$/;
19
20
  const MAX_PROFILE_BYTES = 64 * 1024;
20
21
 
21
22
  export function defaultHelloLynkClientRoot(platform = process.platform, env = process.env) {
22
- const home = resolve(String(env.HOME || env.USERPROFILE || homedir()));
23
- if (platform === "win32") return resolve(String(env.LOCALAPPDATA || join(home, "AppData", "Local")), "HelloLynk");
23
+ const home = resolve(String((platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME || env.USERPROFILE) || homedir()));
24
+ if (platform === "win32") {
25
+ const current = join(home, ".hellolynk", "hellolynk-v2");
26
+ // Once selected, another client's older installer must not move us back.
27
+ if (hasWindowsClientInstallation(current)) return current;
28
+ const existing = resolve(String(env.LOCALAPPDATA || join(home, "AppData", "Local")), "HelloLynk");
29
+ // AppData created from a packaged desktop app can be private to that app.
30
+ // Keep an existing installation; an empty failed setup is not an account.
31
+ if (hasWindowsClientInstallation(existing)) return existing;
32
+ return current;
33
+ }
24
34
  if (platform === "darwin") return join(home, "Library", "Application Support", "HelloLynk");
25
35
  return resolve(String(env.XDG_DATA_HOME || join(home, ".local", "share")), "hellolynk");
26
36
  }
27
37
 
38
+ function hasWindowsClientInstallation(root) {
39
+ for (const directory of ["profiles", "releases", "host", "credentials", "e2ee-private-keys"]) {
40
+ try {
41
+ const entries = readdirSync(join(root, directory), { withFileTypes: true });
42
+ if (entries.some((entry) => directory !== "host" || entry.name !== "runtime")) return true;
43
+ } catch (error) {
44
+ // An unreadable existing location must not silently become a fresh account.
45
+ if (error.code !== "ENOENT") return true;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+
28
51
  function safeId(value, code) {
29
52
  const normalized = String(value || "").trim();
30
53
  if (!SAFE_ID.test(normalized)) throw new Error(code);
@@ -12,10 +12,9 @@ import {
12
12
  } from "node:fs/promises";
13
13
  import { basename, dirname, isAbsolute, join, parse, resolve } from "node:path";
14
14
  import { spawn } from "node:child_process";
15
- import { homedir } from "node:os";
16
15
  import { performance } from "node:perf_hooks";
17
16
  import { withLocalProcessLock } from "./hellolynk-local-process-lock.mjs";
18
- import { readClientProfile } from "./hellolynk-client-profile.mjs";
17
+ import { defaultHelloLynkClientRoot, readClientProfile } from "./hellolynk-client-profile.mjs";
19
18
  import { assertIdentifier } from "./hellolynk-host-auth.mjs";
20
19
  import runtimeSafety from "./hellolynk-runtime-safety.cjs";
21
20
 
@@ -46,10 +45,7 @@ export function defaultNeutralHostStoreRoot(platform = process.platform, env = p
46
45
  }
47
46
  return root;
48
47
  }
49
- const home = resolve(String(env.HOME || env.USERPROFILE || homedir()));
50
- if (platform === "win32") return resolve(String(env.LOCALAPPDATA || join(home, "AppData", "Local")), "HelloLynk", "host");
51
- if (platform === "darwin") return join(home, "Library", "Application Support", "HelloLynk", "host");
52
- return resolve(String(env.XDG_DATA_HOME || join(home, ".local", "share")), "hellolynk", "host");
48
+ return join(defaultHelloLynkClientRoot(platform, env), "host");
53
49
  }
54
50
 
55
51
  export function createNeutralHostInstallationStore(options = {}) {
@@ -1,4 +1,5 @@
1
1
  import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
2
+ import { readdirSync } from "node:fs";
2
3
  import { homedir } from "node:os";
3
4
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
5
  import { randomUUID } from "node:crypto";
@@ -19,12 +20,34 @@ const SAFE_SCOPE = /^[a-z][a-z0-9_.:-]{0,127}$/;
19
20
  const MAX_PROFILE_BYTES = 64 * 1024;
20
21
 
21
22
  export function defaultHelloLynkClientRoot(platform = process.platform, env = process.env) {
22
- const home = resolve(String(env.HOME || env.USERPROFILE || homedir()));
23
- if (platform === "win32") return resolve(String(env.LOCALAPPDATA || join(home, "AppData", "Local")), "HelloLynk");
23
+ const home = resolve(String((platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME || env.USERPROFILE) || homedir()));
24
+ if (platform === "win32") {
25
+ const current = join(home, ".hellolynk", "hellolynk-v2");
26
+ // Once selected, another client's older installer must not move us back.
27
+ if (hasWindowsClientInstallation(current)) return current;
28
+ const existing = resolve(String(env.LOCALAPPDATA || join(home, "AppData", "Local")), "HelloLynk");
29
+ // AppData created from a packaged desktop app can be private to that app.
30
+ // Keep an existing installation; an empty failed setup is not an account.
31
+ if (hasWindowsClientInstallation(existing)) return existing;
32
+ return current;
33
+ }
24
34
  if (platform === "darwin") return join(home, "Library", "Application Support", "HelloLynk");
25
35
  return resolve(String(env.XDG_DATA_HOME || join(home, ".local", "share")), "hellolynk");
26
36
  }
27
37
 
38
+ function hasWindowsClientInstallation(root) {
39
+ for (const directory of ["profiles", "releases", "host", "credentials", "e2ee-private-keys"]) {
40
+ try {
41
+ const entries = readdirSync(join(root, directory), { withFileTypes: true });
42
+ if (entries.some((entry) => directory !== "host" || entry.name !== "runtime")) return true;
43
+ } catch (error) {
44
+ // An unreadable existing location must not silently become a fresh account.
45
+ if (error.code !== "ENOENT") return true;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+
28
51
  function safeId(value, code) {
29
52
  const normalized = String(value || "").trim();
30
53
  if (!SAFE_ID.test(normalized)) throw new Error(code);
@@ -18,7 +18,7 @@ const conversationGuidance = "HelloLynk 是本人授权的 AI 通讯。接着已
18
18
  // What every client shows for the shared tools; nativeTool applies it.
19
19
  const toolText = Object.freeze({
20
20
  hellolynk_reply_authorized_message: { title: "回复来信", description: "接着当前对话的一封来信发送;不接手其他对话。" },
21
- hellolynk_send_authorized_message: { title: "发起通信", description: "向本人授权的联系人发起新通信。contact 填对方名字;本人点名了对话时,targetConversationTitle 填完整标题。接着已有来往用 hellolynk_reply_authorized_message。" },
21
+ hellolynk_send_authorized_message: { title: "发起通信", description: "向本人授权的联系人发起新通信。contact 填对方名字;本人点名了对话时,targetConversationTitle 填完整标题。未指定对话的新通信默认送到对方的 HelloLynk 独立会话;要继续已有来往,用 hellolynk_reply_authorized_message 回复原信。" },
22
22
  hellolynk_list_authorized_contacts: { title: "查找联系人", description: queryDescription },
23
23
  hellolynk_claim_message: { title: "读取来信", description: "按自动提醒里的 batchId 一次读取整批来信(合计不超过 2 万字);手动读单条传 localMessageId 和 conversationKey。" },
24
24
  hellolynk_list_pending_messages: { title: "待处理来信", description: "列出当前对话等待 AI 处理的来信,不含正文。" },
@@ -12,10 +12,9 @@ import {
12
12
  } from "node:fs/promises";
13
13
  import { basename, dirname, isAbsolute, join, parse, resolve } from "node:path";
14
14
  import { spawn } from "node:child_process";
15
- import { homedir } from "node:os";
16
15
  import { performance } from "node:perf_hooks";
17
16
  import { withLocalProcessLock } from "./hellolynk-local-process-lock.mjs";
18
- import { readClientProfile } from "./hellolynk-client-profile.mjs";
17
+ import { defaultHelloLynkClientRoot, readClientProfile } from "./hellolynk-client-profile.mjs";
19
18
  import { assertIdentifier } from "./hellolynk-host-auth.mjs";
20
19
  import runtimeSafety from "./hellolynk-runtime-safety.cjs";
21
20
 
@@ -46,10 +45,7 @@ export function defaultNeutralHostStoreRoot(platform = process.platform, env = p
46
45
  }
47
46
  return root;
48
47
  }
49
- const home = resolve(String(env.HOME || env.USERPROFILE || homedir()));
50
- if (platform === "win32") return resolve(String(env.LOCALAPPDATA || join(home, "AppData", "Local")), "HelloLynk", "host");
51
- if (platform === "darwin") return join(home, "Library", "Application Support", "HelloLynk", "host");
52
- return resolve(String(env.XDG_DATA_HOME || join(home, ".local", "share")), "hellolynk", "host");
48
+ return join(defaultHelloLynkClientRoot(platform, env), "host");
53
49
  }
54
50
 
55
51
  export function createNeutralHostInstallationStore(options = {}) {
@@ -34,7 +34,7 @@ export const HELLOLYNK_NEUTRAL_HOST_PROCESS_CONTRACT = "hellolynk-neutral-host-p
34
34
  export const HELLOLYNK_NEUTRAL_HOST_DISCOVERY_CONTRACT = "hellolynk-neutral-host-discovery-v1";
35
35
  // Raised with every release batch, together with the six client versions. A Host reports it
36
36
  // in its health; the higher release stays and the lower one makes room.
37
- export const HELLOLYNK_HOST_RELEASE_ORDINAL = 13;
37
+ export const HELLOLYNK_HOST_RELEASE_ORDINAL = 15;
38
38
 
39
39
  const INSTALLATION_LOCK_CONTRACT = "hellolynk-neutral-host-installation-lock-v2";
40
40
  const INSTALLATION_LOCK_MAX_BYTES = 4 * 1024;
@@ -15,12 +15,29 @@ const contract = 'hellolynk-release-retention-v1';
15
15
  // receipt: an older worker that cleans up after an update deletes nothing.
16
16
  const ENTRY = { 'claude-code': ['claude-code', 'hellolynk-claude-code.mjs'] };
17
17
  const contractWithEntry = 'hellolynk-release-retention-v2';
18
+ // B255 workers understand v2 but do not read Codex process records.
19
+ const codexContract = 'hellolynk-release-retention-v3';
18
20
  const PROCESS_CONTRACT = 'hellolynk-release-process-v1';
19
21
  const START_TOLERANCE_MS = 15000;
20
22
  export const releaseProcessDirectory = (releasesRoot, client) => path.join(releasesRoot, `.${client}-processes`);
21
23
  const failure = code => Object.assign(new Error(code), { code });
22
24
  const equal = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
23
25
 
26
+ // A cache MCP keeps an installed prepare function in memory. Record its most
27
+ // recent successful binding, even when its command line names only the cache.
28
+ // A checkout/uninstalled fixture has no immutable release to retain.
29
+ export async function recordCodexReleaseProcess(packageRoot) {
30
+ if (process.platform !== 'win32' || !path.isAbsolute(packageRoot || '')
31
+ || path.basename(path.dirname(packageRoot)) !== 'releases' || !/^codex-[a-f0-9]{64}$/.test(path.basename(packageRoot))) return;
32
+ try {
33
+ const root = await rootPath(path.dirname(packageRoot));
34
+ const folder = releaseProcessDirectory(root, 'codex');
35
+ await fs.mkdir(folder, { recursive: true });
36
+ await writeReceipt(path.join(folder, `${process.pid}.json`), { contract: PROCESS_CONTRACT, pid: process.pid,
37
+ startedAt: Math.round(performance.timeOrigin), release: path.basename(packageRoot) });
38
+ } catch { /* A missing record defers cleanup; it must not stop communication. */ }
39
+ }
40
+
24
41
  async function directory(target) {
25
42
  const stat = await fs.lstat(target);
26
43
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw failure('release_retention_path_invalid');
@@ -67,20 +84,25 @@ export async function runningReleaseNames(releasesRoot) {
67
84
  return names;
68
85
  }
69
86
 
70
- // The given pids and every process whose command line runs this installation's
71
- // stable entry: pid, start time and whether it runs the entry. Only processes
87
+ // The given pids, this installation's stable entry, and Codex cache children:
88
+ // pid, start time and whether it runs the entry. Only processes
72
89
  // with a positive pid are listed, and command lines never leave PowerShell.
73
90
  export async function entryProcessList(client, releasesRoot, pids = []) {
74
- if (!ENTRY[client]) return [];
91
+ if (!ENTRY[client] && client !== 'codex') return [];
75
92
  if (process.platform !== 'win32') throw failure('release_retention_process_check_unavailable');
76
93
  if (!Array.isArray(pids) || pids.some(pid => !Number.isSafeInteger(pid) || pid <= 0)) throw failure('release_retention_process_check_invalid');
77
94
  const root = await rootPath(releasesRoot);
78
95
  // This installation's entry only, in its given and canonical spellings:
79
96
  // any case, either slash, quoted or not.
80
97
  const literal = value => value.split(/[\\/]+/).map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('[\\\\/]+');
81
- const entries = [...new Set([root, path.resolve(releasesRoot)])].map(value => literal(path.join(path.dirname(value), ...ENTRY[client])));
82
- const pattern = `(?<=^|["\\s])(?:${entries.join('|')})(?=["\\s]|$)`.replaceAll("'", "''");
83
- const script = `$ErrorActionPreference='Stop'; $wanted=@(${pids.join(',')}); $epoch=[datetime]::new(1970,1,1,0,0,0,[DateTimeKind]::Utc); $list=@(Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -gt 0 } | ForEach-Object { $entry=[bool]($_.CommandLine -and [regex]::IsMatch($_.CommandLine,'${pattern}','IgnoreCase')); if ($entry -or ($wanted -contains [int64]$_.ProcessId)) { [pscustomobject]@{ pid=[int64]$_.ProcessId; startedAt=$(if ($_.CreationDate) { [int64](($_.CreationDate.ToUniversalTime() - $epoch).TotalMilliseconds) } else { $null }); entry=$entry } } }); ConvertTo-Json -Compress -InputObject $list`;
98
+ const entries = ENTRY[client] ? [...new Set([root, path.resolve(releasesRoot)])].map(value => literal(path.join(path.dirname(value), ...ENTRY[client]))) : [];
99
+ const pattern = (client === 'codex' ? '(?:^|["\\s\\\\/])(?:\\.[\\\\/])?server[\\\\/]mcp-proxy\\.mjs(?=["\\s]|$)'
100
+ : `(?<=^|["\\s])(?:${entries.join('|')})(?=["\\s]|$)`).replaceAll("'", "''");
101
+ // Relative cache entries do not reveal their installation. An older Codex
102
+ // child without a record conservatively delays this client's cleanup until
103
+ // it reconnects through a recording release or exits. Other Node apps do not.
104
+ const parentCheck = client === 'codex' ? "$_.Name -ieq 'node.exe' -and ($codexParents -contains $_.ParentProcessId) -and " : '';
105
+ const script = `$ErrorActionPreference='Stop'; $wanted=@(${pids.join(',')}); $epoch=[datetime]::new(1970,1,1,0,0,0,[DateTimeKind]::Utc); $all=@(Get-CimInstance Win32_Process); $codexParents=@($all | Where-Object { $_.Name -ieq 'Codex.exe' } | ForEach-Object { $_.ProcessId }); $list=@($all | Where-Object { $_.ProcessId -gt 0 } | ForEach-Object { $entry=[bool](${parentCheck}$_.CommandLine -and [regex]::IsMatch($_.CommandLine,'${pattern}','IgnoreCase')); if ($entry -or ($wanted -contains [int64]$_.ProcessId)) { [pscustomobject]@{ pid=[int64]$_.ProcessId; startedAt=$(if ($_.CreationDate) { [int64](($_.CreationDate.ToUniversalTime() - $epoch).TotalMilliseconds) } else { $null }); entry=$entry } } }); ConvertTo-Json -Compress -InputObject $list`;
84
106
  const shell = path.join(process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
85
107
  const { stdout } = await execute(shell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script],
86
108
  { windowsHide: true, timeout: 15000, maxBuffer: 1024 * 1024 });
@@ -95,7 +117,7 @@ export async function entryProcessList(client, releasesRoot, pids = []) {
95
117
  // Records of exited or reused pids are removed, so a release can go at the
96
118
  // next install once its last process is gone.
97
119
  async function entryReleases(releasesRoot, root, client, pattern, dependencies) {
98
- if (!ENTRY[client]) return { names: [], unregistered: false };
120
+ if (!ENTRY[client] && client !== 'codex') return { names: [], unregistered: false };
99
121
  const folder = releaseProcessDirectory(root, client);
100
122
  let items = [];
101
123
  try {
@@ -179,21 +201,20 @@ export async function pruneInstalledReleases({ releasesRoot, client, currentRele
179
201
  return result;
180
202
  }
181
203
 
182
- // A stable-entry client calls this before it switches an install on its stable
183
- // entry. An update worker of an earlier release cleans up after that install with
184
- // its own code, which cannot see what a stable-entry process runs and stops
185
- // before deleting only at a receipt other than v1. Resolves once the receipt is
186
- // v2, keeping the adoption it records, with whether it wrote. Throws when that
204
+ // Call before switching the binding/entry. An earlier updater cleans up after
205
+ // setup with its own code. Claude needs v2 to stop pre-B255 workers; Codex needs
206
+ // v3 to stop B255 workers that know only Claude records. Preserve the recorded
207
+ // adoption order and resolve with whether the receipt changed. Throw when that
187
208
  // cannot be confirmed, even for a cause that passes before the worker reads the
188
209
  // receipt (a file another process holds open): the caller then switches nothing.
189
210
  // currentRelease, the release the install starts now, is recorded only when
190
211
  // there is no receipt yet.
191
212
  export async function protectRetentionReceipt({ releasesRoot, client, currentRelease }) {
192
- if (!ENTRY[client]) throw failure('release_retention_client_invalid');
213
+ if (!ENTRY[client] && client !== 'codex') throw failure('release_retention_client_invalid');
193
214
  const root = await rootPath(releasesRoot), pattern = new RegExp(`^${client}-[a-f0-9]{64}$`);
194
215
  const receiptPath = path.join(root, `.${client}-retention.json`);
195
216
  const old = await readReceipt(receiptPath, client, pattern);
196
- if (old?.contract === contractWithEntry) return false;
217
+ if (old?.contract === receiptContract(client)) return false;
197
218
  let current = old?.current;
198
219
  if (!current) {
199
220
  if (typeof currentRelease !== 'string' || !path.isAbsolute(currentRelease) || !pattern.test(path.basename(currentRelease))
@@ -201,11 +222,11 @@ export async function protectRetentionReceipt({ releasesRoot, client, currentRel
201
222
  current = path.basename(currentRelease);
202
223
  await directory(path.join(root, current));
203
224
  }
204
- await writeReceipt(receiptPath, { contract: contractWithEntry, client, current, previous: old ? old.previous : null });
225
+ await writeReceipt(receiptPath, { contract: receiptContract(client), client, current, previous: old ? old.previous : null });
205
226
  return true;
206
227
  }
207
228
 
208
- const receiptContract = client => ENTRY[client] ? contractWithEntry : contract;
229
+ const receiptContract = client => client === 'codex' ? codexContract : ENTRY[client] ? contractWithEntry : contract;
209
230
 
210
231
  async function readReceipt(receiptPath, client, pattern) {
211
232
  let old = null;
@@ -213,7 +234,7 @@ async function readReceipt(receiptPath, client, pattern) {
213
234
  const stat = await fs.lstat(receiptPath);
214
235
  if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 4096) throw failure('release_retention_receipt_invalid');
215
236
  old = JSON.parse(await fs.readFile(receiptPath, 'utf8'));
216
- if (![contract, contractWithEntry].includes(old?.contract) || old.client !== client || !pattern.test(old.current)
237
+ if (![contract, contractWithEntry, ...(client === 'codex' ? [codexContract] : [])].includes(old?.contract) || old.client !== client || !pattern.test(old.current)
217
238
  || old.previous !== null && !pattern.test(old.previous) || old.current === old.previous) throw failure('release_retention_receipt_invalid');
218
239
  } catch (error) {
219
240
  if (error.code === 'ENOENT') return null;
@@ -245,7 +266,7 @@ export async function removeInstalledReleases({ releasesRoot, client }, dependen
245
266
  if (!result.deferred.length) {
246
267
  await fs.rm(path.join(root, `.${client}-retention.json`), { force: true });
247
268
  // entryReleases checked above that this is a folder in the releases root.
248
- if (ENTRY[client]) await fs.rm(releaseProcessDirectory(root, client), { recursive: true, force: true });
269
+ if (ENTRY[client] || client === 'codex') await fs.rm(releaseProcessDirectory(root, client), { recursive: true, force: true });
249
270
  }
250
271
  result.complete = result.deferred.length === 0;
251
272
  } catch (error) { result.error = safeCode(error); }
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  {
6
6
  "path": "release/package.json",
7
- "sha256": "bd762c92db97e96c4b9a1e1bef99a13d96cdd0e06bd58f4cf875fbcd00186cf9"
7
+ "sha256": "befbca064334e8ff7f9d192f095ac1bd786a3882385a450fc2c2d2a5c9b2b7dd"
8
8
  },
9
9
  {
10
10
  "path": "neutral-host-bootstrap.mjs",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  {
38
38
  "path": "core/hellolynk-client-profile.mjs",
39
- "sha256": "4b45a78b4d6c4794a1f494bd95777579c32c56a68321a1d3b484048942ae4742"
39
+ "sha256": "8be37831c96610e3d0a64848ada86488970cc6079e49764b47d8a8b2ba852269"
40
40
  },
41
41
  {
42
42
  "path": "core/hellolynk-client-update-entry.mjs",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  {
54
54
  "path": "core/hellolynk-release-retention.mjs",
55
- "sha256": "4ba5607e92f98a9a33b9ffd9ad166e559bc3197fcb1a6d54cef96b2b91ddc38e"
55
+ "sha256": "9b02a42a510fa5ef4a251b6e675f9ccc60f20b718d17c1617027cd048c5634dd"
56
56
  },
57
57
  {
58
58
  "path": "core/hellolynk-contact-request-e2ee.mjs",
@@ -168,7 +168,7 @@
168
168
  },
169
169
  {
170
170
  "path": "core/hellolynk-native-addressing-tools.cjs",
171
- "sha256": "252251de855c3fe9c5ebdd54a97ba976a8009f4ac57ec44f492bed8c7fb903de"
171
+ "sha256": "f4bd81b3d03277e28955a62409383a0c4072c6212567f71425df4e18ece75d63"
172
172
  },
173
173
  {
174
174
  "path": "core/hellolynk-native-human-confirmation-broker.mjs",
@@ -180,7 +180,7 @@
180
180
  },
181
181
  {
182
182
  "path": "core/hellolynk-neutral-host-installation-store.mjs",
183
- "sha256": "b5f6288732af6f9b9c0c750f6002eae657dde9513fc97674f6e59200f3cc9dc6"
183
+ "sha256": "dfb6bba4c627082cd6fca8cb38dc786f905e38f3e83ccf91dbfe57e55639e8a2"
184
184
  },
185
185
  {
186
186
  "path": "core/hellolynk-neutral-host-launcher.mjs",
@@ -188,7 +188,7 @@
188
188
  },
189
189
  {
190
190
  "path": "core/hellolynk-neutral-host-process.mjs",
191
- "sha256": "48f9bd6451adafdf3b43591a3b1469e36174c30fe04214cd2a84fb0aeebea023"
191
+ "sha256": "e744bacf65f7b86cbcf7d654189c0e07e09401083e2b6f9053b17c1c3bacd1b4"
192
192
  },
193
193
  {
194
194
  "path": "core/hellolynk-neutral-host-store-adapters.mjs",
@@ -312,7 +312,7 @@
312
312
  },
313
313
  {
314
314
  "path": "bootstrap/core/hellolynk-client-profile.mjs",
315
- "sha256": "4b45a78b4d6c4794a1f494bd95777579c32c56a68321a1d3b484048942ae4742"
315
+ "sha256": "8be37831c96610e3d0a64848ada86488970cc6079e49764b47d8a8b2ba852269"
316
316
  },
317
317
  {
318
318
  "path": "bootstrap/core/hellolynk-host-auth.mjs",
@@ -328,7 +328,7 @@
328
328
  },
329
329
  {
330
330
  "path": "bootstrap/core/hellolynk-neutral-host-installation-store.mjs",
331
- "sha256": "b5f6288732af6f9b9c0c750f6002eae657dde9513fc97674f6e59200f3cc9dc6"
331
+ "sha256": "dfb6bba4c627082cd6fca8cb38dc786f905e38f3e83ccf91dbfe57e55639e8a2"
332
332
  },
333
333
  {
334
334
  "path": "bootstrap/core/hellolynk-platform-credential-store.mjs",
@@ -344,7 +344,7 @@
344
344
  },
345
345
  {
346
346
  "path": "release/server/codex-neutral-production.mjs",
347
- "sha256": "34b865d80d7eb3c8e6986635b158c8583ddddb40bb528c4a75f0a498bdb3e1fb"
347
+ "sha256": "7b3249f1b35c331cf2b00ac2873a54d12fc6f61cf341bb82cec98e49ce221b92"
348
348
  },
349
349
  {
350
350
  "path": "release/server/neutral-host-adapter.mjs",
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "contract": "hellolynk-neutral-host-release-pin-v1",
3
- "manifestSha256": "98a0a761956020462c08bc6c02000b06fd37e0c2a5f44266ca5d4657c3ea28ce"
3
+ "manifestSha256": "3f8ad933949136ed1ebf5172c9e49ead0b0e2829650bc960eef1bdddd27c6a8f"
4
4
  }
@@ -7,6 +7,7 @@ import { discoverNeutralHost } from "./neutral-host-adapter.mjs";
7
7
  import { launchPinnedNeutralHostBootstrap, verifyPinnedNeutralHostRuntime } from "../runtime/core/hellolynk-neutral-host-launcher.mjs";
8
8
  import { resolveClientAccountRoot } from "../runtime/core/hellolynk-client-account.mjs";
9
9
  import { startInstalledClientUpdate } from "../runtime/core/hellolynk-client-update-entry.mjs";
10
+ import { recordCodexReleaseProcess } from "../runtime/core/hellolynk-release-retention.mjs";
10
11
 
11
12
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
12
13
  const PLUGIN_ROOT = path.dirname(MODULE_DIR);
@@ -23,9 +24,9 @@ const TRUSTED_BOOTSTRAP_FILES = Object.freeze(new Map([
23
24
  ["runtime/neutral-host-bootstrap.mjs", "53513deed7f0155a6603a1489d1609fae37776442358181ff7650d0cd9351f0a"],
24
25
  ["runtime/bootstrap/core/hellolynk-runtime-safety.cjs", "43d576d9f774409f4a735793c1503a6f397e787b10c59a4ac9cc9257299f5efe"],
25
26
  ["runtime/bootstrap/core/hellolynk-host-protected-store.mjs", "89b6e6c4ce9c6ba2984f8c301de4251da6004bdd402cc44d578a4505c21b75b1"],
26
- ["runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs", "b5f6288732af6f9b9c0c750f6002eae657dde9513fc97674f6e59200f3cc9dc6"],
27
+ ["runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs", "dfb6bba4c627082cd6fca8cb38dc786f905e38f3e83ccf91dbfe57e55639e8a2"],
27
28
  ["runtime/bootstrap/core/hellolynk-host-auth.mjs", "db1ec56f4e60e6450fba50188eecf89748f39f5d7a75c2456c30a6053233555d"],
28
- ["runtime/bootstrap/core/hellolynk-client-profile.mjs", "4b45a78b4d6c4794a1f494bd95777579c32c56a68321a1d3b484048942ae4742"],
29
+ ["runtime/bootstrap/core/hellolynk-client-profile.mjs", "8be37831c96610e3d0a64848ada86488970cc6079e49764b47d8a8b2ba852269"],
29
30
  ["runtime/bootstrap/core/hellolynk-platform-credential-store.mjs", "f27f5f377cc4d6d2b5664212cf8fcc4c98d910064c1677ff9c1f776df1a59dba"],
30
31
  ["runtime/bootstrap/core/hellolynk-client-core.mjs", "94bbad1d0ccee969bbffed176449e427188f1b892512a64167ab31f63d258e2b"],
31
32
  ["runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1", "24357e8ce54090b4ec68e457f02c9e834a80f3273ef3c03a8c0fa7258797fb49"],
@@ -216,6 +217,7 @@ async function prepareSelectedRelease(options, verifiedInitial = null) {
216
217
  assertReleaseBinding(binding, inspection);
217
218
  const release = await loader.activateNeutralHostRelease(inspection);
218
219
  if (!release || typeof release.run !== "function") throw codeError("neutral_codex_runner_integrity_invalid");
220
+ await recordCodexReleaseProcess(path.resolve(releaseRoot, "../.."));
219
221
  return Object.freeze({ release, inspection, binding, initialBindingFingerprint,
220
222
  releaseRoot, storeRoot, installationStore, protectedStore,
221
223
  runnerBinding: (root) => currentRunnerBinding(storeRoot, root, binding, initialBindingFingerprint) });