Skip to content

fix: sequence store locking, MCP auto-increment, round5 precision#17

Merged
dbejarano820 merged 2 commits intomainfrom
fix/code-review-findings
Feb 22, 2026
Merged

fix: sequence store locking, MCP auto-increment, round5 precision#17
dbejarano820 merged 2 commits intomainfrom
fix/code-review-findings

Conversation

@dbejarano820
Copy link
Contributor

Fixes from Code Review

Addresses 3 code issues found during the full codebase review.

🔒 #4 — Sequence store race condition

File: packages/sdk/src/config/sequence-store.ts

Added file-based locking via atomic mkdir to prevent concurrent getNextSequence() calls from reading the same value and producing duplicate sequence numbers. Lock includes timeout (5s) and stale lock recovery.

🔢 #5 — MCP create_invoice hardcoded sequence=1

File: packages/mcp/src/tools/create-invoice.ts

Integrated the create_invoice MCP tool with SequenceStore.getNextSequence() so each invoice gets a unique, auto-incrementing sequence number. Previously every invoice would have clave/consecutivo ending in 0000000001, causing 409 Conflict on submission.

🔬 #8 — round5() floating-point precision

File: packages/sdk/src/tax/calculator.ts

Replaced Math.round(value * 10^5) / 10^5 with Number(value.toFixed(5)) to avoid IEEE 754 multiplication artifacts. For example, 1.0000025 * 100000 evaluates to 100000.24999999999 in JS, which would round incorrectly with the old approach.


All 623 tests pass. Build succeeds across all 4 packages.

…ent, round5 precision

- fix(sdk): add file-based locking to sequence store to prevent race conditions
  when concurrent processes call getNextSequence() simultaneously (issue #4)

- fix(mcp): integrate create_invoice tool with SequenceStore for auto-incrementing
  sequence numbers instead of hardcoding sequence=1 on every call (issue #5)

- fix(sdk): improve round5() to use Number.toFixed(5) instead of
  Math.round(value * factor) / factor to avoid IEEE 754 floating-point
  multiplication artifacts (issue #8)

All 623 tests pass.
@greptile-apps
Copy link

greptile-apps bot commented Feb 22, 2026

Greptile Summary

This PR fixes three distinct issues identified during code review: sequence store race conditions, hardcoded MCP invoice sequences, and floating-point precision in tax rounding.

Key Changes:

  • Sequence locking (packages/sdk/src/config/sequence-store.ts): Added file-based mutex using atomic mkdir to prevent concurrent getNextSequence() calls from generating duplicate sequence numbers. Includes 5s timeout and stale lock cleanup.
  • MCP auto-increment (packages/mcp/src/tools/create-invoice.ts): Replaced hardcoded sequence=1 with getNextSequence() call so each invoice gets a unique consecutivo number.
  • Precision fix (packages/sdk/src/tax/calculator.ts): Switched round5() from Math.round(value * 10^5) / 10^5 to Number(value.toFixed(5)) to avoid IEEE 754 multiplication errors.

Issues Found:

  • The stale lock cleanup in sequence-store.ts:46-56 removes the lock but immediately throws an error without retrying. This means the lock cleanup is effectively pointless - the function should retry acquisition after removing a stale lock instead of throwing immediately.

Confidence Score: 3/5

  • Safe to merge with one logic issue that reduces effectiveness of stale lock handling
  • The PR correctly fixes all three issues and tests pass (623/623). However, the stale lock cleanup logic has a flaw where it removes the lock but throws immediately instead of retrying, making the cleanup essentially useless. The MCP and calculator changes are clean and correct.
  • packages/sdk/src/config/sequence-store.ts needs the stale lock retry logic fixed (lines 46-57)

Important Files Changed

Filename Overview
packages/sdk/src/config/sequence-store.ts Added mkdir-based file locking to prevent race conditions. One logic issue: stale lock removal doesn't retry acquisition.
packages/mcp/src/tools/create-invoice.ts Integrated getNextSequence() to auto-increment invoice numbers. Clean implementation with proper formatting.
packages/sdk/src/tax/calculator.ts Changed round5() to use toFixed() to avoid IEEE 754 multiplication artifacts. Correct fix for precision issue.

Sequence Diagram

sequenceDiagram
    participant MCP as create_invoice MCP Tool
    participant Store as SequenceStore
    participant Lock as File Lock (.sequences.lock)
    participant File as sequences.json
    
    MCP->>Store: getNextSequence("01", "001", "00001")
    Store->>Lock: mkdir(.sequences.lock) - Atomic
    
    alt Lock acquired
        Lock-->>Store: Success
        Store->>File: Read sequences.json
        File-->>Store: {"01-001-00001": 42}
        Store->>Store: Increment: 42 + 1 = 43
        Store->>File: Write atomically via temp file
        File-->>Store: Write complete
        Store->>Lock: rm(.sequences.lock)
        Store-->>MCP: Return 43
        MCP->>MCP: Build clave with sequence=43
        MCP->>MCP: Format consecutivo: "001000010000000043"
    else Lock exists (EEXIST)
        Lock-->>Store: EEXIST error
        Store->>Store: Sleep 50ms and retry
        Store->>Lock: mkdir(.sequences.lock) - Retry
    else Timeout after 5s
        Lock-->>Store: Still EEXIST
        Store->>Lock: rm(.sequences.lock) - Force cleanup
        Store-->>MCP: Throw timeout error
    end
Loading

Last reviewed commit: bbf31c1

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +46 to +57
if (Date.now() >= deadline) {
// Stale lock — force remove and retry once
try {
await rm(lockPath, { recursive: true });
} catch {
// ignore
}
throw new Error(
`Failed to acquire sequence lock at ${lockPath} after ${String(LOCK_TIMEOUT_MS)}ms. ` +
`If this persists, manually remove the lock directory.`,
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: stale lock removal happens AFTER deadline check fails.

When timeout occurs, the function removes the stale lock but then immediately throws an error. If another concurrent call removed the lock between the deadline check and the rm() call, this could throw unnecessarily.

Suggested change
if (Date.now() >= deadline) {
// Stale lock — force remove and retry once
try {
await rm(lockPath, { recursive: true });
} catch {
// ignore
}
throw new Error(
`Failed to acquire sequence lock at ${lockPath} after ${String(LOCK_TIMEOUT_MS)}ms. ` +
`If this persists, manually remove the lock directory.`,
);
}
if (Date.now() >= deadline) {
// Stale lock — force remove and retry once
try {
await rm(lockPath, { recursive: true });
// Retry acquisition after removing stale lock
continue;
} catch {
// ignore
}
throw new Error(
`Failed to acquire sequence lock at ${lockPath} after ${String(LOCK_TIMEOUT_MS)}ms. ` +
`If this persists, manually remove the lock directory.`,
);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/sdk/src/config/sequence-store.ts
Line: 46-57

Comment:
Race condition: stale lock removal happens AFTER deadline check fails.

When timeout occurs, the function removes the stale lock but then immediately throws an error. If another concurrent call removed the lock between the deadline check and the rm() call, this could throw unnecessarily.

```suggestion
        if (Date.now() >= deadline) {
          // Stale lock — force remove and retry once
          try {
            await rm(lockPath, { recursive: true });
            // Retry acquisition after removing stale lock
            continue;
          } catch {
            // ignore
          }
          throw new Error(
            `Failed to acquire sequence lock at ${lockPath} after ${String(LOCK_TIMEOUT_MS)}ms. ` +
              `If this persists, manually remove the lock directory.`,
          );
        }
```

How can I resolve this? If you propose a fix, please make it concise.

Address Greptile review: after removing a stale lock, continue the
loop to retry mkdir instead of immediately throwing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dbejarano820 dbejarano820 merged commit 6aef909 into main Feb 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant