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
2 changes: 2 additions & 0 deletions changes/485.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**Activity shows what a chat tool was given on Windows again.** A line like "query: Saturday plans" could be missing
under a tool Sentient used in a chat, because two messages saved in the same instant got mixed up.
6 changes: 4 additions & 2 deletions sentient/activity/timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,10 +335,12 @@ def _chat_item(self, row: Any) -> Item:
app_label=label, ref=ref, undo=undo, _args=args, _apps=also)

async def _call_arguments(self, session_id: str, call_id: str, at: str) -> dict | None:
"""The arguments a chat tool call was given, read from the assistant message that made it."""
"""The arguments a chat tool call was given, read from the assistant message that made it. Found by the call's
id: Windows' clock can give a tool result and the next call the same ``created_at`` (#485)."""
row = await self.store.fetchone(
"SELECT tool_calls FROM messages WHERE session_id = ? AND role = 'assistant' AND created_at <= ?"
" AND tool_calls IS NOT NULL ORDER BY created_at DESC LIMIT 1", (session_id, at),
" AND tool_calls IS NOT NULL AND instr(tool_calls, ?) > 0 ORDER BY created_at DESC LIMIT 1",
(session_id, at, json.dumps(call_id)), # quoted and escaped as stored; the loop below checks the id
)
calls = _loads(row["tool_calls"]) if row else None
for call in calls if isinstance(calls, list) else []:
Expand Down
4 changes: 2 additions & 2 deletions tests/test_claude_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -822,8 +822,8 @@ async def one() -> str:
return await llm.complete_text("fast", [{"role": "user", "content": "hi"}])

first = asyncio.create_task(one())
for _ in range(300):
if claude_code._live:
for _ in range(300): # running and logged, not just spawned: slow to start on Windows (claude.cmd, then Python)
if claude_code._live and fake_claude.runs():
break
await asyncio.sleep(0.05)
second = asyncio.create_task(one())
Expand Down
39 changes: 39 additions & 0 deletions tests/test_timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ async def _all(s: SentientApp, **kw) -> list[dict]:
raise AssertionError("paging never ended")


async def test_a_call_id_that_json_escapes_is_still_found(config, isolated_home):
s = await _app(config, isolated_home)
try:
sid = await s.store.create_session(channel="web", title="Trip")
call = {"id": "call_é1", "type": "function",
"function": {"name": "mail_search", "arguments": json.dumps({"query": "Goa"})}}
await s.store.add_message(sid, "assistant", "", tool_calls=[call])
row = await s.store.fetchone("SELECT created_at FROM messages WHERE session_id = ?", (sid,))
assert await s.timeline._call_arguments(sid, "call_é1", row["created_at"]) == {"query": "Goa"}
finally:
await s.stop()


async def test_chat_tool_calls_and_approvals_say_what_and_why(config, isolated_home):
sent: list[str] = []
llm = FakeProvider(replies=[
Expand Down Expand Up @@ -147,6 +160,32 @@ async def test_chat_tool_calls_and_approvals_say_what_and_why(config, isolated_h
await s.stop()


async def test_a_calls_details_survive_a_shared_timestamp(config, isolated_home):
# Windows' clock (Python 3.12) ticks every 15.6 ms: a tool result and the next call often share created_at
s = await _app(config, isolated_home)
try:
sid = await s.store.create_session(channel="web", title="Weekend plans")
search = {"id": "c1", "type": "function",
"function": {"name": "mail_search", "arguments": json.dumps({"query": "Saturday plans"})}}
send = {"id": "c2", "type": "function", # its arguments mention c1, the way a reply to it might
"function": {"name": "mail_send", "arguments": json.dumps({"to": "Arjun", "body": "re c1",
"ref": "c1"})}}
await s.store.add_message(sid, "assistant", "", tool_calls=[search])
await s.store.add_message(sid, "tool", json.dumps({"results": []}), tool_call_id="c1", name="mail_search")
await s.store.add_message(sid, "assistant", "", tool_calls=[send])
# c1 is asked a tick earlier; its result and the next call (c2) land in the same tick
at = "2026-10-20T10:00:00+00:00"
await s.store.execute("UPDATE messages SET created_at = ? WHERE session_id = ?", (at, sid))
await s.store.execute(
"UPDATE messages SET created_at = ? WHERE session_id = ? AND instr(tool_calls, 'mail_search') > 0",
("2026-10-20T09:59:59.984000+00:00", sid),
)
assert await s.timeline._call_arguments(sid, "c1", at) == {"query": "Saturday plans"}
assert await s.timeline._call_arguments(sid, "c2", at) == {"to": "Arjun", "body": "re c1", "ref": "c1"}
finally:
await s.stop()


async def test_incognito_chats_never_show(config, isolated_home):
s = await _app(config, isolated_home, FakeProvider(replies=[[tool_call("mail_search", query="secret")], "ok"]))
try:
Expand Down
Loading