Files
hapi/ios/Packages/HapiKit/Sources/HapiProtocol/Models/Session.swift
T
weishu 5ef4304a4a fix: synchronize read state across web and native clients
Track readable content revisions and durable read cursors in the hub. Acknowledge only rendered foreground tails, persist offline read operations, and preserve newer manual unread choices across devices.

Migrate existing history to a read baseline and cover pagination receipts with shared native conformance fixtures.
2026-11-01 09:52:51 +08:00

150 lines
7.0 KiB
Swift

import Foundation
/// The full session record returned by `GET /api/sessions/:id` and carried by
/// full-payload `session-updated` SSE events.
///
/// Mirrors `SessionSchema` (`shared/src/schemas.ts:304-338`) field by field.
/// `metadataVersion` / `agentStateVersion` / `todosUpdatedAt` /
/// `teamStateUpdatedAt` are the watermarks the versioned-patch gate in
/// `applySessionDetailPatch(session:patch:)` checks against.
public struct Session: Codable, Equatable, Sendable {
public var contentRevision: Int?
public var readState: SessionReadState?
public var id: String
public var namespace: String
public var seq: Int
public var createdAt: Int
public var lastUserMessageAt: Int?
public var updatedAt: Int
public var pinned: Bool?
public var globalPinned: Bool?
public var active: Bool
/// Last keep-alive stamp (epoch ms). The wire may carry `null` for legacy
/// rows; zod coerces to `0` and this decoder replicates that.
public var activeAt: Int
public var metadata: SessionMetadata?
public var metadataVersion: Int
public var agentState: AgentState?
public var agentStateVersion: Int
public var thinking: Bool
public var thinkingAt: Int
public var activeTurnStartedAt: Int?
public var backgroundTaskCount: Int?
public var todos: [TodoItem]?
/// Team-mode state, kept wire-verbatim (no v1 UI consumes its internals).
public var teamState: JSONValue?
/// Version watermark for `todos` patches (absent = 0 when gating).
public var todosUpdatedAt: Int?
/// Version watermark for `teamState` patches (absent = 0 when gating).
public var teamStateUpdatedAt: Int?
public var model: String?
public var modelReasoningEffort: String?
public var effort: String?
public var serviceTier: String?
public var permissionMode: PermissionMode?
public var collaborationMode: CodexCollaborationMode?
public var copilotAgentMode: CopilotAgentMode?
public init(
contentRevision: Int? = nil,
readState: SessionReadState? = nil,
id: String,
namespace: String,
seq: Int,
createdAt: Int,
updatedAt: Int,
lastUserMessageAt: Int? = nil,
pinned: Bool? = nil,
globalPinned: Bool? = nil,
active: Bool,
activeAt: Int,
metadata: SessionMetadata? = nil,
metadataVersion: Int,
agentState: AgentState? = nil,
agentStateVersion: Int,
thinking: Bool,
thinkingAt: Int,
activeTurnStartedAt: Int? = nil,
backgroundTaskCount: Int? = nil,
todos: [TodoItem]? = nil,
teamState: JSONValue? = nil,
todosUpdatedAt: Int? = nil,
teamStateUpdatedAt: Int? = nil,
model: String? = nil,
modelReasoningEffort: String? = nil,
effort: String? = nil,
serviceTier: String? = nil,
permissionMode: PermissionMode? = nil,
collaborationMode: CodexCollaborationMode? = nil,
copilotAgentMode: CopilotAgentMode? = nil
) {
self.id = id
self.namespace = namespace
self.seq = seq
self.createdAt = createdAt
self.updatedAt = updatedAt
self.lastUserMessageAt = lastUserMessageAt
self.pinned = pinned
self.globalPinned = globalPinned
self.contentRevision = contentRevision
self.readState = readState
self.active = active
self.activeAt = activeAt
self.metadata = metadata
self.metadataVersion = metadataVersion
self.agentState = agentState
self.agentStateVersion = agentStateVersion
self.thinking = thinking
self.thinkingAt = thinkingAt
self.activeTurnStartedAt = activeTurnStartedAt
self.backgroundTaskCount = backgroundTaskCount
self.todos = todos
self.teamState = teamState
self.todosUpdatedAt = todosUpdatedAt
self.teamStateUpdatedAt = teamStateUpdatedAt
self.model = model
self.modelReasoningEffort = modelReasoningEffort
self.effort = effort
self.serviceTier = serviceTier
self.permissionMode = permissionMode
self.collaborationMode = collaborationMode
self.copilotAgentMode = copilotAgentMode
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
contentRevision = try container.decodeIfPresent(Int.self, forKey: .contentRevision)
readState = try container.decodeIfPresent(SessionReadState.self, forKey: .readState)
id = try container.decode(String.self, forKey: .id)
namespace = try container.decode(String.self, forKey: .namespace)
seq = try container.decode(Int.self, forKey: .seq)
createdAt = try container.decode(Int.self, forKey: .createdAt)
updatedAt = try container.decode(Int.self, forKey: .updatedAt)
lastUserMessageAt = try container.decodeIfPresent(Int.self, forKey: .lastUserMessageAt)
pinned = try container.decodeIfPresent(Bool.self, forKey: .pinned)
globalPinned = try container.decodeIfPresent(Bool.self, forKey: .globalPinned)
active = try container.decode(Bool.self, forKey: .active)
// zod: `z.number().nullish().transform((value) => value ?? 0)`.
activeAt = try container.decodeIfPresent(Int.self, forKey: .activeAt) ?? 0
metadata = try container.decodeIfPresent(SessionMetadata.self, forKey: .metadata)
metadataVersion = try container.decode(Int.self, forKey: .metadataVersion)
agentState = try container.decodeIfPresent(AgentState.self, forKey: .agentState)
agentStateVersion = try container.decode(Int.self, forKey: .agentStateVersion)
thinking = try container.decode(Bool.self, forKey: .thinking)
thinkingAt = try container.decode(Int.self, forKey: .thinkingAt)
activeTurnStartedAt = try container.decodeIfPresent(Int.self, forKey: .activeTurnStartedAt)
backgroundTaskCount = try container.decodeIfPresent(Int.self, forKey: .backgroundTaskCount)
todos = try container.decodeIfPresent([TodoItem].self, forKey: .todos)
teamState = try container.decodeIfPresent(JSONValue.self, forKey: .teamState)
todosUpdatedAt = try container.decodeIfPresent(Int.self, forKey: .todosUpdatedAt)
teamStateUpdatedAt = try container.decodeIfPresent(Int.self, forKey: .teamStateUpdatedAt)
model = try container.decodeIfPresent(String.self, forKey: .model)
modelReasoningEffort = try container.decodeIfPresent(String.self, forKey: .modelReasoningEffort)
effort = try container.decodeIfPresent(String.self, forKey: .effort)
serviceTier = try container.decodeIfPresent(String.self, forKey: .serviceTier)
permissionMode = try container.decodeIfPresent(PermissionMode.self, forKey: .permissionMode)
collaborationMode = try container.decodeIfPresent(CodexCollaborationMode.self, forKey: .collaborationMode)
copilotAgentMode = try container.decodeIfPresent(CopilotAgentMode.self, forKey: .copilotAgentMode)
}
}