@foxden-app/foxclaw 0.5.45 → 0.5.46

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,18 @@
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.46 - 2026-06-20
6
+
7
+ ### 中文
8
+ - 修复跨节点 pull 回来的较新 auth 候选在本机 runtime 忙碌时被立即判定失败的问题;现在会先接受并进入远端导入队列,等 runtime 空闲后再验证和导入,避免 RT/WSL 节点已有更新却同步不到本机。
9
+ - 调整主动刷新租约策略:如果 peer 明确拒绝仍会停止,但 peer 慢回包或暂时无响应时不再让刷新一直失败;FoxClaw 会记录 `granted_partial` 事件并优先保持授权可用性和连续性。
10
+ - 增加回归测试覆盖忙碌期间的 pull recovery 排队导入,以及 peer 超时但无明确拒绝时的刷新租约继续执行。
11
+
12
+ ### English
13
+ - 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.
14
+ - 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.
15
+ - Added regression tests for queued pull recovery while busy and refresh lease continuation when peers time out without an explicit denial.
16
+
5
17
  ## 0.5.45 - 2026-06-18
6
18
 
7
19
  ### 中文
@@ -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
- enqueueImport(bundle, sourceNodeId, sourceLabel, fromPeer) {
802
- const queued = this.importProcessorActive || !this.callbacks.isIdle() || this.pendingImports.length > 0;
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, 'push');
888
+ await this.validateAndImport(pending.bundle, pending.sourceNodeId, pending.sourceLabel, pending.fromPeer, pending.mode);
846
889
  }
847
890
  }
848
891
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.45",
3
+ "version": "0.5.46",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",