Wave 103 (original): ChatRecentActionsControllerNode.peer Peer -> EnginePeer migration ABANDONED after pre-flight discovered a 75-site ADD-bridge cascade through chatRecentActionsHistoryPreparedTransition into Message.peers SimpleDictionary<PeerId, Peer> store sites. Lessons captured in docs/superpowers/postbox-refactor-log.md "Wave 103 ABANDONED" section + ~/.claude/projects/-Users-isaac-build-telegram -telegram-ios/memory/feedback_wave71_shadow_risk.md (4-layer pre-flight checklist for stored-Peer-field migrations). Wave 103 (retry, 92230b0691): drained 5 accountManager.mediaBox .storeResourceData Shape-A sites against the wave-94 facade. 2 files / 3 Edit calls (1 single + 2 replace_all) / 1-iter / 29.5s build. Closes the storeResourceData accountManager-side drain entirely. Wave 104 (08fc3f721e): drained 3 of 8 accountManager.mediaBox.resourceData Shape-A sites against the wave-32/wave-94 AccountManagerResources.data (resource:) facade + 3 consumer-side .complete -> .isComplete renames (EngineMediaResource.ResourceData field rename). 1 file / 6 Edit calls / 1-iter / 11.7s build. 5 of 8 candidates deferred behind Postbox-typed -function-parameter barriers (fetchCachedScaled*Representation cascade, combineLatest typed-tuple coupling). Established the "Postbox-typed -function-parameter barrier registry" pattern. Wave 105 (0c76724409): DeviceContactInfoSubject enum 3 case Peer? payloads + 2 callback signatures + 1 computed property migrate to EnginePeer?. 5 files / 17 edits / 1-iter / 203s build (foundational AccountContext touch). Net wrap delta -8 (10 drops, 2 ADD bridges, 1 downcast->case-let). First wave-71-shadow-style migration after the wave-103 abandonment forced a discipline reset; first-pass-clean via thorough pre-flight inventory (~15 min). Net session progress: -16 wraps across 4 wave attempts plus durable scaffolding (feedback memory, barrier registry, 4-layer pre-flight checklist). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
19 KiB
Wave 105: DeviceContactInfoSubject enum payload Peer? → EnginePeer? Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
Goal: Migrate DeviceContactInfoSubject enum's 3 case payloads + 2 callback signatures + 1 computed property from raw Postbox Peer? to EnginePeer?. Wave 105 of the Postbox → TelegramEngine refactor.
Architecture: Multi-module enum-payload migration (wave-91 shape). 17 edits across 5 files. AccountContext.swift hosts the enum + property. DeviceContactInfoController.swift is the primary consumer. 4 construction sites in TelegramUI/PeerInfoUI/StoryContainerScreen/ChatController. Net wrap delta: −8 (drops 10, adds 2 at Chat-side construction barriers documented per spec).
Tech Stack: Swift, Bazel via Make.py, no unit tests. Verification is the full-project debug-sim-arm64 build.
Iteration budget: 1-3 (wave-91 precedent: 2 iter for similar shape).
Note on TDD: No unit tests in this project. Each task writes the edits, then verifies via Bazel build + residue grep.
File Structure
| File | Role | Edits |
|---|---|---|
submodules/AccountContext/Sources/AccountContext.swift |
Enum definition + computed property | 4 type-line edits |
submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift |
Primary consumer | 9 edits (5 _asPeer drops + 3 .flatMap simplifications + 1 downcast rewrite) |
submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift |
Chat-side construction (Pattern E ADD bridges) | 1 Edit (replace_all=true covers 2 sites) |
submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift |
Story-side construction | 1 edit |
submodules/TelegramUI/Sources/OpenChatMessage.swift |
OpenChatMessage construction | 1 edit |
Task 1: AccountContext.swift — enum + computed property type changes
File: submodules/AccountContext/Sources/AccountContext.swift
- Step 1: Migrate the 3 enum case payloads (single Edit covers consecutive lines)
Find:
public enum DeviceContactInfoSubject {
case vcard(Peer?, DeviceContactStableId?, DeviceContactExtendedData)
case filter(peer: Peer?, contactId: DeviceContactStableId?, contactData: DeviceContactExtendedData, completion: (Peer?, DeviceContactExtendedData) -> Void)
case create(peer: Peer?, contactData: DeviceContactExtendedData, isSharing: Bool, shareViaException: Bool, completion: (Peer?, DeviceContactStableId, DeviceContactExtendedData) -> Void)
public var peer: Peer? {
Replace with:
public enum DeviceContactInfoSubject {
case vcard(EnginePeer?, DeviceContactStableId?, DeviceContactExtendedData)
case filter(peer: EnginePeer?, contactId: DeviceContactStableId?, contactData: DeviceContactExtendedData, completion: (EnginePeer?, DeviceContactExtendedData) -> Void)
case create(peer: EnginePeer?, contactData: DeviceContactExtendedData, isSharing: Bool, shareViaException: Bool, completion: (EnginePeer?, DeviceContactStableId, DeviceContactExtendedData) -> Void)
public var peer: EnginePeer? {
This single Edit covers all 4 type-line changes in AccountContext.swift. The contactData: DeviceContactExtendedData computed property (lines 719-727) is unaffected.
Task 2: DeviceContactInfoController.swift — Pattern D downcast rewrite (1 edit)
File: submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift
- Step 1: Rewrite the
as? TelegramUserdowncast at line 849
Find:
if let peer = peer as? TelegramUser {
Replace with:
if case let .user(peer) = peer {
The leading whitespace (8 spaces) must match exactly. The outer peer: EnginePeer? (from case let .create(peer, ...) = subject at L845) is shadowed inside the if-body by peer: TelegramUser (the .user case associated value). Inner body access (peer.firstName, peer.lastName, peer.phone) works on the rebinding.
Task 3: DeviceContactInfoController.swift — Pattern C .flatMap simplifications (3 edits)
File: submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift
- Step 1: Simplify
.vcardcase body at line 942
Find:
case let .vcard(peer, id, data):
contactData = .single((peer.flatMap(EnginePeer.init), id, data))
Replace with:
case let .vcard(peer, id, data):
contactData = .single((peer, id, data))
- Step 2: Simplify
.filtercase body at line 944
Find:
case let .filter(peer, id, data, _):
contactData = .single((peer.flatMap(EnginePeer.init), id, data))
Replace with:
case let .filter(peer, id, data, _):
contactData = .single((peer, id, data))
- Step 3: Simplify
.createcase body at line 946
Find:
case let .create(peer, data, share, shareViaExceptionValue, _):
contactData = .single((peer.flatMap(EnginePeer.init), nil, data))
Replace with:
case let .create(peer, data, share, shareViaExceptionValue, _):
contactData = .single((peer, nil, data))
After Task 1's enum migration, the destructured peer: EnginePeer? is the target type — .flatMap(EnginePeer.init) becomes a redundant round-trip.
Task 4: DeviceContactInfoController.swift — Pattern B _asPeer drops at completion calls (2 edits)
File: submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift
- Step 1: Drop
_asPeer()at completion call line 1105
Find:
completion(peerAndContactData.0?._asPeer(), filteredData)
Replace with:
completion(peerAndContactData.0, filteredData)
peerAndContactData.0 is EnginePeer? from the typed signal at L939. Completion's first parameter type changes from Peer? to EnginePeer? per Task 1.
- Step 2: Drop
_asPeer()at completion call line 1224
Find:
completion(contactIdAndData.2?._asPeer(), contactIdAndData.0, contactIdAndData.1)
Replace with:
completion(contactIdAndData.2, contactIdAndData.0, contactIdAndData.1)
contactIdAndData.2 is EnginePeer? per the typed signal (DeviceContactStableId, DeviceContactExtendedData, EnginePeer?)? declared at L1175.
Task 5: DeviceContactInfoController.swift — Pattern A _asPeer drops at construction (3 edits)
File: submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift
- Step 1: Drop
_asPeer()at line 1289
Find:
replaceControllerImpl?(deviceContactInfoController(context: context, environment: environment, subject: .vcard(peer?._asPeer(), contactId, contactData), completed: nil, cancelled: nil))
Replace with:
replaceControllerImpl?(deviceContactInfoController(context: context, environment: environment, subject: .vcard(peer, contactId, contactData), completed: nil, cancelled: nil))
- Step 2: Drop
_asPeer()at line 1443
Find:
parentController.present(deviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer?._asPeer(), contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in
Replace with:
parentController.present(deviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer, contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in
- Step 3: Drop
_asPeer()at line 1489
Find:
controller?.present(context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer?._asPeer(), contactData: contactData, isSharing: peer != nil, shareViaException: false, completion: { _, _, _ in
Replace with:
controller?.present(context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer, contactData: contactData, isSharing: peer != nil, shareViaException: false, completion: { _, _, _ in
All 3 sites have peer source already typed as EnginePeer? per inventory.
Task 6: ChatControllerOpenAttachmentMenu.swift — Pattern E ADD wraps (1 Edit, 2 sites via replace_all=true)
File: submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift
- Step 1: Add
.flatMap(EnginePeer.init)wrap at lines 683 and 1850
Use Edit with replace_all=true. Find:
subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in
Replace with:
subject: .filter(peer: peerAndContactData.0.flatMap(EnginePeer.init), contactId: nil, contactData: contactData, completion: { peer, contactData in
replace_all=true is required — both sites at L683 and L1850 share identical text. The upstream signal type is (Peer?, DeviceContactExtendedData?) (verified at L634 and L1822); .flatMap(EnginePeer.init) wraps Peer? to EnginePeer? to satisfy the migrated .filter(peer: EnginePeer?, ...) signature.
Task 7: StoryItemSetContainerViewSendMessage.swift — Pattern A _asPeer drop (1 edit)
File: submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift
- Step 1: Drop
_asPeer()at line 2132
Find:
let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: component.context), environment: ShareControllerAppEnvironment(sharedContext: component.context.sharedContext), subject: .filter(peer: peerAndContactData.0?._asPeer(), contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in
Replace with:
let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: component.context), environment: ShareControllerAppEnvironment(sharedContext: component.context.sharedContext), subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in
peerAndContactData.0 is EnginePeer? from the typed signal at this site (the presence of ?._asPeer() confirms it).
Task 8: OpenChatMessage.swift — Pattern A _asPeer drop (1 edit)
File: submodules/TelegramUI/Sources/OpenChatMessage.swift
- Step 1: Drop
_asPeer()at line 443
Find:
let controller = deviceContactInfoController(context: ShareControllerAppAccountContext(context: params.context), environment: ShareControllerAppEnvironment(sharedContext: params.context.sharedContext), updatedPresentationData: params.updatedPresentationData, subject: .vcard(peer?._asPeer(), nil, contactData), completed: nil, cancelled: nil)
Replace with:
let controller = deviceContactInfoController(context: ShareControllerAppAccountContext(context: params.context), environment: ShareControllerAppEnvironment(sharedContext: params.context.sharedContext), updatedPresentationData: params.updatedPresentationData, subject: .vcard(peer, nil, contactData), completed: nil, cancelled: nil)
peer source is already EnginePeer? (the ?._asPeer() confirms the source type).
Task 9: Full-project Bazel build
Files: none (verification only).
- Step 1: Run the build with
--continueOnError
source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \
--cacheDir ~/telegram-bazel-cache \
build \
--configurationPath build-system/appstore-configuration.json \
--gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \
--gitCodesigningType development --gitCodesigningUseCurrent \
--buildNumber=1 --configuration=debug_sim_arm64 --continueOnError
--continueOnError enabled — multi-module wave; surface all errors at once if iter-1 fails.
Expected: clean build. AccountContext is foundational; expect 60-180s build cost.
- Step 2: If build fails, triage iteration
Common failure modes (per wave-91 precedent):
- Type mismatch on a destructured
peer— a destructure body may usepeer.XwhereXis a Peer-protocol-only method not on EnginePeer. Pre-flight inventory found ZERO such sites, but verify the failing line. .idaccess on EnginePeer? doesn't compile — would indicate an EnginePeer.Id typealias regression (very unlikely; would have failed all prior waves).case let .user(peer) = peerdoesn't compile — verify the outerpeerisEnginePeer?(after migration) and not stillPeer?.- A construction site missed an
_asPeer()drop — re-grep_asPeer\(\)over the 5 touched files. - Hidden
Peer?-typed completion call site — would indicate an unmigrated callback consumer. Re-grep across consumer module sources.
If errors land outside the 5 touched files: STOP and report BLOCKED — the wave is supposed to be self-contained.
Iteration budget: 3.
Task 10: Post-edit residue grep
Files: none (verification only).
- Step 1: Construction-site
_asPeerresidue (expected empty)
grep -nE "subject:\s*\.(vcard|filter|create)\(.*_asPeer\(\)" \
submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift \
submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift \
submodules/TelegramUI/Sources/OpenChatMessage.swift
- Step 2: Completion
_asPeerresidue (expected empty)
grep -nE "completion\(.*_asPeer\(\)" submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift
- Step 3:
.flatMap(EnginePeer.init)simplification residue (expected empty)
grep -nE "peer\.flatMap\(EnginePeer\.init\)" submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift
- Step 4: Downcast residue (expected empty)
grep -nE "peer as\? TelegramUser" submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift
- Step 5: ADD wraps applied (expected 2 lines)
grep -nE "peerAndContactData\.0\.flatMap\(EnginePeer\.init\)" submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift
Expected: 2 lines (originally L683 and L1850, line numbers may have shifted slightly).
Task 11: Commit the wave
Files: none (git only).
- Step 1: Stage the 5 modified files
git add \
submodules/AccountContext/Sources/AccountContext.swift \
submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift \
submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift \
submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift \
submodules/TelegramUI/Sources/OpenChatMessage.swift
- Step 2: Confirm staging
git status --short | grep -v "^??"
Expected: 5 staged files (lines starting with M ). The pre-existing m build-system/bazel-rules/sourcekit-bazel-bsp WIP marker should NOT appear in staged.
- Step 3: Commit
git commit -m "$(cat <<'EOF'
Postbox -> TelegramEngine wave 105
Migrate DeviceContactInfoSubject enum 3 case Peer? payloads + 2 callback
(Peer?, ...) -> Void signatures + 1 computed peer: Peer? property to
EnginePeer?. Wave-91-pattern multi-module enum-payload migration.
Drops 10 wraps:
- 5 _asPeer() at construction sites: DeviceContactInfoController:1289,
1443, 1489 + StoryItemSetContainerViewSendMessage:2132 +
OpenChatMessage:443.
- 2 _asPeer() at completion-call sites:
DeviceContactInfoController:1105, 1224.
- 3 .flatMap(EnginePeer.init) simplifications at
DeviceContactInfoController:942, 944, 946.
Adds 2 ADD bridges: ChatControllerOpenAttachmentMenu:683, 1850 — both
construct .filter(peer:) from peerAndContactData.0 typed (Peer?, ...);
.flatMap(EnginePeer.init) wraps to EnginePeer?. Net wrap delta: -8.
Plus 1 downcast rewrite: DeviceContactInfoController:849 — `if let peer
= peer as? TelegramUser` to `if case let .user(peer) = peer`.
5 files / 17 edits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
- Step 4: Verify commit
git log --oneline -1
Task 12: Update outcome log + memory
Files:
-
Modify:
docs/superpowers/postbox-refactor-log.md -
Modify:
~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md -
Modify:
~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md -
Step 1: Append wave 105 outcome to refactor log
Include:
-
Commit hash (from Task 11 step 4).
-
Iteration count (1 if first-pass-clean; 2-3 if Task 9 step 2 fired).
-
Bazel build duration.
-
Net-delta accounting: −10 wrap drops, +2 ADD wraps, +1 downcast rewrite. Net −8 wraps.
-
Wave-shape note: wave-91-pattern multi-module enum-payload migration with full pre-flight inventory clearing layers 1-4 of the wave-71-shadow checklist. Documents the value of thorough pre-flight inventory: 17 mechanical edits with 0 surprises.
-
Step 2: Update next-wave memory
Edit project_postbox_refactor_next_wave.md:
-
Add wave 105 outcome line into the recent-waves section.
-
Mark
DeviceContactInfoSubjectcandidate as drained (currently bullet 9 in deferred list). -
Promote next candidate.
-
Step 3: Update MEMORY.md index
Update the [Postbox refactor next wave] line.
- Step 4: Commit the doc update
git add docs/superpowers/postbox-refactor-log.md
git commit -m "$(cat <<'EOF'
docs: log wave 105 outcome
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
(Memory file updates not committed — they live outside the repo.)
Net delta projection
| Category | Count | Sites |
|---|---|---|
_asPeer() drops at construction |
−5 | DCIC:1289, 1443, 1489 + SISCVSM:2132 + OCM:443 |
_asPeer() drops at completion calls |
−2 | DCIC:1105, 1224 |
.flatMap(EnginePeer.init) simplifications |
−3 | DCIC:942, 944, 946 |
.flatMap(EnginePeer.init) ADD wraps |
+2 | CCOAM:683, 1850 |
| Downcast → case-let | +1 | DCIC:849 |
| Type annotations migrated | 4 | AccountContext: 3 enum cases + 1 computed property |
Total commit footprint: 17 line edits across 5 files, plus a docs commit for the outcome log.
Net wrap delta: −8 (the wave's headline metric).