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:
2026-08-28 12:51:28 +02:00
parent c7681d227f
commit 8006f22d69
5 changed files with 178 additions and 6 deletions
+5
View File
@@ -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,