Add real Transcript-based session persistence (step 8)
Eighth piece of the Apple Intelligence tool-calling work (see /Users/rune/.claude/plans/apple-intelligence-tool-calling-plan.md). Reopening a saved conversation now restores the model's real tool-call/tool-output history from a saved FoundationModels.Transcript instead of falling back to Phase 1's lossy text-summary replay. Found and confirmed against the real SDK before writing any code: a live session's own .transcript property (needed to capture one to save) is macOS 27.0+ only, while reconstructing a session FROM a saved transcript works at 26.0+ — a real asymmetry, not a minor detail. Checked with Rune before proceeding: build it gated behind #available(macOS 27.0, *), same pattern this file already uses for mapProviderError's LanguageModelError/GenerationError split. Inert on macOS 26.x (falls back cleanly to the existing text-summary replay, not broken), strengthens automatically as the OS matures. AppleTranscriptService (new) persists one JSON file per conversation under Application Support/oAI/apple_transcripts/, named directly by UUID — no DB migration needed, since the filename is fully derivable from the conversation's own id (unlike notes.md, which needs a stored filename since it also embeds a human-readable display name). Transcript itself is Codable at macOS 26.0+, so only the live-session read is gated, not the encode/decode. Cleaned up on conversation deletion (DatabaseService.deleteConversation, alongside the existing notes.md cleanup) so a stale file never lingers for a deleted conversation. Only ever saved for a real, already-saved conversation (persistTranscriptId: UUID?, nil for a not-yet-saved chat) — an unsaved chat's ephemeral session key never gets written, so there's no unbounded orphan-file accumulation from chats that are never saved. 4 new tests (save/load round-trip with a real Transcript() value, missing-file returns nil, delete is idempotent, deterministic path derivation). 429 tests total, stable across two consecutive full runs, and confirmed no leftover test-artifact files on disk after the run.
This commit is contained in:
@@ -1998,6 +1998,10 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
streamingTask?.cancel()
|
||||
|
||||
let conversationKey = appleSessionKey
|
||||
// Only a real, already-saved conversation gets its transcript persisted — an unsaved chat
|
||||
// has no durable id to restore from later even if we tried (⌘S-only persistence, no
|
||||
// autosave). See AppleFoundationProvider.respondWithTools's persistTranscriptId doc comment.
|
||||
let persistTranscriptId = currentConversationId
|
||||
let tools = MCPService.shared.getToolSchemas(onlineMode: onlineMode)
|
||||
let baseInstructions = effectiveSystemPrompt
|
||||
// Excludes the just-appended current-turn user message — only consulted by
|
||||
@@ -2025,6 +2029,7 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
do {
|
||||
let (response, didRebuild) = try await provider.respondWithTools(
|
||||
conversationId: conversationKey,
|
||||
persistTranscriptId: persistTranscriptId,
|
||||
tools: tools,
|
||||
baseInstructions: baseInstructions,
|
||||
priorMessagesForRebuildReplay: priorMessages,
|
||||
|
||||
Reference in New Issue
Block a user