Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/basic_memory/indexing/input_file_adaptation.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ def checksum(self) -> str | None:
def last_modified(self) -> datetime | None:
"""Current storage last-modified timestamp, when available."""

@property
def created_at(self) -> datetime | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Model optional created_at outside the required protocol

When the advertised legacy-loader path is used, LoadedFileWithoutCreatedAt cannot satisfy this newly expanded LoadedIndexFile protocol: running Pyright on the touched files reports that created_at is not present at the added call, and the same incompatibility applies to typed IndexBatchRuntime users whose FileInfoT is a legacy loader even though getattr makes runtime execution succeed. Make the with/without-creation-time alternatives explicit in the input type instead of declaring the member required and masking its absence dynamically.

AGENTS.md reference: AGENTS.md:L133-L134

Useful? React with 👍 / 👎.

"""Current storage creation timestamp, when available.

Used as the parser fallback for notes that do not declare frontmatter
`created`. Callers that cannot provide a creation time may omit the
attribute or return None; adaptation then leaves IndexInputFile.created_at
as None and the parser falls back to now (#1772).
"""

@property
def content(self) -> bytes | None:
"""Loaded file content, when available."""
Expand All @@ -36,6 +46,12 @@ def content_type(self, path: str) -> str | None:
"""Return the canonical content type for the file path."""


def _loaded_created_at(file_info: LoadedIndexFile) -> datetime | None:
"""Read creation time from a loaded payload without requiring every adapter to declare it."""
created_at = getattr(file_info, "created_at", None)
return created_at if isinstance(created_at, datetime) else None


def build_index_input_files(
files: Mapping[str, LoadedIndexFile],
*,
Expand All @@ -49,7 +65,10 @@ def build_index_input_files(
checksum=file_info.checksum,
content_type=content_type_provider.content_type(path),
last_modified=file_info.last_modified,
created_at=None,
# Preserve storage creation time when the loader supplied it. Hardcoding
# None forced the markdown parser's ctime fallback to datetime.now() on
# every full reindex for notes without frontmatter `created` (#1772).
created_at=_loaded_created_at(file_info),
content=file_info.content,
)
for path, file_info in files.items()
Expand Down
60 changes: 57 additions & 3 deletions tests/indexing/test_input_file_adaptation.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Tests for portable index input-file adaptation."""

from dataclasses import dataclass
from datetime import datetime
from datetime import datetime, timezone

from basic_memory.indexing.input_file_adaptation import build_index_input_files
from basic_memory.indexing.models import IndexInputFile


@dataclass(frozen=True, slots=True)
Expand All @@ -14,6 +15,17 @@ class LoadedFile:
checksum: str | None
last_modified: datetime | None
content: bytes | None
created_at: datetime | None = None


@dataclass(frozen=True, slots=True)
class LoadedFileWithoutCreatedAt:
"""Legacy loader shape that never advertised creation time."""

size: int
checksum: str | None
last_modified: datetime | None
content: bytes | None


class PathContentTypeProvider:
Expand All @@ -26,7 +38,8 @@ def content_type(self, path: str) -> str:


def test_build_index_input_files_derives_content_type_from_path() -> None:
last_modified = datetime.now()
last_modified = datetime.now(tz=timezone.utc)
created_at = datetime(2024, 3, 1, 9, 0, tzinfo=timezone.utc)

input_files = build_index_input_files(
{
Expand All @@ -35,6 +48,7 @@ def test_build_index_input_files_derives_content_type_from_path() -> None:
checksum="etag-note",
last_modified=last_modified,
content=b"# note",
created_at=created_at,
),
"image.png": LoadedFile(
size=3,
Expand All @@ -47,11 +61,12 @@ def test_build_index_input_files_derives_content_type_from_path() -> None:
)

assert input_files["note.md"].content_type == "text/markdown"
assert input_files["note.md"].created_at is None
assert input_files["note.md"].created_at == created_at
assert input_files["note.md"].last_modified == last_modified
assert input_files["note.md"].content == b"# note"
assert input_files["image.png"].content_type == "application/octet-stream"
assert input_files["image.png"].checksum == "etag-image"
assert input_files["image.png"].created_at is None


def test_build_index_input_files_accepts_missing_checksum_and_content() -> None:
Expand All @@ -70,3 +85,42 @@ def test_build_index_input_files_accepts_missing_checksum_and_content() -> None:
assert input_files["empty.md"].checksum == ""
assert input_files["empty.md"].content is None
assert input_files["empty.md"].created_at is None


def test_build_index_input_files_preserves_index_input_file_created_at() -> None:
"""Local readers already put created_at on IndexInputFile; adaptation must keep it (#1772)."""
created_at = datetime(2024, 3, 1, 9, 0, tzinfo=timezone.utc)
last_modified = datetime(2025, 6, 1, 9, 0, tzinfo=timezone.utc)
src = IndexInputFile(
path="notes/kept.md",
size=12,
checksum="etag-kept",
content_type="text/markdown",
last_modified=last_modified,
created_at=created_at,
content=b"# kept note",
)

input_files = build_index_input_files(
{"notes/kept.md": src},
content_type_provider=PathContentTypeProvider(),
)

assert input_files["notes/kept.md"].created_at == created_at
assert input_files["notes/kept.md"].last_modified == last_modified


def test_build_index_input_files_tolerates_loaders_without_created_at() -> None:
input_files = build_index_input_files(
{
"legacy.md": LoadedFileWithoutCreatedAt(
size=4,
checksum="etag",
last_modified=None,
content=b"body",
),
},
content_type_provider=PathContentTypeProvider(),
)

assert input_files["legacy.md"].created_at is None
Loading