Problem
A running claw can produce many agent_activity / tool-call rows during bootstrap and implementation. Those rows are persisted in the same messages table as user/claw conversation messages. The current history APIs and UI load a fixed raw message window, so high-volume activity can crowd out the actual conversation history.
Observed UI symptom:
- A claw card initially showed lots of startup/progress history: repo clone, Nix validation, PR creation, etc.
- After refresh or flipping the claw card, the visible history appeared to lose the earlier initialization messages.
- The card instead showed a compact activity summary such as many earlier tool calls plus only the latest claw/user messages.
This makes it look like history was wiped, even though the data is likely still in SQLite.
Investigation
For claw 4842c931-02aa-460e-aa82-fdb9531384f5, the default messages endpoint returned exactly 100 rows:
curl -sS -H "Authorization: Bearer ..." \
"http://localhost:8080/api/messages/4842c931-02aa-460e-aa82-fdb9531384f5" \
| jq '{
total: length,
by_role: (group_by(.role) | map({role: .[0].role, count: length}))
}'
Result:
{
"total": 100,
"by_role": [
{ "role": "activity", "count": 98 },
{ "role": "claw", "count": 1 },
{ "role": "user", "count": 1 }
]
}
This strongly suggests the visible history is not being deleted. Instead, the default latest-100 raw message window is almost entirely activity rows, so earlier user/claw messages fall outside the loaded window.
Relevant code paths:
- Activity rows are persisted as normal messages with
role='activity' and format='activity:<json>' in pkg/hub/server.go.
GET /api/messages/:claw defaults to the latest 100 raw rows from messages.
- The dashboard/card path calls
loadMessages(c.id) once for each claw and does not page older rows in the card view.
- The full conversation view has
useWindowedMessages, but it pages raw message rows and keeps a small window, so scrolling can still fetch mostly activity instead of reliably reaching older conversation messages.
- The browser cache intentionally drops transient live activity rows, so after refresh the UI depends on the API window.
Root Cause
The API and UI treat the raw messages table as the conversation timeline. This table now contains both conversation messages and high-volume activity/tool telemetry. Because pagination is row-count based across all roles, activity rows can consume almost the entire history window.
The UX requirement is different:
- Conversation messages should remain discoverable and pageable from newest to oldest until the first message.
- Tool/activity rows should remain available and expandable.
- Activity rows should not consume the same fixed page budget as user/claw/hub conversation messages.
Desired Behavior
Default history loading should return conversation timeline units, not raw DB rows.
A timeline unit can be:
user message
claw message
hub message
- selected non-hidden
system message if needed
activity_summary placeholder representing activity rows between/around conversation messages
The UI should still show compact summaries such as:
Show 165 earlier tool calls
But those summaries should not count against the same message window in a way that hides actual conversation messages.
When the user expands an activity summary, the UI should fetch the underlying activity rows asynchronously and render/store them under that summary.
When the user scrolls upward in the full conversation view, the UI should page older conversation timeline units and eventually reach the first conversation message, regardless of how many activity rows happened between messages.
Proposed Design
API
Add or revise an endpoint that returns timeline units rather than raw rows. For example:
GET /api/messages/:claw/timeline?before=<cursor>&limit=50
Response shape could extend the current message schema with a summary type:
[
{
"id": "msg-1",
"role": "user",
"content": "...",
"created_at": "2026-06-06T..."
},
{
"id": "activity-summary-...",
"role": "activity_summary",
"count": 165,
"from": "2026-06-06T...",
"to": "2026-06-06T...",
"cursor": "..."
},
{
"id": "msg-2",
"role": "claw",
"content": "Implementation complete...",
"created_at": "2026-06-06T..."
}
]
Add a separate endpoint for activity details:
GET /api/messages/:claw/activity?from=<timestamp>&to=<timestamp>&limit=<n>&before=<cursor>
or:
GET /api/messages/:claw/activity-runs/:summary_id
The activity detail endpoint should return the raw persisted role='activity' messages for that summary/range.
UI
- Use the timeline endpoint for initial history loads in both full conversation view and card/dashboard view.
- Render
activity_summary inline using the existing collapsed activity-summary UI language.
- On expand, fetch activity details for that summary and store them keyed by summary ID/range.
- Do not auto-load all activity rows on page load. Load activity details only on expansion, with optional prefetch for the latest visible summary if needed.
- Keep upward scroll pagination in the full view, but page timeline units, not raw rows.
- Ensure repeated upward scrolling can eventually reach the first user/claw/hub message.
Compatibility
The existing GET /api/messages/:claw endpoint can remain for compatibility, but the React app should migrate to the new timeline API. Alternatively, the existing endpoint can grow a mode parameter such as ?view=timeline, but a separate endpoint is clearer and avoids breaking existing callers.
Implementation Plan
-
Add server-side timeline query logic.
- Page over non-activity conversation rows first.
- Detect/count activity rows that fall between adjacent conversation rows or before/after page boundaries.
- Emit
activity_summary units with stable IDs and range metadata.
-
Add server-side activity detail endpoint.
- Accept summary range/cursor.
- Return activity rows ordered by
created_at.
- Keep existing activity mapping through
format='activity:<json>'.
-
Add shared frontend types/mappers for timeline units.
- Preserve existing
Message shape where possible.
- Add
ActivitySummaryMessage or a discriminated timeline type.
-
Update useHub.loadMessages and useWindowedMessages.
- Load timeline units instead of raw rows.
- Page older timeline units on scroll.
- Keep expanded activity detail state separate from the base timeline.
-
Update conversation/card rendering.
- Render summary placeholders inline.
- On expand, fetch detail rows and render them within the summary.
- Avoid putting all expanded activity rows into the same global message list unless needed.
-
Add tests.
- Server test: a claw with many activity rows and older user/claw messages returns conversation rows plus an activity summary, not 100 activity rows.
- Server test: activity detail endpoint returns the expected activity rows for a summary/range.
- Frontend/unit test if available: scrolling older history requests timeline pages and preserves summary expansion behavior.
Acceptance Criteria
- A claw with hundreds of tool/activity rows still shows recent user/claw conversation messages after refresh.
- The UI displays compact activity summaries such as
Show 165 earlier tool calls.
- Expanding a summary loads and displays the corresponding persisted activity rows.
- Scrolling upward in the full conversation view eventually reaches the first conversation message.
- Activity rows no longer crowd out user/claw/hub messages from the default visible history window.
- Existing message send/stream behavior remains unchanged.
Problem
A running claw can produce many
agent_activity/ tool-call rows during bootstrap and implementation. Those rows are persisted in the samemessagestable as user/claw conversation messages. The current history APIs and UI load a fixed raw message window, so high-volume activity can crowd out the actual conversation history.Observed UI symptom:
This makes it look like history was wiped, even though the data is likely still in SQLite.
Investigation
For claw
4842c931-02aa-460e-aa82-fdb9531384f5, the default messages endpoint returned exactly 100 rows:Result:
{ "total": 100, "by_role": [ { "role": "activity", "count": 98 }, { "role": "claw", "count": 1 }, { "role": "user", "count": 1 } ] }This strongly suggests the visible history is not being deleted. Instead, the default latest-100 raw message window is almost entirely activity rows, so earlier user/claw messages fall outside the loaded window.
Relevant code paths:
role='activity'andformat='activity:<json>'inpkg/hub/server.go.GET /api/messages/:clawdefaults to the latest 100 raw rows frommessages.loadMessages(c.id)once for each claw and does not page older rows in the card view.useWindowedMessages, but it pages raw message rows and keeps a small window, so scrolling can still fetch mostly activity instead of reliably reaching older conversation messages.Root Cause
The API and UI treat the raw
messagestable as the conversation timeline. This table now contains both conversation messages and high-volume activity/tool telemetry. Because pagination is row-count based across all roles, activity rows can consume almost the entire history window.The UX requirement is different:
Desired Behavior
Default history loading should return conversation timeline units, not raw DB rows.
A timeline unit can be:
usermessageclawmessagehubmessagesystemmessage if neededactivity_summaryplaceholder representing activity rows between/around conversation messagesThe UI should still show compact summaries such as:
But those summaries should not count against the same message window in a way that hides actual conversation messages.
When the user expands an activity summary, the UI should fetch the underlying activity rows asynchronously and render/store them under that summary.
When the user scrolls upward in the full conversation view, the UI should page older conversation timeline units and eventually reach the first conversation message, regardless of how many activity rows happened between messages.
Proposed Design
API
Add or revise an endpoint that returns timeline units rather than raw rows. For example:
Response shape could extend the current message schema with a summary type:
[ { "id": "msg-1", "role": "user", "content": "...", "created_at": "2026-06-06T..." }, { "id": "activity-summary-...", "role": "activity_summary", "count": 165, "from": "2026-06-06T...", "to": "2026-06-06T...", "cursor": "..." }, { "id": "msg-2", "role": "claw", "content": "Implementation complete...", "created_at": "2026-06-06T..." } ]Add a separate endpoint for activity details:
or:
The activity detail endpoint should return the raw persisted
role='activity'messages for that summary/range.UI
activity_summaryinline using the existing collapsed activity-summary UI language.Compatibility
The existing
GET /api/messages/:clawendpoint can remain for compatibility, but the React app should migrate to the new timeline API. Alternatively, the existing endpoint can grow a mode parameter such as?view=timeline, but a separate endpoint is clearer and avoids breaking existing callers.Implementation Plan
Add server-side timeline query logic.
activity_summaryunits with stable IDs and range metadata.Add server-side activity detail endpoint.
created_at.format='activity:<json>'.Add shared frontend types/mappers for timeline units.
Messageshape where possible.ActivitySummaryMessageor a discriminated timeline type.Update
useHub.loadMessagesanduseWindowedMessages.Update conversation/card rendering.
Add tests.
Acceptance Criteria
Show 165 earlier tool calls.