@foxden-app/foxclaw 0.5.45 → 0.5.47
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 +24 -0
- package/dist/auth/cross_node_sync.d.ts +2 -0
- package/dist/auth/cross_node_sync.js +48 -5
- package/dist/auth/mirror.d.ts +1 -0
- package/dist/auth/mirror.js +10 -0
- package/dist/main.js +15 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
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.5.47 - 2026-06-20
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- 修复远端 auth 候选已成功导入后,`/auth` 面板仍保留旧的 `needs_repair`/禁用状态,导致候选显示问号且无法切换的问题。
|
|
9
|
+
- 远端导入成功现在会恢复该候选的全局状态和各 Telegram runtime 覆盖状态为 active/enabled,确保“确认可用后文件和 UI 状态一致”。
|
|
10
|
+
- 增加 mirror 远端导入 hook 回归测试,避免后续同步链路再次只更新 auth 文件、不恢复健康标记。
|
|
11
|
+
|
|
12
|
+
### English
|
|
13
|
+
- Fixed `/auth` keeping stale `needs_repair`/disabled state after a remote auth candidate was successfully imported, which left the candidate shown with a question mark and blocked switching.
|
|
14
|
+
- Successful remote imports now restore the candidate's global state and each Telegram runtime override to active/enabled, keeping the UI state aligned with validated auth files.
|
|
15
|
+
- Added a regression test for the mirror remote-import hook so future sync changes do not update auth files without clearing health markers.
|
|
16
|
+
|
|
17
|
+
## 0.5.46 - 2026-06-20
|
|
18
|
+
|
|
19
|
+
### 中文
|
|
20
|
+
- 修复跨节点 pull 回来的较新 auth 候选在本机 runtime 忙碌时被立即判定失败的问题;现在会先接受并进入远端导入队列,等 runtime 空闲后再验证和导入,避免 RT/WSL 节点已有更新却同步不到本机。
|
|
21
|
+
- 调整主动刷新租约策略:如果 peer 明确拒绝仍会停止,但 peer 慢回包或暂时无响应时不再让刷新一直失败;FoxClaw 会记录 `granted_partial` 事件并优先保持授权可用性和连续性。
|
|
22
|
+
- 增加回归测试覆盖忙碌期间的 pull recovery 排队导入,以及 peer 超时但无明确拒绝时的刷新租约继续执行。
|
|
23
|
+
|
|
24
|
+
### English
|
|
25
|
+
- Fixed cross-node pull recovery dropping newer auth candidates when the local runtime is busy; pulled bundles are now accepted into the remote import queue and validated/imported once the runtime becomes idle, so updates from RT/WSL peers are not lost.
|
|
26
|
+
- Relaxed proactive refresh leases for availability: explicit peer denials still stop the refresh, but slow or temporarily silent peers no longer make the refresh fail indefinitely. FoxClaw records `granted_partial` events and prioritizes auth continuity.
|
|
27
|
+
- Added regression tests for queued pull recovery while busy and refresh lease continuation when peers time out without an explicit denial.
|
|
28
|
+
|
|
5
29
|
## 0.5.45 - 2026-06-18
|
|
6
30
|
|
|
7
31
|
### 中文
|
|
@@ -317,10 +317,12 @@ export declare class CrossNodeAuthSync {
|
|
|
317
317
|
private handleServiceUpdateRequest;
|
|
318
318
|
private handlePullRequest;
|
|
319
319
|
private handlePullResponse;
|
|
320
|
+
private finishPendingPullAsAccepted;
|
|
320
321
|
private markPullPeerUnavailable;
|
|
321
322
|
private handleDigest;
|
|
322
323
|
private handleLeaseRequest;
|
|
323
324
|
private handleLeaseReply;
|
|
325
|
+
private shouldQueueRemoteImport;
|
|
324
326
|
private enqueueImport;
|
|
325
327
|
private processPendingImports;
|
|
326
328
|
private enqueueDelete;
|
|
@@ -372,7 +372,22 @@ export class CrossNodeAuthSync {
|
|
|
372
372
|
const expiresAt = Date.now() + LEASE_TTL_MS;
|
|
373
373
|
const result = await new Promise((resolve) => {
|
|
374
374
|
const timer = setTimeout(() => {
|
|
375
|
+
const pending = this.pendingLeases.get(leaseId);
|
|
375
376
|
this.pendingLeases.delete(leaseId);
|
|
377
|
+
if (pending && pending.denies.length === 0) {
|
|
378
|
+
const detail = `partial grants=${pending.grants.size}/${this.peers.length}; reason=${reason}`;
|
|
379
|
+
this.recordEvent({
|
|
380
|
+
direction: 'local',
|
|
381
|
+
kind: 'lease.request',
|
|
382
|
+
stage: 'partial_granted',
|
|
383
|
+
peer: null,
|
|
384
|
+
requestId: leaseId,
|
|
385
|
+
candidateName: null,
|
|
386
|
+
detail,
|
|
387
|
+
});
|
|
388
|
+
resolve({ ok: true, leaseId, reason: detail });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
376
391
|
this.recordEvent({
|
|
377
392
|
direction: 'local',
|
|
378
393
|
kind: 'lease.request',
|
|
@@ -405,7 +420,7 @@ export class CrossNodeAuthSync {
|
|
|
405
420
|
});
|
|
406
421
|
if (result.ok) {
|
|
407
422
|
this.activeLocalLease = { leaseId, expiresAt };
|
|
408
|
-
this.recordEvent({ direction: 'local', kind: 'lease.request', stage: 'granted', peer: null, requestId: leaseId, candidateName: null, detail: reason });
|
|
423
|
+
this.recordEvent({ direction: 'local', kind: 'lease.request', stage: result.reason?.startsWith('partial grants=') ? 'granted_partial' : 'granted', peer: null, requestId: leaseId, candidateName: null, detail: result.reason ?? reason });
|
|
409
424
|
}
|
|
410
425
|
else {
|
|
411
426
|
this.recordEvent({ direction: 'local', kind: 'lease.request', stage: 'denied', peer: null, requestId: leaseId, candidateName: null, detail: result.reason ?? reason });
|
|
@@ -696,6 +711,11 @@ export class CrossNodeAuthSync {
|
|
|
696
711
|
peer,
|
|
697
712
|
sourceNodeId: senderNodeId,
|
|
698
713
|
});
|
|
714
|
+
if (this.shouldQueueRemoteImport()) {
|
|
715
|
+
this.finishPendingPullAsAccepted(pending, peer, 'queued until local runtime is idle');
|
|
716
|
+
this.enqueueImport(message.bundle, senderNodeId, sourceLabel, peer, 'pull');
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
699
719
|
const outcome = await this.validateAndImport(message.bundle, senderNodeId, sourceLabel, peer, 'pull');
|
|
700
720
|
if (!outcome.imported) {
|
|
701
721
|
this.markPullPeerUnavailable(message.requestId, matchedPeer, outcome.reason ?? 'peer candidate was not imported');
|
|
@@ -708,6 +728,21 @@ export class CrossNodeAuthSync {
|
|
|
708
728
|
pending.resolve(true);
|
|
709
729
|
this.recordEvent({ direction: 'local', kind: 'pull.response', stage: 'imported', peer, requestId: message.requestId, candidateName: pending.candidateName, detail: null });
|
|
710
730
|
}
|
|
731
|
+
finishPendingPullAsAccepted(pending, peer, detail) {
|
|
732
|
+
pending.finished = true;
|
|
733
|
+
clearTimeout(pending.timer);
|
|
734
|
+
this.pendingPulls.delete(pending.requestId);
|
|
735
|
+
this.recordEvent({
|
|
736
|
+
direction: 'local',
|
|
737
|
+
kind: 'pull.response',
|
|
738
|
+
stage: 'queued',
|
|
739
|
+
peer,
|
|
740
|
+
requestId: pending.requestId,
|
|
741
|
+
candidateName: pending.candidateName,
|
|
742
|
+
detail,
|
|
743
|
+
});
|
|
744
|
+
pending.resolve(true);
|
|
745
|
+
}
|
|
711
746
|
markPullPeerUnavailable(requestId, peer, reason) {
|
|
712
747
|
const pending = this.pendingPulls.get(requestId);
|
|
713
748
|
if (!pending || pending.finished)
|
|
@@ -798,18 +833,26 @@ export class CrossNodeAuthSync {
|
|
|
798
833
|
pending.resolve({ ok: true, leaseId: message.leaseId });
|
|
799
834
|
}
|
|
800
835
|
}
|
|
801
|
-
|
|
802
|
-
|
|
836
|
+
shouldQueueRemoteImport() {
|
|
837
|
+
return this.importProcessorActive
|
|
838
|
+
|| this.deleteProcessorActive
|
|
839
|
+
|| !this.callbacks.isIdle()
|
|
840
|
+
|| this.pendingImports.length > 0
|
|
841
|
+
|| this.pendingDeletes.length > 0;
|
|
842
|
+
}
|
|
843
|
+
enqueueImport(bundle, sourceNodeId, sourceLabel, fromPeer, mode = 'push') {
|
|
844
|
+
const queued = this.shouldQueueRemoteImport();
|
|
803
845
|
this.pendingImports.push({
|
|
804
846
|
bundle,
|
|
805
847
|
sourceNodeId,
|
|
806
848
|
sourceLabel,
|
|
807
849
|
receivedAt: Date.now(),
|
|
808
850
|
fromPeer,
|
|
851
|
+
mode,
|
|
809
852
|
});
|
|
810
853
|
this.recordEvent({
|
|
811
854
|
direction: 'local',
|
|
812
|
-
kind: 'push.bundle',
|
|
855
|
+
kind: mode === 'pull' ? 'pull.response' : 'push.bundle',
|
|
813
856
|
stage: queued ? 'queued' : 'processing',
|
|
814
857
|
peer: fromPeer,
|
|
815
858
|
requestId: bundle.requestId ?? null,
|
|
@@ -842,7 +885,7 @@ export class CrossNodeAuthSync {
|
|
|
842
885
|
if (!this.callbacks.isIdle())
|
|
843
886
|
return;
|
|
844
887
|
const pending = this.pendingImports.shift();
|
|
845
|
-
await this.validateAndImport(pending.bundle, pending.sourceNodeId, pending.sourceLabel, pending.fromPeer,
|
|
888
|
+
await this.validateAndImport(pending.bundle, pending.sourceNodeId, pending.sourceLabel, pending.fromPeer, pending.mode);
|
|
846
889
|
}
|
|
847
890
|
}
|
|
848
891
|
finally {
|
package/dist/auth/mirror.d.ts
CHANGED
|
@@ -61,6 +61,7 @@ export interface AuthMirrorSyncAllResult {
|
|
|
61
61
|
}
|
|
62
62
|
export interface AuthMirrorHooks {
|
|
63
63
|
onSynced?: (event: AuthMirrorSyncedEvent) => Promise<void> | void;
|
|
64
|
+
onRemoteImported?: (event: AuthMirrorSyncedEvent) => Promise<void> | void;
|
|
64
65
|
}
|
|
65
66
|
export type AuthMirrorNotification = {
|
|
66
67
|
kind: 'local_synced';
|
package/dist/auth/mirror.js
CHANGED
|
@@ -304,6 +304,16 @@ export class AuthCandidateMirror {
|
|
|
304
304
|
sourceLabel,
|
|
305
305
|
};
|
|
306
306
|
await Promise.allSettled(this.runtimes.map((target) => target.notify?.(notification)));
|
|
307
|
+
await this.hooks.onRemoteImported?.({
|
|
308
|
+
status: this.lastStatus,
|
|
309
|
+
record: {
|
|
310
|
+
raw,
|
|
311
|
+
...metadata,
|
|
312
|
+
candidateName,
|
|
313
|
+
sourceRuntimeId: `remote:${source.nodeId}`,
|
|
314
|
+
sourceLabel,
|
|
315
|
+
},
|
|
316
|
+
});
|
|
307
317
|
return {
|
|
308
318
|
ok: true,
|
|
309
319
|
imported: true,
|
package/dist/main.js
CHANGED
|
@@ -410,6 +410,7 @@ async function runServeCli() {
|
|
|
410
410
|
seeds.push({ id, home, authDir, sharedDefaultRuntime, config: runtimeConfig, bot, app });
|
|
411
411
|
}
|
|
412
412
|
let authSync = null;
|
|
413
|
+
const authRuntimeIds = seeds.map((runtime) => runtime.id);
|
|
413
414
|
const mirror = new AuthCandidateMirror(canonicalAuthDir, seeds.map((runtime) => ({
|
|
414
415
|
id: runtime.id,
|
|
415
416
|
label: runtime.bot.username ? `@${runtime.bot.username}` : runtime.id,
|
|
@@ -423,6 +424,9 @@ async function runServeCli() {
|
|
|
423
424
|
onSynced: async (event) => {
|
|
424
425
|
await authSync?.publishCandidate(event.record.candidateName);
|
|
425
426
|
},
|
|
427
|
+
onRemoteImported: (event) => {
|
|
428
|
+
restoreImportedAuthCandidateState(store, event.record.candidateName, authRuntimeIds);
|
|
429
|
+
},
|
|
426
430
|
});
|
|
427
431
|
await mirror.initialize();
|
|
428
432
|
activeAuthMirror = mirror;
|
|
@@ -757,6 +761,9 @@ async function runServeCli() {
|
|
|
757
761
|
onSynced: async (event) => {
|
|
758
762
|
await singleAuthSync?.publishCandidate(event.record.candidateName);
|
|
759
763
|
},
|
|
764
|
+
onRemoteImported: (event) => {
|
|
765
|
+
restoreImportedAuthCandidateState(store, event.record.candidateName, ['default']);
|
|
766
|
+
},
|
|
760
767
|
});
|
|
761
768
|
await singleMirror.initialize();
|
|
762
769
|
activeAuthMirror = singleMirror;
|
|
@@ -863,6 +870,14 @@ async function runServeCli() {
|
|
|
863
870
|
throw error;
|
|
864
871
|
}
|
|
865
872
|
}
|
|
873
|
+
function restoreImportedAuthCandidateState(store, candidateName, runtimeIds) {
|
|
874
|
+
store.setCodexAuthCandidateState(candidateName, 'active');
|
|
875
|
+
store.setCodexAuthCandidateDisabled(candidateName, false);
|
|
876
|
+
for (const runtimeId of runtimeIds) {
|
|
877
|
+
store.setCodexAuthCandidateState(candidateName, 'active', runtimeId);
|
|
878
|
+
store.setCodexAuthCandidateDisabled(candidateName, false, runtimeId);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
866
881
|
function createAuthMirrorNotifier(store, botId, bot, aggregator, options = {}) {
|
|
867
882
|
return async (event) => {
|
|
868
883
|
if (options.suppressBackgroundNotifications?.())
|