Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,12 @@ jobs:
args: check

windows-compile:
# Preserve the required-check name while expanding its coverage.
name: windows-compile
runs-on: windows-latest
env:
# Use cgofuse's native Go Windows loader, matching the preview build.
CGO_ENABLED: "0"
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-go@v7.0.0
Expand All @@ -80,7 +84,16 @@ jobs:
cache: true
- name: Compile all test packages without claiming runtime validation
run: go test ./... -run '^$' -count=1
- name: Test Windows adapters, worker lifecycle and tray state
run: go test ./internal/tray ./internal/enroll ./internal/cli ./internal/service ./internal/mountfs ./internal/pack ./internal/dirsync ./internal/sessionns ./internal/vfs ./internal/storage -run 'TestMonitor|TestPolicy|TestDashboard|TestHistory|TestIncidents|TestUnreadable|TestPeriodicEnrollment|TestEnrollmentWorker|TestPersistentEnrollment|TestWindows|TestFuse|TestPublishedLogicalBytes|TestNamespaceFolding|TestReadReserve' -count=1 -timeout=120s
- name: Test the WinFsp frontend and engine handover
run: go test -tags winfsp ./internal/mountfs -run 'TestWindowsCore|TestWindowsFuseErrorMapping|TestFuse' -count=1 -timeout=120s
- run: go build ./cmd/codexfold
- name: Build Windows mount and native tray
run: |
go build -tags winfsp -o "$env:RUNNER_TEMP/codexfold-winfsp.exe" ./cmd/codexfold
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
go build -ldflags='-H=windowsgui' -o "$env:RUNNER_TEMP/codexfold-tray.exe" ./cmd/codexfold-tray

race:
name: race
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
/*.sqlite-shm
/*.sqlite-wal
*.prof
*.profraw
*.test
.DS_Store
.worktrees/
/.tmp/
/.tmp-test-windows-runtime/

# Xcode build products and derived data
platform/darwin/fskit/build/
Expand Down
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,66 @@ pre-1.0, so a minor version may contain compatibility changes.

## [Unreleased]

### Added

- Windows WinFsp drive hosting, persistent SCM folding service, native tray with
a WebView2 dashboard, policy controls, batch progress, measured space accounting,
aggregate history and diagnostics. Activation and ordinary-storage recovery
scripts retain the original namespace and verified recovery materials.
- Windows resident-frontend/storage-engine separation with paused-policy and
open-handle checks, verified candidate images and failed-candidate rollback.
Legacy installations need an offline upgrade before live engine replacement;
frontend updates still require an offline operation.
- Windows CI now executes the focused adapter, service, worker, tray and shared
integration tests in addition to compiling every package. The required check
keeps its existing `windows-compile` name. This is not a full Desktop GUI or
power-loss qualification.

### Fixed

- Foreground session opens can ask registered background maintenance to yield
and retry without bypassing writer locks. Copy-on-write budget checks reuse
a shared inventory instead of scanning the store while holding file locks;
current free space is still checked for each operation.
- A failed deletion cleanup no longer blocks unrelated managed-session reloads.
macOS provenance-only metadata changes are accepted only when the original
deletion proof recorded that metadata form; unknown materials remain intact.
- Transport health probes have one bounded deadline. Slow inventory refreshes
no longer manufacture a session-access outage; actual outstanding FSKit file
requests retain the ten-second incident threshold and bounded diagnostics.
- Failed or short appends roll back only that append, and storage budget errors
map to ENOSPC. A verified 256 KiB read-lease reserve can be released under
disk pressure without bypassing reader/collector coordination.
- macOS incidents use quiet, deduplicated system notifications instead of
automatically activating a fault window. Recovery removes notifications;
denied permission does not fall back to a modal window. Read-only disk-space
monitoring still works when status files cannot be written.
- Daemon live updates accept a bounded `--takeover-timeout` for slower stores;
the default remains nine seconds and failed takeovers retain rollback.
- Replacement-file background folding follows the effective external-worker
policy on Windows, including the fresh paused-builtin acknowledgement. Native
repair readers allow delete sharing there, and the read reserve recognizes
Windows writable file modes while retaining identity and exact-content checks.
- Canonical managed rollouts now support same-namespace backup renames and
native temporary-file replacement used by `jstarctl codex rebind`. Durable
names preserve backups and old readers across replacement/reload; stable
replacements fold in the background, respect the automatic-folding policy,
and do not get enrolled again under their logical Codex thread ID.
- A damaged active rollout no longer keeps the mount down. After the disk
filled, about 10 KB of binary memory was left inside one rollout; the
startup preflight rejected the file, `fs serve` exited, and launchd restarted
it every two seconds with no mount, so Codex hung on a dead
`~/.codex/sessions`. The startup preflight now repairs such files: damaged
records and an unfinished final record are moved to
`<native-root>/.codexfold-native-quarantine/` with a manifest that records
their offsets and SHA-256, and the rollout is rewritten through an atomic
rename of a clone (APFS clones share unchanged blocks, so this needs little
free space). Mode and modification time are kept. A complete final record
that only lacks its newline gets the newline. Anything that cannot be
repaired is logged and served unchanged instead of aborting startup.
- `fs validate-native --repair` applies the same repair by hand while the
filesystem service is stopped. Without the flag the command stays read-only.

## [0.4.0-beta.2] - 2026-10-01

### Added
Expand Down
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,41 @@ Client version and build identity are diagnostic metadata only. Unknown or newly

The requirements and release gates for normal JSONL paths backed transparently by shared storage are defined in [the transparent filesystem product contract](docs/superpowers/specs/2026-07-11-transparent-session-filesystem-design.md). No release may claim `随点随开`, transparent session access, or production-ready virtual sessions before the platform-specific gates in that contract pass.

| Platform | Adapter | Evidence in this release | Readiness |
| Platform | Adapter | Evidence in current source | Readiness |
|---|---|---|---|
| macOS 27 | Apple-native Swift FSKit | Historical signed build 102 real-client and native-mount matrix; current worktree requalification is partial | `fs-engine-preview` |
| Linux | FUSE3 | Real unprivileged mount, mutation, remount, recovery, performance, and user-service lifecycle | Preview; real Codex client validation remains incomplete |
| Windows | WinFsp | Cross-build and compile coverage | Not runtime-validated |
| Windows | WinFsp, SCM services and native WebView2 tray | Complete local-copy Codex app-server lifecycle, exact-byte restart, persistent worker and guarded live engine replacement on an isolated mount | Tested preview flows; Desktop GUI, reboot and SYSTEM engine-split qualification remain incomplete |

The transparent filesystem preview has these explicit boundaries:

- macOS now targets an Apple-native Swift FSKit extension connected over a versioned Unix-domain-socket protocol to the Go CodexFold daemon. The signed 2026-07-23 build 102 App/extension and its then-current helper historically passed the isolated mounted-operation, exact-byte, cache-coherency, performance, crash/host-restart, rollback, and real Codex CLI/Desktop matrix. That evidence does not automatically transfer to the current worktree candidate.
- Release source metadata is `0.3.0 (103)`. Build 103 compiles and passes nested signature verification, while the completed mounted and real-client matrix remains historical build 102 evidence. Changes in the current worktree require their own candidate attachment, real-client workload, restart, fault-injection, and recovery evidence.
- FSKit source metadata is `0.4.0 (116)`. Dated local reports below identify the exact installed helper/UI subsets and their checks; historical build 102 client evidence does not automatically qualify the complete combined source.
- The earlier synchronous FUSE-T NFS route remains historical validation evidence and a development fallback only. FUSE-T's third-party FSKit backend remains rejected after deterministic byte-loss and cache-invalidation failures; it is not the Apple-native FSKit implementation in this repository.
- Linux FUSE3 has real unprivileged read, append, copy-on-write, truncate, archive rename, crash recovery, remount, performance, and `systemd --user` lifecycle evidence.
- Windows has a WinFsp adapter and native Windows Service host that cross-compile, but no real Windows/WinFsp host has validated them yet.
- The production service and production Codex home remain disabled by default. The current worktree's isolated flow has observed a real Cockpit **Start**, an isolated Desktop/app-server process chain, and an unchanged production Codex process. It has not yet attached the current CodexFold candidate, observed a real Desktop task mutation through that managed route, or completed candidate-only fault injection and recovery; `codexInstanceAcceptanceComplete`, `codexFoldCandidateAcceptanceComplete`, and `realAcceptanceComplete` therefore remain false. Promotion is blocked by that exact evidence matrix, not by a fixed observation period.
- The Windows contributor reports complete local-copy tests with Codex 0.159.2 app-server: resume, history reads, recorder writes, fork, archive/unarchive of a compressed thread, and a separate managed deletion with physical purge. SCM stop/start preserves exact bytes and client access. A persistent SCM folding worker supports pause/resume and fresh progress; the native tray adds policy controls, 30-day aggregate history, incident details and JSON diagnostics. An isolated real WinFsp mount passed storage-engine replacement and failed-candidate rollback while preserving the frontend PID and mount nonce. A legacy installation needs one offline upgrade to enable that split. The full Desktop GUI, reboot/power loss and SYSTEM engine-split qualification remain unverified. See the [Windows preview guide](docs/windows-preview.md) for that tested revision and its limits; the combined source still needs its own target-machine checks.
- Production routing remains opt-in. The dated macOS reports below record authorized local helper/UI updates, real mounted checks and continuity observations. The complete combined macOS/Windows source is not installed by this integration, and those subset observations do not establish a new combined-binary Desktop or fault-recovery result.
- CodexFold recovery operates only on CodexFold-owned components. It never quits, restarts, signals, or reopens a running Codex process. On macOS the backend, supervisor, and incident helper are resident; the menu-bar App is restarted after a crash but an explicit user Quit remains a quit.
- The native macOS menu-bar App shows current health, measured space saved, current read/write speed, compact trends, selectable 1-hour/24-hour/7-day/30-day history, and the timeline of failures that lasted at least ten seconds. A standalone menu-bar launch that has never observed or registered the file-service runtime shows an unconnected state instead of reporting a critical incident. History stores only timestamps, health, counts, byte totals, and aggregate transfer rates, never session content.
- The current worktree validation has not installed, updated, stopped, restarted, or signaled the production CodexFold service, FSKit App/extension, or LaunchAgent set. Any exact production target requires a new explicit user authorization before an apply or lifecycle operation.
- This source integration does not install, stop, restart or signal the running CodexFold service, FSKit App/extension or Codex clients. Production installation and lifecycle actions remain separate, explicitly authorized operations.

See [the Linux FUSE3 validation](docs/validation-linux-fuse3.md) and [the macOS canary validation](docs/validation-macos-canary.md) for the evidence boundary. The default build remains storage-only; platform mounts require explicit build tags and installed host prerequisites.

### Unreleased local fixes

Development since the release adds foreground-priority storage access,
durable backup/replacement names, isolated deletion cleanup, native-rollout
repair, bounded transport health checks, disk-full append recovery and quiet
macOS system notifications. These changes are not in the published archives.
The dated [replacement compatibility](docs/rebind-compatibility-2026-10-16.md),
[incident recovery](docs/incident-recovery-fix-2026-10-16.md), and
[notification and disk-space](docs/notification-space-safety-fix-2026-10-17.md)
reports distinguish the exact locally installed subsets from the complete
development source. Combining those changes does not by itself validate a new
production binary; this source update does not reinstall the running service.
The [Windows integration record](docs/windows-integration-2026-10-18.md) describes
how the local fixes and Windows preview are combined without dropping either.

## Install

Install the versioned preview with Go:
Expand Down
17 changes: 17 additions & 0 deletions cmd/codexfold-tray/assets/app.manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="0.4.0.0" processorArchitecture="*" name="CodexFold.Tray" type="win32"/>
<description>CodexFold</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security><requestedPrivileges><requestedExecutionLevel level="asInvoker" uiAccess="false"/></requestedPrivileges></security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application><supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/></application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>
Binary file not shown.
Binary file added cmd/codexfold-tray/assets/codexfold.ico
Binary file not shown.
100 changes: 100 additions & 0 deletions cmd/codexfold-tray/assets/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//go:build ignore

// Draw the Windows storage glyph at multiple resolutions without external assets.
// Run from cmd/codexfold-tray: go run assets/generate.go
package main

import (
"bytes"
"encoding/binary"
"image"
"image/color"
"image/png"
"math"
"os"
)

func rounded(x, y, left, top, right, bottom, radius float64) bool {
dx := math.Max(math.Max(left+radius-x, 0), x-(right-radius))
dy := math.Max(math.Max(top+radius-y, 0), y-(bottom-radius))
return x >= left && x <= right && y >= top && y <= bottom && dx*dx+dy*dy <= radius*radius
}

func glyph(size int, warning bool) *image.NRGBA {
img := image.NewNRGBA(image.Rect(0, 0, size, size))
const samples = 4
for y := 0; y < size; y++ {
for x := 0; x < size; x++ {
var r, g, b, a float64
for sy := 0; sy < samples; sy++ {
for sx := 0; sx < samples; sx++ {
px := (float64(x) + (float64(sx)+.5)/samples) * 64 / float64(size)
py := (float64(y) + (float64(sy)+.5)/samples) * 64 / float64(size)
c := color.NRGBA{}
if rounded(px, py, 2, 2, 62, 62, 14) {
c = color.NRGBA{uint8(66 - py*.13), uint8(125 - py*.2), uint8(201 - py*.15), 255}
}
// Stacked sheets flowing into a compact storage drive.
if rounded(px, py, 19, 15, 45, 18, 1.5) || rounded(px, py, 16, 22, 48, 25, 1.5) || rounded(px, py, 12, 30, 52, 49, 4) {
c = color.NRGBA{248, 251, 255, 255}
}
if rounded(px, py, 18, 38, 35, 41, 1.5) || math.Hypot(px-44, py-39.5) <= 1.8 {
c = color.NRGBA{58, 112, 185, 255}
}
if warning && math.Hypot(px-51, py-51) <= 10 {
c = color.NRGBA{255, 255, 255, 255}
if math.Hypot(px-51, py-51) <= 8 {
c = color.NRGBA{208, 142, 39, 255}
}
}
r += float64(c.R) * float64(c.A) / 255
g += float64(c.G) * float64(c.A) / 255
b += float64(c.B) * float64(c.A) / 255
a += float64(c.A)
}
}
if a > 0 {
img.SetNRGBA(x, y, color.NRGBA{uint8(r * 255 / a), uint8(g * 255 / a), uint8(b * 255 / a), uint8(a / (samples * samples))})
}
}
}
return img
}

func writeIcon(name string, warning bool) {
sizes := []int{16, 20, 24, 32, 40, 48, 64, 128, 256}
var frames [][]byte
for _, size := range sizes {
var frame bytes.Buffer
if err := png.Encode(&frame, glyph(size, warning)); err != nil {
panic(err)
}
frames = append(frames, frame.Bytes())
}
var output bytes.Buffer
write := func(value any) {
if err := binary.Write(&output, binary.LittleEndian, value); err != nil {
panic(err)
}
}
write(uint16(0))
write(uint16(1))
write(uint16(len(sizes)))
offset := 6 + 16*len(sizes)
for i, size := range sizes {
write([4]byte{byte(size % 256), byte(size % 256), 0, 0})
write(uint16(1))
write(uint16(32))
write(uint32(len(frames[i])))
write(uint32(offset))
offset += len(frames[i])
}
for _, frame := range frames {
output.Write(frame)
}
if err := os.WriteFile("assets/"+name+".ico", output.Bytes(), 0644); err != nil {
panic(err)
}
}

func main() { writeIcon("codexfold", false); writeIcon("codexfold-attention", true) }
7 changes: 7 additions & 0 deletions cmd/codexfold-tray/main_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build !windows

package main

import "fmt"

func main() { fmt.Println("The CodexFold tray companion is available on Windows.") }
61 changes: 61 additions & 0 deletions cmd/codexfold-tray/main_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//go:build windows

package main

//go:generate go run assets/generate.go
//go:generate go run github.com/akavel/rsrc@v0.10.2 -manifest assets/app.manifest -ico assets/codexfold.ico,assets/codexfold-attention.ico -arch amd64 -o rsrc_windows_amd64.syso
//go:generate go run github.com/akavel/rsrc@v0.10.2 -manifest assets/app.manifest -ico assets/codexfold.ico,assets/codexfold-attention.ico -arch arm64 -o rsrc_windows_arm64.syso

import (
"flag"
"fmt"
"os"
"path/filepath"
"time"

"github.com/samekind/codexfold/internal/codex"
"github.com/samekind/codexfold/internal/tray"
)

func main() {
store := flag.String("store", "", "CodexFold store directory; defaults to CODEX_HOME/fold-store")
background := flag.Bool("background", false, "Start in the notification area")
quit := flag.Bool("quit", false, "Close the tray for this store without stopping the filesystem")
diagnostics := flag.String("diagnostics", "", "Export aggregate diagnostics to a JSON file and exit")
flag.Parse()
if *store == "" {
home, err := codex.ResolveHome("")
if err != nil {
tray.ShowError(err)
os.Exit(1)
}
*store = filepath.Join(home, "fold-store")
}
absolute, err := filepath.Abs(*store)
if err == nil && *diagnostics != "" {
output, outputErr := filepath.Abs(*diagnostics)
if outputErr == nil {
monitor := tray.NewMonitor(absolute)
outputErr = tray.ExportDiagnostics(output, monitor.DiagnosticSnapshot(time.Now()))
}
if outputErr != nil {
fmt.Fprintln(os.Stderr, outputErr)
os.Exit(1)
}
return
}
if err == nil && *quit {
if err := tray.RequestExit(absolute); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
return
}
if err == nil {
err = tray.Run(absolute, *background)
}
if err != nil {
tray.ShowError(fmt.Errorf("CodexFold: %w", err))
os.Exit(1)
}
}
Loading
Loading