@evomap/evolver-core 2.0.0-beta.6 → 2.0.0-beta.8

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.
@@ -1,7 +1,11 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { redactString } from '../hub/sanitize.js';
3
- const REUSABLE_RE = /\b(reusable|repeatable|workflow|playbook|runbook|capability|procedure|pattern|recipe|documented|future runs?|next time|can reuse|reuse this)\b|可复用|复用|能力|流程|工作流/i;
4
- const PROOF_RE = /\b(validated|verified|passed|green|success(?:ful|fully)?|succeeded|works?|completed|published|uploaded|recorded:true|exit code:?\s*0|all tests passed)\b|验证|通过|成功|已发布|可用/i;
3
+ // The Chinese branches require compound/intentful phrases on purpose: everyday words (能力/流程/成功/通过)
4
+ // occur in nearly every Chinese dev session, and matching them bare made this fallback the DOMINANT distill
5
+ // path — 105 of 118 drafts in one bulk ingest (#562). English gets specificity from \b word boundaries;
6
+ // Chinese has no \b, so specificity must come from the phrase itself.
7
+ const REUSABLE_RE = /\b(reusable|repeatable|workflow|playbook|runbook|capability|procedure|pattern|recipe|documented|future runs?|next time|can reuse|reuse this)\b|复用|可重用|工作流|方法论|沉淀/i;
8
+ const PROOF_RE = /\b(validated|verified|passed|green|success(?:ful|fully)?|succeeded|works?|completed|published|uploaded|recorded:true|exit code:?\s*0|all tests passed)\b|(?:验证|校验|测试|检查|构建|编译|运行|执行|部署|发布)(?:都|均|全部)?(?:通过|成功)|全部通过|跑通|已(?:验证|发布|上线)/i;
5
9
  const FAILURE_RE = /\b(failed|failure|error|exception|traceback|exit code:?\s*[1-9]|not working|unable to)\b|失败|错误|报错/i;
6
10
  const EXIT_ZERO_RE = /\bexit code:?\s*0\b/i;
7
11
  const EXIT_NON_ZERO_RE = /\bexit code:?\s*[1-9]\d*\b/i;
@@ -1,22 +1,22 @@
1
1
  /** ~/.evomap, 可 EVOLVER_HOME/EVOMAP_HOME 覆盖. */
2
2
  export declare function evomapHome(env?: Readonly<Record<string, string | undefined>>): string;
3
- export declare function rootEventsPath(): string;
4
- export declare function mvDir(): string;
3
+ export declare function rootEventsPath(env?: Readonly<Record<string, string | undefined>>): string;
4
+ export declare function mvDir(env?: Readonly<Record<string, string | undefined>>): string;
5
5
  /** 可进化人格模型持久化文件 (五维向量 + 各键统计 + 变更历史). v1 personality_state.json 的 v2 落点. */
6
- export declare function personalityStatePath(): string;
7
- export declare function assetsDir(): string;
6
+ export declare function personalityStatePath(env?: Readonly<Record<string, string | undefined>>): string;
7
+ export declare function assetsDir(env?: Readonly<Record<string, string | undefined>>): string;
8
8
  /** M1 raw-material substrate (append-only jsonl consumed by the cycle). */
9
- export declare function materialDir(): string;
9
+ export declare function materialDir(env?: Readonly<Record<string, string | undefined>>): string;
10
10
  /** MaterialStore backing file. */
11
- export declare function materialStorePath(): string;
11
+ export declare function materialStorePath(env?: Readonly<Record<string, string | undefined>>): string;
12
12
  /** Per-source watermark cursor used to make re-ingest idempotent (file-level dedup). */
13
- export declare function materialWatermarkPath(): string;
13
+ export declare function materialWatermarkPath(env?: Readonly<Record<string, string | undefined>>): string;
14
14
  /** Proxy LLM-trace day-files dir (`llm-trace-YYYYMMDD.jsonl`), the route-savings source for the value ledger.
15
15
  * Mirrors the proxy's EVOLVER_LLM_TRACE_DIR default so the outreach layer reads what the proxy wrote. */
16
- export declare function tracesDir(): string;
16
+ export declare function tracesDir(env?: Readonly<Record<string, string | undefined>>): string;
17
17
  /** Hub-asset audit trail (`asset_call_log.jsonl`): one JSONL line per hub-asset interaction (search hit/miss,
18
18
  * reuse/reference, publish, review). Written best-effort by AssetCallLog; the desktop's RecallHistory / 召回历程
19
19
  * reads it. Defaults to the evomap home root, beside assets/ + evolution/.
20
20
  * NOTE(reuse/#234): confirm this matches where the proxy/RecallHistory reads — best-effort append, so a path
21
21
  * mismatch degrades the audit line, never the reuse write itself. */
22
- export declare function assetCallLogPath(): string;
22
+ export declare function assetCallLogPath(env?: Readonly<Record<string, string | undefined>>): string;
@@ -4,41 +4,41 @@ import { join } from 'node:path';
4
4
  export function evomapHome(env = process.env) {
5
5
  return env['EVOLVER_HOME'] ?? env['EVOMAP_HOME'] ?? join(homedir(), '.evomap');
6
6
  }
7
- export function rootEventsPath() {
8
- return join(evomapHome(), 'evolution', 'root_events.jsonl');
7
+ export function rootEventsPath(env = process.env) {
8
+ return join(evomapHome(env), 'evolution', 'root_events.jsonl');
9
9
  }
10
- export function mvDir() {
11
- return join(evomapHome(), 'evolution', 'mv');
10
+ export function mvDir(env = process.env) {
11
+ return join(evomapHome(env), 'evolution', 'mv');
12
12
  }
13
13
  /** 可进化人格模型持久化文件 (五维向量 + 各键统计 + 变更历史). v1 personality_state.json 的 v2 落点. */
14
- export function personalityStatePath() {
15
- return join(evomapHome(), 'evolution', 'personality_state.json');
14
+ export function personalityStatePath(env = process.env) {
15
+ return join(evomapHome(env), 'evolution', 'personality_state.json');
16
16
  }
17
- export function assetsDir() {
18
- return join(evomapHome(), 'assets');
17
+ export function assetsDir(env = process.env) {
18
+ return join(evomapHome(env), 'assets');
19
19
  }
20
20
  /** M1 raw-material substrate (append-only jsonl consumed by the cycle). */
21
- export function materialDir() {
22
- return join(evomapHome(), 'evolution', 'material');
21
+ export function materialDir(env = process.env) {
22
+ return join(evomapHome(env), 'evolution', 'material');
23
23
  }
24
24
  /** MaterialStore backing file. */
25
- export function materialStorePath() {
26
- return join(materialDir(), 'material.jsonl');
25
+ export function materialStorePath(env = process.env) {
26
+ return join(materialDir(env), 'material.jsonl');
27
27
  }
28
28
  /** Per-source watermark cursor used to make re-ingest idempotent (file-level dedup). */
29
- export function materialWatermarkPath() {
30
- return join(materialDir(), 'watermark.json');
29
+ export function materialWatermarkPath(env = process.env) {
30
+ return join(materialDir(env), 'watermark.json');
31
31
  }
32
32
  /** Proxy LLM-trace day-files dir (`llm-trace-YYYYMMDD.jsonl`), the route-savings source for the value ledger.
33
33
  * Mirrors the proxy's EVOLVER_LLM_TRACE_DIR default so the outreach layer reads what the proxy wrote. */
34
- export function tracesDir() {
35
- return process.env['EVOLVER_LLM_TRACE_DIR'] ?? join(evomapHome(), 'proxy', 'traces');
34
+ export function tracesDir(env = process.env) {
35
+ return env['EVOLVER_LLM_TRACE_DIR'] ?? join(evomapHome(env), 'proxy', 'traces');
36
36
  }
37
37
  /** Hub-asset audit trail (`asset_call_log.jsonl`): one JSONL line per hub-asset interaction (search hit/miss,
38
38
  * reuse/reference, publish, review). Written best-effort by AssetCallLog; the desktop's RecallHistory / 召回历程
39
39
  * reads it. Defaults to the evomap home root, beside assets/ + evolution/.
40
40
  * NOTE(reuse/#234): confirm this matches where the proxy/RecallHistory reads — best-effort append, so a path
41
41
  * mismatch degrades the audit line, never the reuse write itself. */
42
- export function assetCallLogPath() {
43
- return join(evomapHome(), 'asset_call_log.jsonl');
42
+ export function assetCallLogPath(env = process.env) {
43
+ return join(evomapHome(env), 'asset_call_log.jsonl');
44
44
  }
@@ -108,6 +108,9 @@ export declare class ExecBridgeForbiddenError extends Error {
108
108
  export declare class UnsandboxedFullAccessRequiresIsolationError extends Error {
109
109
  constructor();
110
110
  }
111
+ export declare class UnsafeWorktreePathError extends Error {
112
+ constructor(reason: string);
113
+ }
111
114
  /**
112
115
  * Whitelist-filter `env` for a spawned agent/tool: keep ONLY the minimal runtime env + the caller-declared
113
116
  * extras (the runner's own auth via allowPrefixes/allowKeys); drop everything else. Fail-safe by construction —
@@ -14,7 +14,7 @@
14
14
  import { randomUUID } from 'node:crypto';
15
15
  import { resolve as resolvePath, sep, join as joinPath } from 'node:path';
16
16
  import { tmpdir } from 'node:os';
17
- import { closeSync, existsSync, mkdtempSync, openSync, rmSync, writeFileSync } from 'node:fs';
17
+ import { closeSync, lstatSync, mkdtempSync, openSync, realpathSync, rmdirSync, rmSync, writeFileSync, } from 'node:fs';
18
18
  import { renderExecPrompt } from './prompt.js';
19
19
  import { parseGitShortstat, gitDiffProof } from './proofOfWork.js';
20
20
  // Policy enforcement core (#107): checkPolicy runs the always-on global guards (blast hard cap +
@@ -55,6 +55,112 @@ export class UnsandboxedFullAccessRequiresIsolationError extends Error {
55
55
  this.name = 'UnsandboxedFullAccessRequiresIsolationError';
56
56
  }
57
57
  }
58
+ export class UnsafeWorktreePathError extends Error {
59
+ constructor(reason) {
60
+ super(`worktree isolation refused: ${reason}`);
61
+ this.name = 'UnsafeWorktreePathError';
62
+ }
63
+ }
64
+ function errorCode(error) {
65
+ return typeof error === 'object' && error !== null && 'code' in error
66
+ ? String(error.code)
67
+ : undefined;
68
+ }
69
+ function assertPathAbsent(path) {
70
+ try {
71
+ lstatSync(path);
72
+ }
73
+ catch (error) {
74
+ if (errorCode(error) === 'ENOENT')
75
+ return;
76
+ throw error;
77
+ }
78
+ throw new UnsafeWorktreePathError('reserved destination already exists');
79
+ }
80
+ function reserveWorktreePath() {
81
+ // The random 0700 container is atomically created by the OS. The git destination remains absent inside it,
82
+ // so `git worktree add` cannot adopt an attacker-precreated path from the shared temp directory.
83
+ const container = mkdtempSync(joinPath(tmpdir(), 'evolver-wt-'));
84
+ const containerStat = lstatSync(container);
85
+ if (containerStat.isSymbolicLink() || !containerStat.isDirectory()) {
86
+ throw new UnsafeWorktreePathError('temporary reservation is not a real directory');
87
+ }
88
+ const workDir = joinPath(container, 'worktree');
89
+ assertPathAbsent(workDir);
90
+ return {
91
+ container,
92
+ containerDev: containerStat.dev,
93
+ containerIno: containerStat.ino,
94
+ workDir,
95
+ expectedRealPath: joinPath(realpathSync(container), 'worktree'),
96
+ };
97
+ }
98
+ function verifyWorktreePath(reservation) {
99
+ const stat = lstatSync(reservation.workDir);
100
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
101
+ throw new UnsafeWorktreePathError('git worktree destination is not a real directory');
102
+ }
103
+ if (realpathSync(reservation.workDir) !== reservation.expectedRealPath) {
104
+ throw new UnsafeWorktreePathError('git worktree destination resolves outside its reservation');
105
+ }
106
+ return { dev: stat.dev, ino: stat.ino };
107
+ }
108
+ function worktreePathStillOwned(reservation, identity) {
109
+ try {
110
+ const stat = lstatSync(reservation.workDir);
111
+ return !stat.isSymbolicLink()
112
+ && stat.isDirectory()
113
+ && stat.dev === identity.dev
114
+ && stat.ino === identity.ino
115
+ && realpathSync(reservation.workDir) === reservation.expectedRealPath;
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ function removeEmptyReservation(reservation) {
122
+ try {
123
+ const stat = lstatSync(reservation.container);
124
+ if (stat.isSymbolicLink() || !stat.isDirectory()
125
+ || stat.dev !== reservation.containerDev || stat.ino !== reservation.containerIno)
126
+ return;
127
+ rmdirSync(reservation.container);
128
+ }
129
+ catch (error) {
130
+ // Never recurse through a path that may have been replaced. Empty, owned reservations are the only thing
131
+ // removed directly; non-empty or already-gone containers are intentionally left for safe diagnosis.
132
+ if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(errorCode(error) ?? ''))
133
+ throw error;
134
+ }
135
+ }
136
+ async function cleanupWorktreeReservation(reservation, identity, git, repoCwd) {
137
+ let cleanupIdentity = identity;
138
+ if (!cleanupIdentity) {
139
+ try {
140
+ cleanupIdentity = verifyWorktreePath(reservation);
141
+ }
142
+ catch (error) {
143
+ if (errorCode(error) !== 'ENOENT')
144
+ throw error;
145
+ // `git worktree add` may register metadata before failing without creating the destination. Give Git a
146
+ // signal-shielded chance to prune that partial registration, then reclaim only the still-empty container.
147
+ try {
148
+ await git(['worktree', 'remove', '--force', reservation.workDir], repoCwd, undefined, { processSignalMode: 'ignore' });
149
+ }
150
+ catch { /* no child exists, so an unregistered cleanup failure is harmless */ }
151
+ removeEmptyReservation(reservation);
152
+ return;
153
+ }
154
+ }
155
+ if (cleanupIdentity) {
156
+ if (!worktreePathStillOwned(reservation, cleanupIdentity)) {
157
+ throw new UnsafeWorktreePathError('verified worktree path changed before cleanup');
158
+ }
159
+ await git(['worktree', 'remove', '--force', reservation.workDir], repoCwd, undefined, { processSignalMode: 'ignore' });
160
+ assertPathAbsent(reservation.workDir);
161
+ }
162
+ removeEmptyReservation(reservation);
163
+ }
58
164
  /** Whether `child` is the same as, or nested under, `root` (both resolved to absolute paths). */
59
165
  function isWithinRoot(child, root) {
60
166
  const c = resolvePath(child);
@@ -278,27 +384,26 @@ export function makeClaudeExecBridge(opts) {
278
384
  // use-case ①: inject the personality style block from the state applySelectForRun just persisted.
279
385
  ...(opts.personality ? { personality: opts.personality.currentState() } : {}),
280
386
  });
281
- // Isolation: run in a throwaway git worktree so the agent's edits never touch the real working tree.
387
+ // Isolation: atomically reserve an unpredictable private temp container, then let git create the absent
388
+ // worktree path inside it. Cleanup is armed only after git succeeds and the resulting directory is verified.
282
389
  const isolate = opts.isolation === 'worktree';
283
390
  if (opts.signal?.aborted)
284
391
  return cancelledExecutionResult(undefined);
285
- // Reserve a parent namespace atomically before handing its child path to Git. The parent stays owned until
286
- // cleanup completes, so another run cannot reuse the worktree path between Git removal and disk cleanup.
287
- const worktreeReservation = isolate ? mkdtempSync(joinPath(tmpdir(), 'evolver-wt-')) : undefined;
288
- const workDir = worktreeReservation ? joinPath(worktreeReservation, 'worktree') : opts.cwd;
289
- let worktreeRegistered = false;
392
+ const reservation = isolate ? reserveWorktreePath() : undefined;
393
+ const workDir = reservation?.workDir ?? opts.cwd;
394
+ let worktreeIdentity;
395
+ let result;
290
396
  let observedRun;
291
397
  let patchRef;
292
398
  let ownsPatchRef = false;
293
399
  let preservePatchRef = false;
294
400
  let failedProof;
295
- const proofGit = async (args, cwd, onResolved) => {
401
+ const proofGit = async (args, cwd, checkCancellationAfter = true) => {
296
402
  if (opts.signal?.aborted)
297
403
  throw new ExecBridgeRunCancelledError();
298
404
  try {
299
405
  const output = await git(args, cwd, opts.signal);
300
- onResolved?.();
301
- if (opts.signal?.aborted)
406
+ if (checkCancellationAfter && opts.signal?.aborted)
302
407
  throw new ExecBridgeRunCancelledError();
303
408
  return output;
304
409
  }
@@ -311,8 +416,11 @@ export function makeClaudeExecBridge(opts) {
311
416
  }
312
417
  };
313
418
  try {
314
- if (isolate) {
315
- await proofGit(['worktree', 'add', '--detach', workDir, 'HEAD'], opts.cwd, () => { worktreeRegistered = true; });
419
+ if (reservation) {
420
+ await proofGit(['worktree', 'add', '--detach', workDir, 'HEAD'], opts.cwd, false);
421
+ worktreeIdentity = verifyWorktreePath(reservation);
422
+ if (opts.signal?.aborted)
423
+ throw new ExecBridgeRunCancelledError();
316
424
  }
317
425
  const run = await agent(prompt, {
318
426
  cwd: workDir,
@@ -443,7 +551,7 @@ export function makeClaudeExecBridge(opts) {
443
551
  reason = summarizeViolations(violations);
444
552
  }
445
553
  preservePatchRef = patchRef !== undefined && ownsPatchRef;
446
- return {
554
+ result = {
447
555
  outcome: { status: passed ? 'success' : 'failed', score, ...(reason ? { reason } : {}) },
448
556
  proofOfWork: proof,
449
557
  strongEvidence: passed && stat.files > 0,
@@ -457,12 +565,22 @@ export function makeClaudeExecBridge(opts) {
457
565
  }
458
566
  catch (error) {
459
567
  if (error instanceof ExecBridgeRunCancelledError || opts.signal?.aborted) {
460
- return cancelledExecutionResult(observedRun);
568
+ result = cancelledExecutionResult(observedRun);
461
569
  }
462
- if (error instanceof GitProofError && observedRun) {
463
- return failedProofExecutionResult(observedRun, error, failedProof);
570
+ else if (error instanceof GitProofError && observedRun) {
571
+ result = failedProofExecutionResult(observedRun, error, failedProof);
572
+ }
573
+ else {
574
+ if (reservation) {
575
+ try {
576
+ await cleanupWorktreeReservation(reservation, worktreeIdentity, git, opts.cwd);
577
+ }
578
+ catch {
579
+ // Preserve the primary execution error. The abandoned reservation remains private and diagnosable.
580
+ }
581
+ }
582
+ throw error;
464
583
  }
465
- throw error;
466
584
  }
467
585
  finally {
468
586
  if (!preservePatchRef && ownsPatchRef && patchRef) {
@@ -471,21 +589,18 @@ export function makeClaudeExecBridge(opts) {
471
589
  }
472
590
  catch { /* best-effort cleanup must not replace the execution result or its primary failure */ }
473
591
  }
474
- if (isolate) {
475
- let worktreeRemoved = false;
476
- try {
477
- await git(['worktree', 'remove', '--force', workDir], opts.cwd, undefined, { processSignalMode: 'ignore' });
478
- worktreeRemoved = true;
479
- }
480
- catch { /* best-effort cleanup */ }
481
- const addFailedBeforeCreatingWorktree = !worktreeRegistered && !existsSync(workDir);
482
- if (worktreeReservation && (worktreeRemoved || addFailedBeforeCreatingWorktree)) {
483
- try {
484
- rmSync(worktreeReservation, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
485
- }
486
- catch { /* best-effort cleanup of the owned reservation */ }
487
- }
592
+ }
593
+ if (reservation) {
594
+ try {
595
+ await cleanupWorktreeReservation(reservation, worktreeIdentity, git, opts.cwd);
596
+ }
597
+ catch (error) {
598
+ // A successful run must not hide abandoned edits/resources. Failed and cancelled results retain their
599
+ // primary classification; the private reservation remains available for diagnosis.
600
+ if (result.outcome.status === 'success')
601
+ throw error;
488
602
  }
489
603
  }
604
+ return result;
490
605
  };
491
606
  }
@@ -10,6 +10,7 @@ const STATE_VERSION = 1;
10
10
  const DEFAULT_DEDUP_WINDOW_MS = 24 * 60 * 60 * 1000;
11
11
  const DEFAULT_RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000;
12
12
  const DEFAULT_MAX_SUBMISSIONS = 2;
13
+ const PENDING_RESOLUTION_GRACE_MS = 5 * 60 * 1000;
13
14
  const MAX_IDS = 5;
14
15
  const MAX_REMOTE_RECONCILE_PAGES = 10;
15
16
  const REMOTE_RECONCILE_PAGE_SIZE = 100;
@@ -902,6 +903,11 @@ function safeIsoNow(now) {
902
903
  }
903
904
  return new Date().toISOString();
904
905
  }
906
+ function pendingSubmissionIsLive(reservedAt, nowMs) {
907
+ const reservedAtMs = Date.parse(reservedAt);
908
+ return !Number.isFinite(reservedAtMs)
909
+ || nowMs - reservedAtMs < PENDING_RESOLUTION_GRACE_MS;
910
+ }
905
911
  function latestSubmission(state, fingerprint) {
906
912
  for (let index = state.submissions.length - 1; index >= 0; index -= 1) {
907
913
  const record = state.submissions[index];
@@ -1353,6 +1359,18 @@ export async function submitIssueDraft(draft, submit, options) {
1353
1359
  status: 'rejected',
1354
1360
  updatedAt: priorRejection.rejectedAt,
1355
1361
  });
1362
+ const submittedGuard = {
1363
+ version: STATE_VERSION,
1364
+ fingerprint: prepared.draft.fingerprint,
1365
+ attemptId: prepared.attemptId,
1366
+ reservedAt: prepared.reservedAt,
1367
+ status: 'submitted',
1368
+ };
1369
+ writeSubmissionGuard(options.rootDir, submittedGuard);
1370
+ state.reservations = state.reservations.filter((record) => !(record.fingerprint === prepared.draft.fingerprint
1371
+ && record.attemptId === prepared.attemptId
1372
+ && record.reservedAt === prepared.reservedAt));
1373
+ writeState(options.rootDir, state);
1356
1374
  throw new Error('issue_report_rejected_before_finalize');
1357
1375
  }
1358
1376
  const priorSubmission = latestSubmission(state, prepared.draft.fingerprint);
@@ -1498,18 +1516,21 @@ export function resolveIssueSubmission(fingerprint, resolution, options) {
1498
1516
  const storedGuard = readSubmissionGuard(options.rootDir, fingerprint);
1499
1517
  const guard = recoverPreparingReservation(options.rootDir, state, quotaIndex, storedGuard);
1500
1518
  const reservations = state.reservations.filter((record) => record.fingerprint === fingerprint);
1501
- if (guard?.status === 'pending'
1502
- || reservations.some((record) => record.status === 'pending')) {
1519
+ const timestamp = safeIsoNow(options.now);
1520
+ const nowMs = Date.parse(timestamp);
1521
+ if ((guard?.status === 'pending' && pendingSubmissionIsLive(guard.reservedAt, nowMs))
1522
+ || reservations.some((record) => (record.status === 'pending' && pendingSubmissionIsLive(record.reservedAt, nowMs)))) {
1503
1523
  throw new IssueDraftConflictError('issue_report_submission_in_flight');
1504
1524
  }
1505
1525
  if (guard?.status === 'submitted' && resolution.outcome !== 'submitted') {
1506
1526
  throw new IssueDraftConflictError('issue_report_submission_ambiguous');
1507
1527
  }
1508
- const reservation = reservations.filter((record) => record.status === 'ambiguous').at(-1);
1528
+ const reservation = reservations.filter((record) => record.status === 'ambiguous' || record.status === 'pending').at(-1);
1509
1529
  const quotaEntry = quotaIndex.entries.filter((entry) => entry.fingerprint === fingerprint).at(-1);
1510
1530
  const attempt = state.attempts.filter((record) => record.fingerprint === fingerprint).at(-1);
1511
1531
  const guardEvidence = guard?.status === 'ambiguous'
1512
1532
  || (guard?.status === 'submitted' && resolution.outcome === 'submitted')
1533
+ || guard?.status === 'pending'
1513
1534
  ? guard
1514
1535
  : storedGuard?.status === 'preparing' ? storedGuard : undefined;
1515
1536
  if (!guardEvidence && !reservation && !quotaEntry && !attempt) {
@@ -1523,7 +1544,6 @@ export function resolveIssueSubmission(fingerprint, resolution, options) {
1523
1544
  ?? reservation?.reservedAt
1524
1545
  ?? quotaEntry?.countedAt
1525
1546
  ?? attempt.attemptedAt;
1526
- const timestamp = safeIsoNow(options.now);
1527
1547
  if (resolution.outcome === 'submitted') {
1528
1548
  const submitted = {
1529
1549
  ...draft,
@@ -20,9 +20,11 @@ export interface AcquireLockOptions {
20
20
  * The lock file records the owner pid and token. If a waiter finds the lock held by a pid that is no longer
21
21
  * alive (the owner crashed without releaseLock), it reclaims the stale lock instead of spinning
22
22
  * until timeout — otherwise one crashed process would deadlock every future writer until the file
23
- * is removed by hand. Acquisition stays atomic (O_EXCL), and stale reclaim moves the old lock aside
24
- * under a mutation guard before deletion so waiters cannot delete each other's newly-created locks.
25
- * A live owner's lock (including this process's own) is never stolen.
23
+ * is removed by hand. Empty or truncated locks are reclaimed only after the same inode and contents
24
+ * remain malformed for a grace period, so a live creator can finish publishing its owner payload.
25
+ * Acquisition stays atomic (O_EXCL), and stale reclaim moves the old lock aside under a mutation
26
+ * guard, then verifies the inode snapshot before deletion. A live owner's lock (including this
27
+ * process's own) is never stolen.
26
28
  *
27
29
  * NOTE: still synchronous (blocks the event loop while waiting) by design — it guards short
28
30
  * synchronous critical sections (append-only writes).