@foxden-app/foxclaw 0.4.8 → 0.4.9
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 +8 -0
- package/dist/auth/mirror.d.ts +1 -0
- package/dist/auth/mirror.js +54 -44
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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.4.9 - 2026-06-02
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- 修复 ChatGPT auth 到期自动刷新后,主动同步和后台镜像扫描竞态导致同一候选重复发送镜像广播的问题。
|
|
9
|
+
|
|
10
|
+
### English
|
|
11
|
+
- Fixed duplicate auth mirror broadcasts when an automatic ChatGPT auth refresh was observed by both the direct sync path and the background mirror scan.
|
|
12
|
+
|
|
5
13
|
## 0.4.8 - 2026-06-02
|
|
6
14
|
|
|
7
15
|
### 中文
|
package/dist/auth/mirror.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export declare class AuthCandidateMirror {
|
|
|
39
39
|
private timer;
|
|
40
40
|
private readonly lastSyncedRefresh;
|
|
41
41
|
private readonly lastValidationFailures;
|
|
42
|
+
private readonly activeCandidateSyncs;
|
|
42
43
|
private activeOperations;
|
|
43
44
|
private lastStatus;
|
|
44
45
|
constructor(canonicalDir: string, runtimes: AuthMirrorRuntime[], logger: Logger, statusPath?: string | null);
|
package/dist/auth/mirror.js
CHANGED
|
@@ -9,6 +9,7 @@ export class AuthCandidateMirror {
|
|
|
9
9
|
timer = null;
|
|
10
10
|
lastSyncedRefresh = new Map();
|
|
11
11
|
lastValidationFailures = new Map();
|
|
12
|
+
activeCandidateSyncs = new Set();
|
|
12
13
|
activeOperations = 0;
|
|
13
14
|
lastStatus = null;
|
|
14
15
|
constructor(canonicalDir, runtimes, logger, statusPath = null) {
|
|
@@ -116,55 +117,64 @@ export class AuthCandidateMirror {
|
|
|
116
117
|
});
|
|
117
118
|
}
|
|
118
119
|
async propagateValidatedCandidate(runtime, name) {
|
|
119
|
-
|
|
120
|
-
const record = await readChatGptAuthRecord(sourcePath);
|
|
121
|
-
if (!record)
|
|
122
|
-
return false;
|
|
123
|
-
const canonicalPath = path.join(this.canonicalDir, name);
|
|
124
|
-
const canonical = await readChatGptAuthRecord(canonicalPath);
|
|
125
|
-
if (canonical && canonical.accountId !== record.accountId) {
|
|
126
|
-
this.logger.warn('auth.mirror.account_conflict', { runtimeId: runtime.id, name });
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
const previousRefresh = Math.max(canonical?.lastRefreshMs ?? 0, this.lastSyncedRefresh.get(name) ?? 0);
|
|
130
|
-
if (record.lastRefreshMs <= previousRefresh) {
|
|
120
|
+
if (this.activeCandidateSyncs.has(name)) {
|
|
131
121
|
return false;
|
|
132
122
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
});
|
|
123
|
+
this.activeCandidateSyncs.add(name);
|
|
124
|
+
try {
|
|
125
|
+
const sourcePath = path.join(runtime.authDir, name);
|
|
126
|
+
const record = await readChatGptAuthRecord(sourcePath);
|
|
127
|
+
if (!record)
|
|
128
|
+
return false;
|
|
129
|
+
const canonicalPath = path.join(this.canonicalDir, name);
|
|
130
|
+
const canonical = await readChatGptAuthRecord(canonicalPath);
|
|
131
|
+
if (canonical && canonical.accountId !== record.accountId) {
|
|
132
|
+
this.logger.warn('auth.mirror.account_conflict', { runtimeId: runtime.id, name });
|
|
133
|
+
return false;
|
|
145
134
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
135
|
+
const previousRefresh = Math.max(canonical?.lastRefreshMs ?? 0, this.lastSyncedRefresh.get(name) ?? 0);
|
|
136
|
+
if (record.lastRefreshMs <= previousRefresh) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const validation = await this.validateRuntimeCandidate(runtime, name, record);
|
|
140
|
+
if (!validation.ok) {
|
|
141
|
+
const reason = validation.reason ?? 'unknown';
|
|
142
|
+
const failureKey = `${runtime.id}:${name}`;
|
|
143
|
+
const failureValue = `${record.lastRefreshMs}:${reason}`;
|
|
144
|
+
if (this.lastValidationFailures.get(failureKey) !== failureValue) {
|
|
145
|
+
this.lastValidationFailures.set(failureKey, failureValue);
|
|
146
|
+
this.logger.warn('auth.mirror.validation_failed', {
|
|
147
|
+
runtimeId: runtime.id,
|
|
148
|
+
name,
|
|
149
|
+
reason,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
153
|
}
|
|
154
|
+
this.lastValidationFailures.delete(`${runtime.id}:${name}`);
|
|
155
|
+
await atomicWrite(canonicalPath, record.raw);
|
|
156
|
+
for (const target of this.runtimes) {
|
|
157
|
+
if (target.id !== runtime.id) {
|
|
158
|
+
await atomicWrite(path.join(target.authDir, name), record.raw);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.lastSyncedRefresh.set(name, record.lastRefreshMs);
|
|
162
|
+
const sourceLabel = runtime.label ?? runtime.id;
|
|
163
|
+
this.lastStatus = {
|
|
164
|
+
candidateName: name,
|
|
165
|
+
sourceRuntimeId: runtime.id,
|
|
166
|
+
sourceLabel,
|
|
167
|
+
syncedAt: new Date().toISOString(),
|
|
168
|
+
};
|
|
169
|
+
await writeMirrorStatus(this.statusPath, this.lastStatus);
|
|
170
|
+
this.logger.info('auth.mirror.synced', { sourceRuntimeId: runtime.id, name });
|
|
171
|
+
const message = `${name} has been refreshed by ${sourceLabel} and synchronized to the other Codex homes.`;
|
|
172
|
+
await Promise.allSettled(this.runtimes.map((target) => target.notify?.(message)));
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
this.activeCandidateSyncs.delete(name);
|
|
154
177
|
}
|
|
155
|
-
this.lastSyncedRefresh.set(name, record.lastRefreshMs);
|
|
156
|
-
const sourceLabel = runtime.label ?? runtime.id;
|
|
157
|
-
this.lastStatus = {
|
|
158
|
-
candidateName: name,
|
|
159
|
-
sourceRuntimeId: runtime.id,
|
|
160
|
-
sourceLabel,
|
|
161
|
-
syncedAt: new Date().toISOString(),
|
|
162
|
-
};
|
|
163
|
-
await writeMirrorStatus(this.statusPath, this.lastStatus);
|
|
164
|
-
this.logger.info('auth.mirror.synced', { sourceRuntimeId: runtime.id, name });
|
|
165
|
-
const message = `${name} has been refreshed by ${sourceLabel} and synchronized to the other Codex homes.`;
|
|
166
|
-
await Promise.allSettled(this.runtimes.map((target) => target.notify?.(message)));
|
|
167
|
-
return true;
|
|
168
178
|
}
|
|
169
179
|
async validateRuntimeCandidate(runtime, name, record) {
|
|
170
180
|
if (!runtime.validate) {
|