Telegram-iOS/docs/superpowers/plans/2026-04-26-postbox-wave-103-chat-recent-actions-controller-node.md
isaac 7ccb382f53 Postbox -> TelegramEngine waves 103-105 (squashed)
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>
2026-04-26 16:57:55 +04:00

13 KiB
Raw Blame History

Wave 103: ChatRecentActionsControllerNode peer 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. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Migrate ChatRecentActionsControllerNode's stored peer: Peer field to EnginePeer, dropping the _asPeer() boundary call at the single caller site (wave 103 of the Postbox → TelegramEngine refactor).

Architecture: Wave-71-shadow close. Single-file private stored-form migration plus a 1-line caller drop. The caller (ChatRecentActionsController) already holds peer: EnginePeer and demotes once before passing into the node init. The wave drops the demotion and rewrites 3 as? TelegramChannel downcasts inside the node body to case let .channel(...) (wave-41/45 idiom). All scope is within submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/.

Tech Stack: Swift, Bazel via Make.py, no unit tests (per CLAUDE.md). Verification is the full-project debug-sim-arm64 build.

Iteration budget: 1 (target first-pass-clean given the 7-edit scope and validated pre-flight grep).

Note on TDD: This project has no unit tests. The standard TDD test-first cycle does not apply. Each task writes the edits, then verifies via Bazel build + residue grep.


File Structure

File Role Changes
submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift (CRACN) Recent-actions screen controller node Drop import Postbox, retype stored field + init param, rewrite 3 as? TelegramChannel downcasts (6 edits)
submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift (CRAC) Recent-actions screen controller (caller) Drop _asPeer() at the node init (1 edit)

No public-API ripple — ChatRecentActionsControllerNode is local to the module and has a single caller verified by grep.


Task 1: CRACN.swift — drop import Postbox + type changes + downcast rewrites

Files:

  • Modify: submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift

Edits in this task: 6 (1 import drop, 1 stored-field retype, 1 init-param retype, 3 cast → case-let).

  • Step 1: Drop import Postbox at line 5

Find:

import Postbox

Replace with: (delete the line entirely)

This file imports TelegramCore at line 4, which provides the EnginePeer type and the typealiases needed for the rest of this task.

  • Step 2: Retype stored field at line 46

Find:

    private let peer: Peer

Replace with:

    private let peer: EnginePeer
  • Step 3: Retype init parameter at line 111

Find:

    init(context: AccountContext, controller: ChatRecentActionsController, peer: Peer, presentationData: PresentationData, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, PresentationContextType, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?) {

Replace with:

    init(context: AccountContext, controller: ChatRecentActionsController, peer: EnginePeer, presentationData: PresentationData, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, PresentationContextType, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?) {
  • Step 4: Rewrite downcast at line 899

Find:

                            if let peer = strongSelf.peer as? TelegramChannel {

Replace with:

                            if case let .channel(peer) = strongSelf.peer {

The bound name peer is preserved so the inner block (switch peer.info { case .group: ... }) ports verbatim. case let .channel(peer) binds peer: TelegramChannel directly (the associated value of EnginePeer.channel).

  • Step 5: Rewrite downcast at line 948

Find:

        if let channel = self.peer as? TelegramChannel, case .broadcast = channel.info {

Replace with:

        if case let .channel(channel) = self.peer, case .broadcast = channel.info {

The compound condition (, case .broadcast = channel.info) ports verbatim because the bound channel is still TelegramChannel-typed.

  • Step 6: Rewrite downcast at line 1088

Find:

            if let channel = self.peer as? TelegramChannel {

Replace with:

            if case let .channel(channel) = self.peer {

The inner block (channel.hasPermission(.banMembers), case .broadcast = channel.info) ports verbatim.

The self.peer.id accesses at lines 145, 161, 1138, 1490 require no edit — EnginePeer.id is a typealiased PeerId, identical at the call sites.


Task 2: CRAC.swift — drop boundary _asPeer()

Files:

  • Modify: submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift

Edits in this task: 1.

  • Step 1: Drop _asPeer() at line 277

Find:

        self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer._asPeer(), presentationData: self.presentationData, pushController: { [weak self] c in

Replace with:

        self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer, presentationData: self.presentationData, pushController: { [weak self] c in

ChatRecentActionsController.peer is already declared EnginePeer at line 42 (public init(context: AccountContext, peer: EnginePeer, ...)) — the type carries through to the now-EnginePeer-typed init parameter.


Task 3: Full-project Bazel build

Files: none (verification only).

  • Step 1: Run the build

Run:

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

Expected: clean build (bazel build complete or equivalent green output). No --continueOnError because the small scope makes the first error informative.

Build cost projection: consumer-only, ~25s. If it exceeds ~60s, suspect a cascade leak.

  • Step 2: If build fails, triage iteration

If errors land in ChatRecentActionsControllerNode.swift or ChatRecentActionsController.swift:

  • Read the failing line.
  • Common failure modes from prior waves:
    • Always-false is warning under -warnings-as-errors: none expected here (pre-flight grep confirmed no is TelegramChannel checks on self.peer). If one surfaces anyway, convert to case .channel = self.peer.
    • Always-failing as? cast warning: leftover as? TelegramX not converted in step 4/5/6. Re-grep (self|strongSelf)\.peer as\? over the file.
    • Type mismatch on closure-capture alias: none expected here (pre-flight grep confirmed only strongSelf.peer and self.peer aliases, both ride the type change).
    • Type mismatch on .id access: would indicate a regression in the EnginePeer.Id typealias — STOP and re-read CLAUDE.md, this is not a wave-103 issue.
    • Unused-variable warning under -warnings-as-errors: a case let .channel(peer) binding not used inside the body. Re-read step 4/5/6 — if the inner block never references the bound name, switch to case .channel = ... and remove the binding.

Fix in place and re-run step 1. Budget: 2 iterations.

If errors land outside those 2 files: STOP. The wave was supposed to be self-contained. Re-read the spec, identify the missed call site, decide whether to add it or abandon the wave.


Task 4: Post-edit residue grep

Files: none (verification only).

  • Step 1: Cast residue grep

Run:

grep -nE "(self|strongSelf)\.peer as\? Telegram(Channel|Group|User)" \
  submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/

Expected: empty output.

  • Step 2: Boundary _asPeer() residue grep

Run:

grep -nE "self\.peer\._asPeer\(\)" \
  submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/

Expected: empty output.

  • Step 3: import Postbox residue grep

Run:

grep -rn "^import Postbox$" \
  submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/

Expected: empty output. The module is now Postbox-import-free.

  • Step 4: Sanity check — peer: Peer annotations

Run:

grep -nE "peer: Peer\b" \
  submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift

Expected: empty output. (The 3 as? TelegramChannel downcasts on self.peer were the only sources; both peer: Peer annotations on stored field and init param are now peer: EnginePeer.)


Task 5: Commit the wave

Files: none (git only).

  • Step 1: Stage the 2 modified files
git add \
  submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift \
  submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift
  • Step 2: Confirm staging is clean
git status --short | grep -v "^??"

Expected output: only the 2 staged files (lines starting with M ). If other modified files appear, they predate the wave (per CLAUDE.md memory: build-system/bazel-rules/sourcekit-bazel-bsp submodule marker is pre-existing WIP).

  • Step 3: Commit
git commit -m "$(cat <<'EOF'
Postbox -> TelegramEngine wave 103

Migrate ChatRecentActionsControllerNode.peer Peer -> EnginePeer.
Closes the wave-71 shadow: caller already held EnginePeer and demoted
at the boundary. 7 edits / 2 files.

Drops 1 boundary _asPeer() at ChatRecentActionsController:277, drops
import Postbox at ChatRecentActionsControllerNode:5, rewrites 3
`as? TelegramChannel` downcasts to `case let .channel(...)` (wave-41/45
idiom).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
  • Step 4: Verify commit
git log --oneline -1

Expected: shows the wave 103 commit as HEAD.


Task 6: 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 103 outcome to refactor log

Append a "Wave 103 outcome" entry at the chronological end of docs/superpowers/postbox-refactor-log.md. Use the most recent wave-outcome entry as a structural template. Include:

  • Commit hash (from Task 5 step 4).

  • Iteration count (1 if first-pass-clean; 2 if Task 3 step 2 fired).

  • Net-bridge accounting: 1 boundary _asPeer() (CRAC:277), 1 import Postbox (CRACN:5). 0 ADD wraps. 3 cast → case-let conversions (CRACN:899/948/1088).

  • Bazel build duration (from Task 3 step 1 output).

  • Wave-shape note: wave-71-shadow close, single-iter target validated.

  • Step 2: Update next-wave memory

Edit ~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md:

  • Add the wave 103 outcome line into the recent-waves section (commit hash + 7-edit / 2-file / 1-iter summary).

  • Remove the now-stale ChatRecentActionsControllerNode.peer: Peer -> EnginePeer candidate line (currently bullet 5 in the candidates list).

  • Update the top frontmatter description to reflect wave 103 landed and propose wave 104.

  • Promote the next candidate (likely one of: cachedResourceRepresentation foundational facade, RenderedPeer cascade kickoff, SelectivePrivacyPeer foundational, or another Shape-C/D mini-refactor) to the top of the candidates list.

  • Step 3: Update MEMORY.md index

Edit ~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md:

  • Update the [Postbox refactor next wave] line to mention wave 103 landed and shift the "Wave 103+ Shape-C/D candidates" framing forward to "Wave 104+ candidates".

  • Step 4: Commit the doc update

git add docs/superpowers/postbox-refactor-log.md
git commit -m "$(cat <<'EOF'
docs: log wave 103 outcome

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"

(Memory file updates at ~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/ are not committed — they live outside the repo.)


Net delta projection (from spec)

Category Count Sites
Internal bridge drops 1 CRAC:277 (_asPeer())
import Postbox drops 1 CRACN:5
ADD wraps 0 no Peer-only property accesses on bare self.peer
Cast → case-let conversions 3 CRACN:899, CRACN:948, CRACN:1088
Type annotations updated 2 CRACN:46 (stored field), CRACN:111 (init param)
Postbox-free module count +1 Components/Chat/ChatRecentActionsController/ joins the list

Total commit footprint: 7 line edits across 2 files, plus a docs commit for the outcome log.