Skip to content

feat(ns-exec): add exec session supervisor skeleton (#538) - #64

Draft
mengmengmengqiang wants to merge 3 commits into
feat/ns-exec-helperfrom
feat/exec-supervisor-skeleton
Draft

mengmengmengqiang wants to merge 3 commits into
feat/ns-exec-helperfrom
feat/exec-supervisor-skeleton

Conversation

@mengmengmengqiang

@mengmengmengqiang mengmengmengqiang commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on Ephemeral-AI-Lab#499 (fork base feat/ns-exec-helper, head 386f1b22). This PR is the skeleton only; it is not wired to the Engine and does not change workspace.exec behavior.

Scope

A hidden ns-exec-supervisor mode that owns one command across the Ephemeral-AI-Lab#499 mount-namespace seam, plus one hidden ns-exec-child mode on the same binary.

  • Supervisor. Receives the daemon's mount-namespace descriptor over the existing SCM_RIGHTS socket, spawns the child, and closes its own copy. It never calls setns and never chdirs into the Workspace, so it cannot become a FUSE mount holder.
  • Child. Reopens the inherited descriptor through /proc/self/fd (with the existing mnt: check), setns(CLONE_NEWNS), chdir(workspace), drops supplementary groups/gid/uid exactly as feat(sandbox): run workspace.exec in the daemon's mount namespace (ns-exec helper) Ephemeral-AI-Lab/layerfs#499's ns-exec does, then setsid() and PR_SET_PDEATHSIG(SIGKILL), re-checks getppid, and execs the command.
  • Control socket. 0600 Unix socket, four-byte big-endian length frames with a fixed small body: Started {pid, pgid}, Exited {code, signal, group_alive}, Status/StatusReply, Signal {signo}, and a thin Stop {grace_ms} SIGTERM wrapper. A late caller receives the current Started/Exited snapshot under the same lock that registers it. Signal/Stop use kill(-pgid, signo) only for the child's own group; the supervisor's own group and arbitrary pids are refused. Peer admission is the supervisor's effective uid; further authorization is deferred.
  • wait4. Reaps the direct child exactly once, reports code/signal, exits 128 + signal when signalled, and reports group_alive: true (via kill(-pgid, 0)) when grandchildren remain. Grandchildren are not killed in this slice.

Architecture modules

The design note is organized in layers (module map, interfaces, startup/signal/exit sequences, failure modes and invariants, implemented vs deferred, test matrix): EXEC-SUPERVISOR-SKELETON.md.

  • Supervisor process. Owns one session: receives the mount-namespace descriptor, binds the control socket, spawns the child, observes its session, serves callers, reaps once and exits with the child's status. Never setns/chdirs.
  • Child launcher. The only process that joins the namespace: reopens/validates the descriptor, setns, chdir, drops groups/gid/uid, setsid, PDEATHSIG, checks its parent and execs the command.
  • Control link and protocol. 0600 socket, four-byte length frames, peer-uid admission, Started/Exited/Status/Signal/Stop, and a snapshot for late callers under the same lock as registration.
  • Process table / session state. One in-memory slot (pid, pgid, started, exit, clients) behind one mutex; all state dies with the supervisor.
  • Launcher / orchestrator (future, ownership open). Starts one supervisor per command, hands it the descriptor and maps the session to the Engine. Not implemented; where it lives conflicts with the current "daemon has no command supervisor" rule.
  • Session registry / presence (future). Session ids, reconnect and caller-presence; required because the control socket lives only as long as the supervisor.

The note also records gaps that the skeleton does not close: starttime is not captured (identity is only (pid, pgid)), Stop is implemented but untested, a session-poll timeout yields Exited without Started, and group_alive is an observable-group probe rather than ownership.

Presence + registry decisions

The six decisions the design note left open are now locked — launcher ownership, lease granularity, daemon-restart semantics, lease authentication, background/disconnect policy and cancellation states. Full text and rationale: Presence + registry decisions (locked).

  • Launcher. Engine exec spawns the supervisor and owns stdio and the exit code; the daemon only receives the registration and owns registry, presence and policy. The daemon does not become a command I/O broker and does not spawn the supervisor in this phase.
  • Lease granularity. One dedicated lease connection per client process per Sandbox, multiplexing all of its sessions — not one per exec. Session ids/tokens identify sessions; the connection budget is one lease plus pooled RPC slots.
  • Daemon restart. P1 does not adopt old sessions. A restart loses the registry and lease; supervisors act on control EOF by foreground (cancel/cleanup) vs background (keep) policy, and only host records remain. This is lost-session semantics, not recovery.
  • Lease auth. SO_PEERCRED plus a daemon-issued per-session capability/token delivered over the authenticated control channel; never in argv, because same-uid processes can read /proc/<pid>/cmdline. Hostile same-Sandbox multi-tenancy additionally requires user/namespace isolation.
  • Background. Not one boolean: mode: foreground|background and a separate onDisconnect: cancel|keep|reconnectWindow. This phase ships background + cancel; reconnectWindow comes later. detachOnDisconnect is foreground + keep.
  • Cancelling. No reversible 500 ms Cancelling window. The reconnect window is the Orphaned phase (5 s default); once Cancelling starts it is one-way, and the grace period is only TERM → KILL cleanup time.

Phase impact: P1 registry + starttime; P2 presence + reaper; P3 reconnect/background; P4 Engine wiring.

Deliberately deferred

  • client-presence detection and reconnect;
  • the daemon session registry and session ids;
  • force-kill orchestration, kill policy/escalation;
  • close(force) integration;
  • reaping/killing grandchildren;
  • authorization beyond the socket mode and peer uid;
  • retaining session state after the supervisor exits (the control socket lives only as long as the supervisor).

Tests

engine/crates/layerfs-daemon/tests/exec_supervisor.rs (Linux-gated; runs in the mount lane with CAP_SYS_ADMIN, like ns_exec.rs). It builds a private mount namespace with unshare -m, serves its /proc/<pid>/ns/mnt descriptor over SCM_RIGHTS, and drives the real binary over the control socket:

test result
join the served namespace, enter the Workspace directory, read a namespace-local marker, Started has pgid == pid and pid != supervisor, exit 7 pass
supervisor's own mount namespace and cwd stay outside the Workspace pass
command runs as 65534:65534 when told to drop there pass
sleep 30 + Signal(SIGTERM) → Exited {signal: 15}, supervisor exit 143 pass
Status reports the running child; Signal(SIGKILL) → signal 9, exit 137 pass
sh -c 'sleep 5 & sleep 1' → group_alive: true instead of a clean exit pass

Run in a privileged rust:1.85.1-bookworm container against a20f5ae81:

  • cargo fmt --all -- --check — pass
  • cargo check -p layerfs-daemon --all-targets — pass
  • cargo clippy -p layerfs-daemon --all-targets -- -D warnings — pass
  • cargo test -p layerfs-daemon --test exec_supervisor --no-run then exec_supervisor --test-threads=1 — 6 passed, 0 failed; repeated 10 consecutive runs, all green.

Docs

core/docs/issues/538/EXEC-SUPERVISOR-SKELETON.md is the modular architecture note: module map, interfaces (SCM_RIGHTS handoff, control frames, process identity, exit/group_alive semantics), startup/signal/exit sequences, failure modes and invariants, implemented-vs-deferred table and test matrix.

Dependency

Depends on Ephemeral-AI-Lab#499 (feat/ns-exec-helper, 386f1b22). Merging this before Ephemeral-AI-Lab#499 would not compile because it builds on Ephemeral-AI-Lab#499's ns_fd/cli seam. No changes to Ephemeral-AI-Lab#499 or upstream.

…#538)

One supervisor owns one command across the Ephemeral-AI-Lab#499 mount-namespace seam:

- hidden `ns-exec-supervisor` receives the daemon's mount-namespace
  descriptor over SCM_RIGHTS, spawns `ns-exec-child`, and closes its own
  copy. The supervisor never setns()es and never chdir()es into the
  Workspace, so it cannot become a mount holder; only the child joins.
- the child runs Ephemeral-AI-Lab#499's sequence (setns, chdir, drop uid/gid) and then takes
  a new session with setsid(); it sets PR_SET_PDEATHSIG and re-checks its
  parent so a lost supervisor does not leave an unsupervised command.
- a 0600 Unix control socket frames Started/Exited/Status/Signal/Stop with a
  four-byte length and a small fixed body. Signal and Stop only target the
  child's own process group (kill(-pgid, signo)); the peer must have the
  supervisor's effective uid.
- wait4 reaps exactly once, reports exit code or signal, and reports
  group_alive (kill(-pgid, 0)) when grandchildren remain instead of
  pretending a clean exit. Grandchildren are not killed in this slice.

Deliberately deferred: client-presence detection, the daemon session
registry, force-kill orchestration and close(force) integration.

Tests (Linux, mount lane): join the namespace served over SCM_RIGHTS and
report exit 7; pgid == pid and different from the supervisor; supervisor
stays outside the Workspace namespace/cwd; identity drop to 65534; sleep +
SIGTERM -> 143; Status + SIGKILL -> 137; group_alive for a background
sleep. Design note in core/docs/issues/538/EXEC-SUPERVISOR-SKELETON.md.
mengmengmengqiang added a commit that referenced this pull request Nov 4, 2026
…AI-Lab#538)

Replace the rejected `sandbox.close(force)` recovery with a workspace-scoped
force kill. `workspace.unmount(ws, discard?, force?, kill?)` takes `kill`
as a string enum; only `"workspace"` is accepted and any other value, or
`force` together with `kill`, is refused `InvalidInput`.

With `kill: "workspace"` the call enumerates only the target Workspace's
holders — foreground `/tmp` records naming that path and background processes
the binding started there — and cancels each: TERM to its process group, a
bounded wait, then KILL only if it is still there, and a second wait. It then
retries a *normal* daemon unmount (`unmount_checked`), never the force/abort
path, and it never stops or deletes the Sandbox and never touches another
Workspace. A holder that survives answers `Busy` with its id.

Precedence for a holder's pid/start time is the supervisor registry (future,
#64), then the foreground record, then the background map; `/proc` validates
pid and start time at kill time, so a stale or reused record is not a holder.
A group with a member whose cwd or open file is outside the target Workspace
is not killed at all: the call answers `Busy` with `scope_conflict` instead of
taking collateral. That check is best-effort until #64's registry is the
authority.

`discard` still applies only to the normal unmount that follows a successful
cancel; the call never drops data while the mount is still Busy. The old
`force` gate is kept: a forced unmount with a live foreground record answers
`Busy` with the ids instead of producing `Retained(Detach)`.

Docs: SDK workspace reference, agent doc table, agent/backend docs. The
`sandbox.close(force)` API and its verify script are gone; an invisible
escaped holder (Ephemeral-AI-Lab#460) is described as needing a future operator/CLI-only
`sandbox.destroy(id, { confirm: true })`, not a close force mode.

Tests: `layerfs-node` unit tests for the kill validation, the force/kill
conflict, holder merging and the per-Workspace record filter. New E2E
`workspace_kill_real.mjs`: a client SIGKILLed during a foreground `sleep 30`
leaves an orphan; unmount is `Busy` with its id; `kill: "workspace"` answers
`Unmounted`, `/proc/<pid>/stat` and the record are gone, another Workspace's
foreground orphan and live background command survive, the Sandbox container
still exists; a `cd /tmp && sleep 30` holder is `scope_conflict` and is not
killed; a target's background holder is killed while another Workspace's
survives.
@mengmengmengqiang
mengmengmengqiang marked this pull request as draft November 4, 2026 04:23
mengmengmengqiang pushed a commit that referenced this pull request Nov 6, 2026
…emon open (layerfs-project #64)

- A PostgreSQL Store opens with shared readers (`Store::with_shared_readers`):
  every Workspace's healthy reader returns to the idle readers at unmount
  (at most IDLE_READERS = 4) and the next mount takes one, so a mount after
  an unmount opens no connection. SQLite keeps its readers as before.
- An idle reader is checked before a mount takes it (`StoreReader::usable`,
  `PostgresStorage::usable`: one empty query, outside the ReadSet lock); a
  quarantined or dead one is dropped and a new reader opens, so no operation
  runs on a broken connection and none is retried. Quarantine rules in
  transaction.rs are unchanged.
- The control reader of a PostgreSQL Store opens at its first use
  (`ReadSet::control`), not at Store open. In the daemon the first Commit
  uses it, so its connection moves from sandbox.create to the first Commit.
- Readers and a Sandbox daemon's writer open with the light check (#63);
  the host Store service and Init keep the full check.
- Lane pre-open is NOT changed: the lane connection still opens inside the
  first Commit (owner decision).

Server connections per daemon: writer + lane (first Commit) + control
(first use) + one per mounted Workspace + up to 4 idle.

Counts (measure count test 0bdc970, PG conn/rt): store_open 2/28 at base
-> 2/2 after #63 -> 1/1; workspace_reader_open 1/30 -> 1/17 (first mount
of a fresh daemon still opens); commit.net_empty_1000 connects 1 -> 2 (lane
+ lazily opened control). Wire proxy (postgresql_open_cost): daemon Store
open 2 conns/34 RTs at base -> 1/4; second mount 1/17 -> 0/1; a mount after
the idle reader's connection was cut opens a new reader (1/4).

Tests: PASS postgresql_open_cost, store-service postgresql_open,
postgresql_round_trips (PG); SQLite workspace_reader_cache, sdk_fixed_costs,
object_demand_errors, daemon store_reader_lifecycle. daemon
mounted_store_read NOT_RUN here (needs fusectl mount in a privileged
container; fails at fusectl.rs:55 before any LayerFS code). Clippy clean
for persistence, store-service, daemon (postgresql features). No tests
deleted.

Production LOC: 138049 -> 138163 (delta +114); tools/production_loc.py
combined scope, parent tree vs staged tree via git archive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang pushed a commit that referenced this pull request Nov 6, 2026
rustfmt +1.85.1 on the files these commits changed; no behaviour change.

Production LOC: 138429 -> 138459 (delta +30, formatting only: wrapped
lines); tools/production_loc.py combined scope, parent tree vs staged tree
via git archive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang pushed a commit that referenced this pull request Nov 9, 2026
… session (layerfs-project #64)

A forwarded Sandbox's workspace.mount and workspace.status (Docker, Lima:
`sandbox/direct.rs`) found the Branch through `store_call`, which opened a
whole Store service in the caller's process: a fully checked writer and,
at the first read, its control reader. A CLI process is new per call, so
every mount paid both connections. These lookups now run on the process's
one held read session (`store::reader`, #65), as the native and worker
routes already did: one light-checked connection, no writer, no full
check, no overlay template. A process that already holds the Store's
service open reads there and opens nothing. SQLite is unchanged.

Counts (store-service `postgresql_open_cost`, wire proxy, PostgreSQL 17):
caller side of a mount 2 connections / 16 round trips -> 1 / 12. With
measure's statement split (Ephemeral-AI-Lab#94): caller 2 / 15 statements -> 1 / 9; mount
3 / 30 -> 2 / 24. The daemon side is unchanged: a fresh daemon's first
mount opens its Workspace reader (1 / 4); later mounts reuse an idle one
(0 / 1).

- store/call.rs: `held_branch`, the PostgreSQL route of the lookup.
- store/reader.rs: `branch` (id and head) on the held session; shared
  `branch_on` with `branch_id`.
- store/open.rs: `holds`, whether this process holds a Store open.
- sandbox/direct.rs (create agent's file, 3 lines): `branch_of` asks
  `held_branch` first.
- Test: postgresql_open_cost gains the before and after host-mount rows.
- Docs: workspace.mount, routing table, Store service (first mount).

No tests deleted.

Production LOC: 139048 -> 139101 (delta +53); api 20764 -> 20817 (+53), core, engine and connect unchanged
Scope: tools/production_loc.py combined (core, engine, connect, api);
parent tree vs staged tree; reference absent (retired R9c; last counted
65,417 at tree 498dd19).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang pushed a commit that referenced this pull request Nov 9, 2026
…ts held session

Production LOC: 139048 -> 139101 (delta +53), first-parent comparison (pg-r2 b4fa173 -> merged tree), counted with tools/production_loc.py; all in api.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang pushed a commit that referenced this pull request Nov 9, 2026
…rfs-project #64 option B)

Owner decision 2026-11-06: mount option B approved. A CLI mount opened a
PostgreSQL connection in the caller to find the Branch's id (option A: one
light-checked held session, 9 statements). The caller now sends the daemon
the project's name and the Branch's (`Request::MountNamed`, wire tag 15),
and the daemon finds the Branch with its snapshot in one statement on the
Workspace's own Store reader, the read it already made by id. The caller
reads no Store on any route (worker, local, forwarded).

- core history: `NamedSnapshot` and `HistoryCatalog::branch_snapshot_named`,
  a default method (stack, Branch, snapshot reads) providers may answer in
  one statement.
- persistence: `branch_snapshot_named` statement (SQLite and PostgreSQL;
  PostgreSQL runs it alone, no BEGIN/COMMIT) and the provider override.
  A new query; no schema or format change.
- store-service: `Store::bind_named` (`NamedBindRequest`, `BindIntent`;
  `BindError::NoSuchProject` / `NoSuchBranch`), `ReadSession::snapshot_named`;
  `lookup::no_project` / `no_branch` are the one source of the refusals.
- bridge: `Request::MountNamed`.
- daemon: serves it; a missing project or Branch is `Reply::SdkRefused` with
  the lookup's code and message, after the admission is withdrawn (an
  uncertain cleanup keeps the original failure).
- control: `WorkspaceApi::mount_named`; `serve::mount` sends names and passes
  the daemon's refusal through unchanged; the caller's Branch-id lookups
  (`store::service::branch_id`, `store::reader::branch_id`) are removed.
  `sandbox/direct.rs` (create agent's file): mount calls `serve::mount`.
  Status still reads its head on the caller's held session (option A).
- Exact refusals kept: invalid project name, missing project -> NoSuchProject;
  missing Branch, `main`, invalid Branch name -> NoSuchBranch, same messages.

Tests: daemon `named_mount` (new: success, the six refusals, withdrawn
admission); bridge `control_records` gains MountNamed; store-service
`postgresql_open_cost` gains the by-name snapshot rows;
`postgresql_phase_attribution` models the new mount (no caller steps;
`mount.caller_open` and `mount.caller_lookup` removed with the route).
No tests deleted.

Counts (PostgreSQL 17): CLI mount caller 1 connection / 9 statements
(option A) -> 0 / 0; daemon bind unchanged at 1 snapshot statement.

Production LOC: 139305 -> 139503 (delta +198); core 38706 -> 38728 (+22), engine 63714 -> 63893 (+179), api 20817 -> 20814 (-3), connect 16068 unchanged; reference absent (retired R9c; last counted 65,417 at tree 498dd19). Method: python3 tools/production_loc.py --root on the parent (git archive HEAD) and the staged tree (git checkout-index -a).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang pushed a commit that referenced this pull request Nov 9, 2026
…s (layerfs-project #64)

Owner decision 2026-11-06: one eager idle reader, opened after the daemon
reported ready, never delaying readiness. A fresh daemon held only its
writer at its first mount, which then opened the Workspace's reader (one
connection, the light check). After a `Hello` answered ControlReady has
been sent, the daemon now opens one reader into its idle readers on a
thread of its own; the first mount takes it (one usable check) and opens
no connection.

- store-service `ReadSet::prefill` / `Store::prefill_reader`: once per
  Store (later calls return at once), only with shared readers
  (PostgreSQL), only when no reader is idle, under the idle cap of 4.
  One attempt: a failure is counted (`ReuseWork::prefill_failures`) and
  returned, nothing is kept and nothing opens it again; the first mount
  then opens its own. `ReuseWork::readers_prefilled` counts the opened one.
- daemon `application/connection.rs`: after the ControlReady `Hello` reply
  is written, `reader_after_ready` spawns the open; a failure goes to the
  daemon's log. owner.rs and bootstrap.rs are untouched.
- A mount that arrives before the idle reader is open opens its own, as
  before; the late one joins the idle readers.
- Server connections per idle PostgreSQL daemon after create: 1 -> 2 (the
  writer and the idle reader). The maximum is unchanged: writer, lane,
  control reader, one per mounted Workspace, 4 idle.

Tests: store-service `postgresql_open_cost` gains the eager-reader rows
(open once, first mount takes it, a refused open is one counted failure and
is not tried again); `postgresql_phase_attribution` models it as
`create.daemon_idle_reader`; daemon `named_mount` checks a SQLite Store
opens none. No tests deleted.

Counts (PostgreSQL 17): first mount after readiness 1 connection / 4 round
trips -> 0 / 1 (the idle reader's usable check); the open itself, 1 / 4,
runs after the create's readiness reply. Create's counts are unchanged.

Production LOC: 139503 -> 139590 (delta +87); engine 63893 -> 63980 (+87), core 38728, api 20814 and connect 16068 unchanged; reference absent (retired R9c; last counted 65,417 at tree 498dd19). Method: python3 tools/production_loc.py --root on the parent (git archive HEAD) and the staged tree (git checkout-index -a).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang pushed a commit that referenced this pull request Nov 9, 2026
…e reader after readiness

Conflicts resolved: persistence postgresql/history/statements.rs (kept Ephemeral-AI-Lab#95's by-id and #64's named statements) and docs store-calls/service.md (kept #64's eager-reader text with Ephemeral-AI-Lab#95's validate note).

Production LOC: 139330 -> 139615 (delta +285), first-parent comparison (pg-r2 2bb6782 -> merged tree), counted with tools/production_loc.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang pushed a commit that referenced this pull request Nov 9, 2026
…fs-project #64)

`BindRefusal::request` became a `BindIntent` in 9962df8 (option B), and
two daemon test targets still read `.branch` / `.workspace` on it, so they
no longer compiled: `store_reader_lifecycle` (a failed bind returns the
original request) and `installed_store` (a duplicate bind after stop).
Both now require `BindIntent::Id` and compare the same two fields, so each
assertion is as strong as before and also checks the variant. The route
they drive (bind by identity) survives; no tests deleted.

Production LOC: 139590 -> 139590 (delta 0); test-only change. Reference
absent (retired R9c; last counted 65,417 at tree 498dd19). Method:
python3 tools/production_loc.py --root on the parent (git archive HEAD) and
the staged tree (git checkout-index -a).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang pushed a commit that referenced this pull request Nov 9, 2026
Tests only. Production LOC: 139680 -> 139680 (delta 0), first-parent comparison, counted with tools/production_loc.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang pushed a commit that referenced this pull request Nov 9, 2026
…ent (layerfs-project Ephemeral-AI-Lab#96)

A Sandbox create's reach check (control `store/reader.rs` `reach`, on the
launching side's held read session) finds its project with
`layer_stack_named`: one `layer_stack` row by its unique name. On
PostgreSQL that ran as a read transaction (BEGIN REPEATABLE READ with SET
LOCAL search_path, the SELECT, COMMIT). It is one statement, so it now runs
as `Mode::Statement` (one autocommit statement, one snapshot) with a
schema-qualified text, as Ephemeral-AI-Lab#95 did for a Commit or Layer by id. SQLite is
unchanged (`read_statement` is its one read transaction).

Create counts (`postgresql_case_attribution`, C01/C13/C14/C16, server
statements via pg_stat_statements; base 91198bf):
- create.reach: 5 statements / 5 round trips -> 2 statements / 3 round
  trips (light check + prepare + SELECT), 1 connection
- create.daemon_open: 1 statement, 1 connection (unchanged)
- create total: 6 statements / 2 connections -> 3 / 2 (with a held reach
  session: 5/1 -> 2/1)
- create.daemon_idle_reader (#64, after readiness, off the create path):
  1 connection, 1 statement, unchanged and not counted above
`postgresql_open_cost` "host create (attach + project lookup)": 1
connection, 8 -> 6 round trips.

Kept: the held reach session (it still serves a status's head and is
reused by the next create within 60 s; closing it would save nothing per
create) and the daemon's light-checked open (one statement; lower needs
dropping the check or an eager connection). Nothing moves into mount or
Commit.

Docs: store/postgresql.md lists the read under `Statement`;
api/sdk/sandbox/create.md gives the new counts, the idle reader, and drops
the stale mount use of the held session (#64 option B).

Granted minimal edit in persistence history reads (`history/catalog.rs`,
`backend/postgresql/history/statements.rs`, one new SQL text).

Tests (Linux, PostgreSQL 17.11): persistence postgresql_open_check 3/3,
postgresql_commit_round_trips 1/1, postgresql_acquisition 4/4,
postgresql_identical_ids 3/3, layerstack_removal 2/2; store-service
postgresql_round_trips 2/2, postgresql_open_cost 1/1, postgresql_open 1/1,
postgresql_phase_attribution (case) 1/1, operator_history (SQLite) 2/2;
project init_postgresql 1/1; api postgres_connect 2/2, postgres_refusals
4/4, postgres_sandbox_reach 1/1 (with layerfs-api/machine). Clippy -D
warnings for layerfs-persistence with postgresql, all targets.

No tests deleted.

Production LOC: 139680 -> 139684 (delta +4)
Scope: tools/production_loc.py (pinned) on the parent tree and the staged
tree, combined core/engine/connect/api production source (includes shipped
runtime SQL); reference absent (retired R9c; last counted 65,417 at tree
498dd19).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang pushed a commit that referenced this pull request Nov 9, 2026
Brings in Ephemeral-AI-Lab#582-Ephemeral-AI-Lab#593 (SQLite idle WAL checkpoint, ring admission, chain-root rebase, PG Init cleanup). One conflict: api/crates/layerfs-control/src/store/open.rs, both sides only added functions (#64 holds; Ephemeral-AI-Lab#86 idle-checkpoint helpers); kept both.

Production LOC: 139821 -> 140178 (delta +357), first-parent comparison (pg-r2 76a32a9 -> merged tree), counted with tools/production_loc.py; the delta is the upstream changes.

Checked on the merged tree: Clippy -D warnings (persistence, store-service, daemon, control, storage, content, workspace, history, bridge with postgresql/machine) and rustfmt clean; 219 test binaries of storage, content, workspace, persistence and store-service run one by one against PostgreSQL 17: 218 pass (1,020 tests), filesystem_attributes fails 1/13 only because its fixture path is relative to the crate directory (known, same at base).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang pushed a commit that referenced this pull request Nov 9, 2026
PostgreSQL round 2 (layerfs-project #61: Ephemeral-AI-Lab#94-Ephemeral-AI-Lab#98, #64, #66): fewer connections and round trips in create, mount, Commit and reads, now on top of Ephemeral-AI-Lab#84's storage changes (Ephemeral-AI-Lab#594, Ephemeral-AI-Lab#595/Ephemeral-AI-Lab#596).

Quick verification on this exact tree dd191ba (layerfs-benchmark#10): backup B1-B4 51/51; fs-bench-quick q84-2 + q84-2x 36/36 PASS with PostgreSQL counts equal to the round-2 control except C16 Commit 251 -> 224 statements, no aggregate or per-row timing flag; fs-storage-quick Q1/Q5/Q6 x SQLite/PostgreSQL 6/6 PASS, every metric 'same' as the Ephemeral-AI-Lab#84 final. Clippy -D warnings and cargo fmt --check clean.

Production LOC: 140338 -> 141118 (delta +780)
Scope: combined first-party production source, tools/production_loc.py --json on git archive trees, first parent e20c6a2. +773 round 2 (Ephemeral-AI-Lab#94-Ephemeral-AI-Lab#98, #64) and +7 from a rustfmt-only commit on Ephemeral-AI-Lab#92 files. Reference absent (retired R9c; last counted 65,417 at tree 498dd19).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang pushed a commit that referenced this pull request Nov 9, 2026
…ct#101 M-1)

Integrates the advanced base by merge, not rebase, so the commits cited in
the Ephemeral-AI-Lab#102-Ephemeral-AI-Lab#107 issue comments keep their ids (owner, 2026-11-06, M-1).
44 base commits: PostgreSQL round-trip work (#64, Ephemeral-AI-Lab#90, Ephemeral-AI-Lab#94-Ephemeral-AI-Lab#97), inode
value blocks and schema v2 (Ephemeral-AI-Lab#91), directory/extent page deltas (Ephemeral-AI-Lab#92),
provider exit polling (#66) and Workspace hard links to symlink inodes
(Ephemeral-AI-Lab#580).

Hand-ported hunks in the files the split moved (git's rename detection
mapped all 11; 5 code files and 8 docs conflicted):
- #64 option B, the daemon finds a mount's Branch by name: `MountNamed`
  in layerfs-sandbox `daemon/{channel,workspace/mount,workspace/recovery}.rs`;
  `workspace/serve.rs` `mount` sends project and Branch names and keeps a
  daemon lookup refusal as the answer; `mount_with` and
  `layerfs_store::service::branch_id` are gone. The forwarded-Sandbox
  mount and status arms, which the split moved from `direct.rs` to
  `workspace/direct.rs`, take the base's change there.
- #64 status head read on the held PostgreSQL session:
  `layerfs_store::call::held_branch` (pub: layerfs-sandbox calls it),
  `open::holds`, `postgresql/reader.rs` `branch`.
- #66 provider exit polling: layerfs-platform `setup/tool.rs` and its
  `machine_probe.rs` test (auto-merged).
- Docs: the base's wording with the split's link targets.

Checks (all pass, /tmp/cs101/checks-merge.txt): test --no-run; clippy
-D warnings on the workspace, with machine,postgresql,tls, and on Linux;
fmt; boundary guard; tools unittest (173); host and guest CLI builds,
guest tree without layerfs-platform; per-package tests for all 12
control and api packages, each under 120 s: 143 targets pass, including
all 98 targets of the Ephemeral-AI-Lab#107 run with the same pass counts.

Deleted tests: none.

Production LOC: 139843 -> 141554 (delta +1711), first-parent comparison,
`python3 tools/production_loc.py --detail` on `git archive` snapshots of
the first parent and the merge tree. The base itself grew 139405 ->
141113 (+1708) from 8b701f4 to b180233; the other +3 are the
ported hunks' imports at their new paths. control 33926 -> 33981
(platform 4160, sandbox 6186, store 6177); api 3422 unchanged; core
38811 -> 39399, engine 63684 -> 64752 (base only). Reference absent
(retired R9c; last counted 65,417 at tree 498dd19).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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