Skip to content

v0.3.7 maintenance: CI Node.js 24 upgrade + 3 deferred features#25

Merged
magicnight merged 7 commits intomainfrom
feat/v0.3.7-maintenance
Apr 18, 2026
Merged

v0.3.7 maintenance: CI Node.js 24 upgrade + 3 deferred features#25
magicnight merged 7 commits intomainfrom
feat/v0.3.7-maintenance

Conversation

@magicnight
Copy link
Copy Markdown
Owner

@magicnight magicnight commented Apr 18, 2026

Summary

  • Task 1 (this commit): bump `actions/checkout` and `actions/cache` from @v4 to @v5 (Node 20 → Node 24). Silences the runner deprecation warning from v0.3.6 release.
  • Tasks 2–4 (coming): MLX stdout capture, HF model-update detection, GUI↔CLI PIDFile sharing. See plan at docs/superpowers/plans/2026-04-18-v0.3.7-maintenance.md.

This PR will grow as Tasks 2–4 land.

Test plan

  • CI passes with @v5 actions
  • Release workflow (only triggers on tag push) — smoke-test by tagging a throwaway if risk-averse

Note

Medium Risk
Medium risk because it touches server lifecycle coordination (shared PID file + CLI preflight), adds new background stdout/stderr capture, and introduces network-based update checks that could affect UX or rate limits if misbehaving.

Overview
Bundles v0.3.7 maintenance work: CI/release workflows now use actions/checkout@v5 and actions/cache@v5 to move off deprecated Node 20.

Adds GUI↔CLI server coordination by moving PIDFile into MacMLXCore, tagging records with an owner (gui/cli), having the GUI write/clear the PID on server start/stop, and making macmlx serve refuse to start if a live PID exists; macmlx ps now prints the owner.

Introduces MLX stdout/stderr capture into the Logs tab via a launch-time StdoutCapture.install() (fd dup/pipe tee) and updates app initialization to install this before AppState is created.

Adds Hugging Face model update detection: downloads now write a .macmlx-meta.json sidecar (DownloadedModelMeta) with commit/timestamp info, HFDownloader can compare local meta to Hub head, and the Models UI periodically checks and shows an orange Update available badge.

Reviewed by Cursor Bugbot for commit c68e94a. Bugbot is set up for automated code reviews on this repo. Configure here.

magicnight and others added 7 commits April 18, 2026 20:30
Four tasks: Node.js 24 upgrade, MLX stdout capture, HF update
detection, GUI/CLI PIDFile sharing. Independent, execute in any
order. Recommended: 1 -> 4 -> 2 -> 3.
Silences the 'Node.js 20 deprecated' runner warning that surfaced
during v0.3.6 release. Both major bumps are drop-in replacements
— same inputs, same outputs, only the runtime changes.
dup2 STDOUT_FILENO and STDERR_FILENO to a Pipe at launch, tee each
line to both the original fd (terminal visibility preserved) and
LogManager.shared.debug(category:.system). Runs on a detached
background Task, never blocks app lifecycle.

Users reporting "model is slow" or "got a cryptic crash" can now
see what mlx-swift-lm is printing without attaching a debugger.

Install point is macMLXApp.init() BEFORE AppState is constructed —
the engine's first load triggers the first MLX prints, so the
redirect must be in place by then. @State(initialValue:) is the
idiomatic way to defer AppState construction while keeping it as a
@State var.
Write `.macmlx-meta.json` sidecar to every freshly-downloaded model
directory capturing HF commit SHA + lastModified at download time.
On Models-tab open, throttle-check each sidecar against the Hub
(bounded to once a day). Rows whose head has advanced get an
orange 'Update available' badge.

Delete-and-re-download is the update action for now; a one-click
refresh is a v0.4 follow-up.

New unit tests: DownloadedModelMetaTests (3 cases) covering
roundtrip, missing sidecar, corrupt sidecar.
Moves PIDFile from macmlx-cli to MacMLXCore so both targets read
and write the same file. Record gains an Owner enum (.gui | .cli):

- GUI startServer() writes a record tagged .gui on bind; stopServer()
  clears it.
- CLI's `macmlx serve` now checks for a live PID with kill(pid, 0)
  and refuses to start when one exists, naming the owner so the
  user knows which process to shut down.
- `macmlx ps` shows "Owner: GUI" or "Owner: CLI".

Backward compat: decodes pre-v0.3.7 PID files (no owner key) as
.cli so upgrading in place doesn't require cleanup.

Full Ollama-style daemon mode (CLI commands proxied through the
GUI's HTTP API) remains a later scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swift 6 strict concurrency (default on in CI's Xcode 16.4) rejects
'static var installed' as non-concurrency-safe mutable global state.
The flag is touched exactly once from macMLXApp.init() on the main
thread before any other code runs, so nonisolated(unsafe) is the
accurate annotation. Local dev build missed this because DerivedData
was still cached from before StdoutCapture existed.
@magicnight magicnight merged commit 0fba533 into main Apr 18, 2026
3 checks passed
@magicnight magicnight deleted the feat/v0.3.7-maintenance branch April 18, 2026 13:58
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c68e94a. Configure here.

// Pipe's write fd is now duplicated to the original fd; close the
// pipe's side to avoid double-ownership. Foundation's Pipe retains
// its own reference so the fd stays live.
_ = close(writeFD)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Double-close of pipe write fd corrupts file descriptors

Medium Severity

The manual close(writeFD) on line 46 closes the pipe's write-side fd, but pipe.fileHandleForWriting still owns that fd number. When redirect() returns, pipe goes out of scope — only reader (the read-side FileHandle) is captured in the Task closure, not the Pipe object itself. So Pipe is deallocated, which releases fileHandleForWriting, whose deinit calls close() on the same fd number again. In a multi-threaded app, that fd may have been reused by another subsystem between the two closes, silently corrupting an unrelated file handle. The comment claiming "Foundation's Pipe retains its own reference so the fd stays live" is incorrect.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c68e94a. Configure here.

let envelope = try JSONDecoder.huggingFace.decode(ModelDetailsEnvelope.self, from: data)
let files = envelope.siblings.map { HFRemoteFile(path: $0.rfilename, size: $0.size, lfs: false) }
return (files, envelope.sha, envelope.lastModified)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New downloadMeta duplicates existing files method logic

Low Severity

downloadMeta(for:) is a strict superset of files(for:) — both hit the same /api/models/{id} endpoint, decode the same ModelDetailsEnvelope, and map siblings identically. files(for:) (still called by sizeBytes(for:)) could simply delegate to downloadMeta(for:).files, eliminating the duplicated URL construction, fetch, decode, and map logic. Maintaining two copies risks them drifting apart on future changes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c68e94a. Configure here.

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