wendkeep 0.68.0 → 0.68.5
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 +67 -0
- package/README.en.md +14 -6
- package/README.md +14 -6
- package/docs/en/commands/changes-and-verification.md +12 -0
- package/docs/en/commands/memory.md +3 -1
- package/docs/en/commands/operating-profiles.md +11 -0
- package/docs/en/commands/sessions-and-import.md +15 -0
- package/docs/pt-BR/commands/changes-and-verification.md +12 -0
- package/docs/pt-BR/commands/memory.md +3 -1
- package/docs/pt-BR/commands/operating-profiles.md +11 -0
- package/docs/pt-BR/commands/sessions-and-import.md +15 -0
- package/hooks/brain-core.mjs +159 -159
- package/hooks/brain-recall.mjs +32 -32
- package/hooks/brain-reindex.mjs +13 -13
- package/hooks/change-guard.mjs +142 -103
- package/hooks/git-snapshot.mjs +25 -6
- package/hooks/obsidian-common.mjs +40 -0
- package/hooks/project-scope.mjs +435 -0
- package/hooks/session-backfill.mjs +1 -1
- package/hooks/session-ensure.mjs +23 -4
- package/hooks/session-iteration-outcome.mjs +143 -0
- package/hooks/session-start.mjs +15 -0
- package/hooks/session-stop.mjs +263 -42
- package/hooks/token-usage.mjs +13 -11
- package/package.json +3 -3
- package/packages/harness/src/flow-store.mjs +12 -3
- package/packages/integrations/src/host-hooks.mjs +7 -7
- package/packages/integrations/src/prompt-content.mjs +12 -0
- package/packages/integrations/src/transcripts.mjs +37 -8
- package/packages/vault/src/memory-store.mjs +63 -1
- package/packages/vault/src/vault-path-safety.mjs +50 -25
- package/src/init.mjs +3 -0
- package/src/memory.mjs +10 -3
|
@@ -8,6 +8,18 @@ export function isBootstrapPrompt(text = '') {
|
|
|
8
8
|
|| clean.startsWith('## Memory');
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const SYNTHETIC_EVENT_TAG = /^<\/?(?:task-notification|system-reminder|local-command-stdout|local-command-stderr|local-command-caveat|command-message|command-name|command-args|user-prompt-submit-hook|subagent_notification|subagent-notification|ide_[A-Za-z0-9_-]+|environment_context)\b/i;
|
|
12
|
+
|
|
13
|
+
// Shared by transcript and token-usage parsers. These wrappers are harness metadata, not human
|
|
14
|
+
// prompts; keeping the filter here prevents the two import paths from drifting.
|
|
15
|
+
export function isSyntheticTranscriptText(text = '') {
|
|
16
|
+
const trimmed = String(text || '').trim();
|
|
17
|
+
return SYNTHETIC_EVENT_TAG.test(trimmed)
|
|
18
|
+
|| isBootstrapPrompt(trimmed)
|
|
19
|
+
|| /^Generate a concise( UI)? title/i.test(trimmed)
|
|
20
|
+
|| /^You are a helpful assistant\. You will be presented with a user prompt/i.test(trimmed);
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
export function redactSecrets(text) {
|
|
12
24
|
if (!text) return '';
|
|
13
25
|
return String(text)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
isSyntheticTranscriptText,
|
|
3
3
|
redactSecrets,
|
|
4
4
|
sanitizeAssistantMessage,
|
|
5
5
|
} from './prompt-content.mjs';
|
|
@@ -20,14 +20,8 @@ function extractContentText(content) {
|
|
|
20
20
|
.trim();
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const SYNTHETIC_EVENT_TAG = /^<\/?(?:task-notification|system-reminder|local-command-stdout|local-command-stderr|command-message|command-name|command-args|user-prompt-submit-hook|ide_selection|ide_opened_file|environment_context)\b/i;
|
|
24
|
-
|
|
25
23
|
function shouldIgnoreUserText(text) {
|
|
26
|
-
|
|
27
|
-
return SYNTHETIC_EVENT_TAG.test(trimmed)
|
|
28
|
-
|| isBootstrapPrompt(trimmed)
|
|
29
|
-
|| /^Generate a concise( UI)? title/i.test(trimmed)
|
|
30
|
-
|| /^You are a helpful assistant\. You will be presented with a user prompt/i.test(trimmed);
|
|
24
|
+
return isSyntheticTranscriptText(text);
|
|
31
25
|
}
|
|
32
26
|
|
|
33
27
|
function addUnique(list, value) {
|
|
@@ -180,6 +174,16 @@ export function completedCodexTurnIdsContent(content = '') {
|
|
|
180
174
|
return completed;
|
|
181
175
|
}
|
|
182
176
|
|
|
177
|
+
export function abortedCodexTurnIdsContent(content = '') {
|
|
178
|
+
const aborted = new Set();
|
|
179
|
+
for (const event of jsonLines(content)) {
|
|
180
|
+
if (event.type !== 'event_msg' || event.payload?.type !== 'turn_aborted') continue;
|
|
181
|
+
const turnId = String(event.payload?.turn_id || event.turn_id || '').trim();
|
|
182
|
+
if (turnId) aborted.add(turnId);
|
|
183
|
+
}
|
|
184
|
+
return aborted;
|
|
185
|
+
}
|
|
186
|
+
|
|
183
187
|
export function parseCodexTranscriptContent(content, options = {}) {
|
|
184
188
|
const result = createResult('codex');
|
|
185
189
|
const eventUserPrompts = [];
|
|
@@ -208,6 +212,11 @@ export function parseCodexTranscriptContent(content, options = {}) {
|
|
|
208
212
|
ensureTurn(result.latestTurnId, event.timestamp);
|
|
209
213
|
continue;
|
|
210
214
|
}
|
|
215
|
+
if (event.type === 'event_msg' && event.payload?.type === 'turn_aborted') {
|
|
216
|
+
const turn = ensureTurn(event.payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
|
|
217
|
+
turn.status = 'aborted';
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
211
220
|
if (event.type === 'turn_context') {
|
|
212
221
|
result.latestTurnId = event.payload?.turn_id || result.latestTurnId;
|
|
213
222
|
result.model = event.payload?.model || result.model;
|
|
@@ -280,6 +289,26 @@ export function parseCodexTranscriptContent(content, options = {}) {
|
|
|
280
289
|
}
|
|
281
290
|
}
|
|
282
291
|
}
|
|
292
|
+
if (payload.type === 'custom_tool_call') {
|
|
293
|
+
const name = payload.name || payload.tool_name || payload.tool || 'custom_tool_call';
|
|
294
|
+
const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
|
|
295
|
+
addUnique(result.tools, name);
|
|
296
|
+
addUnique(turn.tools, name);
|
|
297
|
+
const parsed = parseToolArguments(payload.arguments ?? payload.input ?? payload.parameters);
|
|
298
|
+
const combined = typeof parsed.raw === 'string' ? parsed.raw : toolArgumentText(parsed);
|
|
299
|
+
for (const path of extractPaths(combined, paths)) {
|
|
300
|
+
addUnique(result.consultedFiles, path);
|
|
301
|
+
addUnique(turn.consultedFiles, path);
|
|
302
|
+
}
|
|
303
|
+
for (const path of extractPatchFiles(combined)) {
|
|
304
|
+
addUnique(result.changedFiles, path);
|
|
305
|
+
addUnique(turn.changedFiles, path);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (payload.type === 'custom_tool_call_output') {
|
|
309
|
+
// A custom tool output closes an existing call; it is deliberately not a second tool.
|
|
310
|
+
ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
|
|
311
|
+
}
|
|
283
312
|
if (payload.type === 'tool_search_call') {
|
|
284
313
|
const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
|
|
285
314
|
addUnique(result.tools, 'tool_search');
|
|
@@ -446,6 +446,27 @@ function currentEventFromRecord(record) {
|
|
|
446
446
|
return { ...record.source, value: record.value, revision: record.revision };
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
+
function supersededTransitively(superseded, sourceEventId, finalEventId) {
|
|
450
|
+
if (!sourceEventId || !finalEventId || sourceEventId === finalEventId) return false;
|
|
451
|
+
const edges = new Map();
|
|
452
|
+
for (const item of superseded) {
|
|
453
|
+
if (!edges.has(item.event_id)) edges.set(item.event_id, []);
|
|
454
|
+
edges.get(item.event_id).push(item.by_event_id);
|
|
455
|
+
}
|
|
456
|
+
const pending = [sourceEventId];
|
|
457
|
+
const visited = new Set();
|
|
458
|
+
while (pending.length) {
|
|
459
|
+
const current = pending.shift();
|
|
460
|
+
if (visited.has(current)) continue;
|
|
461
|
+
visited.add(current);
|
|
462
|
+
for (const next of edges.get(current) || []) {
|
|
463
|
+
if (next === finalEventId) return true;
|
|
464
|
+
if (!visited.has(next)) pending.push(next);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
|
|
449
470
|
function isCausallyOlder(event, current) {
|
|
450
471
|
if (!current) return false;
|
|
451
472
|
if (sameCausalActivation(event, current)) {
|
|
@@ -636,6 +657,24 @@ export function reduceMemoryEvents(inputEvents = [], {
|
|
|
636
657
|
}
|
|
637
658
|
records.set(item.memory_key, { value: item.value, revision: current.revision + 1, source: item });
|
|
638
659
|
tombstones.delete(item.memory_key);
|
|
660
|
+
const explicitlySupersededIds = new Set(
|
|
661
|
+
Array.isArray(item.supersedes)
|
|
662
|
+
? item.supersedes
|
|
663
|
+
: (item.supersedes_event_id ? [item.supersedes_event_id] : []),
|
|
664
|
+
);
|
|
665
|
+
if (item.candidate_decision?.action === 'promote' && explicitlySupersededIds.size) {
|
|
666
|
+
for (const candidate of candidates) {
|
|
667
|
+
if (candidate.reason !== 'conflict' || !candidate.event_ids?.length) continue;
|
|
668
|
+
if (candidate.event_ids.every((eventId) => explicitlySupersededIds.has(eventId))) {
|
|
669
|
+
resolvedCandidateIds.add(candidate.candidate_id);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
for (const pending of pendingAssertConflicts) {
|
|
673
|
+
if (explicitlySupersededIds.has(pending.event.event_id)) {
|
|
674
|
+
resolvedCandidateIds.add(pending.candidate.candidate_id);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
639
678
|
revision += 1;
|
|
640
679
|
appliedEventIds.push(item.event_id);
|
|
641
680
|
}
|
|
@@ -680,13 +719,36 @@ export function reduceMemoryEvents(inputEvents = [], {
|
|
|
680
719
|
}
|
|
681
720
|
}
|
|
682
721
|
|
|
722
|
+
const pendingByCandidateId = new Map(
|
|
723
|
+
pendingAssertConflicts.map((pending) => [pending.candidate.candidate_id, pending]),
|
|
724
|
+
);
|
|
725
|
+
const reanchoredCandidates = candidates
|
|
726
|
+
.map((candidate) => {
|
|
727
|
+
const pending = pendingByCandidateId.get(candidate.candidate_id);
|
|
728
|
+
if (!pending) return candidate;
|
|
729
|
+
const finalSource = currentEventFromRecord(records.get(candidate.memory_key));
|
|
730
|
+
const previousSource = candidate.events?.find(
|
|
731
|
+
(event) => event.event_id !== pending.event.event_id,
|
|
732
|
+
);
|
|
733
|
+
if (!finalSource || !previousSource || finalSource.event_id === previousSource.event_id) {
|
|
734
|
+
return candidate;
|
|
735
|
+
}
|
|
736
|
+
if (!sameCompleteCausalLineage(previousSource, finalSource)
|
|
737
|
+
|| !supersededTransitively(superseded, previousSource.event_id, finalSource.event_id)) {
|
|
738
|
+
return candidate;
|
|
739
|
+
}
|
|
740
|
+
if (hashMemoryValue(finalSource.value) === hashMemoryValue(pending.event.value)) return null;
|
|
741
|
+
return conflictCandidate(candidate.memory_key, [finalSource, pending.event], finalSource);
|
|
742
|
+
})
|
|
743
|
+
.filter(Boolean);
|
|
744
|
+
|
|
683
745
|
const stateEntries = [...records].map(([key, record]) => [key, record.value]);
|
|
684
746
|
const recordEntries = [...records].map(([key, record]) => [key, record]);
|
|
685
747
|
const tombstoneEntries = [...tombstones];
|
|
686
748
|
const state = sortedObject(stateEntries);
|
|
687
749
|
const recordObject = sortedObject(recordEntries);
|
|
688
750
|
const tombstoneObject = sortedObject(tombstoneEntries);
|
|
689
|
-
const unresolvedCandidates =
|
|
751
|
+
const unresolvedCandidates = reanchoredCandidates
|
|
690
752
|
.filter((item) => !candidateDecisions.has(item.candidate_id)
|
|
691
753
|
&& !resolvedCandidateIds.has(item.candidate_id));
|
|
692
754
|
unresolvedCandidates.sort((left, right) => left.candidate_id.localeCompare(right.candidate_id));
|
|
@@ -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);
|
package/src/init.mjs
CHANGED
|
@@ -220,6 +220,7 @@ export function mergeCodexHooks(existing, { force = false } = {}) {
|
|
|
220
220
|
const owning = groups.find((g) => (g.hooks || []).some(owns));
|
|
221
221
|
if (owning) {
|
|
222
222
|
const hk = owning.hooks.find(owns);
|
|
223
|
+
const matcher = CODEX_MATCHER_EVENTS.has(h.event) ? h.matcher : null;
|
|
223
224
|
// `timeout` is the pre-0.46 key: Codex never read it. Migrate it even without --force,
|
|
224
225
|
// otherwise the hook keeps running at the 600s default forever.
|
|
225
226
|
const legacyTimeout = 'timeout' in hk;
|
|
@@ -233,6 +234,8 @@ export function mergeCodexHooks(existing, { force = false } = {}) {
|
|
|
233
234
|
if (entry.statusMessage) hk.statusMessage = entry.statusMessage;
|
|
234
235
|
}
|
|
235
236
|
}
|
|
237
|
+
if (force && matcher) owning.matcher = matcher;
|
|
238
|
+
if (force && !matcher) delete owning.matcher;
|
|
236
239
|
file.hooks[h.event] = groups;
|
|
237
240
|
continue;
|
|
238
241
|
}
|
package/src/memory.mjs
CHANGED
|
@@ -387,9 +387,16 @@ function promotedSupersedes(vault, candidate, selected) {
|
|
|
387
387
|
const memberIds = new Set(Array.isArray(candidate.event_ids) ? candidate.event_ids : []);
|
|
388
388
|
const ledger = readMemoryLedger(vault);
|
|
389
389
|
if (ledger.status !== 'ok') throw new Error('Ledger de memória inválido durante a promoção.');
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
if (!current?.event_id
|
|
390
|
+
const projection = deriveMemoryProjection(vault, ledger.events);
|
|
391
|
+
const current = projection.records?.[candidate.memory_key]?.source;
|
|
392
|
+
if (!current?.event_id) return [...memberIds].sort();
|
|
393
|
+
if (memberIds.has(current.event_id)) {
|
|
394
|
+
projection.superseded
|
|
395
|
+
.filter((item) => item.by_event_id === current.event_id)
|
|
396
|
+
.map((item) => item.event_id)
|
|
397
|
+
.forEach((eventId) => memberIds.add(eventId));
|
|
398
|
+
return [...memberIds].sort();
|
|
399
|
+
}
|
|
393
400
|
|
|
394
401
|
const currentSelectedId = current.candidate_decision?.selected_event_id;
|
|
395
402
|
const currentSelected = currentSelectedId
|