Skip to content

Fix stale backend after update, plus title truncation and hour timestamps#65

Merged
nmbrthirteen merged 3 commits into
mainfrom
fix/backend-refresh-on-update
Jul 8, 2026
Merged

Fix stale backend after update, plus title truncation and hour timestamps#65
nmbrthirteen merged 3 commits into
mainfrom
fix/backend-refresh-on-update

Conversation

@nmbrthirteen

@nmbrthirteen nmbrthirteen commented Jul 8, 2026

Copy link
Copy Markdown
Owner

podcli update never updated the backend

update downloaded and swapped only the launcher binary. The embedded Python backend was extracted solely by podcli setup, and ensureRuntime ran setup only when runtime/backend/cli.py was missing. An updated launcher kept driving whatever backend the release that first provisioned it had installed.

The drift was invisible. main.go exports PODCLI_VERSION into the backend and backend/version.py reads it before its own fallback, so a stale backend reported the launcher's version as its own.

It also silently pinned dependencies. ensureBackendDeps reads requirements-runtime.txt from the backend tree and stamps on that file's hash, so a stale tree matched its old stamp and deps never reinstalled, even though the check ran on every invocation. This is the likely shape of the "missing questionary on Windows" class of report.

Fix

  • Stamp the extracted backend with the launcher version, reusing the marker provision already writes for the studio and Remotion bundles. An unstamped tree counts as stale, so existing installs self-heal.
  • Re-extract on version mismatch from every path that runs Python: the runtime commands, the other engine verbs, and the mcp server (stderr-only, so the JSON-RPC channel on stdout stays clean). Previously mcp never called ensureRuntime at all, stranding MCP-only clients.
  • Extract before ensureBackendDeps, so a refreshed tree yields a refreshed dep set.
  • Add setup --refresh to re-provision version-bound artifacts without pulling a model, and run it from update against the newly installed binary, since the new backend is embedded in that binary rather than the running one.
  • Extract before any network provisioning, so an offline or interrupted setup still leaves a backend matching the launcher.
  • Report backend drift in doctor.

Version had three sources of truth

The git tag, package.json, and a hardcoded var Version = "2.2.1" in main.go were independent. The default had rotted two minors behind while the project shipped 2.4.0.

package.json is now the single source: go generate writes cli/VERSION and the launcher embeds it. A test fails when they drift, and the release workflow refuses to build a tag that disagrees. Drops -ldflags "-X main.Version", which the linker applies only to a constant-initialized string and would have silently ignored.

Clip titles were truncated, timestamps never reached hours

Suggested titles were cut to 55 chars with a raw slice before storage, so cards showed ...on an automot. A display concern had leaked into the data layer: the stored title, the render payload, and clip history all carried the shortened text. Titles are stored whole now; truncate_title cuts on a word boundary where a fixed width genuinely applies.

fmt() divided seconds by 60 without rolling into hours, so a clip an hour into an episode read 78:31. HighlightsPage had its own copy of the same bug and now uses the shared helper.

Verification

  • go build, go vet, go test ./... clean.
  • New backend package tests cover the stale/unstamped/mismatch cases.
  • End-to-end in a sandboxed PODCLI_HOME with a stale unstamped backend: a v2.4.1 launcher re-extracts on first run, is idempotent on the second, leaves a repo checkout untouched, refreshes on the mcp path without writing to stdout, and lands the backend before the network stalls.
  • setup --refresh verified to download no model.
  • Verified a lone package.json bump propagates to podcli version after go generate, and that drift fails the test.
  • tsc --noEmit, 61 vitest tests, and the Python suites pass.

Releases 2.4.1.

Summary by CodeRabbit

  • Bug Fixes

    • Improved version consistency so releases only build and publish when the app, backend, and release tag all match.
    • Updated update/refresh flows to automatically refresh runtime components after upgrades, reducing stale-client issues.
    • Made clip titles display more cleanly by normalizing spacing and improving truncation/wrapping behavior.
  • New Features

    • Added clearer time formatting, including proper hour display for longer clips.
    • Added a status check in the setup and diagnostic flow to help identify outdated or unmanaged backends.

`podcli update` downloaded and swapped only the launcher binary. The embedded
Python backend was extracted solely by `podcli setup`, and `ensureRuntime` only
ran setup when `runtime/backend/cli.py` was absent. An updated launcher therefore
kept driving whatever backend the release that first provisioned it had installed.

The drift was invisible: main.go exports PODCLI_VERSION into the backend, and
backend/version.py reads it before its own fallback, so a stale backend reported
the launcher's version as its own.

It also silently pinned dependencies. ensureBackendDeps reads
requirements-runtime.txt from the backend tree and stamps on that file's hash, so
a stale tree matched its old stamp and deps never reinstalled, even though the
check ran on every invocation.

- Stamp the extracted backend with the launcher version, reusing the marker name
  provision already writes for the studio and Remotion bundles. An unstamped tree
  is treated as stale, so existing installs self-heal on first run.
- Re-extract on version mismatch from every path that executes Python: the
  runtime commands, the other engine verbs, and the mcp server (stderr-only, so
  the JSON-RPC channel on stdout stays clean).
- Extract before ensureBackendDeps, so a refreshed tree yields a refreshed dep set.
- Add `setup --refresh` to re-provision the version-bound artifacts without
  pulling a model, and run it from `update` against the newly installed binary,
  since the new backend is embedded in that binary rather than the running one.
- Extract before any network provisioning in setup, so an offline or interrupted
  run still leaves a backend matching the launcher.
- Report backend drift in `doctor`.

Release 2.4.1.
The version lived in three independent places: the git tag, package.json, and a
hardcoded default in main.go. The default had rotted to 2.2.1 while the project
shipped 2.4.0, so any build without the release workflow's -ldflags reported a
version two minors stale.

package.json is now the single source. `go generate` writes cli/VERSION from it
and the launcher embeds that. A test fails when VERSION drifts from package.json,
and the release workflow refuses to build a tag that disagrees with it.

Drops -ldflags "-X main.Version": the linker applies -X only to a
constant-initialized string and would silently ignore an embed-initialized one.
Suggested clip titles were cut to 55 characters with a raw slice before being
stored, so cards showed "...on an automot" and "...is evolving, bu". The cut was a
display concern that had leaked into the data layer: the stored title, the render
payload, and the clip history all carried the shortened text, and `.clip-title`
already had `text-overflow: ellipsis` that never got a chance to run.

Titles are now stored whole. Where a fixed width genuinely applies, truncate_title
cuts on a word boundary instead:

- claude_suggest keeps the full title (whitespace normalized).
- The render payload no longer slices to 40 chars. clip_generator already
  slugifies and caps the filename on its own, so the slice only corrupted the
  title stored in clip history.
- The CLI's heuristic fallback, which builds a title out of raw transcript text,
  uses truncate_title. So does terminal output, where the width is real.
- .clip-title wraps instead of clipping to one line.

Separately, fmt() divided seconds by 60 without rolling into hours, so a clip an
hour into an episode read "78:31". HighlightsPage carried its own copy of the same
bug; it now uses the shared helper.
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR establishes package.json as the single source of truth for versioning, generating cli/VERSION and embedding it into the Go binary, with backend extraction stamping/refresh logic and CI/release validation. Separately, it centralizes title cleaning/truncation in a shared utility, removes 40-character title truncation in exports/UI, and improves time formatting.

Changes

Version stamping and backend refresh

Layer / File(s) Summary
Version generation and embedding
cli/gen-version.sh, cli/VERSION, package.json, cli/version.go, cli/version_test.go
Adds a script deriving VERSION from package.json, embeds VERSION into the binary as Version, and tests that it matches package.json and is trimmed.
Backend extraction stamping
cli/internal/backend/embed.go, cli/internal/backend/embed_test.go
Extract now takes a version parameter and writes a .podcli-version stamp; new Version and IsCurrent helpers read/compare the stamp, validated by new tests.
CLI refresh wiring and setup --refresh
cli/main.go, cli/internal/update/update.go
Wires refreshBackend() into mcp/ensureRuntime/runEngine paths, adds a setup --refresh flag to re-extract the backend, adds backendStamp drift reporting in doctor, and runs a post-update runtime refresh.
Release CI and docs
.github/workflows/release.yml, RELEASE.md
Validates the pushed tag against the generated VERSION, drops the ldflags version override, and documents the new package.json-driven release procedure.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Title cleanup and formatting

Layer / File(s) Summary
Shared title text utility
backend/utils/text.py, backend/cli.py, backend/services/claude_suggest.py, tests/test_text_utils.py
Adds clean_title/truncate_title helpers and adopts them in place of inline truncation logic across the suggestion pipeline.
Full-title export and UI wrapping
src/ui/client/EpisodeWorkspace.jsx, src/ui/web-server.ts, src/ui/public/css/styles.css
Removes 40-character title truncation from export/retry payloads and MCP export endpoint, updating clip-title CSS to wrap instead of ellipsize.
Shared time formatting
src/ui/client/lib.ts, src/ui/client/lib.test.ts, src/ui/client/HighlightsPage.tsx
Rewrites fmt to support hour rollover and negative-time clamping, adds tests, and switches HighlightsPage to use the shared formatter instead of a local mmss helper.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related PRs

  • nmbrthirteen/podcli#16: Introduces the same release pipeline and embedded backend machinery (.github/workflows/release.yml, cli/internal/backend/embed.go) extended here.
  • nmbrthirteen/podcli#49: Also modifies suggest_with_claude in backend/services/claude_suggest.py, overlapping at the same function level.
  • nmbrthirteen/podcli#52: Also modifies title handling in suggest_with_claude within backend/services/claude_suggest.py.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the three main themes of the change: backend refresh after update, title truncation cleanup, and hour timestamp formatting.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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/backend-refresh-on-update

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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.

@nmbrthirteen nmbrthirteen merged commit 5963f2d into main Jul 8, 2026
5 of 6 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
backend/cli.py (1)

2002-2003: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hoist the import out of the loop.

truncate_title is imported on every iteration of the nested win_size/window loop. Functionally harmless (module caching), but inconsistent with the top-level import style used for clean_title in claude_suggest.py.

♻️ Proposed fix
-            if not title:
-                title = text[:60].strip()
-            from utils.text import truncate_title
-            title = truncate_title(title)
+            if not title:
+                title = text[:60].strip()
+            title = truncate_title(title)

Add from utils.text import truncate_title near the top of the file alongside other module-level imports.

🤖 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 `@backend/cli.py` around lines 2002 - 2003, The `truncate_title` import inside
the nested `win_size`/window loop in `backend/cli.py` should be hoisted to the
module level to match the existing import style used elsewhere. Move `from
utils.text import truncate_title` up with the other top-level imports and keep
the loop body only calling `truncate_title(title)` so the import is no longer
repeated on every iteration.
cli/gen-version.sh (1)

6-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider skipping the write when VERSION is already current.

The script unconditionally overwrites VERSION, which touches the file's mtime even when content is unchanged. This can trigger unnecessary rebuilds in build systems that watch file timestamps. A quick guard before writing would make go generate idempotent:

♻️ Optional optimization
 printf '%s\n' "$ver" > "$here/VERSION"

Could become:

+if [ "$(cat "$here/VERSION" 2>/dev/null)" = "$ver" ]; then
+  echo "version $ver (unchanged)"
+  exit 0
+fi
 printf '%s\n' "$ver" > "$here/VERSION"
🤖 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 `@cli/gen-version.sh` around lines 6 - 8, The gen-version.sh script always
rewrites VERSION even when the version is unchanged. Update the version
generation flow around the existing ver assignment and VERSION write so it first
checks the current contents of "$here/VERSION" and skips the write when it
already matches ver, keeping go generate idempotent and avoiding unnecessary
mtime changes.
🤖 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 `@cli/main.go`:
- Around line 45-50: Both the MCP path in main and the non-runtime branch in
runEngine refresh the backend but then skip ensureBackendDeps, which leaves the
Python environment out of sync after backend requirements change. Update those
flows so they invoke ensureBackendDeps immediately after refreshBackend,
matching the existing ensureRuntime sequence and preserving the clear setup
error path. If adding EnsurePython to the MCP path, verify it is stdout-safe
there; otherwise keep its output off the JSON-RPC channel.

---

Nitpick comments:
In `@backend/cli.py`:
- Around line 2002-2003: The `truncate_title` import inside the nested
`win_size`/window loop in `backend/cli.py` should be hoisted to the module level
to match the existing import style used elsewhere. Move `from utils.text import
truncate_title` up with the other top-level imports and keep the loop body only
calling `truncate_title(title)` so the import is no longer repeated on every
iteration.

In `@cli/gen-version.sh`:
- Around line 6-8: The gen-version.sh script always rewrites VERSION even when
the version is unchanged. Update the version generation flow around the existing
ver assignment and VERSION write so it first checks the current contents of
"$here/VERSION" and skips the write when it already matches ver, keeping go
generate idempotent and avoiding unnecessary mtime changes.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c3cbc1e-ede5-4631-aaa5-7c9a65b3df26

📥 Commits

Reviewing files that changed from the base of the PR and between 1d3ef03 and 163cee2.

📒 Files selected for processing (21)
  • .github/workflows/release.yml
  • RELEASE.md
  • backend/cli.py
  • backend/services/claude_suggest.py
  • backend/utils/text.py
  • cli/VERSION
  • cli/gen-version.sh
  • cli/internal/backend/embed.go
  • cli/internal/backend/embed_test.go
  • cli/internal/update/update.go
  • cli/main.go
  • cli/version.go
  • cli/version_test.go
  • package.json
  • src/ui/client/EpisodeWorkspace.jsx
  • src/ui/client/HighlightsPage.tsx
  • src/ui/client/lib.test.ts
  • src/ui/client/lib.ts
  • src/ui/public/css/styles.css
  • src/ui/web-server.ts
  • tests/test_text_utils.py

Comment thread cli/main.go
Comment on lines +45 to +50
// Safe on this path despite stdout being the JSON-RPC channel: it only writes
// to stderr. Skipping it would strand MCP-only clients on a stale backend.
if err := refreshBackend(); err != nil {
fmt.Fprintln(os.Stderr, "podcli:", err)
os.Exit(1)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing ensureBackendDeps after refreshBackend on MCP and non-runtime paths.

Both the mcp path (line 47) and the runEngine non-runtime else if branch (line 162) call refreshBackend but skip ensureBackendDeps. The refreshBackend comment itself states it "Must run before ensureBackendDeps: the dep stamp hashes the backend's own requirements-runtime.txt, so a stale tree pins a stale dep set." After a backend refresh that changes requirements-runtime.txt, the Python venv still holds the old packages, so engine.RunMCP / engine.Run can fail with a confusing ImportError rather than the clear "run podcli setup" message that ensureBackendDeps provides.

ensureRuntime (line 107) correctly calls both. The podcli update flow is also safe because refreshRuntime shells out to setup --refresh, which installs deps. The gap only bites when a new binary is installed outside the update flow (e.g., manual replacement) and the user immediately runs an MCP or non-runtime command.

Verify that provision.EnsurePython does not write to stdout before adding it to the MCP path — stdout is the JSON-RPC channel there. If it does, redirect or suppress stdout for that call.

Proposed fix for runEngine non-runtime path
 	if wantsRuntime(args) {
 		if err := ensureRuntime(); err != nil {
 			fmt.Fprintln(os.Stderr, "podcli:", err)
 			return 1
 		}
-	} else if err := refreshBackend(); err != nil {
-		fmt.Fprintln(os.Stderr, "podcli:", err)
-		return 1
+	} else {
+		if err := refreshBackend(); err != nil {
+			fmt.Fprintln(os.Stderr, "podcli:", err)
+			return 1
+		}
+		if err := ensureBackendDeps(); err != nil {
+			fmt.Fprintln(os.Stderr, "podcli:", err)
+			return 1
+		}
 	}
Proposed fix for MCP path (if EnsurePython is stdout-safe)
 		if err := refreshBackend(); err != nil {
 			fmt.Fprintln(os.Stderr, "podcli:", err)
 			os.Exit(1)
 		}
+		if err := ensureBackendDeps(); err != nil {
+			fmt.Fprintln(os.Stderr, "podcli:", err)
+			os.Exit(1)
+		}
 		code, err := engine.RunMCP()

Also applies to: 162-164

🤖 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 `@cli/main.go` around lines 45 - 50, Both the MCP path in main and the
non-runtime branch in runEngine refresh the backend but then skip
ensureBackendDeps, which leaves the Python environment out of sync after backend
requirements change. Update those flows so they invoke ensureBackendDeps
immediately after refreshBackend, matching the existing ensureRuntime sequence
and preserving the clear setup error path. If adding EnsurePython to the MCP
path, verify it is stdout-safe there; otherwise keep its output off the JSON-RPC
channel.

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