@foxden-app/foxclaw 0.6.9 → 0.7.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
4
4
 
5
+ ## 0.7.0 - 2026-08-26
6
+
7
+ ### 中文
8
+ - 版本对齐发布:代码内容与 0.6.10 相同,无功能性变更。
9
+
10
+ ### English
11
+ - Version alignment release: code content is identical to 0.6.10, no functional changes.
12
+
13
+ ## 0.6.10 - 2026-08-23
14
+
15
+ ### 中文
16
+ - 修复 Linux 重启后旧 `bridge.lock` 中的 PID 被无关进程复用时,FoxClaw 误判已有实例并陷入 systemd 重启失败的问题。新锁记录系统启动 ID 和进程启动标识,同时兼容清理上个系统启动遗留的纯 PID 锁。
17
+ - 修复 `doctor` 在显式配置的 `CODEX_CLI_BIN` 或 `OPENCODE_CLI_BIN` 不存在时仍因 PATH 中有同名命令而误报 `[OK]` 的问题;显式路径现在必须真实存在且可执行。
18
+
19
+ ### English
20
+ - Fixed FoxClaw mistaking an unrelated process for the existing bridge when Linux reuses a PID left in an old `bridge.lock` after reboot. New locks record the boot ID and process start identity while still cleaning legacy PID-only locks from a previous boot.
21
+ - Fixed `doctor` reporting `[OK]` when an explicitly configured `CODEX_CLI_BIN` or `OPENCODE_CLI_BIN` is missing but a same-named command exists on PATH. Explicit paths must now exist and be executable.
22
+
5
23
  ## 0.6.9 - 2026-08-15
6
24
 
7
25
  ### 中文
package/dist/lock.js CHANGED
@@ -15,7 +15,10 @@ export function acquireProcessLock(lockPath) {
15
15
  function acquireProcessLockInternal(lockPath, allowStaleRetry) {
16
16
  try {
17
17
  const fd = fs.openSync(lockPath, 'wx');
18
- fs.writeFileSync(fd, `${process.pid}\n`, 'utf8');
18
+ fs.writeFileSync(fd, `${JSON.stringify({
19
+ pid: process.pid,
20
+ processIdentity: readLinuxProcessIdentity(process.pid),
21
+ })}\n`, 'utf8');
19
22
  let released = false;
20
23
  return {
21
24
  release() {
@@ -42,26 +45,73 @@ function acquireProcessLockInternal(lockPath, allowStaleRetry) {
42
45
  if (!isAlreadyExistsError(error)) {
43
46
  throw error;
44
47
  }
45
- const pid = readLockPid(lockPath);
46
- if (allowStaleRetry && pid !== null && !isProcessAlive(pid)) {
48
+ const record = readLockRecord(lockPath);
49
+ if (allowStaleRetry && record.pid !== null && !isLockOwnerAlive(lockPath, record)) {
47
50
  fs.rmSync(lockPath, { force: true });
48
51
  return acquireProcessLockInternal(lockPath, false);
49
52
  }
50
- throw new LockHeldError(lockPath, pid);
53
+ throw new LockHeldError(lockPath, record.pid);
51
54
  }
52
55
  }
53
- function readLockPid(lockPath) {
56
+ function readLockRecord(lockPath) {
54
57
  try {
55
58
  const value = fs.readFileSync(lockPath, 'utf8').trim();
56
59
  if (!value) {
57
- return null;
60
+ return { pid: null, processIdentity: null };
61
+ }
62
+ if (value.startsWith('{')) {
63
+ const parsed = JSON.parse(value);
64
+ return {
65
+ pid: typeof parsed.pid === 'number' && Number.isInteger(parsed.pid) ? parsed.pid : null,
66
+ processIdentity: typeof parsed.processIdentity === 'string' ? parsed.processIdentity : null,
67
+ };
58
68
  }
59
69
  const pid = Number.parseInt(value, 10);
60
- return Number.isFinite(pid) ? pid : null;
70
+ return { pid: Number.isFinite(pid) ? pid : null, processIdentity: null };
61
71
  }
62
72
  catch {
73
+ return { pid: null, processIdentity: null };
74
+ }
75
+ }
76
+ function isLockOwnerAlive(lockPath, record) {
77
+ if (record.pid === null || !isProcessAlive(record.pid)) {
78
+ return false;
79
+ }
80
+ const currentIdentity = readLinuxProcessIdentity(record.pid);
81
+ if (record.processIdentity !== null && currentIdentity !== null) {
82
+ return record.processIdentity === currentIdentity;
83
+ }
84
+ return !wasLockCreatedBeforeCurrentBoot(lockPath);
85
+ }
86
+ function readLinuxProcessIdentity(pid) {
87
+ if (process.platform !== 'linux') {
63
88
  return null;
64
89
  }
90
+ try {
91
+ const bootId = fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
92
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
93
+ const commandEnd = stat.lastIndexOf(')');
94
+ const startTicks = commandEnd >= 0 ? stat.slice(commandEnd + 2).split(' ')[19] : undefined;
95
+ return bootId && startTicks ? `${bootId}:${startTicks}` : null;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ function wasLockCreatedBeforeCurrentBoot(lockPath) {
102
+ if (process.platform !== 'linux') {
103
+ return false;
104
+ }
105
+ try {
106
+ const bootTimeSeconds = fs.readFileSync('/proc/stat', 'utf8').match(/^btime (\d+)$/m)?.[1];
107
+ if (!bootTimeSeconds) {
108
+ return false;
109
+ }
110
+ return fs.statSync(lockPath).mtimeMs < Number(bootTimeSeconds) * 1000;
111
+ }
112
+ catch {
113
+ return false;
114
+ }
65
115
  }
66
116
  function isProcessAlive(pid) {
67
117
  if (!Number.isFinite(pid) || pid <= 0) {
package/dist/main.js CHANGED
@@ -1784,13 +1784,13 @@ function runDoctorChecks() {
1784
1784
  const configuredCodexBin = process.env.CODEX_CLI_BIN;
1785
1785
  const checks = [
1786
1786
  ['node >= 24', Number(process.versions.node.split('.')[0]) >= 24],
1787
- ['codex cli available', hasConfiguredCodexBin(configuredCodexBin) || hasCommand('codex')],
1787
+ ['codex cli available', hasConfiguredCommand(configuredCodexBin, 'codex')],
1788
1788
  ['telegram bot token(s) configured', Boolean(process.env.TG_BOT_TOKENS?.trim() || process.env.TG_BOT_TOKEN?.trim())],
1789
1789
  ['telegram allowed user configured', Boolean(process.env.TG_ALLOWED_USER_ID)],
1790
1790
  ];
1791
1791
  if (process.env.OPENCODE_BOT_TOKEN?.trim()) {
1792
1792
  const configuredOpencodeBin = process.env.OPENCODE_CLI_BIN;
1793
- checks.push(['opencode cli available', hasConfiguredCodexBin(configuredOpencodeBin) || hasCommand('opencode')]);
1793
+ checks.push(['opencode cli available', hasConfiguredCommand(configuredOpencodeBin, 'opencode')]);
1794
1794
  const codexTokens = [
1795
1795
  ...(process.env.TG_BOT_TOKENS ?? '').split(','),
1796
1796
  process.env.TG_BOT_TOKEN ?? '',
@@ -2337,9 +2337,9 @@ function resolveCommand(commandName) {
2337
2337
  return null;
2338
2338
  }
2339
2339
  }
2340
- function hasConfiguredCodexBin(binPath) {
2341
- if (!binPath || !binPath.trim())
2342
- return false;
2340
+ function hasConfiguredCommand(binPath, fallbackCommand) {
2341
+ if (!binPath?.trim())
2342
+ return hasCommand(fallbackCommand);
2343
2343
  try {
2344
2344
  fs.accessSync(binPath, fs.constants.X_OK);
2345
2345
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.6.9",
3
+ "version": "0.7.0",
4
4
  "description": "Foxden local execution claw for controlling Codex and OpenCode from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",