Conversation
A detached accepted-command refresh could finish after deleteThread and publish the deleted thread again. Cancel it once the server accepts the delete. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ng older state A detail read that started before a newer send was accepted could publish after it and hide the newer message until the next read. Skip publishing a superseded read (its refresh loop reads again), and stop a cancelled shell refresh from writing a shell read before the change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ives A Stop accepted during a send's detail read discarded that read without requesting another, and an older detail could replace the transcript before the accepted message reached it. Re-read detail after a discarded read, and keep delivered messages until a server transcript includes them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Do not retain a delivered message that a server transcript already confirmed, keep only its display copy, and drop retained messages when their thread details are removed or cleared. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Evicting a cached thread detail forgot its accepted messages, so a stale reopen hid them. A restored outbox or a new task's local detail was also recorded as the server transcript, so its accepted prompt was treated as confirmed and disappeared on the next older read. Only server transcripts now confirm delivery, and eviction keeps retained messages. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A newer send can be delivered while an older one waits to retry. Delivered copies were placed before every queued copy, which reversed them. Merge both by send time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A transient environment catalog read failure ended the active client's configuration subscription, so later provider and settings updates were ignored. Skip that publish instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e probe A passive peer's catalogue probe started at bootstrap could finish after the user saved a preference there and replace it with the older value. Discard the probe when a config already arrived. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A peer whose credential was rejected before its catalogue loaded kept opening a probe client every 20 seconds. The probe's own error cannot show the rejection (the socket ticket 401 surfaces as a timeout), so stop once the peer's shell read has marked it as needing pairing; re-pairing starts a new worker. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ismissed early Reopening a thread in the compact split view can report the new thread view as disappeared right after it appears while it stays on screen. That cancelled the draft restore, so the composer showed a busy send button until relaunch. Restore outside the view task's lifetime. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cted Pairing rejection cancelled the polling tasks but left the reconciliation loop issuing rejected shell reads every 30 seconds. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A passive peer whose socket stayed open kept its 30-second quiet shell reads after a 401, and showed as connected. Handle the rejected credential before the live-stream shortcut and switch to the long back-off. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The open socket's updates marked a rejected peer connected again, and stream repairs woke its HTTP loop before the back-off. Keep the rejection in the peer's shared state. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The live-peer fixture this test needs now lands below the Stop layer, so the regression moved out of the command-acknowledgement layer returns here.
Stop status refreshes the shell cache and reads it back. A live turn advancing the shell during that read made the refresh discard itself as superseded, so the status check failed and Stop could not be confirmed or retried. While the environment's stream is authoritative on the current socket, the cache it keeps current answers instead; after a disconnect or lost authority the check still fails. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The receipt line in the thread header showed a snapshot or update receipt time, which read as message freshness and stayed static while a quiet thread was connected. Show the age of the last sent message on the activity row while the agent works instead, advancing only at displayed unit boundaries, and remove the header receipt line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| await perform { | ||
| try await client.cancelTurn(threadID: threadID) | ||
| } | ||
| try await client.cancelTurn(threadID: threadID) |
There was a problem hiding this comment.
🟠 High Root/FeatureRootModel.swift:1219
cancelClaimedTurn can cancel a newer turn instead of the claimed key.turnID when a pending creation appears in snapshot while its outbox entry is being removed. This path calls client.cancelTurn(threadID:) without an expected turn ID, so pass expectedTurnID: key.turnID to reject the cancellation if the turn changed.
| try await client.cancelTurn(threadID: threadID) | |
| try await client.cancelTurn(threadID: threadID, expectedTurnID: key.turnID) |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/swift-ios/Features/Root/FeatureRootModel.swift around line 1219:
`cancelClaimedTurn` can cancel a newer turn instead of the claimed `key.turnID` when a pending creation appears in `snapshot` while its outbox entry is being removed. This path calls `client.cancelTurn(threadID:)` without an expected turn ID, so pass `expectedTurnID: key.turnID` to reject the cancellation if the turn changed.
| return (messages + activities).sorted { $0.createdAt < $1.createdAt } | ||
| // Older pages contain earlier turn windows, but carry current session | ||
| // metadata. That metadata must not revive their historical tool rows. | ||
| return NativeTranscriptTimeline( |
There was a problem hiding this comment.
🟡 Medium App/NativeFeatureClient.swift:7062
Historical pagination renders a completed-looking work-log row for a start-only tool.started activity instead of omitting it. renderedHistoryMessages passes sessionIsLive: false, and NativeTranscriptTimeline freezes active work in finishActiveWork(), so current-session activity carried by older pages becomes a stale transcript row; preserve the inactive-path behavior that drops active-only work before rendering history.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/swift-ios/App/NativeFeatureClient.swift around line 7062:
Historical pagination renders a completed-looking work-log row for a start-only `tool.started` activity instead of omitting it. `renderedHistoryMessages` passes `sessionIsLive: false`, and `NativeTranscriptTimeline` freezes active work in `finishActiveWork()`, so current-session activity carried by older pages becomes a stale transcript row; preserve the inactive-path behavior that drops active-only work before rendering history.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This is a large Swift iOS runtime and UI overhaul rather than a narrowly scoped display fix, introducing new cancellation semantics, transport reconciliation, transcript reconstruction, and presentation lifecycle behavior across production paths. Unresolved threshold-level correctness findings add further risk to the central client and stop flow. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |
|
@t3dotgg heads-up: #10766 had two concerns, so it's now split.
New merge order: #10758 → #10759 → #10761 → #10762 → #10763 → #10764 → #10765 → #10766 → #14021 → #10767. #12655 stays independent. Each PR's GitHub diff includes the whole chain below it, so every description now starts with a link to that layer's own change:
|
Review this layer
GitHub shows this PR against
t3code/rebuild-mobile-app-swift, so its diff includes every earlier PR in the chain. Review only this layer's change:This layer's diff: +90 / −68 lines across 6 files; app code +62 / −63, the rest is tests and docs.
Show time since the latest conversation message beneath “Agent is working” at the end of the transcript, and remove the thread-header receipt line added in #10766. Snapshot and connection receipts do not reset the message age; a new user or assistant message that is not queued does. The label wakes only at displayed unit boundaries (second, minute, hour, day), and the activity row disappears when work finishes. The user guide now describes the message age instead of the header receipt.
Delivery: stacked. This branch contains #10758, #10759, #10761, #10762, #10763, #10764, #10765, #10766; merge the chain in order (#10758 → #10759 → #10761 → #10762 → #10763 → #10764 → #10765 → #10766 → #14021 → #10767). Each PR's own change is its top commit(s).
Split from #10766 (2026-09-28). This is the second concern of the original #10766 (its last two commits, squashed because the second undid most of the first), plus the user-guide update. The code is byte-identical to those commits, and the chain top is unchanged apart from that guide paragraph.
Current layout,
9eab0475d:Same native WorkspaceView flow on the previous PR revision
578f1c68cand current9eab0475d: receive a snapshot, append a conversation message, then finish work. Synthetic data; iPhone 17 Pro Simulator / iOS 26.5, dark mode, full 402×874-point viewport scaled to 390 pixels wide. Recordings play at real time; GIFs sample 12 fps. A three-second hold of the final captured frame makes the idle result readable. No taps are simulated in this event-driven fixture.Before — prior header receipt presentation. This baseline recording includes transient incomplete redraws; their cause is unconfirmed and this PR does not claim to fix them. The current-head recording below is clean through idle:
After — age beside transcript activity, then clean idle state:
Before clean video · After clean video · Captioned after video · Idle screenshot · Capture/edit receipt
Focused verification: candidate capture plus two message-age tests passed (3 tests, xcodebuild exit 0); matching previous-revision capture passed (1 test, exit 0). Tests cover conversation-message selection, queued/tool/system exclusions, future clock skew, and elapsed-unit formatting. CI passed on that original revision. The clock wakes at displayed unit boundaries. No live-backend or physical-device performance measurement is claimed.
Capture limits: earlier current-head captures contained incomplete redraws and were rejected; the replacement recording-only flow was inspected through completion. Xcode compiler-probe stalls required retries. An earlier capture fixture overfulfilled its navigation expectation; that fixture was corrected before the passing runs.
Verification
2e25d10a9e's code;FeatureMessageAgeTests,FeatureThreadReceiptTests,NativeTranscriptTimelineTests,FeatureRootModelTests,NativeRetryIdentityTests, iOS Simulator): 23 XCTest and 126 Swift Testing tests passed.T3CodeTestsat the chain top (68d92c2752): 343 XCTest cases passed (1 skipped). Swift Testing ran 892 tests; the only 2 failures areUsageModelsTestscurrency formatting, which fail identically on unmodified157476f1fb.codex exec -m gpt-6-astra -c model_reasoning_effort="xhigh" --sandbox read-only) confirmed that the chain top's code is identical to the pre-split top, that this layer's code diff is byte-identical to the two original commits, and that the user guide matches each layer's UI. Its one finding (fix(swift-ios): record thread receipts and keep tool history chronological #10766 did not compile on its own) was fixed, and a re-check returned ready.Model and harness: GPT-6 / Codex (original change). Split, guide update and verification: Claude Opus 5.5 / Claude Code.