Skip to content

fix(update): stage native updates on exit + move notice below input - #172

Merged
elkaix merged 2 commits into
mainfrom
fix/update-notice-and-silent-update-corruption
Jun 20, 2026
Merged

fix(update): stage native updates on exit + move notice below input#172
elkaix merged 2 commits into
mainfrom
fix/update-notice-and-silent-update-corruption

Conversation

@elkaix

@elkaix elkaix commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two separate, user-reported update-flow bugs.

1. Update notice rendered inside the input box → now below it

The persistent "Updated → vX. Restart to apply." line was prepended above the footer separator rule, so it sat between the input line and the box's bottom border — visually inside the input area. It now renders after the separator, underneath the box.

──────────────  (top border)
❯ your input
──────────────  (bottom border)
Updated 0.48.0 → 0.49.0. Restart Pythinker to apply.   ← now here
◇ · ~ · yolo                                  1m elapsed

_prepend_update_notice_append_update_notice; both toolbar call sites (card + pythinker styles) place it after the separator.

2. zlib.error: incorrect header check after ChatGPT login

Root cause (confirmed from the crash trace): the silent auto-updater's _install_native_archive did os.replace(...) over sys.executable, overwriting the running PyInstaller onefile bundle in place. This build reads its Python module archive lazily from the exe path, so the first not-yet-loaded import after the swap — llm.py:358, the openai_codex login branch importing openai_responses — read a stale archive and crashed fatally.

Fix: the native update now stages the new binary beside the running exe (.{exe}.staged) and promotes it via os.replace at process exit (atexit), so it goes live on the next launch — matching the "Restart to apply" notice. The post-install smoke check now validates the staged binary; a binary that fails smoke is discarded, never promoted. A narrow boundary guard (_is_post_update_bundle_corruption) converts any residual archive/zlib crash on a frozen build into a clean "restart to apply" message instead of a fatal traceback.

Scope: limited to the native-archive path (the reported bug). Linux-package and pip in-place paths are unchanged.

Known residual (documented, not silent)

Promotion at atexit leaves two far-smaller windows than the mid-session crash it replaces: a SIGKILL skips atexit (promotion self-heals on the next clean exit), and a first-time lazy import during the rest of interpreter shutdown could read stale bytes. Fully closing both needs a detached post-exit swapper (the deferred seamless-relaunch design); noted in code.

Testing

  • make check-pythinker-code (ruff + pyright + ty) — green
  • Full tests suite — 6388 passed, 7 skipped, 1 xfailed
  • New tests: notice below-separator placement, staging-without-touching-running-exe, promote/discard, smoke-targets-staged binary, idempotent atexit registration, corruption-guard detection (incl. cause-chain + cyclic-chain)

Honest status: root cause confirmed from the trace and verified by unit-tested mechanics; not yet reproduced end-to-end on a packaged native build (requires a live release + update).

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Silent updates no longer crash active sessions; native updates are now safely staged and applied on restart.
    • Added a recovery guard for post self-update bundle corruption to show “restart to apply” instead of failing abruptly.
  • Improvements

    • Update availability notice now renders below the prompt’s input/footer separator for clearer visibility.

Two separate update-flow bugs reported together.

1. The "Updated → vX. Restart to apply." notice rendered inside the prompt
   input box (between the input line and the footer separator). It now renders
   below the separator rule, underneath the box. Helper renamed
   `_prepend_update_notice` → `_append_update_notice` and both toolbar call
   sites place it after the separator.

2. Logging into ChatGPT after a silent auto-update crashed with
   `zlib.error: incorrect header check`. Root cause: the silent updater's
   `_install_native_archive` did `os.replace` over `sys.executable`, overwriting
   the running PyInstaller onefile bundle in place. This build reads its Python
   archive lazily from the exe path, so the first not-yet-loaded import after the
   swap (llm.py `openai_codex` branch) read a stale archive and died.

   The native update now stages the new binary beside the running exe
   (`.{exe}.staged`) and promotes it via `os.replace` at process exit (atexit),
   so it goes live on the next launch — matching the restart notice. The smoke
   check validates the staged binary; a binary that fails smoke is discarded,
   never promoted. A boundary guard converts any residual post-update archive
   corruption into a clean "restart to apply" message instead of a fatal
   traceback.

Verified: make check-pythinker-code (ruff + pyright + ty) green; full test
suite 6388 passed, 7 skipped, 1 xfailed.
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6d9f6bf7-139f-44d6-bd31-18c097210305

📥 Commits

Reviewing files that changed from the base of the PR and between 700e20e and 1714120.

📒 Files selected for processing (4)
  • src/pythinker_code/cli/__init__.py
  • src/pythinker_code/ui/shell/update.py
  • tests/cli/test_post_update_corruption_guard.py
  • tests/ui/test_update_native.py

📝 Walkthrough

Walkthrough

Native self-updates now stage the downloaded binary beside the running executable and promote it via atexit instead of overwriting in-place. The orchestrator smoke-checks the staged binary and either registers promotion or discards it. A new corruption guard intercepts frozen-build zlib.error crashes post-update and converts them to a restart message. The "update available" notice is repositioned below the prompt separator.

Changes

Native update staging and corruption guard

Layer / File(s) Summary
Staged native update helpers and reworked install
src/pythinker_code/ui/shell/update.py
Adds atexit import, staged_native_path(), register_staged_native_promotion(), discard_staged_native_update(), and reworks _install_native_archive to write to a deterministic staged path via a per-PID temp file instead of overwriting the running executable.
Orchestrator staging lifecycle and smoke-check targeting
src/pythinker_code/ui/shell/update_orchestrator.py
_smoke_check_command runs the staged binary path with --version when present. _finalize_native_staging(promote) either registers exit-time promotion or discards the staged file based on smoke-check result.
Post-update bundle corruption classifier and CLI handler
src/pythinker_code/cli/__init__.py
_is_post_update_bundle_corruption walks the exception chain for a zlib.error on frozen builds (cycle-guarded). The CLI top-level exception handler intercepts matching exceptions, logs a warning, and exits with code 1 after a restart instruction.
Tests: staging lifecycle and promotion
tests/ui/test_update_native.py
Covers staging without touching running exe, promotion swap, no-op promotion when staged file absent, discard, idempotent registration, and repeated promotion.
Orchestrator smoke-check targeting staged binary
tests/ui_and_conv/test_update_orchestrator.py
Test verifies orchestrator smoke-check targets the staged binary when present.
Tests: corruption guard detection and edge cases
tests/cli/test_post_update_corruption_guard.py
Covers zlib.error detection on frozen builds via direct exception, explicit cause chain, and implicit context; false negatives when unfrozen or mismatched message; and cyclic cause-chain termination.
Changelog: native staging and corruption guard
CHANGELOG.md
Documents staged native update behavior and post-update corruption crash handling.

Update notice repositioning below prompt separator

Layer / File(s) Summary
Prompt helper rename and toolbar reorder
src/pythinker_code/ui/shell/prompt.py, src/pythinker_code/ui/shell/__init__.py, tests/ui_and_conv/test_prompt_tips.py
_prepend_update_notice is replaced by _append_update_notice; both standard and card-style toolbars emit the separator first then append the notice below it. Shell comment and tests updated to match.

Sequence Diagram(s)

N/A — the changes compose existing APIs (staging, promotion, orchestration, CLI error handling) with no novel multi-component flows that would benefit from visualization beyond the checkpoints already shown in the hidden stack.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Pythoughts-labs/pythinker-code#130: Both PRs modify the update-job orchestration path in ui/shell/update_orchestrator.py around post-install smoke-check handling/failure reporting (main PR adds native staged promotion finalization; retrieved PR changes the smoke-check failure message prefix).

Suggested labels

bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.95% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional commits format with type(scope) and clearly describes the main changes: staging native updates and repositioning the update notice.
Description check ✅ Passed Description comprehensively covers both bug fixes with root cause analysis, implementation details, testing scope, and known limitations, following the template structure.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/update-notice-and-silent-update-corruption

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

Comment thread src/pythinker_code/ui/shell/update.py Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pythinker_code/ui/shell/update_orchestrator.py (1)

377-389: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Smoke-check failure is still reported as UPDATED.

When smoke-check fails, the staged binary is discarded, but reported_result and final_state stay
derived from UpdateResult.UPDATED. That can persist a success status and return success even though no
update will be applied.

Suggested fix
         reported_result = result
         final_state = _result_state(result)
         message = result.name.replace("_", " ").lower()
         if result is UpdateResult.UPDATED and not check_only:
             smoke_ok, smoke_message = run_post_install_smoke_check()
             append_update_log(smoke_message)
             if smoke_ok:
                 message = smoke_message
                 _write_last_success(job_id=job_id, message=message)
                 _finalize_native_staging(promote=True)
             else:
                 message = f"{SMOKE_CHECK_FAILED_PREFIX}{smoke_message}"
                 # Never promote a staged binary that can't even print --version.
                 _finalize_native_staging(promote=False)
+                reported_result = UpdateResult.FAILED
+                final_state = UpdateJobState.FAILED

As per coding guidelines, “Never return success … after a required internal step failed.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pythinker_code/ui/shell/update_orchestrator.py` around lines 377 - 389,
When the smoke check fails (smoke_ok is False in the else block), the
final_state variable still reflects UpdateResult.UPDATED (success) because it
was set before the smoke check ran. Update the final_state variable in the else
block where smoke_ok is False to reflect a failure state instead of keeping it
as the success state derived from the original UpdateResult.UPDATED result. This
ensures the function returns the correct failure status when a required internal
step (the smoke check) fails.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pythinker_code/cli/__init__.py`:
- Around line 360-363: The corruption classifier in the block checking for
zlib.error is too broad and will match any zlib.error instance, but the
documented signature for post-update corruption is specifically "zlib.error:
incorrect header check". Narrow the condition that checks if cls.__module__ ==
"zlib" and cls.__name__ == "error" to also validate that the error message
contains "incorrect header check". This ensures only the known bundle corruption
case is classified as such, preventing misclassification of other zlib
decompression failures.

---

Outside diff comments:
In `@src/pythinker_code/ui/shell/update_orchestrator.py`:
- Around line 377-389: When the smoke check fails (smoke_ok is False in the else
block), the final_state variable still reflects UpdateResult.UPDATED (success)
because it was set before the smoke check ran. Update the final_state variable
in the else block where smoke_ok is False to reflect a failure state instead of
keeping it as the success state derived from the original UpdateResult.UPDATED
result. This ensures the function returns the correct failure status when a
required internal step (the smoke check) fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d5bd1748-551c-4230-aa3f-084fd3df3636

📥 Commits

Reviewing files that changed from the base of the PR and between 2e0f4a4 and 700e20e.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • src/pythinker_code/cli/__init__.py
  • src/pythinker_code/ui/shell/__init__.py
  • src/pythinker_code/ui/shell/prompt.py
  • src/pythinker_code/ui/shell/update.py
  • src/pythinker_code/ui/shell/update_orchestrator.py
  • tests/cli/test_post_update_corruption_guard.py
  • tests/ui/test_update_native.py
  • tests/ui_and_conv/test_prompt_tips.py
  • tests/ui_and_conv/test_update_orchestrator.py

Comment thread src/pythinker_code/cli/__init__.py Outdated
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.72727% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/pythinker_code/ui/shell/update_orchestrator.py 46.66% 7 Missing and 1 partial ⚠️
src/pythinker_code/ui/shell/update.py 84.00% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

…p flag

- _is_post_update_bundle_corruption now also requires the documented
  "incorrect header check" message, so an unrelated zlib decompression failure
  on a frozen build isn't misclassified as a stale bundle and masked behind a
  restart-only message (CodeRabbit).
- Drop the _staged_promotion_registered module global; register_staged_native
  _promotion now registers unconditionally and relies on the already-idempotent
  atexit handler (no-ops once the staged file is promoted), removing the unused-
  global finding (github-code-quality).
- Tests updated/added: unrelated-zlib-message rejection; handler idempotency on
  double-run; registration registers the atexit handler.
@elkaix
elkaix merged commit 546a71b into main Jun 20, 2026
39 checks passed
@elkaix
elkaix deleted the fix/update-notice-and-silent-update-corruption branch June 20, 2026 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant