Wisplog as single source of truth for chat history#136
Conversation
Eliminates SwiftData message persistence entirely, replacing it with the on-sprite wisplog file as the canonical record of conversation history. Key changes: - SpriteChat: remove messagesData, streamEventUUIDsData, lastSessionComplete - PersistedChatMessage.swift: deleted - WispSchema.swift: add SchemaV1→V2 lightweight migration for column drops - parseWispLog: returns (messages, sessionId, eventUUIDs) so reattach can seed processedEventUUIDs from the wisplog and avoid replaying duplicates - loadFromWispLog: seeds processedEventUUIDs, sets firstMessagePreview, calls saveSession/restoreUndeliveredDraft to keep exec state consistent - reconnectIfNeeded: simplified — if execSessionId set, reattach; else if messages empty, load wisplog; else restoreUndeliveredDraft - reattachToExec: pre-loads wisplog before attaching to exec WebSocket, seeds UUID set from the wisplog to prevent duplicate content; on timeout reloads wisplog instead of calling restoreFromSessionFile - restoreFromSessionFile: deleted - executeClaudeCommand: removes all persistMessages calls and the 1-second periodic persist; on disconnect schedules reconnect without persisting - processExecStream: removes periodic persist, sessionInfo calls saveSession without isComplete - loadSession: no longer loads messages from SwiftData; metadata only - saveSession: removes isComplete parameter; result events clear execSessionId directly so execSessionId==nil signals a completed session - DashboardView: queries execSessionId != nil instead of !lastSessionComplete - fork path: replaces saveMessages with an on-sprite awk command that copies the parent wisplog up to the fork point into the new chat's wisplog - importJSONLSession: calls loadFromWispLog after jq conversion instead of persistMessages - SpriteChatMigration: no longer copies messagesData - Tests: remove all SwiftData persistence tests; update parseWispLog callers for the new 3-tuple return; update migration and checkpoint tests Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
test connection |
|
Code review — 2 bugs and 1 CLAUDE.md violation found. Bug 1: loadFromWispLog incorrectly clears execSessionId for in-progress sessions wisp/Wisp/ViewModels/ChatViewModel.swift Lines 720 to 727 in 472df5f The else branch fires whenever the last wisplog message is not a .user message — but an in-progress session (exec still running, Claude mid-stream) also ends with a partial assistant tail. loadFromWispLog clears execSessionId and calls saveSession for a live session. reattachToExec then hits the guard immediately after and bails: wisp/Wisp/ViewModels/ChatViewModel.swift Lines 1282 to 1290 in 472df5f Impact: A user who backgrounds the app mid-stream and returns will have their in-flight Claude response silently abandoned. The execSessionId is also cleared in SwiftData, so subsequent dashboard reconnects won't pick this chat up. Root cause: parseWispLog returns no signal about whether a result event was seen, so the caller can't distinguish a completed session from a still-running one — both end with an assistant tail. Fix: return whether a result event was observed, and only clear execSessionId when confirmed complete. Bug 2: processedEventUUIDs blocks the exec replay from rebuilding the removed content wisp/Wisp/ViewModels/ChatViewModel.swift Lines 1299 to 1309 in 472df5f loadFromWispLog (called just above at line ~1281) sets processedEventUUIDs = seenUUIDs — which includes the UUIDs of every event that built the trailing assistant message being removed here. When the exec WebSocket replays those events, handleOrSkip finds their UUIDs already in processedEventUUIDs and skips them without emitting content. wisp/Wisp/ViewModels/ChatViewModel.swift Lines 1166 to 1203 in 472df5f Impact: The comment 'the exec replay will rebuild it' is incorrect. The new empty assistant bubble will only receive truly new events arriving after the wisplog snapshot. In the common case where the wisplog is fully caught up, the bubble stays empty for the rest of the turn. Fix options: (a) before removing the last message, remove its events' UUIDs from processedEventUUIDs, or (b) don't pre-seed processedEventUUIDs from the wisplog at all. Note: this bug is only reachable once Bug 1 is fixed. CLAUDE.md violation: Missing test for parseWispLog's new eventUUIDs return value wisp/WispTests/ChatViewModelTests.swift Lines 1261 to 1265 in 472df5f Per CLAUDE.md (https://github.com/mcintyre94/wisp/blob/472df5f3c7e131bf6e5cd7ce17cd93ac45b1c7d4/CLAUDE.md): 'Add new unit tests when adding or modifying logic (models, parsers, utilities, view models)'. The PR adds eventUUIDs: Set as a new return value from parseWispLog, collecting event UUIDs from the wisplog. This output directly drives deduplication in loadFromWispLog (processedEventUUIDs = seenUUIDs). All test call sites discard it with _ — no test verifies that events with UUIDs are collected, events without UUIDs don't break the parse, or that empty input returns an empty set. Disregard the earlier 'test connection' comment — posted during review tooling setup. |
The file was deleted from disk but its four references (PBXBuildFile, PBXFileReference, group entry, Sources build phase entry) remained in project.pbxproj, causing the CI build to fail with "Build input file cannot be found". Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Bug 1: loadFromWispLog cleared execSessionId for in-progress sessions. Both completed and in-progress sessions end with an assistant tail in the wisplog, so the caller had no way to distinguish them. Add isComplete: Bool to parseWispLog's return (set only when a result event is observed) and gate the execSessionId clear + saveSession on isComplete. Bug 2: reattachToExec stripped the trailing assistant then seeded processedEventUUIDs from the wisplog — meaning the exec replay skipped the events that would have rebuilt the removed content, leaving the new bubble permanently empty. Fix: keep the trailing assistant as the streaming target; processedEventUUIDs correctly prevents the replay from duplicating content already shown, and only truly new events extend it. Test: add parseWispLog_isCompleteAndEventUUIDs covering the new isComplete field and UUID collection, addressing the CLAUDE.md test requirement. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The file was created on disk but never registered in project.pbxproj, causing 'cannot find WispMigrationPlan in scope' build errors in CI. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Summary
Eliminates SwiftData message persistence entirely. The on-sprite wisplog file (
/home/sprite/.wisp/chats/UUID.wisplog) is now the canonical record of conversation history — written in real-time bytee -aduring streaming, read on demand when needed.Removed:
SpriteChat.messagesData,streamEventUUIDsData,lastSessionCompletePersistedChatMessage.swift(whole file)persistMessages(14 call sites)restoreFromSessionFilelinkToolResults(only needed for SwiftData round-trip)processExecStreamChanged:
parseWispLognow returnseventUUIDs: Set<String>soreattachToExeccan seedprocessedEventUUIDsfrom the wisplog, preventing duplicate content when the exec session replays events already shownreconnectIfNeededsimplified: execSessionId set → reattach; messages empty → load wisplog; otherwise → restore draftreattachToExecpre-loads the wisplog before connecting to the exec WebSocket (instant history display during reconnect), then streams live events with UUID deduplicationsaveSessiondrops theisCompleteparameter; the result event clearsexecSessionIddirectly, soexecSessionId == nilsignals a completed sessionexecSessionId != nilinstead of!lastSessionCompleteawkcommand copies the parent wisplog up to the fork point into the new chat's wisplog (replacessaveMessages+PersistedChatMessageserialisation)importJSONLSession: callsloadFromWispLogafter jq conversion (already wrote the wisplog, no separate persist needed)WispSchema.swiftwith aSchemaV1 → SchemaV2lightweight migration to drop the removed columnsTrade-offs
catthe wisplog) instead of an instant SwiftData read. The sprite is typically warm when you're actively using a chat.checkpointId/checkpointCommentonChatMessage): in-memory only, not persisted. Can be fixed later by adding awisp_checkpointevent type to the wisplog format.Test plan
🤖 Generated with Claude Code