principles-disciple 1.7.1 → 1.7.3

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.
@@ -10,6 +10,16 @@
10
10
  */
11
11
  import * as fs from 'fs';
12
12
  import * as path from 'path';
13
+ export class LockAcquisitionError extends Error {
14
+ filePath;
15
+ lockPath;
16
+ constructor(message, filePath, lockPath) {
17
+ super(message);
18
+ this.name = 'LockAcquisitionError';
19
+ this.filePath = filePath;
20
+ this.lockPath = lockPath;
21
+ }
22
+ }
13
23
  /** 默认锁选项 */
14
24
  const DEFAULT_OPTIONS = {
15
25
  maxRetries: 50,
@@ -134,15 +144,39 @@ function calculateBackoff(attempt, baseMs, maxMs) {
134
144
  const jitter = exponentialDelay * 0.2 * Math.random();
135
145
  return Math.floor(exponentialDelay + jitter);
136
146
  }
137
- /**
138
- * 同步 sleep
139
- */
147
+ function sleep(ms) {
148
+ return new Promise((resolve) => setTimeout(resolve, ms));
149
+ }
140
150
  function sleepSync(ms) {
141
151
  const end = Date.now() + ms;
142
152
  while (Date.now() < end) {
143
- // busy wait(同步函数无法使用 setTimeout)
153
+ // busy wait for synchronous retry
144
154
  }
145
155
  }
156
+ function buildLockError(filePath, lockPath) {
157
+ const holderPid = readLockPid(lockPath);
158
+ const holderStatus = holderPid !== null
159
+ ? (isProcessAlive(holderPid) ? `alive (PID ${holderPid})` : `dead (PID ${holderPid})`)
160
+ : 'unknown';
161
+ return new LockAcquisitionError(`Failed to acquire lock for ${filePath}. Lock holder: ${holderStatus}.`, filePath, lockPath);
162
+ }
163
+ function tryAcquireWithStaleCleanup(filePath, opts, pid) {
164
+ const lockPath = filePath + opts.lockSuffix;
165
+ if (tryAcquireLock(lockPath, pid)) {
166
+ const actualPid = readLockPid(lockPath);
167
+ if (actualPid === pid) {
168
+ return { lockPath, pid, acquiredAt: Date.now() };
169
+ }
170
+ }
171
+ cleanupStaleLock(lockPath, opts.lockStaleMs);
172
+ if (tryAcquireLock(lockPath, pid)) {
173
+ const actualPid = readLockPid(lockPath);
174
+ if (actualPid === pid) {
175
+ return { lockPath, pid, acquiredAt: Date.now() };
176
+ }
177
+ }
178
+ return null;
179
+ }
146
180
  /**
147
181
  * 获取文件锁
148
182
  *
@@ -153,37 +187,33 @@ function sleepSync(ms) {
153
187
  */
154
188
  export function acquireLock(filePath, options = {}) {
155
189
  const opts = { ...DEFAULT_OPTIONS, ...options };
156
- const lockPath = filePath + opts.lockSuffix;
157
190
  const pid = process.pid;
158
191
  for (let attempt = 0; attempt < opts.maxRetries; attempt++) {
159
- // 1. 尝试原子获取锁
160
- if (tryAcquireLock(lockPath, pid)) {
161
- // 2. 二次验证:确保我们真的持有锁
162
- const actualPid = readLockPid(lockPath);
163
- if (actualPid === pid) {
164
- return {
165
- lockPath,
166
- pid,
167
- acquiredAt: Date.now(),
168
- };
169
- }
170
- // PID 不匹配,说明被其他进程抢占,继续重试
192
+ const ctx = tryAcquireWithStaleCleanup(filePath, opts, pid);
193
+ if (ctx) {
194
+ return ctx;
171
195
  }
172
- // 3. 获取失败,检查是否需要清理过期锁
173
- cleanupStaleLock(lockPath, opts.lockStaleMs);
174
- // 4. 计算退避时间并等待
175
196
  if (attempt < opts.maxRetries - 1) {
176
197
  const delay = calculateBackoff(attempt, opts.baseRetryDelayMs, opts.maxRetryDelayMs);
177
198
  sleepSync(delay);
178
199
  }
179
200
  }
180
- // 5. 所有重试都失败,抛出异常(不静默丢弃!)
181
- const holderPid = readLockPid(lockPath);
182
- const holderStatus = holderPid !== null
183
- ? (isProcessAlive(holderPid) ? `alive (PID ${holderPid})` : `dead (PID ${holderPid})`)
184
- : 'unknown';
185
- throw new Error(`Failed to acquire lock for ${filePath} after ${opts.maxRetries} attempts. ` +
186
- `Lock holder: ${holderStatus}. Consider increasing maxRetries or checking for zombie processes.`);
201
+ throw buildLockError(filePath, filePath + opts.lockSuffix);
202
+ }
203
+ export async function acquireLockAsync(filePath, options = {}) {
204
+ const opts = { ...DEFAULT_OPTIONS, ...options };
205
+ const pid = process.pid;
206
+ for (let attempt = 0; attempt < opts.maxRetries; attempt++) {
207
+ const ctx = tryAcquireWithStaleCleanup(filePath, opts, pid);
208
+ if (ctx) {
209
+ return ctx;
210
+ }
211
+ if (attempt < opts.maxRetries - 1) {
212
+ const delay = calculateBackoff(attempt, opts.baseRetryDelayMs, opts.maxRetryDelayMs);
213
+ await sleep(delay);
214
+ }
215
+ }
216
+ throw buildLockError(filePath, filePath + opts.lockSuffix);
187
217
  }
188
218
  /**
189
219
  * 释放文件锁
@@ -210,6 +240,15 @@ export function withLock(filePath, fn, options = {}) {
210
240
  releaseLock(ctx);
211
241
  }
212
242
  }
243
+ export async function withLockAsync(filePath, fn, options = {}) {
244
+ const ctx = await acquireLockAsync(filePath, options);
245
+ try {
246
+ return await fn();
247
+ }
248
+ finally {
249
+ releaseLock(ctx);
250
+ }
251
+ }
213
252
  /**
214
253
  * 异步版本的文件锁(使用 Promise 链)
215
254
  *
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.7.1",
5
+ "version": "1.7.3",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
@@ -54,7 +54,7 @@
54
54
  "forced"
55
55
  ],
56
56
  "default": "auto",
57
- "description": "auto: 遇到困难自动触发; forced: 每次回答前都强制反思(极耗时间,不推荐)"
57
+ "description": "auto: 遇到困难自动触发; forced: 每次回答前都强制反思 (极耗时间,不推荐)"
58
58
  }
59
59
  }
60
60
  }
@@ -69,7 +69,7 @@
69
69
  "placeholder": "medium"
70
70
  },
71
71
  "riskPaths": {
72
- "label": "☠️ 绝对高危目录 (空表示不设限)"
72
+ "label": "⚠️ 绝对高危目录 (空表示不设限)"
73
73
  },
74
74
  "deep_reflection": {
75
75
  "label": "💡 AI 深度反思功能"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",