| 1 | package sessioncatalog |
| 2 | |
| 3 | import "context" |
| 4 | |
| 5 | type sessionUpsertMode uint8 |
| 6 | |
| 7 | const ( |
| 8 | upsertExactSource sessionUpsertMode = iota |
| 9 | upsertDirectoryProjection |
| 10 | ) |
| 11 | |
| 12 | // sessionProjectionFieldsBelongToDirectory reports whether an exact-path |
| 13 | // record must retain the last complete directory projection. A physical |
| 14 | // recovery file cannot prove its logical topic, canonical leaf, or ordinary |
| 15 | // visibility without seeing its siblings and ancestors. |
| 16 | func sessionProjectionFieldsBelongToDirectory(existing, incoming SessionRecord) bool { |
| 17 | return existing.Recovered || incoming.Recovered || |
| 18 | existing.RecoveryGroupID != "" || incoming.RecoveryGroupID != "" || |
| 19 | existing.RecoveryRole != "" && existing.RecoveryRole != RecoveryRoleNormal || |
| 20 | incoming.RecoveryRole != "" && incoming.RecoveryRole != RecoveryRoleNormal |
| 21 | } |
| 22 | |
| 23 | func preserveDirectoryProjection(existing SessionRecord, record *SessionRecord) { |
| 24 | record.RecoveryCopy = existing.RecoveryCopy |
| 25 | record.RecoveryGroupID = existing.RecoveryGroupID |
| 26 | record.RecoveryRole = existing.RecoveryRole |
| 27 | record.RecoveryCanonical = existing.RecoveryCanonical |
| 28 | record.LogicalTopicID = existing.LogicalTopicID |
| 29 | record.OrdinaryVisible = existing.OrdinaryVisible |
| 30 | if sessionProjectionFieldsBelongToDirectory(existing, *record) { |
| 31 | record.TopicID = existing.TopicID |
| 32 | record.TopicTitle = existing.TopicTitle |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | // prepareExactPathProjection avoids publishing unchanged snapshots while |
| 37 | // retaining known counts when only legacy sidecar metadata is stale. |
| 38 | func (c *Catalog) prepareExactPathProjection(ctx context.Context, raw SessionRecord) (record SessionRecord, skip, projectionDirty bool, err error) { |
| 39 | record = classifyRecoveryLineage(normalizeSessionRecord(raw)) |
| 40 | if record.LogicalTopicID == "" { |
| 41 | record.LogicalTopicID = record.TopicID |
| 42 | } |
| 43 | existing, ok, err := c.GetSession(ctx, record.Path) |
| 44 | if err != nil { |
| 45 | return SessionRecord{}, false, false, err |
| 46 | } |
| 47 | if !ok || existing.Path == "" || existing.MissingSince != 0 { |
| 48 | if record.Recovered { |
| 49 | // A brand-new recovery file has no sibling-aware logical identity yet. |
| 50 | // Store a hidden physical shell and let the queued directory pass |
| 51 | // publish its final topic and representative atomically. |
| 52 | record.TopicID = "" |
| 53 | record.TopicTitle = "" |
| 54 | record.LogicalTopicID = "" |
| 55 | record.OrdinaryVisible = false |
| 56 | } |
| 57 | return record, false, record.Recovered, nil |
| 58 | } |
| 59 | projectionDirty = existing.TopicID != raw.TopicID || |
| 60 | existing.Recovered != record.Recovered || |
| 61 | existing.ParentID != record.ParentID || |
| 62 | existing.RecoveryDigest != record.RecoveryDigest || |
| 63 | existing.RecoveryReason != record.RecoveryReason |
| 64 | if sessionProjectionFieldsBelongToDirectory(existing, record) && existing.MetaFingerprint != record.MetaFingerprint { |
| 65 | // The persisted projection intentionally does not duplicate source and |
| 66 | // projected topic ids. A changed recovery sidecar may therefore have |
| 67 | // changed a lineage input that only a full directory pass can prove. |
| 68 | projectionDirty = true |
| 69 | } |
| 70 | preserveDirectoryProjection(existing, &record) |
| 71 | if sameSessionIndexInput(existing, record) { |
| 72 | return record, true, projectionDirty, nil |
| 73 | } |
| 74 | // A sidecar can be observed in an older, transient state while the |
| 75 | // transcript itself has not changed. Never let an exact-path refresh move a |
| 76 | // known conversation backwards in the activity-sorted sidebar. |
| 77 | if existing.ContentFingerprint == record.ContentFingerprint && |
| 78 | existing.LastActivityAt > record.LastActivityAt { |
| 79 | record.LastActivityAt = existing.LastActivityAt |
| 80 | } |
| 81 | if existing.TurnsState != TurnsUnknown && record.TurnsState == TurnsUnknown { |
| 82 | if existing.ContentFingerprint == record.ContentFingerprint { |
| 83 | record.Preview = existing.Preview |
| 84 | record.Turns = existing.Turns |
| 85 | record.TurnsState = existing.TurnsState |
| 86 | } else { |
| 87 | fillKnownCountHints(&record, existing.Preview, existing.Turns) |
| 88 | } |
| 89 | } |
| 90 | return record, false, projectionDirty, nil |
| 91 | } |
| 92 |