# ask-gpt attachment download progress

Status: DONE
Priority: 7
Task IDs: #5, #6, #17
Source request: 2026-08-11 owner reported `ask-gpt --download-attachments 6a79c500-04bc-83ec-99e6-24bb70fb8de1` appeared dead and requested immediate background activity, full pre-download file list, file-count progress bar, elapsed time, ETA, UX improvements, and implementation by native `gpt-5.6-sol` subagent. 2026-08-11 implementation dispatch requires this worker to change only attachment recovery and focused tests in the preserved worktree, run focused tests plus the full gptbridge suite, and hand the uncommitted diff to the main session for personal review. This worker MUST NOT commit, land, deploy, live-probe, or modify registry/list/bootstrap/account-binding behavior. 2026-08-11 main-session review requires two surgical corrections before handoff: normalize all DOM-derived file/button labels to one printable deterministic line before any plan/item/failure event or rendering, and replace direct destination writes with private same-directory temporary writes plus atomic no-overwrite publication and unconditional failure cleanup. Add deterministic regressions for terminal-control injection and partial-write cleanup while preserving prior completed outputs; rerun focused and full gptbridge suites; keep changes uncommitted. 2026-08-11 coordinator correction identifies placeholder reservation plus `os.replace` as still overwrite-racy; publication MUST instead use same-filesystem `os.link(temp_path, destination)`, which atomically fails on an existing raced destination, then remove the temp path in cleanup. Regression MUST create the destination immediately before publish and prove its content survives with no temp residue. 2026-08-11 gate-correction dispatch requires this worker to add one validated generic `--remote-env NAME=value` seam to `local-gate`, forward it through every remote-build payload only, pass `CI=true` from `finish-branch` test gates, add deterministic focused regressions, and run only `node local-gate.test.mjs` plus `bash test-finish-branch.sh`. This worker MUST NOT change containment, attachment, registry/bootstrap, or unrelated files; MUST NOT invoke bootstrap; and MUST leave all work uncommitted, undeployed, and uninstalled for main-session review. 2026-08-11 installed proof found the target conversation renders its generated artifact as a filename-only button containing `p.not-prose.truncate`, not a `Download <name>` button; the landed selector therefore reported a false empty manifest. Owner `go` requires a surgical selector correction, deterministic regression, installed runtime update, and repeat live proof without invoking registry bootstrap.

## Outcome

`ask-gpt --download-attachments <conversation-id>` gives immediate, continuous, truthful progress without `--live`: visible queue/open/scan/fetch phases, manifest before transfer, item-count progress, elapsed/approximate ETA, per-item result, and final summary. Direct and daemon paths expose the same lifecycle.

Canonical contract: `docs/specs/2026-08-11-ask-gpt-download-progress-design.md`.

## Acceptance criteria

- Print activity immediately and heartbeat during every blocking phase.
- Discover and print complete attempted/unavailable/invalid manifest before first fetch.
- TTY shows one in-place current-file/completed-count/elapsed/approximate-ETA bar.
- Non-TTY shows bounded plain status and heartbeat lines; no ANSI.
- Default download command renders progress independently of `--live`; `--json` stdout remains valid machine output.
- Every attempted item emits one started and one saved/failed event; daemon preserves all control events and structured terminal metadata.
- Direct interruption exits 130 and truthfully reports completed files kept. Daemon interruption says only watching stopped and daemon may continue.
- Existing collision, deduplication, empty-manifest, and partial-failure behavior remains compatible.
- `python3 -m pytest modules/gptbridge/tests/ -q` is clean.
- Installed `$PATH` entrypoint visibly proves behavior against conversation `6a79c500-04bc-83ec-99e6-24bb70fb8de1`.
- Land, deploy locally, and record owner-visible receipt after main-session review; outside this implementation worker's scope.
- Current implementation handoff: only the three named gptbridge modules, focused tests, and required plan receipts may change; focused tests and `python3 -m pytest modules/gptbridge/tests/ -q` MUST pass cleanly; leave changes uncommitted for main-session review; do not live-probe.
- Main-review correction acceptance: every DOM-derived file/button label MUST be normalized before entering events; rendered manifests MUST contain no raw newline, C0/DEL control, or ANSI escape from labels; attachment blob writes MUST publish atomically from a private same-directory temp without overwriting a raced destination; every failed write MUST remove partial temp/destination artifacts and preserve earlier successful outputs.
- Gate-correction acceptance: `local-gate --remote-env NAME=value` MUST validate names and reject newline/NUL values; every initial/retry/wait remote payload MUST receive the parsed map; local fallback MUST retain caller environment without injected remote values; `finish-branch` test gate MUST request `CI=true`; both requested focused suites MUST pass.
- Current ChatGPT generated-file pills MUST be discovered and clicked when their button label is a filename without a `Download ` prefix. Selector discovery and click ordering MUST use the same predicate; ordinary response-action buttons MUST remain excluded. A deterministic regression MUST cover both legacy `Download <name>` and filename-pill markup.

## Preserved WIP

- Worktree: `/home/user/Projects/overdeck/.worktrees/ask-gpt-download-progress`
- Branch: `wt/ask-gpt-download-progress`
- Current WIP: reviewed progress implementation committed as `eac2ce60c`; landed registry merged from `origin/main`, conflicts resolved by preserving progress lifecycle plus required account ownership checks; verification pending.
- Account registry is landed/deployed and bootstrapped. Integration preserves canonical lookup, unknown-ID rejection, and expected-account verification.

## Constraints

- Change only `modules/gptbridge/chat.py`, `modules/gptbridge/solwebd.py`, `modules/gptbridge/ask_gpt.py`, and focused gptbridge tests unless an evidenced seam requires less/more.
- Do not modify registry/list/bootstrap/account-binding behavior. This branch predates the separate registry work; correction code MUST remain merge-compatible. Final integration after registry lands MUST preserve server-side conversation lookup, unknown-ID rejection, and expected-account verification rather than duplicating those controls here.
- No fake byte progress: current blob boundary exposes only item completion.
- Keep answer deltas bounded/coalescible; download/control events lossless and ordered.
- Do not add daemon cancellation, whole-command retry UX, or new button-download idempotency scope.
- Renderer failures must not abort core recovery.
- Native implementation subagent inherits `gpt-5.6-sol`; main session personally reviews its diff.

## Execution steps

1. Freeze reviewed manifest/event/progress/error contract in design spec.
2. Dispatch native inherited-model implementation worker in preserved worktree.
3. Main-session review against spec and current account-registry integration boundary; correct defects surgically.
4. Run focused and full gptbridge suites with all warnings/errors addressed.
5. Integrate latest landed registry from `origin/main`; rerun affected verification.
6. Commit locally, install/deploy through project delivery path, and prove installed `$PATH` UX on target conversation.
7. Land through `.claude/scripts/ship.sh`, deploy landed source, and record final receipts.

## Current receipt

2026-08-11: reviewed implementation committed as `eac2ce60c`. DOM-derived file/button labels normalize at discovery to one printable whitespace-collapsed line with deterministic fallback; plan, item, unavailable, and failure surfaces inherit only normalized labels. Blob/link data writes to private unique same-directory temp files, detects short writes, atomically publishes with `os.link(temp_path, destination)`, never overwrites an existing or raced destination, and always removes the temp path while preserving prior successful outputs. Race regression creates the destination inside the publish call and proves raced content survives with no temp residue. Focused attachment suite passed 30/30. Definitive pre-integration full gptbridge suite passed 257 with 1 skipped and no warnings. `py_compile` and `git diff --check` passed. Main-session review passed manifest ordering, renderer lifecycle, ETA/count semantics, direct/daemon fallback and interruption behavior, lossless control-event ordering, terminal-label safety, and atomic no-overwrite publication.

Registry integration receipt: merged landed `origin/main` at `b59a7f7f`. Resolved only `ask_gpt.py`, `chat.py`, `solwebd.py`, and plan-index conflicts. Direct download now passes registry-owned `expected_account` into the manifest-first lifecycle; daemon independently reads canonical ownership and passes it into the same lifecycle; `download_attachments_on` performs account verification before target navigation. Unknown IDs still fail before browser access. Conflict-marker, unmerged-path, and staged/unstaged diff checks are clean. Focused registry/progress integration suite passed 81. Full suite excluding two real-Xvfb environment probes passed 266 with 1 skipped and 2 deselected; the complete run passed the same 266 and failed only `test_a_second_display_starts_alongside_the_first` plus `test_the_display_is_usable` because host Xvfb reported `Failed to find a socket to listen on`. The independent orphan-reaping failure from that complete run passed immediately in isolation, confirming startup race flakiness. `health.sh` exited cleanly but explicitly skipped its installed-runtime suite because this worktree is not installed; landed deployment and installed-entrypoint proof remain required.

First canonical land stopped without publishing after two candidate test failures. Reproduction against current main found installed `build.slice` drift (`MemoryHigh` and `MemoryMax` were finite while landed tests require `infinity`); deploying current main reconciled both to `infinity`. Deployment reported pre-existing shim-copy drift in unrelated worktrees; live shims match `origin/main`, so preserving those owners' WIP is required and no task-scoped fix applies. A later remote gate attempt hit transient `EADDRINUSE` on controller test port `19876`; both reachable buildboxes had no listener after the failed run. The immediate identical canonical retry passed completely (`tests/os: all suites green`) with only expected warning words inside passing fixture names. Latest `origin/main` is merged as `6885d2c20`.

Authorized retry `ticket.3a73c43691144f4a9c53ef4339fd8234` completed and failed its first candidate test attempt; no publication occurred. `ask-gpt-download-progress-land.service` is inactive/dead with `Result=success`, `ExecMainStatus=0`, and no process. Remote candidate mirrors ran workstation containment tests on buildboxes without `CI=true`; both reachable buildboxes retain finite root-owned `build.slice` ceilings while repository policy intentionally removes those ceilings. `containment-invariants.test.sh` already preserves source/repository checks and skips only installed host-state checks under `CI=true`. Debian1 hardening refused before mutation because the independent Tailscale SSH safety door was unavailable; this refusal MUST NOT be bypassed. The blocker is the remote candidate gate execution context, not attachment-progress behavior or workstation deployment.

2026-08-11 task #6 implementation complete and committed as `40b2b3f97`. `local-gate` accepts repeated `--remote-env NAME=value`, validates POSIX-style environment names, rejects CR/LF/NUL values, and forwards the resulting map only through the shared `tryRemote()` payload closure, covering initial dispatch, transient retries, queue waits, timeout requeues, and post-kill retries. Local `spawnSync` still derives only from caller `process.env` plus existing local gate fields. `finish-branch.sh` requests `--remote-env CI=true` only for `run_test_gate`. Main-session review confirmed the shared closure covers all remote retries and the local child environment remains independent. Deterministic tests prove three remote attempts each receive `{CI:"true"}`, local fallback does not gain `CI`, malformed names/newline/NUL fail before dispatch or local execution, and the finish-branch seam receives the exact flag/value. Clean verification with `LC_ALL=C`: `node modules/workstation/claude/tests/local-gate.test.mjs` passed; `bash modules/workstation/claude/workflows/lib/test-finish-branch.sh` passed `PASS=106 FAIL=0`; root `tests/os/run-all.sh` passed remotely with status 0; mandatory local factory suite passed `151 passed in 129.53s`; `git diff --check` passed. The first remote factory attempt failed collection because the buildbox lacks `pydantic`; this is buildbox dependency drift, not a test failure, and the required suite then passed locally. Direct pre-land mutation of the live deploy clone was denied by `main-checkout-guard` as intended, so no installed file changed. Latest `origin/main` was merged after the correction commit; canonical candidate gates remain the authoritative post-merge verification.

Bootstrap constraint: installed `finish-branch.sh` predates `--remote-env`, while direct mutation of its deploy-clone source is correctly forbidden. The one canonical landing run may set `FINISH_BRANCH_LOCAL_GATE` to a private worktree launch adapter that inserts `--remote-env CI=true` and delegates to this branch's tested `local-gate`; local fallback remains non-CI. This adapter only bootstraps the old installed controller and is removed with the worktree after the landed deploy installs the real source.

Canonical retry `ticket.b0726e0e8c214780bbf0d35643b0734a` completed without publication. Candidate typecheck and candidate test attempt 1 passed far enough to reach the final source check, which correctly stopped on `source-dirty-tree` for `docs/plans/2026-08-11-ask-gpt-download-progress.md`; this receipt file had been updated while the controller was active. Service `ask-gpt-download-progress-land.service` is now inactive/dead with `Result=success`, `ExecMainStatus=0`, and no process. No duplicate controller was spawned. The remote-only `CI=true` correction removed the prior buildbox host-state failure; the only terminal blocker in this run is the dirty tracked plan receipt. No land, deploy, installed health, or live download proof occurred. Registry bootstrap remains forbidden and was not invoked. Owner said `go`; this authorizes the next single clean canonical retry and end-to-end installed proof.

Clean retry `ticket.3b8ce7f97c0747c58ac662687382a6bc` waited behind queue depth 2 for 810 seconds, then became conductor but produced no ticket verdict. The controller stopped fail-closed with `land-queue-failed: conducted the queue without a verdict for this ticket`; no candidate gates, publication, deployment, installed health, or live download proof ran. Service is inactive/dead with no process. No duplicate controller or tracked-source edit occurred while active. This is a landing-queue orchestration failure, not an attachment-progress test failure. Registry bootstrap remains forbidden and was not invoked.

Repeated owner recheck request preserves scope and acceptance unchanged: inspect only this service and append-only log; never duplicate an active controller; on green, verify `origin/main`, deployment, installed health, target progress UX, and account binding; never invoke registry bootstrap.

Final landing ticket `ticket.ea028234c5454063a4ec8f97fa69e529` succeeded: candidate merged into `origin/main`, source worktree removed, branch deleted, and local deployment installed SHA `052b1f706d44a00c2f0d42bcb2dda68ad2322443`. Installed queue source contains the fail-closed listing-status fix. Installed gptbridge runtime was then created with pinned requirements and checksum-verified tunnel client. Health executed 281 tests: 279 passed; only the two known real-Xvfb probes failed because this Claude session tmp-jail maps `/tmp/.X11-unix` owner to overflow uid and Xvfb rejects it. The complete installed suite excluding that environmental file passed 278.

Installed live proof ran outside the tmp jail through a transient user service and verified registry ownership as `chatgpt@alex.org.il`, immediate `starting` output, one-second opening/scanning heartbeats with elapsed time, manifest-before-collection, and final summary. It exposed a real selector defect: the target DOM contains one generated artifact button labeled `overdeck-k3s-phase1-result-2026…` with a direct `p.not-prose.truncate` child and no `Download ` prefix, while all three landed discovery/click selectors accept only `/^download\s+.+/i`; the command therefore falsely printed `Will download 0 attachments.`

Candidate correction defines one shared generated-file button predicate used by `HARVEST`, `HARVEST_ALL`, and `CLICK_DOWNLOAD`: preserve legacy `Download <name>` buttons and accept current filename pills containing `p.not-prose.truncate`. Focused attachment tests passed 40. Full candidate suite ran outside the tmp jail and passed 282. Runtime-first install linked `$PATH` to the candidate. Exact target command then verified account `chatgpt@alex.org.il`, immediate phases, one-second heartbeats, complete two-item manifest before collection, in-place 1/2 and 2/2 bar, elapsed/ETA, terminal item accounting, final summary, and one real saved artifact: `overdeck-k3s-phase1-result-20260811T092812Z.tar.gz`. The legacy `Download the complete Phase 1 receipt review` item timed out and was truthfully reported as one failed item while the filename pill saved successfully. Capture: `/home/user/.local/state/overdeck/verification/ask-gpt-download-progress-fixed.typescript`.

Acceptance delta: remote candidate execution MUST receive `CI=true` without changing local fallback semantics; deterministic coverage MUST prove remote-only environment forwarding, local execution remains non-CI, and containment source/repository checks still execute. Because this changes shared landing/gate machinery, mandatory factory and controller/gate tests MUST pass before another landing attempt.

2026-08-11 final delivery receipt: selector correction commit `c621d2d30` landed and local deployment completed at `f05e3969`. The deployed runtime was reinstalled from the deploy clone. Installed health passed `282 passed in 64.41s`. Final post-deployment exact-target proof ran from a clean directory through `/home/user/.local/bin/ask-gpt`: immediate starting/opening/scanning phases and one-second heartbeats appeared; the complete one-item filename-pill manifest printed before collection; the TTY showed file count, progress bar, elapsed time, ETA, and terminal accounting; the final structured summary truthfully reported the generated-file provider timeout after 5:17 and exited 1. Capture: `/home/user/.local/state/overdeck/verification/ask-gpt-download-progress-deployed.typescript`. The same selector/runtime had already downloaded the real tarball during candidate installed proof, while truthfully preserving a separate timeout. Registry ownership remained bound to `chatgpt@alex.org.il`; bootstrap was never re-invoked.

## Live resume-download stall — 2026-08-11

Source request: owner reports currently live `ask-gpt --resume` task stuck after opening attachment drawer, then no action. Treat as owner-visible runtime incident. Preserve active process/browser/log evidence; NEVER broad-kill or restart before identifying exact service/process ownership.

Acceptance delta:

- Reproduce exact resume-triggered attachment path through installed `$PATH` entrypoint.
- Show bounded progress or terminal failure after drawer opens; NEVER hang silently.
- Prove whether stall is drawer hydration, selector wait, browser script, generated-file readiness, blob fetch, event transport, or renderer.
- Add deterministic regression at evidenced seam.
- Install candidate runtime before Git; prove real resume answer plus attachment terminal result.
- Preserve account binding, canonical registry, no-overwrite publication, existing standalone `--download-attachments`, and master-queue design.
- NEVER invoke registry bootstrap.

Restoration sequence:

1. Capture exact live `ask-gpt`, daemon, browser-seat, journal/log, and open-conversation state without mutation.
2. Restore only if safe fallback exists; do not destroy resumable evidence.
3. Reproduce smallest blocking seam and add focused regression.
4. Build isolated candidate; run focused test.
5. Install candidate atomically into runtime path and prove installed entrypoint.
6. Run full gptbridge suite; commit, land, deploy, and re-prove landed runtime.

Final task #17 addendum: Fix commits `e7d789c9e` + `b6edc1c9f` landed via canonical controller in `origin/main` merge `d0cae47bd`. Standalone deploy-clone deployment completed successfully at `d0cae47bd` after pinning `BUILDBOX_HOSTS_CONFIG` to landed deploy source. Buildbox registry remains present at SHA-256 `61cd599c5987be4b738b00e450d66ebd5df1a7ead25d44c799f55f0f37829082`. Deploy shim-drift warnings concern unrelated owner WIP worktrees; live shims match `origin/main`, so preserve that WIP and classify warnings benign for this scoped repair. Reinstalled from deploy source; `$PATH ask-gpt` resolves to `/home/user/.local/share/overdeck/deploy/modules/gptbridge/bin/ask-gpt`. `gptbridge doctor` exited 0. Its missing API-credential notice does not apply to authenticated browser mode, which the live proof exercised. Post-deploy installed `ask-gpt --download-attachments 6a79c500-04bc-83ec-99e6-24bb70fb8de1` completed in 22.895 seconds: planned 1, saved 1, failures/unavailable/invalid/reused 0. `overdeck-k3s-phase2-run-bundle.zip` SHA-256 `9721b6fa8bcb7942cff12dedfc5bfcf4ead64bf02af1021fa9269746fd4e5560`; valid ZIP, 7 members, zero `.part` residue. Registry binding uniquely `chatgpt@alex.org.il`; stale solwebd processes 0; registry bootstrap never invoked. Focused 8 and authoritative full 273 passed, 1 skipped, already recorded.

## Next executable action

None — installed, deployed, and owner-visible proof passed. Master queue remains brainstorming-only.
