Skip to content

Offer Try again after a server answer House Scan can't use - #203

Open
SamGu-NRX wants to merge 5 commits into
codex/public-assembly-20260930from
codex/upload-answer-recovery
Open

SamGu-NRX wants to merge 5 commits into
codex/public-assembly-20260930from
codex/upload-answer-recovery

Conversation

@SamGu-NRX

@SamGu-NRX SamGu-NRX commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Base: codex/public-assembly-20260930. This PR needs nothing from other open PRs, and #192 stays unwired.

What changed

An answer House Scan can't use now offers "Try again" instead of sending the homeowner back to their marks. Some answers can't be used: they don't decode as a result, or their stats.input_sha256 doesn't match the scene sent (ResultBinding, #198). The upload screen used to show "We couldn't check this scan" with "Back to review", which offered no way to send the same scan again and implied the measurements were wrong. Those answers now get their own upload state, UploadState.unusableAnswer(attempts:). The screen says the server answered but House Scan couldn't read the answer or match it to the scan. Nothing from that answer reaches the spot check or the result. The scan stays open, and only the homeowner's tap on "Try again" sends it again.

A second unusable answer gives a way out. When "Try again" also gets an unusable answer, the title becomes "The answer still couldn't be used". The screen says asking again might not fix it, "Try again" loses its blue fill and takes the same quiet style as Share scan and Start over, and the note points to those two (UnusableAnswerActions). The engine counts these answers from the last send out of the review or a gap. A usable answer resets the count.

A refusal stays separate. A 4xx still means the server refused this scan, and it still goes to "Back to review" with no "Try again". retryUpload() acts only on .failed and .unusableAnswer.

The words never claim a server answered when none did. A response that isn't HTTP is now classified as an unreachable server, not an unreadable answer (UploadFailureKind.classify). In sample mode no server is asked, so a missing or unreadable bundled sample is reported as a build problem.

The placement HTTP client moved into HouseScanKit so tests can drive it over real HTTP. PlacementHTTPClient sends the same request as the app's old HTTPResultClient: POST /v1/placements, JSON content type and accept, a 120 s timeout. Progress reporting, the 300-byte error body and Retry-After are also unchanged. HTTPResultClient now wraps it.

Why

The product decision for this case: a bad server answer should keep the scan and let the homeowner try again, without suggesting the measurements were wrong. The app must never show an answer that isn't bound to the scan, and it must never retry in a loop on its own. A genuine refusal stays distinguishable.

UI changes

New state on the upload screen, so there is no before capture. Captures from hosted run 37114791601 on a7458e7, iPhone 17 Simulator. The demo pair comes from testRepeatedUnusableAnswersPointToAWayOut. The engine pair comes from testUnusableAnswersAreHeldBackUntilTryAgain, on the synthetic replay with the test answering as the server.

First unusable answer Second unusable answer
Demo: "We couldn't use the server's answer", blue Try again above Share scan and Start over Demo: "The answer still couldn't be used", Try again in the same quiet style as Share scan and Start over
Real engine on the synthetic replay, first unusable answer: same screen as the demo Real engine on the synthetic replay, second unusable answer: same screen as the demo

Verification

  • PlacementHTTPClientTests run the client through URLSession against an HTTP server on 127.0.0.1. The fixture is the synthetic-wall server answer with its input hash set per test. An answer for the scene sent comes back byte for byte. An answer naming another scene, one with no hash, and one that isn't JSON are each refused after exactly one request. 422, 503 and 429 with Retry-After classify as refused, server error and busy(30). Nothing listening classifies as unreachable.
  • UploadRecoveryUITests.testUnusableAnswersAreHeldBackUntilTryAgain runs the real engine on the synthetic replay, with the test playing the server through files. Under the test-only launch flag -answersFromGate, the app's own PlacementHTTPClient sends through a URLProtocol (GateAnswerProtocol) that writes each request into the autopilot's gate folder and answers HTTP 200 with the file the test writes back. The status check and ResultBinding run as they would against a server; only the socket is replaced. The first answer names another scene. The second names the scene sent but has an unsupported schema_version, so it fails decoding rather than binding. The third is usable. After each unusable answer the test checks the matching title and that no "Back to review" appears. It then waits six seconds and checks that no new request went out and no spot check or result appeared. After the second "Try again" it expects the result.
  • The other two UI tests cover both demo states with the accessibility audit, and check that "Try again" from the demo reaches the result.
  • A fresh Sol High review found that both test servers kept their listeners open after the test dropped them. It also found that the engine's repeat count went untested. Both are fixed in 21abb9d.
  • Hosted run 37107233020 on 21abb9d failed this engine test, and only this test. The test then used a server on 127.0.0.1 inside the UI-test runner. The app reached the upload and showed "You're offline" for the whole 300 s wait, so no answer ever arrived. The same listener code answers URLSession on a Mac at 200 B, 300 KB and 3 MB, so the failure is specific to the runner on the Simulator, which is a background app while the app under test is open. The exact URL error wasn't logged by CI. 4bc4ded replaces that server with the file exchange above.
  • A fresh Sol High review of 4bc4ded found no blocker in the intended flow and two fault paths in the test hook: a request the app failed to write left every later one unanswered, and GateServer.stop() returned while its thread could still write into the folder being deleted. Both are fixed in 11d02a3.
  • Hosted run 37112184653 on 11d02a3 passed: 657 package tests in 93 suites, PlacementHTTPClientTests among them, and 59 UI tests with 1 skipped and none failing. The engine test passed in 80 s.
  • Its screenshots showed the repeated state's "Try again" in the dark style the app keeps for camera screens. On this light screen it outweighed Share scan, so a7458e7 gives it the quiet style. Hosted run 37114791601 on a7458e7 passed the same way: 657 package tests and 59 UI tests, 1 skipped, none failing.
  • The engine test is real client and engine execution over a simulated transport, not end-to-end networking. The real socket is covered only by PlacementHTTPClientTests, which run the same client on macOS against a server on 127.0.0.1.
  • No local build or run: heavy-job admission went to other runs.

Risk

  • -answersFromGate is a launch argument in the shipping app, like -serverURL and -sampleResult. It requires -autopilotGate and -serverURL and stops the app at launch without them, and nothing reads it otherwise. A homeowner can't pass launch arguments.
  • The saved-scans branch is changing the .failed recovery note. This PR leaves those lines alone, and the new state's note will be aligned with that wording once the branch lands.

Checklist

  • The scope is focused; related changes belong together
  • I explained what changed and why
  • I included images for visible changes. No before image: the state is new.
  • N/A: video. The change is a static recovery state with no new motion.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a “Try again” option when a server response can’t be read or matched to the submitted scan. Repeated attempts show updated guidance, and users can start over or share the scan when sharing is available.
    • Upload progress and result handling now provide clearer feedback for server refusals and connection issues.
  • Bug Fixes
    • Unusable server responses no longer appear as though a sample result is being prepared.

An answer that doesn't decode or doesn't name the scene sent (ResultBinding) used to
show "We couldn't check this scan" with "Back to review", as if the marks were wrong,
and offered no way to send the same scan again. It now has its own upload state: the
answer is never shown, the scan is kept, and only the homeowner's "Try again" asks
again. From the second such answer the screen says asking again might not help and
points to sharing the scan or starting over. A 4xx refusal still goes back to the
review. A reply that isn't HTTP counts as an unreachable server, so the words never
say a server answered when none did.

The transport moves into HouseScanKit (PlacementHTTPClient) unchanged, so the
package tests drive it through URLSession against a server on 127.0.0.1. A UI test
runs the real engine against the same kind of server: the first answer names another
scene, and the test checks that nothing more is sent and nothing reaches a later
screen until Try again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (1)
ios/README.md — configured

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 661f5111-3d5b-4332-bc5f-ee1a8b13a5fe
📥 Commits

Reviewing files that changed from the base of the PR and between 75f6c37 and a7458e7.

📒 Files selected for processing (17)
  • ios/HouseScan/Contract/ScanContract.swift
  • ios/HouseScan/Runtime/GateAnswerProtocol.swift
  • ios/HouseScan/Runtime/LaunchOptions.swift
  • ios/HouseScan/Runtime/ResultClient.swift
  • ios/HouseScan/Runtime/ScanEngine+Actions.swift
  • ios/HouseScan/Runtime/ScanEngine.swift
  • ios/HouseScan/UI/Copy/ScanCopy.swift
  • ios/HouseScan/UI/Preview/DemoEngine.swift
  • ios/HouseScan/UI/Preview/UIDemo.swift
  • ios/HouseScan/UI/Screens/UploadingScreen.swift
  • ios/HouseScanKit/Sources/HouseScanKit/Upload/PlacementHTTPClient.swift
  • ios/HouseScanKit/Sources/HouseScanKit/Upload/UploadFailureKind.swift
  • ios/HouseScanKit/Tests/HouseScanKitTests/LoopbackPlacementServer.swift
  • ios/HouseScanKit/Tests/HouseScanKitTests/PlacementHTTPClientTests.swift
  • ios/HouseScanKit/Tests/HouseScanKitTests/ResultBindingTests.swift
  • ios/HouseScanKit/Tests/HouseScanKitTests/UploadFailureKindTests.swift
  • ios/HouseScanUITests/UploadRecoveryUITests.swift

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The upload flow now separates unusable server answers from refusals, allows users to retry unusable answers, and tracks consecutive attempts. Placement submission and failure classification are handled by PlacementHTTPClient. The upload screen presents recovery actions, and launch options support gate-backed answers and demo simulations.

Changes

Upload Recovery

Layer / File(s) Summary
Placement submission and classification
ios/HouseScanKit/Sources/HouseScanKit/Upload/PlacementHTTPClient.swift, ios/HouseScanKit/Sources/HouseScanKit/Upload/UploadFailureKind.swift, ios/HouseScanKit/Tests/HouseScanKitTests/LoopbackPlacementServer.swift, ios/HouseScanKit/Tests/HouseScanKitTests/PlacementHTTPClientTests.swift, ios/HouseScanKit/Tests/HouseScanKitTests/ResultBindingTests.swift, ios/HouseScanKit/Tests/HouseScanKitTests/UploadFailureKindTests.swift
PlacementHTTPClient posts scene data, checks responses, and reports upload progress. Failure classification handles HTTP statuses, retry delays, and non-HTTP responses. Loopback tests cover submissions and classifications.
Engine state and retry handling
ios/HouseScan/Contract/ScanContract.swift, ios/HouseScan/Runtime/ResultClient.swift, ios/HouseScan/Runtime/ScanEngine.swift, ios/HouseScan/Runtime/ScanEngine+Actions.swift, ios/HouseScan/Runtime/GateAnswerProtocol.swift, ios/HouseScan/Runtime/LaunchOptions.swift
Upload states distinguish unusable answers from refusals. The engine tracks unusable-answer attempts and retries them on request. The -answersFromGate option selects a gate-backed session and requires a gate folder and server URL.
Recovery interface and validation
ios/HouseScan/UI/Copy/ScanCopy.swift, ios/HouseScan/UI/Preview/DemoEngine.swift, ios/HouseScan/UI/Preview/UIDemo.swift, ios/HouseScan/UI/Screens/UploadingScreen.swift, ios/HouseScanUITests/UploadRecoveryUITests.swift
The upload screen displays recovery instructions and actions for unusable answers. Demo mode can simulate consecutive unusable answers. UI tests verify retry behavior, screen actions, and gate-backed answers.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant ScanEngine
  participant HTTPResultClient
  participant PlacementHTTPClient
  participant PlacementEndpoint
  participant ResultBinding
  participant UploadingScreen
  ScanEngine->>HTTPResultClient: Submit scene
  HTTPResultClient->>PlacementHTTPClient: Submit scene
  PlacementHTTPClient->>PlacementEndpoint: POST /v1/placements
  PlacementEndpoint-->>PlacementHTTPClient: Return HTTP response
  PlacementHTTPClient->>ResultBinding: Check answer against scene
  PlacementHTTPClient-->>HTTPResultClient: Return answer or error
  HTTPResultClient-->>ScanEngine: Return result or upload failure
  ScanEngine->>UploadingScreen: Show unusable-answer state
  UploadingScreen->>ScanEngine: retryUpload after Try again
  ScanEngine->>HTTPResultClient: Resubmit scene
Loading

Suggested reviewers: aidenjohnston

Merge Risk: ⚪ Minimal · up to a7458

The reviewed upload and retry changes are mergeable after normal checks; no issue requiring a fix before merge was established.

Security Architecture Review

Security architecture risk: 🔵 Low · up to a7458

Retries continue to validate that answers belong to the submitted scan, and unusable answers are withheld from results. The main uncertainty is whether the new file-backed testing mode can be activated in distributed installations.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — When explicitly enabled, the gate session affects placement requests in that app process and exposes their scene bodies to the selected directory. The inspected path does not establish remote activation, cross-installation access or additional service privileges.

Security Findings and Attack Paths

  • inferred — A controller able to activate gate mode and write its answer files can supply a syntactically valid result bearing the submitted scene's hash. This is a conditional response-authority path, not a verified production exploit: launch control also permits selecting a server URL, and distributed-installation reachability is unresolved.

Trust Boundaries and Controls

  • observed — Unusable responses cannot reach placement publication without successful binding and decoding. Retry authority is independently restricted by the engine action, so rejected refusals cannot enter the new retry path merely through a UI action.

Resilience and Maintainability Implications

  • observed — Ground-freshness handling treats unusableAnswer as a stopped upload, alongside failed and rejected states. The new recovery state therefore does not itself trigger the automatic resend path used for an active upload invalidated by a ground change.

Hardening Proposals

  • proposed — Restrict file-backed response substitution to dedicated verification builds or an explicit verification capability, making its intended trust boundary independent of production launch-argument behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.65% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 46 functions across 17 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: offering “Try again” when the server answer cannot be used.
Description check ✅ Passed The description explains what changed and why, documents the UI changes, reports verification and its limits, identifies risks, and addresses the checklist items. It provides screenshots for the new U…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size:L 100-499 effective changed lines. label Oct 3, 2026
Both loopback servers retained themselves through their listener's connection
handler, so a dropped server never closed its listener. The handlers now hold the
server weakly, and the UI test's stub also closes on stop().

The engine test now sends two unusable answers before a usable one: the first names
another scene, and the second names the scene sent but has a result schema House Scan
doesn't read. That covers the engine's decode failure and its count. The test checks
the second state's wording and that no request goes out between taps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@SamGu-NRX
SamGu-NRX marked this pull request as ready for review October 3, 2026 07:42
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-03T11:09:13.005392Z a7458e7 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codemetrics-ai

codemetrics-ai Bot commented Oct 3, 2026

Copy link
Copy Markdown

Note

Review skipped

Codemetrics is installed on this repository, but it hasn't been added to your team yet.

To enable AI code reviews for this repo, go to your dashboard and add BaseScanning to your team's repositories.

@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Adds retry handling for server answers the app cannot parse.

The PR appears safe to merge based on the changes since the previous review.

Summary

This PR keeps scans open when a server answer cannot be used, offers a homeowner-initiated retry, and keeps server refusals separate. Since the previous review, the repeated-retry button changed from secondary to quiet styling.

Reviews (4) · Last reviewed commit: "Give the repeated Try again the light sc..."

SamGu-NRX and others added 3 commits October 3, 2026 03:59
On the hosted Simulator (Actions run 37107233020) the app's upload to the
UI test's server on 127.0.0.1 failed as offline, so the test never saw an
unusable answer. That server lived in the test runner, a background app
while the app under test is open; the same listener code answers URLSession
on a Mac at 200 B to 3 MB.

Under -answersFromGate the app's real PlacementHTTPClient sends through a
URLProtocol that writes each request into the -autopilotGate folder and
answers HTTP 200 with the test's answer-n.json. The engine, the status check
and ResultBinding run as against a server; only the socket is replaced.
HouseScanKit's PlacementHTTPClientTests still cover the real loopback socket.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A fresh review found two fault paths in the test hook. The app numbered a
request before reading its body, and the test's server waited for numbers
in order, so one request the app failed to write left every later one
unanswered. The app now numbers a request only once its body is read, and
the server finds requests by number in the folder.

stop() also returned while its thread could still be writing into the gate
folder the test was about to delete. It now waits for the thread and
reports any answer it couldn't write from the test's own thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The second unusable-answer state used the dark secondary style, which the
app keeps for buttons over the camera. On this light screen it drew as a
heavy dark pill, narrower than its neighbours and stronger than Share scan,
the way out the note points to (CI screenshot uploading-unusableAnswer-2,
run 37112184653). It now uses the quiet style the light screens use for a
second choice.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SamGu-NRX added a commit that referenced this pull request Oct 3, 2026
…nch: a close-up the phone couldn't save says so

LaunchOptions.swift conflicted only because #203 and #208 each add a launch option in the same places (-answersFromGate, -failCloseUpSave); both are kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@SamGu-NRX

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: a7458e7c57

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

SamGu-NRX added a commit that referenced this pull request Oct 3, 2026
… packaged

#203 wrote its notes before #205's Saved scans existed, so a packaged scan's note
still said House Scan can't reopen it after the app closes, with no mention that
Saved scans on the first screen can share its file then. Now the first note says
so and that it can't try again from there, and the repeated note adds that Saved
scans can share it later. Without a packaged scan the notes are unchanged, as is
the failed-upload note #205 already aligned.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SamGu-NRX added a commit that referenced this pull request Oct 3, 2026
…branch: let the homeowner answer "I can't check this area" on the spot check

ResultClient.swift conflicted where #203 replaced UploadError.missingSample with MissingSampleResult and #206 added the answerFile early return; both are kept: the answer file first, then the bundled sample with #203's error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@SamGu-NRX

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 effective changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant