Switch Claude execution from services to ephemeral exec sessions#102
Switch Claude execution from services to ephemeral exec sessions#102mcintyre94 merged 3 commits intomainfrom
Conversation
Services restart automatically when a sprite wakes, causing Claude to re-execute stale prompts and creating runaway duplicate sessions. Exec sessions don't restart on wake and persist for max_run_after_disconnect so the app can reattach after backgrounding. - ExecEvent: split .data into .stdout / .stderr to keep heartbeat noise out of the NDJSON parser buffer - ChatViewModel: replace service streaming with exec WebSocket sessions; add processExecStream, reattachToExec, restoreFromSessionFile; update reconnectIfNeeded to reattach via execSessionId; interrupt() now kills the exec session instead of deleting a service - SpriteChat: add execSessionId field for reattach across app restarts - SpritesAPIClient: add killExecSession, cleanupLegacyServices; remove streamService, streamServiceLogs, getServiceStatus, deleteService, ServiceLogsProvider protocol - One-time migration: cleanupLegacyServices deletes leftover wisp-claude-* and wisp-quick-* services on first launch; becomes a no-op once all currentServiceName fields are cleared Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When reattaching after a full app close, an expired exec session causes a WebSocket connection error, which processExecStream returns as .timedOut (no data received). The restore condition only checked for .disconnected, so restoreFromSessionFile was never reached. Now both .timedOut and .disconnected trigger session file restore when a sessionId is available. Also clears any error status set by processExecStream before attempting the restore. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code review1 issue found. Compilation error: StreamResult does not conform to Equatable File: WispTests/ChatViewModelTests.swift, in the new test reattachToExec_setsLastSessionCompleteWhenResultReceived The test uses #expect(result == .completed) where result is of type StreamResult. StreamResult is declared as enum StreamResult: CustomStringConvertible with no Equatable conformance, so the == operator is not synthesized and this will fail to compile. All other tests in this file correctly use pattern matching instead. Fix Option A: Add Equatable to the StreamResult declaration in ChatViewModel.swift: Fix Option B: Change the assertion to pattern matching (consistent with the rest of the test suite): |
Fixes compilation error in ChatViewModelTests where #expect(result == .completed) requires Equatable conformance. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Summary
PUT /services/{name}) with ephemeral exec WebSocket sessions. Services restart automatically on sprite wake, causing Claude to re-execute old prompts and burn tokens; exec sessions do not restart.max_run_after_disconnect=3600s, allowing the app to reattach after backgrounding. If the session has expired (sprite slept), falls back to restoring history from Claude's.jsonlsession file on the sprite.ExecEvent.datainto.stdout/.stderrto prevent heartbeat noise from corrupting the NDJSON parser.wisp-claude-*andwisp-quick-*services on existing sprites (gated oncurrentServiceName != nilso it's a no-op after first run).Changes
ExecSession— split.data(Data)into.stdout(Data)/.stderr(Data)SpriteChat— addexecSessionId: String?for WebSocket reattachSpritesAPIClient— remove service streaming methods; addkillExecSession,cleanupLegacyServicesChatViewModel— replaceprocessServiceStream/runReconnectLoopwithprocessExecStream/reattachToExec/restoreFromSessionFile; fix session restore to trigger on both.timedOutand.disconnectedresultsChatViewModelTests— replaceMockServiceLogsProviderwithmakeExecStreamhelper; update reconnect testsTest plan
--resume {sessionId}wisp-claude-*/wisp-quick-*services cleaned up🤖 Generated with Claude Code