wendkeep 0.68.0 → 0.68.1
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
|
@@ -4,6 +4,22 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.68.1] — 2026-08-08
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **A contenção de locks do Vault deixa de expor `VAULT_PATH_UNSAFE` no Windows.** A revalidação de
|
|
12
|
+
uma falha transiente de resolução do lock público passa a decidir por um walk fresco do
|
|
13
|
+
componente, e não pelo `errno` reportado pela plataforma. O Windows devolve `UNKNOWN`, `EBADF` ou
|
|
14
|
+
`EPERM` onde o Linux devolve `ENOENT`, então a guarda de retry nunca disparava lá e promoções FLOW
|
|
15
|
+
concorrentes falhavam de forma intermitente com o código de fronteira física em vez do conflito de
|
|
16
|
+
promoção. Sufixo ausente ou diretório canônico estabilizado autorizam o retry; junction, symlink,
|
|
17
|
+
reparse point, componente redirecionado ou estado irresolvível persistente continuam falhando
|
|
18
|
+
fechado, e o orçamento de retry permanece único e limitado por aquisição.
|
|
19
|
+
- **Falha de lock nos caminhos FLOW reporta o código do domínio.** `withFlowPromotionLock` e o store
|
|
20
|
+
de sessão passam a propagar `FLOW_VAULT_BOUNDARY` para a fronteira física, alinhando a superfície
|
|
21
|
+
de erro ao resto da saga de promoção.
|
|
22
|
+
|
|
7
23
|
## [0.68.0] — 2026-08-02
|
|
8
24
|
|
|
9
25
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.68.
|
|
3
|
+
"version": "0.68.1",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -70,6 +70,6 @@
|
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"acorn": "^8.18.0",
|
|
73
|
-
"wendkeep": "^0.
|
|
73
|
+
"wendkeep": "^0.68.0"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -84,10 +84,16 @@ function immutableJson(vaultBase, path, value) {
|
|
|
84
84
|
return { created: true, path };
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Lock failures on the FLOW paths surface with the domain boundary code, like every other
|
|
88
|
+
// Vault write in the promotion saga — not with the physical-boundary default.
|
|
87
89
|
function underSessionLock(vaultBase, sessionId, fn) {
|
|
88
90
|
const root = sessionRoot(vaultBase, sessionId);
|
|
89
|
-
mkdirVaultPath(vaultBase, root, {
|
|
90
|
-
|
|
91
|
+
mkdirVaultPath(vaultBase, root, {
|
|
92
|
+
label: 'raiz runtime da sessão FLOW', code: 'FLOW_VAULT_BOUNDARY',
|
|
93
|
+
});
|
|
94
|
+
const outcome = withVaultPathLock(vaultBase, join(root, '.state'), fn, {
|
|
95
|
+
timeoutMs: 5000, code: 'FLOW_VAULT_BOUNDARY',
|
|
96
|
+
});
|
|
91
97
|
if (typeof outcome === 'symbol') {
|
|
92
98
|
const error = new Error(`store FLOW ocupado para sessão ${sessionId}`);
|
|
93
99
|
error.code = 'FLOW_STORE_BUSY';
|
|
@@ -107,10 +113,13 @@ export function withFlowPromotionLock(vaultBase, changeSlug, fn, {
|
|
|
107
113
|
} = {}) {
|
|
108
114
|
const slug = safeId(changeSlug, 'change_slug');
|
|
109
115
|
const root = join(vaultBase, '.brain', 'runtime', 'flow-promotion-locks');
|
|
110
|
-
mkdirVaultPath(vaultBase, root, {
|
|
116
|
+
mkdirVaultPath(vaultBase, root, {
|
|
117
|
+
label: 'raiz de locks de promoção FLOW', code: 'FLOW_VAULT_BOUNDARY',
|
|
118
|
+
});
|
|
111
119
|
const outcome = withVaultPathLock(vaultBase, join(root, slug), fn, {
|
|
112
120
|
timeoutMs,
|
|
113
121
|
staleMs: ownerGraceMs,
|
|
122
|
+
code: 'FLOW_VAULT_BOUNDARY',
|
|
114
123
|
});
|
|
115
124
|
if (outcome === VAULT_LOCK_BUSY) {
|
|
116
125
|
const busy = new Error(`promoção FLOW ocupada para a change ${slug}`);
|
|
@@ -335,19 +335,36 @@ function waitBriefly(ms) {
|
|
|
335
335
|
Atomics.wait(signal, 0, 0, ms);
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
// A public lock may legitimately disappear while another owner releases it.
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
|
|
338
|
+
// A public lock may legitimately disappear while another owner releases it. Structural shape
|
|
339
|
+
// only: which errno a concurrent removal produces is a platform detail — Windows reports
|
|
340
|
+
// UNKNOWN, EBADF or EPERM where Linux reports ENOENT — so the code is merely the trigger to go
|
|
341
|
+
// re-observe, never the answer. Private .pending paths are siblings, not descendants, of the
|
|
342
|
+
// canonical lock and are excluded by the containment check.
|
|
343
|
+
function publicLockRetryCandidate(lock, error, code, { allowRaw = true } = {}) {
|
|
342
344
|
const failure = error?.[VAULT_PATH_FAILURE];
|
|
345
|
+
// A raw error carries no component to re-observe, so it stays on the narrow ENOENT path.
|
|
343
346
|
if (!failure) return allowRaw && error?.code === 'ENOENT' && !error?.cause;
|
|
344
|
-
|
|
345
|
-
return causeCode === 'ENOENT'
|
|
346
|
-
&& (error?.code === code || error?.code === 'VAULT_PATH_UNSAFE')
|
|
347
|
+
return (error?.code === code || error?.code === 'VAULT_PATH_UNSAFE')
|
|
347
348
|
&& ['component-realpath', 'component-missing'].includes(failure.kind)
|
|
348
349
|
&& containedBy(resolve(lock), resolve(failure.component));
|
|
349
350
|
}
|
|
350
351
|
|
|
352
|
+
// The decision itself. Only a fresh walk that settles as a missing suffix or the canonical
|
|
353
|
+
// entry authorizes a retry; junction, symlink, reparse, a redirected component or a state that
|
|
354
|
+
// is still unresolvable keeps failing closed, exactly as a first-time validation would.
|
|
355
|
+
function publicLockComponentSettled(vaultBase, error, code) {
|
|
356
|
+
const component = error?.[VAULT_PATH_FAILURE]?.component;
|
|
357
|
+
if (typeof component !== 'string' || !component) return false;
|
|
358
|
+
try {
|
|
359
|
+
assertVaultPathSafe(vaultBase, component, {
|
|
360
|
+
label: 'revalidação do lock de escrita do Vault', code,
|
|
361
|
+
});
|
|
362
|
+
return true;
|
|
363
|
+
} catch {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
351
368
|
function vaultLockRenameCollision(error, pending, lock) {
|
|
352
369
|
const acceptedCodes = process.platform === 'win32'
|
|
353
370
|
? ['EEXIST', 'ENOTEMPTY', 'EPERM']
|
|
@@ -374,17 +391,22 @@ function vaultLockRetryDeadlineError() {
|
|
|
374
391
|
return error;
|
|
375
392
|
}
|
|
376
393
|
|
|
377
|
-
function withPublicLockRetry(lock, code, retryState, operation, initialError = null) {
|
|
394
|
+
function withPublicLockRetry(vaultBase, lock, code, retryState, operation, initialError = null) {
|
|
378
395
|
let error = initialError;
|
|
379
396
|
while (true) {
|
|
380
397
|
if (error) {
|
|
381
|
-
if (!
|
|
398
|
+
if (!publicLockRetryCandidate(lock, error, code)
|
|
382
399
|
|| retryState.remaining <= 0) throw error;
|
|
383
400
|
const remainingMs = retryState.deadline - Date.now();
|
|
384
401
|
if (remainingMs <= 0) throw vaultLockRetryDeadlineError();
|
|
385
402
|
retryState.remaining -= 1;
|
|
386
403
|
waitBriefly(Math.min(VAULT_LOCK_TOPOLOGY_RETRY_MS, remainingMs));
|
|
387
404
|
if (Date.now() >= retryState.deadline) throw vaultLockRetryDeadlineError();
|
|
405
|
+
// Reclassify only after the backoff, and only while the deadline still allows a retry:
|
|
406
|
+
// "settled" means observed once the concurrent owner had a chance to finish releasing.
|
|
407
|
+
// A raw error carries no component and already passed the narrow ENOENT filter above.
|
|
408
|
+
if (error[VAULT_PATH_FAILURE]
|
|
409
|
+
&& !publicLockComponentSettled(vaultBase, error, code)) throw error;
|
|
388
410
|
}
|
|
389
411
|
try {
|
|
390
412
|
return operation();
|
|
@@ -395,9 +417,11 @@ function withPublicLockRetry(lock, code, retryState, operation, initialError = n
|
|
|
395
417
|
}
|
|
396
418
|
|
|
397
419
|
function inspectVaultLock(vaultBase, lock, code, retryState, initialError = null) {
|
|
398
|
-
return withPublicLockRetry(lock, code, retryState, () => assertVaultPathSafe(
|
|
399
|
-
|
|
400
|
-
|
|
420
|
+
return withPublicLockRetry(vaultBase, lock, code, retryState, () => assertVaultPathSafe(
|
|
421
|
+
vaultBase, lock, {
|
|
422
|
+
expectedType: 'directory', label: 'lock de escrita do Vault', code,
|
|
423
|
+
},
|
|
424
|
+
), initialError);
|
|
401
425
|
}
|
|
402
426
|
|
|
403
427
|
function processIsAlive(pid) {
|
|
@@ -447,7 +471,7 @@ function vaultLockOwner(vaultBase, lock, code, retryState = null) {
|
|
|
447
471
|
}
|
|
448
472
|
};
|
|
449
473
|
return retryState
|
|
450
|
-
? withPublicLockRetry(lock, code, retryState, inspect)
|
|
474
|
+
? withPublicLockRetry(vaultBase, lock, code, retryState, inspect)
|
|
451
475
|
: inspect();
|
|
452
476
|
}
|
|
453
477
|
|
|
@@ -492,7 +516,7 @@ function releaseOwnedVaultLock(vaultBase, lock, {
|
|
|
492
516
|
// The token-specific lease is the filesystem CAS. An old finally/reaper can only
|
|
493
517
|
// remove the directory after successfully unlinking the lease it originally saw;
|
|
494
518
|
// a replacement lock never contains that unguessable path.
|
|
495
|
-
const leaseRemoved = withPublicLockRetry(lock, code, retryState, () => unlinkVaultFile(
|
|
519
|
+
const leaseRemoved = withPublicLockRetry(vaultBase, lock, code, retryState, () => unlinkVaultFile(
|
|
496
520
|
vaultBase, vaultLockLease(lock, token), {
|
|
497
521
|
label: 'lease do lock de escrita do Vault', code,
|
|
498
522
|
},
|
|
@@ -503,12 +527,12 @@ function releaseOwnedVaultLock(vaultBase, lock, {
|
|
|
503
527
|
const current = currentState.owner;
|
|
504
528
|
if (current?.pid !== pid || current?.token !== token) return false;
|
|
505
529
|
const ownerPath = join(lock, VAULT_LOCK_OWNER_FILE);
|
|
506
|
-
if (!withPublicLockRetry(lock, code, retryState, () => unlinkVaultFile(
|
|
530
|
+
if (!withPublicLockRetry(vaultBase, lock, code, retryState, () => unlinkVaultFile(
|
|
507
531
|
vaultBase, ownerPath, {
|
|
508
532
|
label: 'owner do lock de escrita do Vault', code,
|
|
509
533
|
},
|
|
510
534
|
))) return false;
|
|
511
|
-
return withPublicLockRetry(lock, code, retryState, () => removeVaultLockDirectory(
|
|
535
|
+
return withPublicLockRetry(vaultBase, lock, code, retryState, () => removeVaultLockDirectory(
|
|
512
536
|
vaultBase, lock, {
|
|
513
537
|
missingOk: false, label: 'lock de escrita do Vault', code,
|
|
514
538
|
},
|
|
@@ -516,7 +540,7 @@ function releaseOwnedVaultLock(vaultBase, lock, {
|
|
|
516
540
|
}
|
|
517
541
|
|
|
518
542
|
function reapDeadVaultLock(vaultBase, lock, staleMs, code, retryState) {
|
|
519
|
-
const initial = withPublicLockRetry(lock, code, retryState, () => {
|
|
543
|
+
const initial = withPublicLockRetry(vaultBase, lock, code, retryState, () => {
|
|
520
544
|
const checked = assertVaultPathSafe(vaultBase, lock, {
|
|
521
545
|
expectedType: 'directory', label: 'lock de escrita do Vault', code,
|
|
522
546
|
});
|
|
@@ -530,7 +554,7 @@ function reapDeadVaultLock(vaultBase, lock, staleMs, code, retryState) {
|
|
|
530
554
|
if (!observed.lockExists) return true;
|
|
531
555
|
if (observed.owner) {
|
|
532
556
|
if (processIsAlive(observed.owner.pid)) return false;
|
|
533
|
-
const lease = withPublicLockRetry(lock, code, retryState, () => {
|
|
557
|
+
const lease = withPublicLockRetry(vaultBase, lock, code, retryState, () => {
|
|
534
558
|
const current = assertVaultPathSafe(vaultBase, lock, {
|
|
535
559
|
expectedType: 'directory', label: 'lock de escrita do Vault', code,
|
|
536
560
|
});
|
|
@@ -548,7 +572,7 @@ function reapDeadVaultLock(vaultBase, lock, staleMs, code, retryState) {
|
|
|
548
572
|
// Compatibility with owner-aware locks from 0.58.x, which predate token leases.
|
|
549
573
|
// A dead PID plus byte-identical owner and directory identity is sufficient here;
|
|
550
574
|
// a live legacy owner was returned above and is never reaped by age.
|
|
551
|
-
const legacy = withPublicLockRetry(lock, code, retryState, () => {
|
|
575
|
+
const legacy = withPublicLockRetry(vaultBase, lock, code, retryState, () => {
|
|
552
576
|
const current = assertVaultPathSafe(vaultBase, lock, {
|
|
553
577
|
expectedType: 'directory', label: 'lock legado de escrita do Vault', code,
|
|
554
578
|
});
|
|
@@ -565,12 +589,12 @@ function reapDeadVaultLock(vaultBase, lock, staleMs, code, retryState) {
|
|
|
565
589
|
if (currentStat.birthtimeMs !== before.birthtimeMs
|
|
566
590
|
|| currentStat.mtimeMs !== before.mtimeMs
|
|
567
591
|
|| currentOwner.raw !== observed.raw) return false;
|
|
568
|
-
if (!withPublicLockRetry(lock, code, retryState, () => unlinkVaultFile(
|
|
592
|
+
if (!withPublicLockRetry(vaultBase, lock, code, retryState, () => unlinkVaultFile(
|
|
569
593
|
vaultBase, currentOwner.path, {
|
|
570
594
|
label: 'owner legado morto do lock de escrita do Vault', code,
|
|
571
595
|
},
|
|
572
596
|
))) return false;
|
|
573
|
-
return withPublicLockRetry(lock, code, retryState, () => removeVaultLockDirectory(
|
|
597
|
+
return withPublicLockRetry(vaultBase, lock, code, retryState, () => removeVaultLockDirectory(
|
|
574
598
|
vaultBase, checked.target, {
|
|
575
599
|
missingOk: false, label: 'lock legado de escrita do Vault', code,
|
|
576
600
|
},
|
|
@@ -580,7 +604,7 @@ function reapDeadVaultLock(vaultBase, lock, staleMs, code, retryState) {
|
|
|
580
604
|
// Locks are published by atomic directory rename only after owner + lease exist.
|
|
581
605
|
// Thus an old empty/partial directory is legacy or crash residue, never an in-flight
|
|
582
606
|
// live acquisition. Unknown children remain fail-closed.
|
|
583
|
-
const partial = withPublicLockRetry(lock, code, retryState, () => {
|
|
607
|
+
const partial = withPublicLockRetry(vaultBase, lock, code, retryState, () => {
|
|
584
608
|
const current = assertVaultPathSafe(vaultBase, lock, {
|
|
585
609
|
expectedType: 'directory', label: 'lock de escrita do Vault', code,
|
|
586
610
|
});
|
|
@@ -604,12 +628,12 @@ function reapDeadVaultLock(vaultBase, lock, staleMs, code, retryState) {
|
|
|
604
628
|
|| currentStat.mtimeMs !== before.mtimeMs
|
|
605
629
|
|| currentOwner.raw !== observed.raw) return false;
|
|
606
630
|
if (entries.includes(VAULT_LOCK_OWNER_FILE)
|
|
607
|
-
&& !withPublicLockRetry(lock, code, retryState, () => unlinkVaultFile(
|
|
631
|
+
&& !withPublicLockRetry(vaultBase, lock, code, retryState, () => unlinkVaultFile(
|
|
608
632
|
vaultBase, currentOwner.path, {
|
|
609
633
|
label: 'owner parcial do lock de escrita do Vault', code,
|
|
610
634
|
},
|
|
611
635
|
))) return false;
|
|
612
|
-
return withPublicLockRetry(lock, code, retryState, () => removeVaultLockDirectory(
|
|
636
|
+
return withPublicLockRetry(vaultBase, lock, code, retryState, () => removeVaultLockDirectory(
|
|
613
637
|
vaultBase, checked.target, {
|
|
614
638
|
missingOk: false, label: 'lock de escrita do Vault', code,
|
|
615
639
|
},
|
|
@@ -672,7 +696,8 @@ export function withVaultPathLock(vaultBase, path, fn, {
|
|
|
672
696
|
});
|
|
673
697
|
break;
|
|
674
698
|
} catch (error) {
|
|
675
|
-
|
|
699
|
+
// Shape check only; inspectVaultLock below re-observes through the full retry path.
|
|
700
|
+
const retryableRenameRace = publicLockRetryCandidate(lock, error, code, {
|
|
676
701
|
allowRaw: false,
|
|
677
702
|
});
|
|
678
703
|
const nativeRenameCollision = vaultLockRenameCollision(error, pending, lock);
|