Claude Code keeps per-project memory at <home>/.claude/projects/<key>/,
where <key> is the project's absolute path with every separator and the
drive colon turned into a dash. That makes it both rename-fragile and
machine-specific, and this project has hit each of those once:
- D:\DEV\Schunix -> D:\DEV\SchOS changed the key, and 43 memories were
invisible until moved across by hand.
- Development happens from Windows and from Linux, and each looks in a
different place for the same checkout.
Committing .claude/memory/ fixes both -- git carries it across renames
and between hosts -- and `import` puts it wherever the local machine
expects. .claude/settings.local.json stays ignored: those are per-machine
permissions.
The colon maps to a dash rather than being dropped. Deleting it gives
D-DEV-SchOS, which matches nothing and silently finds zero memories; that
was the first attempt. On Git Bash the key comes from `cygpath -w`,
because Claude Code is a Windows program and sees D:\DEV\SchOS, not
/d/DEV/SchOS.
9.9 KiB
| name | description | metadata | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| schfs-phase3-writes | SchFS phase 3 is done — full write API, in-RAM allocation map derived from the tree; the only gap left is VFS integration |
|
Phase 3 done (2026-08-03), including VFS integration. SchFS is a complete, mountable, read-write filesystem. Follows schfs-phase2-mount.
mkdir /mnt && mount -t schfs /dev/hdb /mnt
then ordinary ls/cat/>/>>/mkdir/cp/rm/rmdir all work on it.
The VFS now has a real dispatch layer: struct fs_ops in include/file.h,
one table per filesystem, chosen by iget() from the device number and hung off
the inode. v1's implementations kept their bodies and are named v1_*; SchFS's
live in kernel/fs/schfs/vfs.c. Adding a filesystem is a new table, not
another branch in fifteen functions. getdents64 is an fs_op for the same
reason — v1's struct dirent (16-bit inum, 14-byte name) cannot represent a
SchFS entry, so each filesystem emits the record stream itself.
Mounting is a substitution during path resolution (mount_add/mount_cross in
fs.c), recorded nowhere on disk. .. at a mounted root has to be redirected
out explicitly, or the mount becomes a subtree you can enter and never leave.
Two design decisions that contradict the older design doc — the doc is now right, older notes are not:
- Free space is NOT tracked as tree keys. Allocating from a tree-resident map
mutates the tree, which allocates, which changes the map. Instead the map is
derived at mount by walking the tree (
kernel/fs/schfs/space.c) and lives only in RAM. It cannot disagree with the filesystem because it is computed from it. This is what caps a mounted volume at 64 GiB (SCHFS_SPACE_PAGES). - Data blocks carry a generation too (
schfs_extent.gen, 64-bit). Same rule as tree nodes: a run stamped with the group being built may be overwritten in place. Without it every 100-byte write copied a 4 KB block and split an extent.
Testing. make schfs-check now runs three suites — add schfs-objtest
(tools/schfs_objtest.c, memory-backed, covers files/dirs/space accounting) to
the two that existed. sh tools/mutate.sh injects nine bugs; all nine must be
caught.
The two tests that actually earn their keep, both found by mutation testing after everything else passed:
- Filling and emptying the volume three times on one mount and checking each round stores the same amount. A leaked bitmap heals on remount, so a remount-based check cannot see it; only spending the space again can.
- Overwriting a block after a commit. Within one transaction group both the tree and the data rewrite in place, so a test that never commits never runs the copy-on-write path at all.
struct schfs_vol is not copyable — tree.dev points at its own dev
field. Open into the storage it will live in.
Migrating root off the xv6 legacy FS (2026-08-09)
Nexoniarz: kill the legacy FS, keep SchFS ("SchFS is goat"). Agreed — and it is
a migration, not a rewrite: SchFS is 4,204 lines across 7 files against
kernel/fs/fs.c's 1,830, and it already builds images, mounts, and dispatches
through struct fs_ops. Every xv6 legacy problem (DIRSIZ 14, the 16-byte
dirent, the HTree byte layout, ushort inum) lives below a filesystem that has
already been replaced but never promoted.
The one line holding xv6 in charge:
kernel/fs/fs.c:1642: ip = (*spath == '/') ? iget(ROOTDEV, ROOTINO) : idup(start);
Blocker 1, now cleared: mkschfs would not build on Windows. It needed
sys/sysmacros.h (major/minor) and lstat/readlink/S_ISLNK, none of which
mingw has. Stubbed under __MINGW32__ — honest here because Windows has no
mknod and find sysroot -type l returns nothing, so neither device nodes nor
symlinks can appear in the tree being imaged. If symlinks are ever staged
(procfs wants /proc/<pid>/exe to be one) this host can no longer build a
faithful image and the stub must become a real reparse-point read.
make build/schfs.img now works: 586 of 8000 blocks, 2344 KB, generation 2.
Blocker 2, confirmed and not started: booting with schfs.img as the root
disk panics fsinit: invalid file system — v1 fsinit() probes ROOTDEV for a
v1 superblock and halts. So the order is:
fsinit/nameireach the root throughfs_opsinstead ofiget(ROOTDEV, ROOTINO).- A
ramroot-equivalent SchFS image, so root can be swapped without a disk. - Point
ROOTDEVat the SchFS volume; keep v1 mountable until it is proven. - Delete
fs.c.
Do this with a fresh session. Swapping the root filesystem is the change most able to leave an unbootable image, and step 1 wants room to boot, fail and retry.
Sizing step 1 (2026-08-09) -- it is small
ops_for(dev) in kernel/fs/fs.c:278 already dispatches the whole VFS:
if (dev == SCHFS_DEV) return &schfs_ops;
if (dev == PROCFS_DEV) return &procfs_ops;
return &v1_ops; // legacy, only because it is the default
struct fs_ops (include/file.h:35) is a complete vtable -- iload, istore,
ifree, read, write, truncate, dirops -- and SchFS implements all of it. So
fs.c is no longer "the filesystem"; it is the fallback branch. Killing it is
three edits, not a rewrite:
fsinit()stops requiring a v1 superblock on ROOTDEV; callschfs_kmount().ROOTDEVbecomesSCHFS_DEVsoops_forreturnsschfs_opsfor/.namei'siget(ROOTDEV, ROOTINO)usesschfs_kroot()for the root inode.
Then v1_ops has no caller and fs.c can go. Keep a build flag or a second
disk to boot v1 until SchFS has carried / through a full bootcheck run.
Nexoniarz calls the legacy one "ShitFS v1" -- worth knowing that "SchFS v1" in older notes means the xv6-derived filesystem, not early SchFS.
The recycle bin, removed 2026-08-09
sys_delete (syscall 51, NO_LINUX) set an on-disk inode flag showmode = 0,
and fileread/filewrite then refused every operation with a printf. A
"deleted" file still existed, still had its name, still held its blocks, and
answered read() with -1 -- and since nothing was freed, deleting filled the disk
faster. Both gates removed; the syscall entry and the on-disk field stay only
because pulling them touches the inode layout, and both die with fs.c.
SchFS is the root filesystem -- default since 2026-08-09
make builds it, make qemu boots it, tools/bootcheck.ps1 tests it. There is
no opt-in switch any more: schfsroot, -DSCHFS_ROOT and the -SchFS /
-SchFSRoot bootcheck switches are all gone. schunix.img depends on
build/schfs.img, and a kernel handed anything else on device 1 panics in
forkret().
Full suite green -- base, -Tty, -Ps, -Sock, -Fb, -Usb, -Evdev.
Three edits carried / over:
kernel/proc/proc.cforkret:schfs_kmount(ROOTDEV)+schfs_kmounted()check, replacingfsinit(ROOTDEV). Mounted there and not in main() because mounting reads blocks, bread() sleeps, and sleeping needs a process context.schfs_kmountreturns void -- it reports failure by leaving the volume unmounted, because elsewhere it is a probe.kernel/fs/fs.cnamei: root isiget(SCHFS_DEV, schfs_kroot()), soops_for()routes/throughschfs_ops. SCHFS_DEV is 64 -- a synthetic number for ops dispatch, not the disk the volume lives on.kernel/fs/log.c:begin_op/end_opreturn immediately, the rest is#if 0. The v1 log made multi-block v1 updates atomic; SchFS gets that from CoW. They stay as no-ops because sys_fs.c calls them in dozens of places.
The bug that cost the most, and would cost it again: sinit() sets pid 1's
cwd with namei("/") in main(), before forkret mounts anything. The old v1
root survived that because iget(ROOTDEV, ROOTINO) is two compile-time
constants that touch no disk -- the inode fills in lazily at the first ilock.
SchFS reads its root object id off the uberblock, so pid 1 got a cwd built from
an unmounted volume. Absolute paths never consult cwd, so exec("/bin/init")
worked and everything looked healthy; the first relative lookup is init's
open("console"), which failed, so init had no fd 0/1/2, so every shell it
spawned exited at once and init respawned it forever. Silent boot, no
init: ready, indistinguishable from a hang. Fixed by re-pointing
myproc()->cwd after the mount.
Also: the kernel execs /bin/init (lib/initcode.S), not /init. Probing the
wrong path made lookup look broken when it was not.
Still to do -- the v1 format code is compiled but unreachable from /.
fs.c cannot simply be deleted: iget/ilock/iput/namei/readi, the whole VFS
layer, live in it too. Only the v1 on-disk format goes -- fsinit, the
superblock, bmap/balloc, v1_ops, and the third-party HTree/extent code -- plus
log.c, tools/mkfs.c and build/fs.img. The MIT attribution in NOTICE stays
regardless (schunix-licensing).
fs.img is still built: the -Usb suite uses it as the USB stick.
Two harness bugs found doing this, both of which faked OS failures
- bootcheck slept a flat 4s between commands.
ls /bin | wc -loverran it, so the next command was typed while the shell was busy -- the tty echoed it, no reader took it, and it landed concatenated onto the following one. The transcript readcat /proc/pcicat /proc/schfsand three PCI checks failed, looking exactly like a broken PCI driver. Now it waits for the prompt by polling the logfile QEMU is writing. (It still must not read QEMU's stdout -- that deadlocks, see the script header.) - The USB stick was
$hdb, so the root volume was attached twice, once as a disk and once over USB. That failed the storage checks and the xHCI interrupt check. The stick is pinned tobuild/fs.imgnow and -Usb is green.
The general lesson, twice over: a red check is not evidence until the harness is ruled out. Both of these looked like driver bugs.