Shared Photos timeline stays empty after member opt-in #188

Closed
opened 2026-09-26 15:14:44 +00:00 by kayg · 7 comments
Owner

Finding from the local real-server adversarial run on 2026-09-26.

tests/adversarial/attack2.py created an active Share for the owner’s Photos root, opted the member into Shared/<owner-id>/Photos, then polled the member’s /api/v1/photos/timeline?before=2024-06-03&days=7&tiles_per_day=10 for 12 seconds. Every response was HTTP 200, but days stayed empty, so the probe reported Photos shared timeline: active shared photo never appeared. The owner photo fixture had been created by round one in this full run.

The shared host was heavily loaded during the probe (load averages around 44–51, with multiple Calternal servers and adversarial jobs active). This is a non-SLOW result because the API returned an empty timeline for the whole polling window, so it needs reproduction on a quiet host. Check that a shared Photos root shows the owner’s photo after member opt-in and becomes empty again after Share revocation.

Finding from the local real-server adversarial run on 2026-09-26. `tests/adversarial/attack2.py` created an active Share for the owner’s `Photos` root, opted the member into `Shared/<owner-id>/Photos`, then polled the member’s `/api/v1/photos/timeline?before=2024-06-03&days=7&tiles_per_day=10` for 12 seconds. Every response was HTTP 200, but `days` stayed empty, so the probe reported `Photos shared timeline: active shared photo never appeared`. The owner photo fixture had been created by round one in this full run. The shared host was heavily loaded during the probe (load averages around 44–51, with multiple Calternal servers and adversarial jobs active). This is a non-SLOW result because the API returned an empty timeline for the whole polling window, so it needs reproduction on a quiet host. Check that a shared Photos root shows the owner’s photo after member opt-in and becomes empty again after Share revocation.
Author
Owner

Adversarial evidence for #188 from tests/adversarial/run.sh on job/analytics at HEAD 599c3e1d52f10e3bb197bd75a87d9c2696a191cd: after the owner had an indexed photo dated 2024-06-02, created a Photos share for the member, and the member opted in to Shared/<owner-id>/Photos, repeated GET /api/v1/photos/timeline?before=2024-06-03&days=7&tiles_per_day=10 responses were HTTP 200 with {"days":[]} through the 12-second visibility deadline. The server stayed alive. This occurred in the full shared-host adversarial round (load average about 45–52); filing the exact observed response here for the Photos owner.

Adversarial evidence for #188 from `tests/adversarial/run.sh` on `job/analytics` at HEAD `599c3e1d52f10e3bb197bd75a87d9c2696a191cd`: after the owner had an indexed photo dated 2024-06-02, created a Photos share for the member, and the member opted in to `Shared/<owner-id>/Photos`, repeated `GET /api/v1/photos/timeline?before=2024-06-03&days=7&tiles_per_day=10` responses were HTTP 200 with `{"days":[]}` through the 12-second visibility deadline. The server stayed alive. This occurred in the full shared-host adversarial round (load average about 45–52); filing the exact observed response here for the Photos owner.
Author
Owner

Merged-head rerun on 2026-09-26 reproduced the finding. The owner fixture photo was present, the member opted into Shared/<owner-id>/Photos, and polling /api/v1/photos/timeline?before=2024-06-03&days=7&tiles_per_day=10 still returned HTTP 200 with days: [] through the 12-second window. The server stayed alive. This remains a non-SLOW functional finding; reproduce again on an idle host as the shared host was loaded.

Merged-head rerun on 2026-09-26 reproduced the finding. The owner fixture photo was present, the member opted into `Shared/<owner-id>/Photos`, and polling `/api/v1/photos/timeline?before=2024-06-03&days=7&tiles_per_day=10` still returned HTTP 200 with `days: []` through the 12-second window. The server stayed alive. This remains a non-SLOW functional finding; reproduce again on an idle host as the shared host was loaded.
Author
Owner

Starting from dev at . I read CLAUDE.md, CONTEXT.md, and DESIGN.md sections covering Shares, Photos Library scope, and Index-backed views, plus issues #173, #187, #188, #215, #277, and the #331 cross-User rule. I am tracing the shared root from opt-in through projection to the timeline query.

Starting from dev at . I read CLAUDE.md, CONTEXT.md, and DESIGN.md sections covering Shares, Photos Library scope, and Index-backed views, plus issues #173, #187, #188, #215, #277, and the #331 cross-User rule. I am tracing the shared root from opt-in through projection to the timeline query.
Author
Owner

Starting job/photos-shared from dev at fba83527f2cccf2334934bb1fd0932be7c0e209b. I read CLAUDE.md, CONTEXT.md, and DESIGN.md sections covering Shares, Photos Library scope, and Index-backed views, plus issues #173, #187, #188, #215, #277, and the #331 cross-User rule. I am tracing the shared root from opt-in through projection to the timeline query.

Starting `job/photos-shared` from dev at `fba83527f2cccf2334934bb1fd0932be7c0e209b`. I read CLAUDE.md, CONTEXT.md, and DESIGN.md sections covering Shares, Photos Library scope, and Index-backed views, plus issues #173, #187, #188, #215, #277, and the #331 cross-User rule. I am tracing the shared root from opt-in through projection to the timeline query.
Author
Owner

Finding for #188: the opt-in route started one background Photos index rebuild. When SQLite returned BUSY during that rebuild, the task logged and discarded the error. The recipient then kept an empty timeline until a later Files event triggered another rebuild. The share and timeline scope checks were already correct, so the fix retries the full recipient rebuild on DatabaseBusy only, with bounded delays (25 ms and 75 ms).

Evidence: shared_root_opt_in_recovers_after_writer_contention_and_only_shows_granted_photos failed when I temporarily restored the one-shot rebuild: actual recipient timeline IDs were [], expected ['shared-photo']. With the fix, the test passes and asserts that the recipient sees the granted photo only, excluding the owner's private photo and the recipient's own photo. The full Photos package suite also passes: 43 passed, 0 failed, 2 ignored.

Finding for #188: the opt-in route started one background Photos index rebuild. When SQLite returned BUSY during that rebuild, the task logged and discarded the error. The recipient then kept an empty timeline until a later Files event triggered another rebuild. The share and timeline scope checks were already correct, so the fix retries the full recipient rebuild on DatabaseBusy only, with bounded delays (25 ms and 75 ms). Evidence: `shared_root_opt_in_recovers_after_writer_contention_and_only_shows_granted_photos` failed when I temporarily restored the one-shot rebuild: actual recipient timeline IDs were `[]`, expected `['shared-photo']`. With the fix, the test passes and asserts that the recipient sees the granted photo only, excluding the owner's private photo and the recipient's own photo. The full Photos package suite also passes: 43 passed, 0 failed, 2 ignored.
Author
Owner

Finished #188 on job/photos-shared at HEAD 9bd81553c211b285a2898417a0e3a6d14e790403. The branch is pushed and the worktree is clean. No merge into dev was performed.

Built

A recipient Photos-root refresh now retries the full index rebuild up to three times when SQLite reports DatabaseBusy, with 25 ms and 75 ms waits. Other errors still return immediately. The regression uses two Users, a real Share and opt-in route, and SQLite writer contention. It asserts that the recipient timeline contains exactly the granted photo, excluding both Users' private photos.

Changed files:

  • crates/plugins/photos/src/index.rs
  • crates/plugins/photos/src/routes.rs
  • tests/adversarial/attack2.py

Commits: 049f7aee (Photos retry and regression) and 9bd81553 (one-time merge from dev).

Verification

  • cargo test -p calternal-plugin-photos:
    test result: ok. 43 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 4.90s
    test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
    
  • cargo fmt --check: no output; exit 0.
  • cargo clippy --all-targets -- -D warnings: stopped at the four-hour job limit with exit 130. It emitted no warnings before interruption. Its last output was:
    Checking lz4_flex v0.13.1
    Checking census v0.4.2
    Checking postscript v0.14.1
    Checking calternal-plugin-files v0.0.1 (/home/kayg/Developer/calternal-wt/photos-shared/crates/plugins/files)
    
  • Full cargo test, bun run check, and bun run test were not run because the job reached the four-hour limit.
  • cargo clean:
         Removed 16685 files, 7.6GiB total
    

The two-User round2 isolation probe passed:

server alive at end: True

==== ROUND 2 FINDINGS 0

==== ROUND 2 SLOW 1
 - unindexed shared child upload :: 7.4s status 201

The broader adversarial prelude recorded unrelated findings. Non-SLOW evidence was added to existing issues #217, #258, #362, #368, and #41. The hostile-bytes phase reported 0 findings. Two Bun prelude probes could not find the server binary because their harness used target/debug while this job uses the preset CARGO_TARGET_DIR; those probes were not rerun.

Decisions and limits

DESIGN does not specify retry policy for a busy SQLite writer. I used three total attempts with 25 ms and 75 ms delays, and retry only for DatabaseBusy.

The local adversarial server and clippy command used system OpenSSL 3.5.7 with OPENSSL_NO_VENDOR=1 after the vendored build exceeded the timebox. The repository dependency configuration was not changed.

Finished #188 on `job/photos-shared` at HEAD `9bd81553c211b285a2898417a0e3a6d14e790403`. The branch is pushed and the worktree is clean. No merge into `dev` was performed. ## Built A recipient Photos-root refresh now retries the full index rebuild up to three times when SQLite reports `DatabaseBusy`, with 25 ms and 75 ms waits. Other errors still return immediately. The regression uses two Users, a real Share and opt-in route, and SQLite writer contention. It asserts that the recipient timeline contains exactly the granted photo, excluding both Users' private photos. Changed files: - `crates/plugins/photos/src/index.rs` - `crates/plugins/photos/src/routes.rs` - `tests/adversarial/attack2.py` Commits: `049f7aee` (Photos retry and regression) and `9bd81553` (one-time merge from `dev`). ## Verification - `cargo test -p calternal-plugin-photos`: ```text test result: ok. 43 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 4.90s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` - `cargo fmt --check`: no output; exit 0. - `cargo clippy --all-targets -- -D warnings`: stopped at the four-hour job limit with exit 130. It emitted no warnings before interruption. Its last output was: ```text Checking lz4_flex v0.13.1 Checking census v0.4.2 Checking postscript v0.14.1 Checking calternal-plugin-files v0.0.1 (/home/kayg/Developer/calternal-wt/photos-shared/crates/plugins/files) ``` - Full `cargo test`, `bun run check`, and `bun run test` were not run because the job reached the four-hour limit. - `cargo clean`: ```text Removed 16685 files, 7.6GiB total ``` The two-User round2 isolation probe passed: ```text server alive at end: True ==== ROUND 2 FINDINGS 0 ==== ROUND 2 SLOW 1 - unindexed shared child upload :: 7.4s status 201 ``` The broader adversarial prelude recorded unrelated findings. Non-SLOW evidence was added to existing issues #217, #258, #362, #368, and #41. The hostile-bytes phase reported 0 findings. Two Bun prelude probes could not find the server binary because their harness used `target/debug` while this job uses the preset `CARGO_TARGET_DIR`; those probes were not rerun. ## Decisions and limits DESIGN does not specify retry policy for a busy SQLite writer. I used three total attempts with 25 ms and 75 ms delays, and retry only for `DatabaseBusy`. The local adversarial server and clippy command used system OpenSSL 3.5.7 with `OPENSSL_NO_VENDOR=1` after the vendored build exceeded the timebox. The repository dependency configuration was not changed.
Author
Owner

Merged into dev by Claude after review (413ccaa7), deploying to calternal.cloud. Closing.

Merged into dev by Claude after review (413ccaa7), deploying to calternal.cloud. Closing.
kayg closed this issue 2026-09-28 23:46:30 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#188
No description provided.