Skip to content
Merged
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
14 changes: 11 additions & 3 deletions plugin/src/claude_smart/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@
}
)

# Fields actually put on the wire. `created_at` is a real InteractionData field
# — so it must stay in the set above, which is pinned against the model — but it
# must NOT be emitted. Sending a buffered event time backdates the interaction,
# and the extractor's bookmark is keyed on `created_at` (`created_at >= ?`), so a
# batch recovered after the bookmark moved is stored and then never extracted:
# permanent, silent loss of learning data on exactly the offline-recovery path
# this buffer exists for. Letting the server stamp its own time is the lesser
# evil until ingest ordering stops depending on caller-supplied event time.
_WIRE_FIELDS = _INTERACTION_DATA_FIELDS - {"created_at"}

_VALID_CITATION_KINDS = frozenset(
{"playbook", "profile", "user_playbook", "agent_playbook"}
)
Expand Down Expand Up @@ -436,9 +446,7 @@ def unpublished_slice(
# turn that rot into a publish failure this plugin's adapter swallows
# without advancing its watermark.
turn = {
key: value
for key, value in record.items()
if key in _INTERACTION_DATA_FIELDS
key: value for key, value in record.items() if key in _WIRE_FIELDS
}
turn["role"] = role
# NOTE: deliberately does NOT send `created_at`. Carrying the buffer's
Expand Down
18 changes: 18 additions & 0 deletions tests/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,24 @@ def test_allowlist_covers_every_installed_model_field(self):
" the slicer would silently drop them"
)

def test_created_at_is_never_emitted_even_when_present(self):
"""A literal `created_at` in the buffer must not reach the wire.

The field is in the model-contract set (it is a real InteractionData
field), so filtering on that set alone let it through. Only `ts` was
tested, which could not catch this. Backdating an interaction hides it
from the extractor permanently — see `_WIRE_FIELDS`.
"""
_, turns = state.unpublished_slice(
[{"ts": 1, "role": "User", "content": "x", "created_at": 999}]
)
assert "created_at" not in turns[0], turns[0]

def test_wire_fields_excludes_created_at_but_contract_set_keeps_it(self):
assert "created_at" in state._INTERACTION_DATA_FIELDS
assert "created_at" not in state._WIRE_FIELDS
assert state._WIRE_FIELDS < state._INTERACTION_DATA_FIELDS

def test_buffer_timestamp_is_not_sent(self):
"""`created_at` must stay off the wire.

Expand Down
Loading