diff --git a/app/crowdb-web/ui/src/components/dialogs/createFlows.test.tsx b/app/crowdb-web/ui/src/components/dialogs/createFlows.test.tsx
index e0f86f379..93410ae81 100644
--- a/app/crowdb-web/ui/src/components/dialogs/createFlows.test.tsx
+++ b/app/crowdb-web/ui/src/components/dialogs/createFlows.test.tsx
@@ -40,7 +40,7 @@ const deploymentDefaults = {
diskio: { instance_id: '1', rpc_port: 13010 },
chunkdb: { instance_id: '1', http_port: 12010, rpc_port: 12110 },
'chunk-kv': { instance_id: '1', http_port: 15010, rpc_port: 15110 },
- 'access-server': { instance_id: '1', http_port: 9092, s3_port: 9091, health_port: 9093 },
+ 'access-server': { instance_id: '1', http_port: 9092, s3_port: 9091, health_port: 9094 },
};
function installFetchMock(response: any = {}, status = 200) {
diff --git a/app/crowdb-web/ui/src/services/DeployServiceDialog.tsx b/app/crowdb-web/ui/src/services/DeployServiceDialog.tsx
index 3a97708dc..4c48ee539 100644
--- a/app/crowdb-web/ui/src/services/DeployServiceDialog.tsx
+++ b/app/crowdb-web/ui/src/services/DeployServiceDialog.tsx
@@ -23,7 +23,7 @@ export function DeployServiceDialog({ nodeId, kind, servers, stores, diskGroups,
const [httpPort, setHttpPort] = useState(String(base));
const [rpcPort, setRpcPort] = useState(String(kind === 'diskio' ? base : base + 100));
const [s3Port, setS3Port] = useState('9091');
- const [healthPort, setHealthPort] = useState('9093');
+ const [healthPort, setHealthPort] = useState('9094');
const [diskGroup, setDiskGroup] = useState(String(diskGroups[0]?.id ?? ''));
const [store, setStore] = useState(String(stores.find(store => String(store.store_id) !== '0')?.store_id ?? stores[0]?.store_id ?? ''));
const firstGroup = stores.find(entry => String(entry.store_id) === store)?.groups.find(entry => String(entry.group_id) !== '0');
diff --git a/app/crowdb-web/ui/src/services/useNodeServicePlans.test.tsx b/app/crowdb-web/ui/src/services/useNodeServicePlans.test.tsx
index 1d2533c60..c5b3b4c38 100644
--- a/app/crowdb-web/ui/src/services/useNodeServicePlans.test.tsx
+++ b/app/crowdb-web/ui/src/services/useNodeServicePlans.test.tsx
@@ -19,6 +19,80 @@ beforeEach(() => {
});
afterEach(() => { vi.useRealTimers(); vi.clearAllMocks(); });
describe('node service plans', () => {
+ it('does not publish failed progress when page unload cancels an observation', async () => {
+ const steps = newPlan();
+ for (const kind of serviceOrder) steps[kind] = { state: 'disabled' };
+ steps['chunk-kv'] = { state: 'waiting', detail: 'Waiting: initialize Group 0 in Paxos KV' };
+ let cancel!: (error: Error) => void;
+ vi.mocked(listServers).mockReturnValue(new Promise((_, reject) => { cancel = reject; }));
+ const request = vi.mocked(serviceRequest).getMockImplementation()!;
+ vi.mocked(serviceRequest).mockImplementation(async (path, method, body) => path === '/service-plans'
+ ? { 1: { revision: 8, steps } } : request(path, method, body));
+ const { result, unmount } = renderHook(() => useNodeServicePlans(stores, {}, async () => {}, true));
+ await act(async () => {});
+ await act(async () => {
+ window.dispatchEvent(new Event('pagehide'));
+ cancel(new TypeError('Failed to fetch'));
+ });
+ expect(result.current.plans[1]['chunk-kv'].state).toBe('waiting');
+ expect(vi.mocked(serviceRequest).mock.calls.some(([, method]) => method === 'PUT')).toBe(false);
+ unmount();
+ });
+ it('observes registered DiskIO restart without waiting for Capacity data', async () => {
+ const steps = newPlan();
+ for (const kind of serviceOrder) steps[kind] = { state: 'disabled' };
+ steps['chunk-kv'] = { state: 'waiting' };
+ let restarted = false;
+ vi.mocked(listServers).mockImplementation(async () => [
+ { node_id: 1, service_type: 'chunkdb', pid: 12 },
+ { node_id: 1, service_type: 'diskio', pid: 13 },
+ { node_id: 2, service_type: 'diskio', pid: restarted ? 14 : undefined },
+ ] as Awaited>);
+ const request = vi.mocked(serviceRequest).getMockImplementation()!;
+ vi.mocked(serviceRequest).mockImplementation(async (path, method, body) => {
+ if (path === '/service-plans') return { 1: { revision: 1, steps } };
+ if (path === '/deployment-defaults') return { 'chunk-kv': { instance_id: '1', http_port: 12010, rpc_port: 12110 } };
+ return request(path, method, body);
+ });
+ const { result } = renderHook(() => useNodeServicePlans(stores, {}, async () => {}, true));
+ await act(async () => {});
+ expect(result.current.plans[1]['chunk-kv'].detail).toContain('restart registered DiskIO services');
+ expect(serviceRequest).not.toHaveBeenCalledWith('/deployment-defaults', 'GET');
+ restarted = true;
+ await act(async () => { await vi.advanceTimersByTimeAsync(2000); });
+ expect(result.current.plans[1]['chunk-kv'].state).toBe('deployed');
+ expect(vi.mocked(serviceRequest).mock.calls.filter(([path, method]) => path.endsWith('/services/deploy') && method === 'POST')).toHaveLength(1);
+ });
+ it('loads saved revisions before resuming waiting steps after reload', async () => {
+ let release!: (value: unknown) => void;
+ const saved = new Promise(resolve => { release = resolve; });
+ const request = vi.mocked(serviceRequest).getMockImplementation()!;
+ vi.mocked(serviceRequest).mockImplementation(async (path, method, body) =>
+ path === '/service-plans' ? saved : request(path, method, body));
+ const { result } = renderHook(() => useNodeServicePlans([], {}, async () => {}, true));
+ await act(async () => { await vi.advanceTimersByTimeAsync(2000); });
+ expect(serviceRequest).toHaveBeenCalledTimes(1);
+ const steps = newPlan();
+ steps['paxos-kv'] = { state: 'deployed' };
+ await act(async () => { release({ 1: { revision: 7, steps } }); });
+ expect(result.current.plans[1].diskio.state).toBe('waiting');
+ expect(serviceRequest).toHaveBeenCalledWith('/nodes/1/service-plan', 'PUT', expect.objectContaining({ revision: 7 }));
+ });
+ it('keeps a deployment queued when ownership changes after its readiness probe', async () => {
+ vi.mocked(listServers).mockResolvedValue([{ node_id: 1, service_type: 'diskio', pid: 10 }] as Awaited>);
+ let ownershipPublished = false;
+ const request = vi.mocked(serviceRequest).getMockImplementation()!;
+ vi.mocked(serviceRequest).mockImplementation(async (path, method, body) => {
+ if (path.endsWith('/services/deploy') && !ownershipPublished) throw new Error('HTTP 409: Waiting: every configured disk group needs live DiskIO ownership');
+ return request(path, method, body);
+ });
+ const { result } = renderHook(() => useNodeServicePlans(stores, {}, async () => {}, true));
+ await act(async () => { await result.current.start(1, ['chunkdb']); });
+ expect(result.current.plans[1].chunkdb).toEqual({ state: 'waiting', detail: 'Waiting: every configured disk group needs live DiskIO ownership' });
+ ownershipPublished = true;
+ await act(async () => { await vi.advanceTimersByTimeAsync(10000); });
+ expect(result.current.plans[1].chunkdb.state).toBe('deployed');
+ });
it('finishes every selected ChunkDB deployment before journal bootstrap can block the serial plan', async () => {
const first = newPlan();
const second = newPlan();
diff --git a/app/crowdb-web/ui/src/services/useNodeServicePlans.ts b/app/crowdb-web/ui/src/services/useNodeServicePlans.ts
index 4a3efa6ab..bda00eb39 100644
--- a/app/crowdb-web/ui/src/services/useNodeServicePlans.ts
+++ b/app/crowdb-web/ui/src/services/useNodeServicePlans.ts
@@ -109,6 +109,8 @@ export function useNodeServicePlans(stores: EnrichedStoreView[], groups: Record<
if (!busy.current && input.current.enabled && !stopped.current) {
busy.current = true;
try {
+ await recovery.current;
+ if (disposed || stopped.current) return;
for (const [key, initial] of Object.entries(plansRef.current)) {
if (disposed || stopped.current) break;
if (!serviceOrder.some(kind => ['pending', 'waiting'].includes(initial[kind].state))) continue;
@@ -118,6 +120,7 @@ export function useNodeServicePlans(stores: EnrichedStoreView[], groups: Record<
let existing;
try { existing = await listServers(); }
catch (error) {
+ if (disposed || stopped.current) return;
for (const kind of serviceOrder) if (!['deployed', 'disabled'].includes(plan[kind].state)) await write(kind, { state: 'failed', detail: String(error) });
continue;
}
@@ -182,6 +185,14 @@ export function useNodeServicePlans(stores: EnrichedStoreView[], groups: Record<
wakePending.current = true;
deploymentStarted = true;
} catch (error) {
+ if (disposed || stopped.current) return;
+ // Hardware can change between the readiness probe and mutation.
+ // A rejected prerequisite has not launched a service; keep it queued.
+ const dependency = String(error).match(/^Error: HTTP 409: (Waiting: .+)$/);
+ if (dependency && (kind === 'chunkdb' || kind === 'chunk-kv')) {
+ await write(kind, { state: 'waiting', detail: dependency[1] });
+ continue;
+ }
const listeners = Object.entries(overrides.current[id]?.[kind] ?? {}).map(([name, port]) => `${name}=${port}`).join(', ');
const detail = `${serviceLabels[kind]}${listeners ? ` (${listeners})` : ''}: ${String(error)}`.slice(0, 4096);
// A node without a fixed ChunkDB slot is valid. Keep it as a
@@ -194,21 +205,33 @@ export function useNodeServicePlans(stores: EnrichedStoreView[], groups: Record<
try { await input.current.refresh(); } catch { /* The shared data hooks report refresh errors. */ }
}
} catch (error) {
+ if (disposed || stopped.current) return;
for (const id of Object.keys(plansRef.current)) failProgress(Number(id), `Progress could not be saved: ${String(error)}. Reload to reconcile before retrying.`);
} finally { busy.current = false; }
}
if (!disposed) {
const hasQueuedWork = Object.values(plansRef.current).some(plan => serviceOrder.some(kind => plan[kind].state === 'waiting'));
const waitingForAuthority = Object.values(plansRef.current).some(plan => serviceOrder.some(kind => plan[kind].detail === 'Waiting: initialize Group 0 in Paxos KV'));
+ const waitingForRestart = Object.values(plansRef.current).some(plan => serviceOrder.some(kind => plan[kind].detail?.startsWith('Waiting: restart registered DiskIO services')));
const hasDisks = Object.values(input.current.groups).some(node => Object.values(node.disksByDg).some(disks => disks.length > 0));
- const delay = wakePending.current ? 0 : hasQueuedWork && !hasDisks && !waitingForAuthority ? 10000 : 2000;
+ const delay = wakePending.current ? 0 : hasQueuedWork && !hasDisks && !waitingForAuthority && !waitingForRestart ? 10000 : 2000;
wakePending.current = false;
timer = setTimeout(() => { running.current = tick(); }, delay);
}
}
wake.current = () => { if (busy.current) wakePending.current = true; else { clearTimeout(timer); running.current = tick(); } };
running.current = tick();
- return () => { disposed = true; clearTimeout(timer); };
+ const dispose = () => { disposed = true; clearTimeout(timer); };
+ const restore = (event: PageTransitionEvent) => { if (event.persisted) window.location.reload(); };
+ window.addEventListener('pagehide', dispose);
+ window.addEventListener('beforeunload', dispose);
+ window.addEventListener('pageshow', restore);
+ return () => {
+ dispose();
+ window.removeEventListener('pagehide', dispose);
+ window.removeEventListener('beforeunload', dispose);
+ window.removeEventListener('pageshow', restore);
+ };
}, [persist, failProgress]);
useEffect(() => { wake.current(); }, [prerequisiteKey]);
return { plans, start, stop };
diff --git a/container/crowdb-monitor/tests/liveness_test.rs b/container/crowdb-monitor/tests/liveness_test.rs
index 0b434fd41..41f223491 100644
--- a/container/crowdb-monitor/tests/liveness_test.rs
+++ b/container/crowdb-monitor/tests/liveness_test.rs
@@ -9,9 +9,16 @@ struct TestRunRoot(PathBuf);
impl TestRunRoot {
fn new() -> Self {
- let root = std::env::temp_dir().join(format!("cm-live-{}", Uuid::new_v4().simple()));
+ // macOS limits Unix-domain socket paths to a short fixed buffer. Keep
+ // this test root under /tmp instead of the long per-user temp path.
+ let base = if cfg!(target_os = "macos") {
+ PathBuf::from("/tmp")
+ } else {
+ std::env::temp_dir()
+ };
+ let root = base.join(format!("cm-live-{}", Uuid::new_v4().simple()));
fs::create_dir_all(&root).unwrap();
- Self(root.canonicalize().unwrap())
+ Self(root)
}
}
diff --git a/container/crowdb-monitor/tests/preview_run_test.rs b/container/crowdb-monitor/tests/preview_run_test.rs
index d07530558..1b779b67a 100644
--- a/container/crowdb-monitor/tests/preview_run_test.rs
+++ b/container/crowdb-monitor/tests/preview_run_test.rs
@@ -9,7 +9,12 @@ struct TestRoot(PathBuf);
impl TestRoot {
fn new() -> Self {
- let path = std::env::temp_dir().join(format!("cm-preview-{}", Uuid::new_v4().simple()));
+ let temp_root = if cfg!(target_os = "macos") {
+ PathBuf::from("/tmp")
+ } else {
+ std::env::temp_dir()
+ };
+ let path = temp_root.join(format!("cm-preview-{}", Uuid::new_v4().simple()));
for name in ["bin", "templates", "data", "run"] {
fs::create_dir_all(path.join(name)).unwrap();
}
@@ -35,7 +40,12 @@ impl TestRoot {
let name = service.program.file_name().unwrap();
service.program = self.0.join("bin").join(name);
if service.id == "kv" {
- symlink("/bin/false", &service.program).unwrap();
+ let failing_program = if cfg!(target_os = "macos") {
+ "/usr/bin/false"
+ } else {
+ "/bin/false"
+ };
+ symlink(failing_program, &service.program).unwrap();
for argument in &mut service.args {
if argument == "/opt/crowdb/data/kv/node-1" {
*argument = self.0.join("data/kv/node-1").to_string_lossy().into_owned();
diff --git a/container/crowdb-monitor/tests/single_node_profile_test.rs b/container/crowdb-monitor/tests/single_node_profile_test.rs
index dd1acca5a..879dd7a12 100644
--- a/container/crowdb-monitor/tests/single_node_profile_test.rs
+++ b/container/crowdb-monitor/tests/single_node_profile_test.rs
@@ -52,7 +52,7 @@ fn single_node_preview_has_exact_topology_and_endpoints() {
assert_eq!(access_service.additional_probes.len(), 1);
assert_eq!(
access_service.additional_probes[0].target,
- "http://127.0.0.1:9093/_crowdb/health/ready"
+ "http://127.0.0.1:9094/_crowdb/health/ready"
);
assert_eq!(
access_service.additional_probes[0].failure_threshold,
diff --git a/container/crowdb-monitor/tests/supervisor_test.rs b/container/crowdb-monitor/tests/supervisor_test.rs
index 792edc318..f986bb8fb 100644
--- a/container/crowdb-monitor/tests/supervisor_test.rs
+++ b/container/crowdb-monitor/tests/supervisor_test.rs
@@ -5,6 +5,7 @@ use std::collections::BTreeMap;
use std::fs;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
+use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
@@ -74,7 +75,7 @@ async fn exited_service_restarts_with_same_identity_and_event_log() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let marker = roots.0.join("first-exit");
let script = format!(
- "if [ ! -e '{}' ]; then : > '{}'; sleep 0.2; exit 0; fi; exec sleep 30",
+ "if [ ! -e '{}' ]; then : > '{}'; /bin/sleep 0.2; exit 0; fi; exec /bin/sleep 30",
marker.display(),
marker.display()
);
@@ -110,7 +111,7 @@ async fn restarted_service_waits_for_authority_validation() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let marker = roots.0.join("validation-exit");
let script = format!(
- "if [ ! -e '{}' ]; then : > '{}'; sleep 0.2; exit 0; fi; exec sleep 30",
+ "if [ ! -e '{}' ]; then : > '{}'; /bin/sleep 0.2; exit 0; fi; exec /bin/sleep 30",
marker.display(),
marker.display()
);
@@ -144,7 +145,7 @@ async fn repeated_exits_exhaust_budget_and_leave_unready() {
let roots = TestRoots::new();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let profile = roots.profile(
- "sleep 0.2; exit 1".into(),
+ "/bin/sleep 0.2; exit 1".into(),
listener.local_addr().unwrap().port(),
1,
);
@@ -172,7 +173,11 @@ async fn repeated_exits_exhaust_budget_and_leave_unready() {
async fn stable_health_resets_crash_loop_budget() {
let roots = TestRoots::new();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
- let mut profile = roots.profile("exec sleep 30".into(), listener.local_addr().unwrap().port(), 1);
+ let mut profile = roots.profile(
+ "exec /bin/sleep 30".into(),
+ listener.local_addr().unwrap().port(),
+ 1,
+ );
profile.services[0].restart.stable_after_ms = 50;
let mut supervisor = Supervisor::new(
profile,
@@ -190,11 +195,20 @@ async fn stable_health_resets_crash_loop_budget() {
rustix::process::kill_process(pid, rustix::process::Signal::KILL).unwrap();
let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
loop {
- let status = fs::read_to_string(format!("/proc/{}/status", pid.as_raw_pid())).unwrap();
- if status
- .lines()
- .any(|line| line.starts_with("State:") && line.contains('Z'))
- {
+ let exited = if cfg!(target_os = "linux") {
+ fs::read_to_string(format!("/proc/{}/status", pid.as_raw_pid())).map_or(true, |status| {
+ status
+ .lines()
+ .any(|line| line.starts_with("State:") && line.contains('Z'))
+ })
+ } else {
+ let output = Command::new("ps")
+ .args(["-p", &pid.as_raw_pid().to_string(), "-o", "stat="])
+ .output()
+ .unwrap();
+ !output.status.success() || String::from_utf8_lossy(&output.stdout).contains('Z')
+ };
+ if exited {
break;
}
assert!(tokio::time::Instant::now() < deadline);
@@ -220,7 +234,7 @@ async fn live_listener_prevents_replacement_after_child_exit() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap().to_string();
let mut profile = roots.profile(
- "sleep 0.2; exit 1".into(),
+ "/bin/sleep 0.2; exit 1".into(),
listener.local_addr().unwrap().port(),
1,
);
@@ -249,7 +263,7 @@ async fn transient_probe_failure_clears_readiness_without_restarting() {
let roots = TestRoots::new();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
- let mut profile = roots.profile("exec sleep 30".into(), address.port(), 2);
+ let mut profile = roots.profile("exec /bin/sleep 30".into(), address.port(), 2);
profile.services[0].probe.failure_threshold = 2;
let mut supervisor = Supervisor::new(
profile,
@@ -297,7 +311,7 @@ async fn child_exit_after_probe_failure_is_recorded() {
stream.write_all(response.as_bytes()).await.unwrap();
}
});
- let mut profile = roots.profile("exec sleep 30".into(), address.port(), 2);
+ let mut profile = roots.profile("exec /bin/sleep 30".into(), address.port(), 2);
profile.services[0].probe.kind = ProbeKind::Http;
profile.services[0].probe.target = format!("http://{address}/health");
profile.services[0].probe.failure_threshold = 5;
@@ -349,7 +363,7 @@ async fn dependency_restart_stops_dependents_before_replacement() {
let dependent_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let marker = roots.0.join("first-exit");
let script = format!(
- "if [ ! -e '{}' ]; then : > '{}'; sleep 0.2; exit 0; fi; exec sleep 30",
+ "if [ ! -e '{}' ]; then : > '{}'; /bin/sleep 0.2; exit 0; fi; exec /bin/sleep 30",
marker.display(),
marker.display()
);
@@ -357,7 +371,7 @@ async fn dependency_restart_stops_dependents_before_replacement() {
let mut dependent = profile.services[0].clone();
dependent.id = "web".into();
dependent.dependencies = vec!["kv".into()];
- dependent.args = vec!["-c".into(), "exec sleep 30".into()];
+ dependent.args = vec!["-c".into(), "exec /bin/sleep 30".into()];
dependent.probe.target = dependent_listener.local_addr().unwrap().to_string();
profile.services.push(dependent);
profile.validate().unwrap();
diff --git a/container/single-node-container/README.md b/container/single-node-container/README.md
index a224e6fcb..2033c8691 100644
--- a/container/single-node-container/README.md
+++ b/container/single-node-container/README.md
@@ -35,7 +35,7 @@ pixi run clean-container
```
`start-container` builds the image, starts `crowdb-single-node`, exposes
-ports 9090–9093 on the container network, waits for readiness, and prints client
+ports 9090–9094 on the container network, waits for readiness, and prints client
credentials. Docker creates an anonymous data volume unless configured
otherwise. `inject-container` loads TPC-H SF1 into Iceberg and copies its
Parquet files to the container's S3 endpoint. The loader retains its data and
diff --git a/container/single-node-container/dev-container.sh b/container/single-node-container/dev-container.sh
index 8714f339e..e851700e7 100644
--- a/container/single-node-container/dev-container.sh
+++ b/container/single-node-container/dev-container.sh
@@ -46,7 +46,7 @@ start_container() {
fi
if ! docker run --detach --name "$name" \
- --expose 9090 --expose 9091 --expose 9092 --expose 9093 \
+ --expose 9090 --expose 9091 --expose 9092 --expose 9093 --expose 9094 \
"$image"; then
if docker container inspect "$name" >/dev/null 2>&1; then
docker rm --force --volumes "$name" >/dev/null
diff --git a/container/single-node-container/profile.toml b/container/single-node-container/profile.toml
index 883e3e8af..d3ad83850 100644
--- a/container/single-node-container/profile.toml
+++ b/container/single-node-container/profile.toml
@@ -174,9 +174,9 @@ backoff_max_ms = 5000
id = "access"
program = "/opt/crowdb/bin/crowdb-access-server"
args = ["--config", "/opt/crowdb/run/config/access.toml"]
-env = { CROWDB_S3_PUBLIC_URI = "http://localhost:9091", CROWDB_S3_REGION = "us-east-1", CROWDB_ICEBERG_PUBLIC_URI = "http://localhost:9092", CROWDB_ICEBERG_GC_ENABLED = "0", CROWDB_MANAGEMENT_SEEDS = "http://127.0.0.1:10000", CROWDB_ACCESS_HEALTH_LISTEN = "127.0.0.1:9093", CROWDB_ACCESS_LOG_DIR = "/opt/crowdb/data/log/access" }
+env = { CROWDB_S3_PUBLIC_URI = "http://localhost:9091", CROWDB_S3_REGION = "us-east-1", CROWDB_ICEBERG_PUBLIC_URI = "http://localhost:9092", CROWDB_ICEBERG_GC_ENABLED = "0", CROWDB_MANAGEMENT_SEEDS = "http://127.0.0.1:10000", CROWDB_ACCESS_HEALTH_LISTEN = "127.0.0.1:9094", CROWDB_ACCESS_LOG_DIR = "/opt/crowdb/data/log/access" }
dependencies = ["kv", "chunk-kv", "chunkdb", "diskio"]
-fence_listeners = ["127.0.0.1:9092", "127.0.0.1:9091", "127.0.0.1:9093"]
+fence_listeners = ["127.0.0.1:9092", "127.0.0.1:9091", "127.0.0.1:9094"]
config_template = "/opt/crowdb/etc/templates/access.toml"
[services.probe]
kind = "http"
@@ -186,7 +186,7 @@ timeout_ms = 1000
failure_threshold = 5
[[services.additional_probes]]
kind = "http"
-target = "http://127.0.0.1:9093/_crowdb/health/ready"
+target = "http://127.0.0.1:9094/_crowdb/health/ready"
timeout_ms = 1000
failure_threshold = 5
[services.restart]
diff --git a/container/single-node-container/templates/access.toml b/container/single-node-container/templates/access.toml
index a0471d36b..fd8b76edc 100644
--- a/container/single-node-container/templates/access.toml
+++ b/container/single-node-container/templates/access.toml
@@ -84,3 +84,10 @@ retry_max_ms = 60000
corruption_attempts = 3
minimum_retention_ms = 604800000
catalogs = []
+
+[dataset]
+listen = "0.0.0.0:9093"
+max_in_flight = 3
+request_timeout_seconds = 5
+lease_ttl_seconds = 60
+max_batch_size = 10000
diff --git a/container/single-node-container/tests/container-e2e.sh b/container/single-node-container/tests/container-e2e.sh
index e23bf0c9b..622346956 100644
--- a/container/single-node-container/tests/container-e2e.sh
+++ b/container/single-node-container/tests/container-e2e.sh
@@ -45,7 +45,7 @@ start_container() {
fi
docker run -d --name "$name" \
"${mount_args[@]}" \
- -p 127.0.0.1:9092:9092 -p 127.0.0.1::9091 -p 127.0.0.1::9090 \
+ -p 127.0.0.1:9092:9092 -p 127.0.0.1::9091 -p 127.0.0.1::9090 -p 127.0.0.1::9093 -p 127.0.0.1::9094 \
"$image" >/dev/null
for attempt in $(seq 1 240); do
state=$(docker inspect --format '{{.State.Status}}' "$name")
@@ -70,15 +70,19 @@ port() {
}
verify_public_services() {
- local iceberg_port s3_port web_port token
+ local iceberg_port s3_port web_port dataset_port token
iceberg_port=$(port 9092)
s3_port=$(port 9091)
web_port=$(port 9090)
+ dataset_port=$(port 9093)
docker exec "$name" crowdb-monitor readiness >/dev/null
+ docker exec "$name" curl --fail --silent --show-error --max-time 5 \
+ "http://127.0.0.1:9094/_crowdb/health/ready" >/dev/null
curl --fail --silent --show-error --max-time 5 \
"http://127.0.0.1:$web_port/api/authority" | jq -e '.source == "group0" and .available == true' >/dev/null
curl --fail --silent --show-error --max-time 5 \
"http://127.0.0.1:$web_port/api/preview" | jq -e '.source == "group0" and (.services | length) > 0' >/dev/null
+ [[ "$(curl --silent --output /dev/null --write-out '%{http_code}' --max-time 5 "http://127.0.0.1:$dataset_port/v1/dataset/open")" == 404 ]]
token=$(printf '%s\n' "$client_env" | sed -n 's/^ICEBERG_TOKEN=//p')
[[ -n "$token" ]]
printf 'header = "Authorization: Bearer %s"\nurl = "http://127.0.0.1:%s/v1/config"\n' "$token" "$iceberg_port" |
diff --git a/doc/backlog/R220-dataset-fat-client-plan.md b/doc/backlog/R220-dataset-fat-client-plan.md
new file mode 100644
index 000000000..1fca49109
--- /dev/null
+++ b/doc/backlog/R220-dataset-fat-client-plan.md
@@ -0,0 +1,66 @@
+
+
+
+### R220: Dataset — Fat-client direct access plan
+
+Status: Separate Dataset SDK project maintained by this team. Deferred until R219
+is implemented and the CPU HTTP path has measured latency, throughput, memory,
+and digest costs.
+
+Problem: The SDK fat client can avoid HTTP routing and payload relay by connecting
+directly to internal Chunk-KV and DiskIO, but it also takes responsibility for
+routing freshness, retries, buffer lifetime, backpressure, and worker
+partitioning. Implementing it before the HTTP baseline would make optimization
+claims hard to measure and could duplicate authority logic.
+
+Solution:
+
+- Document the SDK direct-client protocol over the same Dataset authority and Read
+ Plan contract used by HTTP.
+- Compare direct Chunk-KV metadata reads and DiskIO/chunk range reads with the
+ HTTP baseline, including routing refresh, stale topology, failure retry, and
+ 60-second active-read TTL behavior.
+- Define owner-backed buffer lifetime, bounded prefetch, cache admission,
+ cancellation, and checksum/MD5 verification without adding client-side global
+ locks or unbounded queues.
+- Analyze which work belongs in the client versus Chunk-KV, DiskIO, and Dataset
+ library. The client cannot publish placement or bypass Dataset authority.
+- Reserve GPU/direct-memory delivery for a later design after CPU direct access
+ is measured and correct.
+
+Dependencies: R219 HTTP baseline, R217 CPU E2E/container acceptance, and the
+measured Chunk-KV/DiskIO read flows. Unlanded direct-access work remains a design
+artifact; no native client is required for R213–R219 acceptance.
+
+Acceptance:
+
+1. Given HTTP and direct-client prototypes over the same snapshot, produce a
+ comparison of p50/p99 latency, throughput, CPU, memory, and digest cost.
+ Measurement contract. Integration test.
+2. Given stale routing or a disconnected DiskIO endpoint, assert the fat client
+ refreshes or retries without changing Dataset selection or snapshot identity.
+ Non-authoritative topology invariant. Integration test.
+3. Given a slow consumer, assert direct prefetch and in-flight buffers remain
+ bounded and cancellation releases client buffers. Bounded delivery invariant.
+ Integration test.
+
+Resolved decisions:
+
+- The core remains Rust. The separate SDK exposes a narrow C ABI over opaque
+ handles first, then builds Python/PyTorch bindings over the same contract.
+ C++ and RDMA wrappers remain later data-plane work.
+- Client metadata is cached by immutable Snapshot ID and explicitly refreshed
+ when resolving `latest`; no Dataset-specific global watch stream is required
+ for the first version.
+
+Open Issues:
+
+- Define the C ABI ownership, error, cancellation, and buffer lifetime contract
+ before implementing the fat client. Keep the ABI in the separate SDK project.
+
+Verification commands:
+
+- `pixi run rs-fmt-check`
+- `pixi run rs-lint`
+- `pixi run test-access-server`
+- `pixi run test-unit`
diff --git a/doc/backlog/R227-console-multi-node-deployment.md b/doc/backlog/R227-console-multi-node-deployment.md
new file mode 100644
index 000000000..a796d904c
--- /dev/null
+++ b/doc/backlog/R227-console-multi-node-deployment.md
@@ -0,0 +1,280 @@
+
+
+
+### R227: console — Multi-node discovery and light-container deployment
+
+## Problem
+
+CROWDB needs a multi-node deployment flow in which each node runs
+`crowdb-monitor` and a Web UI. Operators should not have to enter every peer
+address before discovering available nodes. The current monitor and console
+surfaces do not establish the LAN discovery and shared bootstrap contract
+described here; existing service registration is not node discovery.
+
+The permanent [console design](../design/console/design-crowdb-console.md)
+defines Group 0 as topology authority, and the
+[Group-0 design](../design/kv/design-crowdb-kv-group0.md) defines its service
+registry. Neither a local discovery cache nor a console-local launch registry
+can replace that authority. Existing console node/server coupling must be
+reviewed when adding nodes before any server exists.
+
+Concrete scenarios:
+
+- Three machines on a home LAN discover each other through their monitors.
+ An operator opens any node's UI, assigns racks, and initializes Group 0.
+- Several containers on one machine simulate independent nodes on one Docker
+ bridge network, with independent identities, state directories and disks.
+- Production Linux nodes use light containers with host networking, host
+ persistent state, and explicit disk/device access. Containers package binaries
+ and dependencies while host resources remain explicitly managed.
+- A network partition must not allow an isolated UI or monitor to create a
+ replacement cluster or execute stale deployment commands.
+
+## Solution
+
+The target architecture is defined in the
+[multi-node deployment design](../design/deploy/design-crowdb-deploy.md).
+This requirement tracks its implementation and acceptance; unresolved decisions
+in that design must be settled before their dependent implementation begins.
+
+### Architecture and authority
+
+```text
+browser on any node
+ -> local crowdb-web / shared console operations
+ -> Group 0: confirmed topology and management authorization
+ -> target crowdb-monitor
+ -> containerd / managed server processes
+
+before Group 0 exists:
+local UI -> local monitor discovery -> peer monitors -> bootstrap preparation
+```
+
+Every node has a monitor management endpoint and a Web UI. No fixed UI node
+is required. Discovery advertises nodes, not each managed server. Group 0 is
+the first managed service allowed to start; more complex server deployment is
+enabled only after Group 0 is ready and the initial topology is published.
+
+### Invariants
+
+- **I1 — Stable node identity:** node identity is persisted outside the image
+ and container writable layer. IP, container name, restart and rack reassignment
+ do not change it. Cloned identity directories are detected as conflicts rather
+ than silently merging two nodes.
+- **I2 — Discovery is a candidate view:** discovery never grants cluster
+ membership, voting rights, rack authority or deployment permission. Records
+ can expire without deleting confirmed members from Group 0.
+- **I3 — Shared authority:** after bootstrap, confirmed node/rack topology and
+ cluster deployment intents are authoritative in Group 0. UI caches can lag,
+ but mutations succeed only through committed control-plane state.
+- **I4 — One bootstrap identity per node:** a node persists its accepted cluster
+ identity and initial Group-0 manifest before starting a replica. Conflicting
+ manifests are rejected; retries of the same operation are idempotent. No
+ timeout or empty discovery result permits automatic replacement bootstrap.
+- **I5 — Quorum-backed authorization:** connectivity to one Group-0 replica is
+ insufficient. New management mutations require current quorum-backed
+ authority. Stale commands cannot execute after authority moves or expires.
+- **I6 — Explicit placement:** virtual racks are editable logical groups.
+ Physical host/failure-domain information is recorded separately; multiple
+ containers on one host do not become independent physical failure domains
+ because their virtual rack labels differ.
+- **I7 — Portable image, explicit resources:** the same OCI image for a supported
+ OS/CPU architecture is used for single-host Docker tests and production
+ containerd deployment. Network, disks, persistent mounts and resource limits
+ are runtime inputs, not baked cluster identity.
+
+### Numbered work items
+
+1. Extend `crowdb-monitor` identity/profile/status surfaces and shared types in
+ `crowdb-protocol` to expose stable node identity, monitor endpoint, protocol
+ compatibility, optional rack hint, physical-host identity and cluster binding.
+ Obtain full hardware/rack information through a direct monitor handshake.
+ A node can exist before any KV or application server is deployed.
+
+2. Add mDNS + DNS-SD announcement and continuous browsing to `crowdb-monitor`.
+ Use a CROWDB node service type (proposed `_crowdb-node._tcp.local.`), SRV
+ address/port resolution and bounded TXT metadata. Select management interfaces
+ explicitly; do not announce on storage data-plane interfaces by default.
+ Support record updates, expiry, clean departure, duplicate identity detection
+ and incompatible-peer reporting. The browser UI reads its backend's candidate
+ view; it does not perform multicast itself.
+
+ mDNS is link-local UDP discovery, not consensus or a health guarantee.
+ DNS-SD structures service enumeration and endpoint metadata. These are borrowed
+ standard discovery mechanisms; CROWDB defines its own node and authorization
+ semantics. See [RFC 6762](https://www.rfc-editor.org/rfc/rfc6762.html) and
+ [RFC 6763](https://www.rfc-editor.org/rfc/rfc6763.html).
+ Provide explicit peer/seed addresses as a fallback when multicast is blocked
+ or nodes span subnets. Neither fallback nor discovered endpoints bypass the
+ same handshake and cluster-binding checks.
+
+3. Extend `crowdb-web` and `crowdb-console-shared` with a pre-bootstrap view:
+ candidate nodes, editable virtual racks, node-to-rack assignments and Group-0
+ initialization. Node-supplied rack information is a hint, not immutable
+ placement. A pre-bootstrap draft is local and visibly uncommitted; differing
+ drafts on different UIs do not constitute confirmed topology. Limit this
+ stage to node/rack/bootstrap operations, excluding other server deployment.
+
+4. Extend the existing shared `bootstrap_intent` and cluster operations to submit
+ one explicit initial Group-0 manifest: cluster identity, selected initial
+ replica members, reachable endpoints and confirmed node/rack draft. Peer
+ monitors persist compatible preparation before replica startup. Concurrent
+ incompatible requests must fail safely without constructing overlapping
+ conflicting groups. Partial preparation and initiator crashes must have an
+ explicit resume/abort procedure; an abort cannot erase a cluster that has
+ committed state. The coordination protocol is a human decision below, not an
+ assumption that multicast or smallest-node-ID election provides agreement.
+
+5. Wait for a usable Group-0 leader/quorum, then publish the initial topology
+ idempotently. Gate general deployment on both consensus readiness and
+ topology publication. Persist cluster binding and Group-0 contact information
+ at each accepted node. Other UIs attach to that cluster and read Group 0;
+ incompatible cluster bindings are shown separately and never auto-merged.
+ Newly discovered nodes remain candidates until a committed admission operation.
+ Later Group-0 voting membership changes use the existing reconfiguration path.
+
+6. Route rack changes and deployment mutations from every UI through shared
+ console operations and Group 0. Use consistent reads where confirmation needs
+ current authority, with watch/refresh for presentation. Record operation
+ identity and execution state so UI retries or UI-node loss cannot duplicate a
+ deployment. Monitor execution validates the current authorization and rejects
+ stale commands. Group-0 unavailability leaves discovery/diagnostics available
+ but disables new cluster management mutations.
+
+7. Add a containerd-backed execution path to `crowdb-monitor` alongside existing
+ managed-process support. Production uses containerd APIs; nerdctl is an operator
+ tool, not a required per-command subprocess. Define image digest, CPU/memory
+ boundaries, host networking, persistent config/state/log mounts and permitted
+ disk/device inputs. No implicit privileged container or unrestricted host
+ device access is required. Preserve process-level liveness and explicit
+ start/stop/upgrade operations; do not add a general scheduler or automatic
+ global reconciliation loop.
+
+8. Extend `container/single-node-container` packaging/test infrastructure for
+ multiple node containers from the same OCI image. Each gets an independent
+ persistent root and simulated disk, joins one dedicated user-defined Docker
+ bridge, and advertises its internally reachable monitor endpoint. Publish
+ distinct host UI ports for browser access. Separate test networks bound the
+ discovery scope. Production uses host networking and real disks on supported
+ Linux hosts; discovery and cluster-management semantics stay identical.
+ OCI compatibility does not claim complete Kubernetes lifecycle integration
+ or unverified CPU/OS support.
+
+## Dependencies
+
+- Existing Group-0 KV operations, consistent reads, service registry and
+ membership reconfiguration are reused; node discovery does not replace them.
+- Existing `crowdb-monitor`, `crowdb-console-shared`, `crowdb-web`,
+ `crowdb-protocol` and single-node image infrastructure are the implementation
+ base. Existing working-tree changes must be preserved.
+- R139 concerns distributed service configuration. This requirement must define
+ the minimal deployment-intent/authorization records it needs without silently
+ assuming R139 is implemented or replacing all local process configuration.
+- R193 concerns failure-budget placement; physical failure-domain records must
+ remain distinguishable from editable virtual racks for that integration.
+- The backlog index currently references absent R210/R211/R212 detail files.
+ Reconcile those workstreams before implementation to avoid duplicating node
+ lifecycle, health and cluster-scope contracts; do not assume they are available.
+- `/nv/cpp/adcp/topic/deploy.md` is discussion context for light-container
+ deployment, not a portable repository dependency. The applicable resource and
+ lifecycle choices are captured above.
+- Implementation must resolve the bootstrap and authorization decisions below.
+ Permanent console, Group-0, configuration and container documentation must be
+ updated to the shipped behavior during implementation cleanup.
+
+## Acceptance
+
+1. Setup: a persisted node root. Action: recreate the container, change its IP
+ and rack. Assertion: node identity remains unchanged; a second live endpoint
+ using the same identity is reported as a conflict (I1). Integration test.
+2. Setup: three monitors on a multicast-capable test network. Action: start,
+ update and stop nodes. Assertion: every backend discovers reachable peer
+ identities/endpoints, removes expired candidate records, and does not modify
+ confirmed membership on expiry (I2). Integration test.
+3. Setup: multicast disabled and explicit reachable seeds supplied. Action:
+ discover peers. Assertion: the same compatibility/binding checks apply;
+ discovery does not itself authorize admission (I2). Integration test.
+4. Setup: multiple network interfaces and an incompatible peer. Action: enable
+ discovery on the management interface only. Assertion: advertisements remain
+ scoped there, incompatible nodes are reported and cannot bootstrap (I2).
+ Integration test.
+5. Setup: uninitialized nodes with rack hints. Action: open any node UI, create
+ virtual racks and move nodes. Assertion: hints can be overridden, drafts are
+ visibly uncommitted and non-Group-0 deployment is blocked (I3, I6). E2E test.
+6. Setup: overlapping candidate nodes and two incompatible bootstrap manifests.
+ Action: submit concurrently and restart a prepared monitor. Assertion: durable
+ conflict rejection prevents conflicting replica startup; the same operation
+ can resume without duplicating a group (I4). Integration test.
+7. Setup: bootstrap interrupted before and after Group-0 commit. Action: follow
+ the specified recovery flow. Assertion: partial setup is resumable, committed
+ cluster identity is preserved and no timeout creates a replacement (I4).
+ Integration test.
+8. Setup: three selected Group-0 replicas. Action: bootstrap and delay initial
+ topology publication. Assertion: general deployment remains gated until both
+ quorum readiness and publication; all UIs then read the same confirmed
+ node/rack state (I3). E2E test.
+9. Setup: an active cluster and a new candidate. Action: discover then admit it
+ through another UI. Assertion: discovery alone changes no membership; admission
+ persists in Group 0 and any voting change uses reconfiguration (I2, I3).
+ Integration test.
+10. Setup: two independent initialized clusters on one LAN. Action: discover
+ each other. Assertion: their identities remain separate and no automatic
+ merge or rebinding occurs (I4). Integration test.
+11. Setup: concurrent UI mutations and retried deployment requests. Action:
+ update racks/deploy and lose the initiating UI. Assertion: committed outcomes
+ are shared and each operation executes at most once or resumes through its
+ recorded idempotent stages (I3, I5). Integration test.
+12. Setup: a three-replica Group 0 partitioned two-to-one. Action: issue management
+ commands on both sides and deliver a previously authorized stale command.
+ Assertion: only quorum-authorized mutations execute, stale execution is
+ rejected, and minority monitors retain diagnostics without replacement
+ bootstrap (I4, I5). Integration test.
+13. Setup: several simulated nodes on one physical host in different virtual
+ racks. Action: inspect placement inputs. Assertion: the shared physical
+ failure domain remains visible and rack changes do not rewrite it (I6).
+ Integration test.
+14. Setup: one supported-architecture OCI image digest. Action: launch multiple
+ Docker bridge nodes and Linux containerd host-network nodes with appropriate
+ disks/mounts. Assertion: both discover/bootstrap/manage successfully, state
+ survives replacement, internal advertised addresses are reachable and the
+ simulated-disk run makes no production-performance claim (I1, I7).
+ Integration test.
+15. Setup: containerd deployment with explicit devices/resources. Action: start,
+ stop, restart and upgrade through monitor. Assertion: requested resource
+ limits and mounts are applied, process health is observable and restart
+ preserves persistent state (I7). Integration test.
+
+Implementation verification commands (these are required future gates, not
+claims that the new acceptance coverage already exists):
+
+```bash
+pixi run cargo test -p crowdb-monitor
+pixi run cargo test -p crowdb-console-shared
+pixi run cargo test -p crowdb-web
+pixi run cargo test -p crowdb-protocol
+pixi run test-console
+pixi run test-console-ui
+pixi run rs-fmt-check
+pixi run rs-lint
+```
+
+## Open Questions
+
+- Bootstrap coordination: use one explicitly selected initiating node with
+ durable reservations and resumable manifest delivery, or a quorum/prepare
+ protocol over a fixed selected membership. Choose recovery and conflict rules
+ before implementation. Neither option guarantees one cluster across arbitrary
+ disjoint selections without an explicit shared identity/admission boundary.
+- Authorization during Group-0 loss: new management writes stop in all cases.
+ Decide whether existing data services must also stop after bounded authority
+ expiry, or may continue under their own Paxos/data-plane authority. The former
+ matches a strict "only Group-0-authorized nodes are active" policy but couples
+ whole-cluster availability to Group 0. Specify lease timing, renewal, fencing
+ enforcement points and recovery before claiming that stricter policy.
+- Discovery admission trust: explicitly approve discovered candidates in UI,
+ or provision deployment credentials for automatic admission. Discovery packets
+ and rack hints alone are not authenticated authorization.
+- Production monitor location: host system service controlling light containers,
+ or a dedicated management container with narrowly scoped runtime access.
+ Choose privilege and recovery boundaries; do not expose the containerd socket
+ to every application/UI container by default.
diff --git a/doc/backlog/R228-tree-range-metrics.md b/doc/backlog/R228-tree-range-metrics.md
new file mode 100644
index 000000000..dd4e121de
--- /dev/null
+++ b/doc/backlog/R228-tree-range-metrics.md
@@ -0,0 +1,151 @@
+
+
+
+### R228: crowdb-tree — Root and range statistics for split and placement
+
+Status: Deferred implementation by user request. Establish the metrics contract
+and review page-format, maintenance and performance costs before implementation.
+Data-weighted placement and its console display remain disabled meanwhile.
+
+## Problem
+
+[Tree storage design](../design/tree/design-crowdb-tree-storage.md) defines leaf
+records and inner separators/child PIDs. Leaves expose local base-record and delta
+counts; inner pages do not carry child-subtree page, byte or live-KV aggregates.
+The existing tree Stats API reports buffer-pool, flush and snapshot activity,
+not current reachable tree size or current live record count. Snapshot pages
+written, resident cache pages and cumulative writes are not these metrics.
+
+[Chunk-KV placement](../design/chunkds/design-crowdb-chunk-kv-server.md) currently
+balances assigned split counts. Automatic split supplements the partition-count
+target with coarse retained-pack size thresholds/ranking. Those pack estimates
+lag ordinary flushes, and split trees share historical chunks. They cannot
+reliably determine the current range's data size or justify size-driven split.
+Dividing an old estimate by a split ratio and accumulating later writes also
+accumulates error without a structural calibration point.
+
+A growing tree needs continuous, reasonably accurate size metrics to decide
+whether another split is needed. Parent and child need independent range
+statistics even while they share physical packs. Overwrites, deletes and
+compaction must not make a historical estimate permanently drift.
+
+## Solution
+
+- **TREE-METRICS-ROOT:** Metrics describe a specific current tree root/version
+ and its key range. Report observation/root identity and coverage frontier;
+ do not silently mix roots, epochs or ranges. Memtable and WAL sizes do not
+ contribute to tree size. Writes enter tree metrics when the normal flush
+ incorporates them; callers must see this coverage boundary.
+- **TREE-METRICS-LOGICAL:** Distinguish reachable inner, leaf, overflow and delta
+ page counts; reachable page bytes; live KV count; live key/value bytes; and
+ physical retained/shared pack storage. Logical range data and physical
+ storage are separate metrics. A key's current visible value counts once;
+ overwritten versions and tombstones do not count as live KV/data. Overflow
+ data must contribute even when stored outside a leaf. State any approximation
+ and its source explicitly rather than claiming exact live bytes.
+- **TREE-METRICS-STRUCTURAL:** Carry child-subtree aggregates in index metadata
+ so a full-tree/root aggregate or a range estimate can be obtained from index
+ pages without scanning leaf records or values. Leaf summaries originate from
+ existing rebuild/fold/flush work. Aggregates can be rebuilt from current
+ structure, not cumulative writes or recursively inherited split ratios.
+ A range boundary crossing a leaf may produce a documented bounded estimate;
+ report that uncertainty rather than extrapolating arbitrary key samples.
+- **TREE-METRICS-NONBLOCKING:** Maintain summaries through existing single-writer
+ flush/page rebuild work. Do not add request-path locks, wait queues, per-key
+ ancestor walks, forced flush/checkpoint, or synchronous remote scans to
+ heartbeat or balance. Background index traversal is bounded and cancellable;
+ heartbeat reads cached identity-fenced results and never waits for traversal.
+ Review additional write amplification, atomic bookkeeping and memory costs.
+- **TREE-METRICS-SHARING:** Split evaluates each resulting root/range's logical
+ data; shared historical chunks are not counted as both ranges' logical data.
+ Transfer preserves metrics for the same logical tree/range. Cleanup/GC of
+ historical physical packs does not reduce current live logical data.
+- **TREE-METRICS-RECOVERY:** Persist summaries consistently with their page/root
+ publication. Reopen reconstructs/verifies metrics against the selected root.
+ Abort or failed publication cannot install candidate statistics on the serving
+ root. Legacy pages without summaries report unavailable until bounded rebuild;
+ unknown is never zero and never authorizes a size-driven split or move.
+
+1. Define metric semantics and coverage in the tree storage design, and review
+ the persisted index/leaf summary format and legacy compatibility. Name the
+ range-boundary error model and separate live logical data from page capacity.
+2. Extend tree page builders/rebuild/flush and root publication to maintain and
+ aggregate summaries. Include overwrite, delete, delta folding, overflow,
+ range narrowing, split, transfer, compaction and reopen. Avoid new hot-path
+ synchronization; measure cost before accepting the maintenance strategy.
+3. Expose cached root/range statistics through the C API, Rust FFI and chunk-KV
+ runtime/heartbeat. Index traversal must have page/I/O/time budgets and expose
+ progress, freshness, unavailable state and identity fences.
+4. Replace retained-pack split size triggering/ranking with these tree metrics;
+ retain safe nonempty separator selection and count-target bootstrap. Use
+ logical-byte distribution to improve boundary selection when summaries allow
+ it, without promising perfectly equal ranges or scanning leaf values.
+5. Re-enable data-weighted placement only after metrics acceptance, with explicit
+ coefficients/tolerance and no count-only pass that reverses useful data moves.
+ Update the console spec before restoring Weight, including metric coverage,
+ uncertainty and the actual split/move decision explanation.
+
+## Dependencies
+
+- Existing tree/root publication and chunk-KV parent-plus-child recovery remain
+ authoritative. This work must not redefine handoff, WAL or catalog cutover.
+- Tree storage/engine and Chunk-KV server designs are upstream contracts.
+- Shared-pack reclamation is separate from logical statistics; it is not required
+ to obtain a range estimate and must not be used to force a physical copy.
+- Until implemented, placement uses split count only and UI hides Weight. The
+ count-target split path remains available; coarse pack sizing remains an
+ explicitly limited interim mechanism, not a reliable current-data metric.
+
+## Acceptance
+
+- Empty tree -> read metrics -> known zero live KV/data and documented structural
+ root-page cost, distinct from unavailable. TREE-METRICS-LOGICAL. Unit test.
+- Known records with overflow values -> normal flush and aggregate via indexes
+ -> expected live count/bytes and reachable page categories without value reads.
+ TREE-METRICS-STRUCTURAL. Integration test.
+- Same keys repeatedly overwritten/deleted -> normal fold/flush -> current live
+ metrics reflect latest state, not accumulated writes; WAL-only changes leave
+ tree metrics unchanged and coverage explicit. TREE-METRICS-ROOT. Integration test.
+- Uneven range tree -> split while sharing old packs -> parent/child statistics
+ describe their own ranges within declared boundary error; aggregate does not
+ duplicate shared physical pack bytes as logical data. TREE-METRICS-SHARING.
+ Integration test.
+- Publish root, crash/reopen and replay/flush -> metrics follow selected root and
+ coverage; fail/abort candidate publication -> serving statistics unchanged.
+ TREE-METRICS-RECOVERY. Integration test.
+- Legacy/cold tree -> bounded index traversal with cancellation -> no leaf/value
+ scan or unbounded heartbeat wait; unavailable/freshness and progress explicit.
+ TREE-METRICS-NONBLOCKING. Integration test.
+- Repeated split, overwrite, delete, compaction and reopen -> rebuild aggregates
+ from current structure -> no history-dependent drift; stale root/range results
+ rejected. TREE-METRICS-STRUCTURAL. Integration test.
+- Known growing tree -> normal flush crosses configured logical-byte threshold
+ -> eligible split planned; below threshold or unknown -> no size-triggered split;
+ count-target bootstrap remains independently tested. Integration test.
+- Metrics accepted, explicit data-weight policy enabled -> weighted move/split
+ decisions use matching cached statistics; console shows the backend decision,
+ coverage and estimate qualification. E2E test.
+- Fixed read/write/flush workload -> compare before/after -> report throughput,
+ latency, page writes, memory, index I/O and traversal budgets; no new request
+ locks, queues or forced checkpoint. Integration test.
+
+## Open Questions
+
+- Review summary encoding and update granularity: exact subtree live aggregates
+ require maintenance during folding/rebuild; bounded estimates may reduce write
+ amplification but must state error and recalibration guarantees.
+- Review crossing-leaf ranges: report a boundary uncertainty interval using leaf
+ summaries, or perform a separate bounded background boundary-leaf inspection.
+ Neither option allows routine full leaf/value scans.
+
+Verification commands:
+
+- `pixi run test-cpp`
+- `pixi run cargo test -p crowdb-tree-ffi`
+- `pixi run cargo test -p crowdb-chunk-kv-server --test balance_test`
+- `pixi run cargo test -p crowdb-kv-server --test chunk_kv_balance_planning_test`
+- `pixi run cargo test -p crowdb-web --test native_cluster_provisioning_test`
+- `pixi run tree-fmt`
+- `pixi run tree-lint`
+- `pixi run rs-fmt-check`
+- `pixi run cargo clippy -p crowdb-tree-ffi -p crowdb-chunk-kv-server -p crowdb-kv-server -p crowdb-web --all-targets -- -D warnings`
diff --git a/doc/backlog/R229-kv-membership-conflict.md b/doc/backlog/R229-kv-membership-conflict.md
new file mode 100644
index 000000000..8ed052b89
--- /dev/null
+++ b/doc/backlog/R229-kv-membership-conflict.md
@@ -0,0 +1,123 @@
+
+
+
+### R229: KV membership — Complete members record with epoch CAS
+
+## Problem
+
+[Reconfiguration design](../design/kv/design-crowdb-kv-reconfiguration.md)
+recommends adding members one at a time. Console currently reads membership,
+wires peers, then CAS-creates individual replica records. Different replica
+keys do not conflict, so two UI servers can concurrently change the same group.
+
+KV server remote mutation rebuilds a captured group, persists its configuration,
+then replaces the running group. Concurrent replacements can start from the
+same snapshot. Checking for conflict after writing disk is too late.
+
+The original concurrent-add topology case fails with two of three ready groups.
+Sequential additions pass the complete five-case spec. Controlled regressions
+must establish the server interleaving; no data-loss claim is established.
+
+## Solution
+
+The approved model is one authoritative Group-0 record per store/group,
+containing the complete members list and a monotonically increasing epoch.
+The epoch identifies the configuration change; no separate operation ID,
+admission record, or administrative version is introduced. Installation state
+is kept with this record so an unfinished change rejects the next submission.
+
+- **MEMBERSHIP-CAS:** Membership mutation APIs accept an expected epoch and the
+ complete requested members list. KV client submits through Group-0 CAS before
+ changing any peer. CAS checks the observed record revision, expected epoch
+ and Ready state together. The server assigns the successor epoch. A stale
+ epoch or Installing state returns explicit 409 Conflict. Different groups
+ remain independent; losing callers do not automatically retry on a new epoch.
+- **MEMBERSHIP-INSTALL:** Successful CAS publishes the successor members and
+ epoch in Installing state. KV client propagates that exact complete
+ configuration through the existing reconfiguration procedure. Keep enough
+ installation scope to reconcile removed as well as retained/new participants
+ after restart. Installation follows existing consensus fencing and catch-up
+ rules; publishing desired members alone is not proof of completed cutover.
+- **MEMBERSHIP-SERVER:** Each KV server checks and atomically guards configuration
+ submission before persistence, covering persistence through memory publication.
+ Lower epoch is rejected. Same epoch and same complete configuration is an
+ idempotent replay; same epoch with different configuration is rejected.
+ A higher epoch must be the authorized successor, not an arbitrary caller
+ value. Guard add/remove, batch wiring, replacement and reconciliation.
+- **MEMBERSHIP-ORDER:** Validate and acquire the local submission guard, persist
+ the authorized successor, then publish that same successor. A losing/stale
+ request must not modify either durable or running configuration.
+ Checking only at in-memory group replacement is insufficient.
+- **MEMBERSHIP-COMPLETE:** After all required installation/fencing confirmations,
+ CAS the exact Installing record to Ready without changing its epoch/members.
+ A stale completion cannot clear a newer record. Only Ready permits the next
+ membership mutation. Same-group UI additions await completion and refresh
+ membership before another submission.
+- **MEMBERSHIP-RECOVERY:** Timeout or lost reply is an unknown outcome, not
+ permission to clear Installing. Linearizably reread the authoritative record
+ and confirm the exact epoch/configuration. Partial installation resumes that
+ epoch; no rollback to older members and no time-based release. Restart after
+ local persistence restores the durable successor and reconciles with Group 0.
+ An interrupted completion is confirmed or retried by exact-record CAS.
+- **MEMBERSHIP-FORMAT:** Use the new complete-record and local epoch format
+ directly. No legacy migration, compatibility reads, or old unconditional
+ mutation path. Endpoint and voting changes participate in the same complete
+ configuration/epoch checks; consensus integration must be reviewed explicitly.
+- **MEMBERSHIP-COST:** Extra CAS, reads and coordination belong to management
+ operations, which may be slower. Add no data-path locks or wait queues.
+ Any proposed new lock requires separate contention/ordering review.
+- **MEMBERSHIP-UI:** Keep submission sequential for each group, show 409 as a
+ conflict, refresh current configuration, and never silently resubmit changes.
+
+1. Define the complete record and epoch/Installing/Ready API contract in
+ protocol types, and review integration with existing reconfiguration.
+2. Implement shared CAS submission and exact-epoch recovery in the corresponding
+ KV-client operation; route Console membership changes through it.
+3. Protect KV server durable configuration submission and publication.
+4. Verify races, lost replies, partial installation and restart boundaries.
+5. Update permanent reconfiguration/UI documents after implementation; maintain
+ separate sequential topology and concurrent rejection coverage.
+
+## Dependencies
+
+- Existing Group-0 CAS and linearizable reads.
+- Existing local configuration durability, catch-up and consensus fencing.
+- Independent of Chunk-KV cutover and deferred tree metrics.
+- Sequential operator changes are the interim behavior, not proof of
+ cross-UI protection. Implementation is not yet complete.
+
+## Acceptance
+
+- Two UI servers read one Ready epoch -> submit different configurations ->
+ one CAS succeeds and the other returns 409 before peer writes
+ (**MEMBERSHIP-CAS**). Integration test
+- A successor is Installing -> submit another change even with its current
+ epoch -> explicit 409 until exact completion (**MEMBERSHIP-COMPLETE**).
+ Integration test
+- Force overlapping local submissions -> losing request changes neither disk
+ nor running configuration; independent groups progress
+ (**MEMBERSHIP-SERVER**, **MEMBERSHIP-ORDER**, **MEMBERSHIP-COST**). Integration test
+- Replay same epoch/configuration, then same epoch/different configuration and
+ lower epoch -> respectively idempotent success and explicit conflicts;
+ authorized higher epoch installs through reconfiguration
+ (**MEMBERSHIP-SERVER**, **MEMBERSHIP-FORMAT**). Integration test
+- Lose CAS/installation/completion replies -> reread and resume exact epoch ->
+ no duplicate configuration change or premature Ready
+ (**MEMBERSHIP-RECOVERY**). Integration test
+- Crash during partial fan-out or after local persistence before publication ->
+ recover the durable successor and finish the current epoch; reject delayed
+ older configuration and stale completion (**MEMBERSHIP-RECOVERY**). Integration test
+- Remove a participant -> restart during propagation -> installation scope
+ preserves the required removal/fencing proof before Ready
+ (**MEMBERSHIP-INSTALL**, **MEMBERSHIP-COMPLETE**). Integration test
+- Two UI sessions change one group -> loser displays conflict; winner waits for
+ completion and refreshes before its next change (**MEMBERSHIP-UI**). E2E test
+
+Verification:
+
+- `pixi run cargo test -p crowdb-kv-server --tests`
+- `pixi run cargo test -p crowdb-kv-client --tests`
+- `pixi run cargo test -p crowdb-console-shared --tests -- --test-threads=1`
+- `pixi run test-console-ui`
+- `pixi run rs-fmt-check`
+- `pixi run cargo clippy -p crowdb-kv-server -p crowdb-kv-client -p crowdb-console-shared --all-targets -- -D warnings`
diff --git a/doc/backlog/backlog.md b/doc/backlog/backlog.md
index 8082d8c9f..cd78740f5 100644
--- a/doc/backlog/backlog.md
+++ b/doc/backlog/backlog.md
@@ -1,254 +1,125 @@
-# New Requirements — Backlog & Analysis
-
-Forward-looking implementation items. Each item is classified by priority,
-complexity, and dependency. Before implementation, follow the
-[Implementation Process](#implementation-process) below.
-
----
-
-## Item Index
-
-**Next R number: R222** — Bump this line in the same commit when adding a new item.
-
-### Next Milestone — Chunk-backed range KV
-
-R144 is a deferred merge follow-up to the completed chunk-KV server and routed
-client split/transfer baseline. R147 is the remaining deferred chunk lifecycle
-follow-up. R148 follows the now-measured mirror-only production baseline and
-keeps stream metadata scale-out and sealed-chunk EC disabled until that
-requirement is implemented.
-- **[R144](R144-chunk-kv-partition-merge.md)** — adjacent partition merge —
- Area: crowdb-tree / KV / server / group 0 — Deferred follow-up that composes
- two adjacent chunk-backed trees, fences both owners, reconciles their WAL
- sequences, and atomically replaces both parent ranges with one destination.
-- **[R147](R147-tree-chunk-gc.md)** — reclaim B+tree chunk strips — Area:
- crowdb-tree / chunkdb / diskdb — Turn tree logical-GC results into durable,
- manifest-fenced reclaim candidates. Repack mixed live strips, then use an
- idempotent generic in-chunk operation to release whole unreachable strips or
- chunks without racing retained manifests, snapshot pins, or layout readers.
-- **[R148](R148-chunk-stream-scale-out.md)** — partition metadata scale-out and
- sealed-chunk EC — Area: chunk-stream / chunk-kv / KV / chunkdb — Move the
- stream namespace and tree root catalog as one fenced binding generation,
- optionally shard their indexes, and convert sealed mirror chunks to EC.
-
-### Planned — S3 data access service
-
-R152–R166 delivered the limited basic S3 service, including the restart
-acceptance baseline. Multipart upload is available; R168–R169 defer
-shared-storage GC without blocking basic large-object deletion. R170 adds
-optional cuObject/RDMA acceleration after the TCP baseline is correct and measured.
-- **[R196](R196-access-upload-benchmark-regression.md)** — S3 and Iceberg HTTP
- upload benchmark regression — Area: CLI / access server / benchmark — Add a
- shared real-protocol CLI workload and retained local regression scripts for
- direct and multipart small/large uploads, with correctness gates, metrics,
- and profile-matched performance baselines.
-- **[R193](R193-chunkdb-node-failure-budget.md)** — configurable node failure
- budget and EC placement — Area: KV / chunkdb / chunk IO / deployment —
- Generalize the fixed one-node and three-node profiles to larger clusters.
- Validate mirror copies and EC against the worst configured set of failed
- nodes. A six-node cluster with a two-node budget admits `4+2` and `8+4` EC,
- but rejects `2+1`. Persist degraded placement and restore full protection
- after capacity returns.
+# CROWDB Backlog Index
+
+This index contains only requirements that are not complete. Completed
+requirements and their temporary execution plans are removed during the final
+`implement-requirement` cleanup. Requirement detail and acceptance criteria
+live in the linked document; implementation follows
+[`implement-requirement`](../../.agents/skills/implement-requirement/SKILL.md).
+
+**Next R number: R230** — R221 is reserved by another workstream.
+
+## Dataset and access
+
+- **[R220](R220-dataset-fat-client-plan.md)** — Dataset SDK fat-client direct
+ access and optimization contract. **Area:** Dataset SDK. **Complexity:** High.
+ **Status:** Deferred until the core Dataset contract has workload measurements.
+
+## Console and service control
+
+- **[R229](R229-kv-membership-conflict.md)** — complete members record with epoch CAS and
+ server submission protection; explicit conflicts across UI servers.
+ **Area:** KV client / KV server / Console. **Complexity:** Medium.
+ **Status:** Members/epoch CAS model approved; implementation pending.
+
+- **[R227](R227-console-multi-node-deployment.md)** — multi-node discovery,
+ Group-0 bootstrap, shared UI authority and light-container deployment.
+ **Area:** monitor / console / deployment. **Complexity:** High.
+ **Dependencies:** existing Group 0; bootstrap and authorization decisions.
+- **[R210](R210-console-service-configuration-health.md)** — unified node service
+ configuration and health. **Area:** console / deployment / FlatBuffer RPC.
+ **Complexity:** High. **Dependencies:** none recorded.
+- **[R211](R211-console-access-health-listener.md)** — independent Access Server
+ health listener. **Area:** console / access server / deployment.
+ **Complexity:** Medium. **Dependencies:** R210.
+- **[R212](R212-console-node-create-and-cluster-scope.md)** — reliable node
+ creation and Cluster scope. **Area:** console / node lifecycle / UI.
+ **Complexity:** High. **Dependencies:** R210, R211.
+
+## Access server and external data
+
- **[R168](R168-s3-shared-object-reclamation.md)** — shared small-object
- reclamation — Area: access server / S3 / chunkdb — **Deferred on R95.** Turn
- exact pending shared ranges into qualified, restart-safe physical deletion.
-- **[R169](R169-s3-shared-chunk-tree-gc.md)** — B+tree and shared-chunk garbage
- collection — Area: access server / S3 / crowdb-tree / chunkdb — **Deferred on
- measurement, R147, and R168.** Compact fragmented shared chunks and retire
- obsolete S3 metadata without erasing reader or recovery authority.
-- **[R170](R170-s3-cuobject-rdma.md)** — optional cuObject RDMA data plane —
- Area: access server / S3 / DiskIO / RDMA — **Deferred until basic TCP S3 is
- stable.** Keep acceleration in a separate optional library and requirement;
- AccessServer coordinates while DiskIO-owned cuObjServer endpoints transfer
- parallel logical spans directly to or from client registered memory.
-
-### Planned — Native Iceberg storage
-
-The native catalog correctness milestone is complete: catalog/service foundation,
-namespace, immutable FileIO, atomic table commits, reclamation and REST/official-SDK
-conformance. Its contract and executable profile are retained in
-[Native Iceberg Storage](../design/access-server/iceberge/design-crowdb-iceberg.md).
-The [container client matrix](../../container/single-node-container/tests/ecosystem/README.md)
-has separate passing engine/restart acceptance. Caches and selected ORC remain
-independent deferred work.
-
-- **[R185](R185-access-iceberg-cache-invalidation.md)** — bounded cache and
- invalidation — Area: access server / Iceberg / Group 0 / Chunk-KV — **Deferred
- pending focused cache measurements on the completed uncached baseline.** Add one budgeted
- cache manager, qualified entries, internal-RPC invalidation, and TTL safety nets.
-
-- **[R186](R186-access-iceberg-orc-validation.md)** — selected ORC validation —
- Area: access server / Iceberg — **Independent follow-up retained by user
- decision; outside the verified client ecosystem.** Add bounded canonical ORC
- schema, row-count and delete validation with official-client fixtures. The Parquet catalog is
- complete; ORC does not block container or client-ecosystem acceptance.
-
-### High Priority
-
-- **[R207](R207-chunkdb-repo-metadata-chunk-kv.md)** — repo chunk metadata and
- tasks on chunk-kv — Area: chunkdb / chunk-kv — High complexity.
- **Deferred beyond the completed direct-KV stage.** When selected, migrate
- S3/IcebergTable/business Stream metadata and associated tasks to chunk-kv;
- system Wal/BtreePage/PageIndex metadata stays in KV groups. Define range-local
- publication, key/split rules, isolated tasks, safe conversion and measured
- Paxos relief. Future Dataset follows the user-data layer with its own type.
+ reclamation. **Area:** access server / S3 / chunkdb. **Complexity:** High.
+ **Status:** Deferred until R95 and the basic S3 delete path are stable.
+- **[R169](R169-s3-shared-chunk-tree-gc.md)** — shared-chunk and B+tree garbage
+ collection. **Area:** access server / S3 / crowdb-tree / chunkdb.
+ **Complexity:** High. **Status:** Deferred until R147, R168, and workload
+ measurements are complete.
+- **[R170](R170-s3-cuobject-rdma.md)** — optional cuObject RDMA data plane.
+ **Area:** access server / S3 / DiskIO / RDMA. **Complexity:** High.
+ **Status:** Deferred until the basic TCP S3 path is stable and measured.
+- **[R185](R185-access-iceberg-cache-invalidation.md)** — bounded Iceberg cache
+ and invalidation. **Area:** access server / Iceberg / Group 0 / Chunk-KV.
+ **Complexity:** Medium. **Status:** Deferred pending focused cache measurements.
+- **[R186](R186-access-iceberg-orc-validation.md)** — selected ORC validation.
+ **Area:** access server / Iceberg. **Complexity:** Medium. **Status:**
+ Deferred as an independent follow-up by user decision.
+- **[R196](R196-access-upload-benchmark-regression.md)** — S3 and Iceberg HTTP
+ upload benchmark regression. **Area:** access server / CLI / benchmarks.
+ **Complexity:** Medium. **Dependencies:** completed upload correctness paths.
+
+## Chunk, Chunk-KV, and recovery
+
+- **[R83](R83-chunkdb-complete-recovery-flow.md)** — complete data recovery and
+ recovery-speed control. **Area:** chunkdb / diskdb / DiskIO.
+ **Complexity:** High. **Dependencies:** chunkdb server and DiskIO recovery path.
+- **[R84](R84-chunkdb-post-disk-move-placement-scanner.md)** — post-move chunk
+ placement scanner. **Area:** chunkdb / diskdb. **Complexity:** Medium.
+ **Dependencies:** R81 Part 2 and the chunkdb server.
+- **[R92](R92-chunkdb-in-chunk-gc.md)** — in-chunk garbage-collection operations.
+ **Area:** chunkdb / chunk IO. **Complexity:** High. **Dependencies:** chunk
+ lifecycle and qualified range ownership.
+- **[R95](R95-chunkdb-chunk-range-delete.md)** — qualified chunk-range deletion
+ and orphan scanning. **Area:** chunkdb / chunk IO / S3.
+ **Complexity:** High. **Dependencies:** chunk-range ownership records.
+- **[R96](R96-chunkdb-console-cli-integration.md)** — chunkdb CLI management
+ integration. **Area:** chunkdb / console / CLI. **Complexity:** Medium.
+ **Dependencies:** stable chunkdb management surface.
+- **[R147](R147-tree-chunk-gc.md)** — reclaim B+tree chunk strips. **Area:**
+ crowdb-tree / chunkdb / diskdb. **Complexity:** High. **Status:** Deferred
+ until immutable page packs and durable reclaim candidates are available.
+- **[R148](R148-chunk-stream-scale-out.md)** — stream metadata scale-out and
+ sealed-chunk EC. **Area:** chunk-stream / chunk-kv / KV / chunkdb.
+ **Complexity:** High. **Status:** Deferred until the measured single-group
+ mirror path is complete.
+- **[R193](R193-chunkdb-node-failure-budget.md)** — configurable node-failure
+ budget and EC placement. **Area:** KV / chunkdb / chunk IO / deployment.
+ **Complexity:** High. **Status:** Planned.
+- **[R207](R207-chunkdb-repo-metadata-chunk-kv.md)** — repo metadata and tasks on
+ chunk-kv. **Area:** chunkdb / chunk-kv. **Complexity:** High.
+ **Status:** Deferred beyond the direct-KV stage.
+
+## KV, DiskDB, and topology
+
+- **[R80](R80-diskdb-rebalance.md)** — disk space rebalance convergence.
+ **Area:** diskdb / DiskIO. **Complexity:** Medium.
+- **[R82](R82-kv-watch-notify-coalescing.md)** — watch/notify coalescing.
+ **Area:** KV / diskdb. **Complexity:** Medium.
+- **[R102](R102-diskdb-dynamic-binding-migration.md)** — dynamic disk-group
+ binding migration. **Area:** diskdb / KV. **Complexity:** High.
- **[R103](R103-chunkdb-range-migration.md)** — dynamic slot ownership and
- KV-group expansion/shrink — Area: chunkdb / kv — **Deferred until dynamic
- changes are requested.** The fixed-topology stage is implemented. Independently
- support fenced server-slot handoff without metadata copy, and storage-slot
- migration of complete chunk/task/index/reservation state when groups are
- added, drained or rebalanced. Include durable recovery and safe cleanup;
- preserve independent bitmap maps, fixed slot identity and DiskIO payload.
- R207 owns future backend conversion; legacy conversion is not a prerequisite.
-- **[R102](R102-diskdb-dynamic-binding-migration.md)** — diskdb dynamic
- disk-group binding migration — Area: diskdb / kv — Reuse the common
- `BindingStrategy` framework
- (`doc/design/chunkdb/design-crowdb-chunkdb-range-binding.md` §5) to
- dynamically rebind diskdb disk-groups to paxos groups, replacing the
- operator-manual `BindMapValue` write with automatic monitoring +
- rebinding. Monitor detects instance join/leave, rebalances disk-group
- assignments, migrates data during rebinding.
-- **[R80](R80-diskdb-rebalance.md)** — diskdb space rebalance across
- disks — Area: diskdb — Complete the existing passive allocator and durable
- relocation baseline with sustained-skew admission, projected-improvement and
- target-headroom gates, bounded repeated moves, truthful stalled/balanced
- status, and a real DiskIO/ChunkDB convergence test for newly added or
- recovered empty disks.
-- **[R82](R82-kv-watch-notify-coalescing.md)** — watch/notify
- coalescing (debounce) — Area: kv / diskdb — the watch/notify
- extension ships without coalescing: one notify per changed key per
- matching prefix. Burst writes to a watched prefix (e.g. diskdb
- `batch_write` touching 10 disks) generate 10 separate notifies,
- amplifying subscriber wakeups + re-read load. Add a per-prefix
- debounce coalescer with timer-task flush between the apply-path hook
- and `WatchRegistry::emit`. The original coalescer was removed because
- the timer task captured no registry/coalescer refs (buffered keys
- were silently dropped); R82 must wire the `Weak` refs properly. Load
- optimization, not correctness — the safety-net poller covers missed
- notifies.
-
-### Data Path (diskio + chunk object writers + read flow)
-
-Chunk reads, read repair, mirror-to-EC conversion, write error handling, and
-end-to-end Chunk IO performance workloads are landed. The RPC migration items
-(R115, R116, R117) are in a separate area (see RPC Migration section below).
-
-### Medium Priority
-
-- **[R83](R83-chunkdb-complete-recovery-flow.md)** — chunkdb
- complete recovery flow (real data recovery + speed control) —
- Area: chunkdb / diskdb / diskio — diskdb's recovery is disk-layer
- only: the R76 `RecoveryScanTask` lists impacted busy blocks +
- `owner_chunk` but the repair step is a placeholder
- (`RecoveryAction::LogOnly`, no data rebuild). There is no chunkdb
- yet (only a reserved proto surface), so when a disk goes `Bad` the
- impacted blocks are handed to a "future recovery/relocation path"
- (§8) that does not exist — no surviving replica/parity is read, no
- rebuilt data is written, no strip is updated. Full data recovery
- needs chunkdb (the chunk→strip→segment owner) to rebuild lost
- mirror replicas / EC data+parity from surviving strips via the
- `diskio` service, `UpdateChunkStrip` to new segments, and free the
- old `Bad`-disk segments. Recovery speed must be throttled at the
- chunkdb layer (configurable bandwidth/IOps/concurrency) so
- foreground traffic is not starved. Blocked on the chunkdb server
- component + the `diskio` service (both unlanded; must be filed as
- their own backlog items first). Replaces R76's `LogOnly` with
- `Relocate` / `RebuildFromEc`.
-- **[R84](R84-chunkdb-post-disk-move-placement-scanner.md)** —
- chunkdb post-disk-move placement scanner — Area: chunkdb / diskdb —
- R81 Part 2 adds disk move with a stable `DiskId` (record copy
- during Maintenance, no full scan). The move is placement-only and
- the data is intact, but there is no verification that chunk
- placement is still consistent after a move: chunks reference blocks
- via `Segment { disk_id, ... }` (in `MirrorStrip` / `EcStrip`), and
- every chunk with a segment on the moved disk must still reach that
- segment via the disk's new placement. Add a placement-integrity
- scanner (chunkdb-side, following diskdb's `ScannerTask` /
- `BgRunner` pattern, §10) that walks chunk→strip→segment after a
- move (and periodically), resolves each segment's `DiskId` to its
- current group-0 placement, and reports unreachable / orphaned
- segments — handing `Bad`/`Missing`-disk segments to R83 for
- rebuild. Triggered on move via watch/notify (R78) with a periodic
- safety net. Blocked on the chunkdb server component (unlanded) and
- R81 Part 2.
-### RPC Migration (legacy → crowdb-rpc)
-
-Historical migration order: R115 → R116 (unary); R117 (streaming) followed
-R114 plus the original R32 consensus migration. R115 first validated the
-migration pattern (schema, server, client, and error mapping) before the
-streaming services. All four migrations follow the
-zero-copy wrapper convention (`design-crowdb-rpc.md` §6): `FB`-prefixed
-flatbuffer types, wrapper classes in `crowdb-protocol`, no owned
-intermediate structs, no per-field copy. The four transport migrations (R115
-diskdb, the original R32 KV consensus scope, R117 KV client-facing, and R116
-chunkdb) are DONE. The post-migration KV server/library review is also complete.
-
-- **[R33](R33-crowdb-tree-rename.md)** — Extract crowdb-tree to separate repo and rename — Area:
- workspace — Move `crowdbtree/` into its own git repository (preserving
- history), wire `crowdb-kv` to depend on `crowdb-tree-ffi` as an external
- dependency, and rename the crate/namespace/macros from `crowdbtree` to
- `crowdb-tree` / `crow::tree` / `CROWDB_TREE_*`. Establishes the `crowdb-kv` →
- `crowdb-tree` dependency boundary analogous to `crowdb-kv` → `crowdb-common`.
- Most naturally done after R12.
-- **[R50](R50-epoch-protected-memtable.md)** — Epoch-protected
- lock-free MemTable — Area: scan / get / crowdb-tree engine —
- **Done.** `MemTable::snapshot()` deep-copied every live L0 entry
- (key + full cell payload) on every scan regardless of range or
- `limit`, and an L0 `get` hit copied twice. Root cause: L0 was the
- only reader-visible structure outside the engine's EBR scheme.
- Replaced the `absl::btree_map` under `mu_` with a
- `ConcurrentSkipList` (inline keys, versioned cell pointers,
- epoch-deferred reclamation). Readers now traverse L0 lock-free
- under their existing epoch guard with zero copy; the cursor seeks
- directly (no `upper_bound` skip pass); `get_view` borrows the
- cell directly off the node. Closes the known gap at
- `crowdb-tree.h:81`. All 383 `test-tree-ct` tests pass.
-
-### Low Priority
-
-**Complexity — Low (placeholder):**
-- **[R5](R5-rdma-alloc.md)** — RDMA-pinned allocation — Blocked by: RDMA backend — Area: crowdbtree
- engine — `buffer::allocate` seam is designed for RDMA-pinned memory but no
- RDMA backend exists yet; placeholder only.
+ KV-group expansion/shrink. **Area:** chunkdb / KV. **Complexity:** High.
+ **Status:** Deferred by user decision until dynamic ownership is requested.
- **[R139](R139-group0-service-config.md)** — Group-0 distributed service
- configuration — Area: config / control plane — Publish versioned,
- scoped config through group 0; each service validates revisions, applies
- dynamic fields atomically, and reports fields that require restart.
-
-**Complexity — Medium:**
-- **[R4](R4-bounded-mempool.md)** — Bounded memory pool — Area: crowdbtree engine — `buffer::allocate` uses
- unbounded `std::malloc`; a burst of large writes can spike RSS without
- backpressure.
-- **[R60](R60-tree-scan-sibling-leaf-readahead.md)** — Sibling-leaf
- readahead on cold scans — **Deferred pending cold file/block-backed
- measurement.** Area: scan / crowdb-tree engine — the scan
- path demand-loads each L1 leaf inline (sync) or one pending page per
- reactor round trip (async), so a cold multi-leaf range pays one
- stall/round-trip per leaf, serialized with merge work on prior
- leaves. The scan knows `right_sibling` before finishing the current
- leaf — issue a readahead for the next
- leaf to overlap I/O with merging. Sync path: prefetch the
- right-sibling page id via a page-cache async-resolve seam. Async
- path: use a fixed one-leaf lookahead to overlap the next leaf read with
- merge and packing of the current resident leaf. Readahead is scan-only and
- conditional on the range, remaining limits, deadline, residency, and async
- disk backend. Win is zero on mem-mode (leaves resident); implement only if
- a cold benchmark with eviction shows a material latency or throughput
- improvement. Medium complexity.
----
-
-## Implementation Process
-
-Each item follows the lifecycle defined in the
-[`/implement-requirement` workflow](../../.agents/skills/implement-requirement/SKILL.md):
-understand → design → plan → implement → merge design → cleanup.
-
-After the PR is merged, all obsolete working docs (design draft, plan doc)
-must be deleted — see the workflow's Post-merge cleanup section.
-
----
-
-
+ configuration. **Area:** configuration / control plane. **Complexity:** Medium.
+ **Status:** Deferred until the file-backed service configuration contract.
+- **[R144](R144-chunk-kv-partition-merge.md)** — adjacent partition merge.
+ **Area:** chunk-kv / KV / group 0. **Complexity:** High. **Status:** Deferred
+ until manifest reuse, transfer fencing, and transition recovery are stable.
+
+## Tree and memory
+
+- **[R4](R4-bounded-mempool.md)** — bounded memory pool for tree allocations.
+ **Area:** crowdb-tree engine. **Complexity:** Medium.
+- **[R5](R5-rdma-alloc.md)** — RDMA-registered host-buffer allocation.
+ **Area:** crowdb-tree / RDMA. **Complexity:** High. **Status:** Design
+ proposal; blocked on an RDMA backend.
+- **[R60](R60-tree-scan-sibling-leaf-readahead.md)** — sibling-leaf readahead on
+ cold scans. **Area:** crowdb-tree / scan / DiskIO. **Complexity:** Medium.
+ **Status:** Deferred pending cold file/block-backed measurements.
+- **[R228](R228-tree-range-metrics.md)** — root/range page, logical-byte and live-KV
+ metrics for continuous split sizing and data-weight placement. **Area:** tree /
+ chunk-KV / console. **Complexity:** High. **Status:** Deferred implementation
+ pending statistics-format/performance review; count placement remains enabled.
diff --git a/doc/design/access-server/dataset/design-crowdb-access-dataset.md b/doc/design/access-server/dataset/design-crowdb-access-dataset.md
index 8f14a06c8..3b1affda0 100644
--- a/doc/design/access-server/dataset/design-crowdb-access-dataset.md
+++ b/doc/design/access-server/dataset/design-crowdb-access-dataset.md
@@ -1,123 +1,269 @@
-# CROWDB - Design: Access Dataset Service
+# CROWDB - Design: Dataset
-Dataset is CROWDB's AI-oriented access model for immutable generations,
-samples, shards, tensors, batches, and streams. It offers both portable HTTP
-access and a topology-aware native client with the same dataset semantics.
+Dataset is CROWDB's sample-oriented data domain for AI, analytics, and data
+preparation. It owns dataset metadata, immutable snapshots, sample selection,
+field projection, bounded reads, and payload lifetime while using CROWDB KV and
+chunks for durable state and bytes.
-Depends on: [Access Server](../design-crowdb-access-server.md),
-[Chunk I/O](../../chunkio/design-crowdb-chunkio.md),
-[Chunk-KV](../../chunkds/design-crowdb-chunk-kv.md), and
-[Chunk-KV client](../../chunkds/design-crowdb-chunk-kv-client.md).
+The normative object and behavior contract is [CROWDB Dataset Specification](spec-crowdb-dataset.md). This design document describes the implementation architecture that realizes that contract; it does not redefine Dataset semantics.
-Satisfies: dataset-oriented access whose routing, batching, and transfer model
-is not constrained by S3 or Iceberg compatibility.
+This design is independent of S3 and Iceberg. A Dataset may record an external
+source reference, but that reference does not replace Dataset publication,
+selection, or reclamation authority.
## Table of contents
-1. [Intent and boundary](#1-intent-and-boundary)
-2. [Dataset model](#2-dataset-model)
-3. [Dual access surfaces](#3-dual-access-surfaces)
-4. [Native and GPU-direct path](#4-native-and-gpu-direct-path)
-5. [Publication and lifecycle](#5-publication-and-lifecycle)
-6. [Relationship to other access models](#6-relationship-to-other-access-models)
-7. [Correctness invariants](#7-correctness-invariants)
-
-## 1. Intent and boundary
-
-Dataset gives AI, analytics, and data-preparation applications concepts closer
-to their work than buckets or tables alone. It owns generation publication,
-sample and shard selection, batching, ordering, shuffle, streaming, prefetch,
-and tensor-oriented delivery.
-
-Dataset does not own physical placement, replication, erasure coding, repair,
-or disks. It is not an S3 extension, an Iceberg table convention, or a query
-engine.
-
-## 2. Dataset model
-
-A dataset has stable identity and immutable generations. A generation selects a
-manifest that maps logical concepts such as samples, shards, tensors, column
-groups, or batches to validated byte ranges and encodings. CROWDB chunks own the
-bytes; Dataset owns their logical organization and selection.
-
-One read selects one generation. New data and metadata are built out of view and
-become visible through atomic generation publication.
-
-## 3. Dual access surfaces
-
-Dataset HTTP is an independent Access Server protocol. It provides portable
-discovery, ingest, selection, and bounded streaming to clients that should not
-embed CROWDB routing.
-
-The native Dataset client is a linked client library, not an HTTP wrapper. It
-loads the same dataset authority, consumes a bounded versioned CROWDB routing
-view, plans work locally, and connects directly to responsible metadata and
-data services. This removes one routing and payload relay layer from the
-performance path.
-
-The two surfaces may use different request shapes and transport mechanics. They
-must select the same generation, enforce the same authorization, and return the
-same logical data and integrity result.
-
-## 4. Native and GPU-direct path
-
-The native client owns routing, retry, prefetch, streaming, and application
-buffer lifetime. A stale topology produces a bounded redirect, refresh, or
-retry. Observing topology lets the client route work but never makes it a
-placement authority.
-
-CPU delivery uses owner-backed CROWDB buffers. The accelerated direction is
-direct delivery from DiskIO-owned buffers across RDMA into registered client GPU
-memory. GPUDirect Storage, GPUDirect RDMA, cuObject-compatible transfer, or
-native CROWDB RDMA may provide the mechanism. The goal is to avoid both an
-Access Server payload bounce and Dataset-client CPU staging.
-
-Direct operations use short-lived capabilities bound to the principal, dataset
-generation, operation, source and destination ranges, limits, expiry, and
-topology epoch. Completion covers every delegated span and integrity check.
-CPU fallback is chosen before direct submission and never continues a partial
-direct transfer.
-
-## 5. Publication and lifecycle
-
-Ingest writes bounded candidates and publishes a complete immutable generation
-only after its manifest, indexes, payloads, integrity state, and durability
-conditions complete. Failed or losing candidates remain invisible.
-
-Readers retain their selected generation for the whole operation. Retention and
-reclamation remove physical data only after no live dataset generation, lease,
-or explicit cross-model reference can reach it.
-
-## 6. Relationship to other access models
-
-Dataset may explicitly reference immutable data associated with S3 or Iceberg.
-Such a reference is generation-bound and preserves the source model's ownership
-and lifetime rules. Dataset cannot overwrite an S3 object, commit an Iceberg
-table, or reclaim source-owned bytes.
-
-Likewise, S3 compatibility and Iceberg table rules cannot restrict native
-Dataset batching, shuffle, scatter/gather, or GPU delivery.
-
-## 7. Correctness invariants
-
-- **DATA-I1 — Generation consistency:** one operation uses one immutable
- dataset generation.
-- **DATA-I2 — Exact selection:** returned samples, shards, tensor ranges, and
- destination offsets correspond exactly to the selected manifest.
-- **DATA-I3 — Atomic publication:** readers observe absence or one complete
- dataset generation, never a partially built manifest.
-- **DATA-I4 — Native directness:** native access reaches responsible CROWDB
- services without an Access Server request or payload hop.
-- **DATA-I5 — Surface equivalence:** HTTP and native access return the same
+1. [Scope and boundary](#1-scope-and-boundary)
+2. [Authority and naming](#2-authority-and-naming)
+3. [Dataset and snapshot model](#3-dataset-and-snapshot-model)
+4. [Manifest, schema, samples, and fields](#4-manifest-schema-samples-and-fields)
+5. [Selection and read plans](#5-selection-and-read-plans)
+6. [Access surfaces and bounded delivery](#6-access-surfaces-and-bounded-delivery)
+7. [Progress and lifecycle](#7-progress-and-lifecycle)
+8. [Interactions](#8-interactions)
+9. [Correctness invariants](#9-correctness-invariants)
+10. [Implementation and I/O flows](#10-implementation-and-io-flows)
+11. [Risks and open questions](#11-risks-and-open-questions)
+
+## 1. Scope and boundary
+
+Dataset provides a stable logical model for samples and heterogeneous fields. It
+supports HTTP access and a separately maintained SDK with thin and fat client
+modes, with equivalent selection and integrity semantics. It does not own disk placement, replication,
+erasure coding, repair, or physical chunk layout. It does not become a query
+engine or a training-framework-specific storage protocol.
+
+The core objects are Dataset, Snapshot, Manifest, Schema, Sample, Field,
+Selection, Read Plan, and Cursor. Worker scheduling, tensor decoding, collation,
+and model checkpoints remain in adapters or execution systems above Dataset.
+
+## 2. Authority and naming
+
+Dataset authority always supplies the fixed `dataset-ns` default/root namespace. Every Dataset
+identity carries that namespace, even though the first implementation exposes no
+namespace create, delete, or mutation operation. Dataset names are unique within
+the supplied namespace. The representation keeps namespace in the key so a
+future multi-namespace authority can be added without changing Dataset identity,
+snapshot inheritance, chunk placement, or read ordering.
+
+## 3. Dataset and snapshot model
+
+A Dataset owns metadata, snapshots, manifests, samples, fields, and the payload
+references needed to read them. A Snapshot is an immutable published view
+identified by an opaque `snapshot_id`. `latest` and `stable` are mutable names
+that resolve to real snapshot IDs; cursors and read plans store the real ID.
+
+A Snapshot has at most one `parent_snapshot_id`. A child applies additions,
+deletions, and membership rules to its parent; the numeric order of IDs never
+implies ancestry. Snapshot merge is explicit: a merge produces a new snapshot
+with a declared parent and delta. Old snapshots remain readable until retention
+releases them.
+
+Publication builds and validates the manifest, schema binding, indexes, payload
+references, checksums, and durability state before atomically advancing a
+mutable name. Readers observe either the old complete snapshot or the new
+complete snapshot.
+
+## 4. Manifest, schema, samples, and fields
+
+The Manifest is the authority for a snapshot's schema binding, sample membership,
+splits, selections, field locators, checksums, statistics, and parent delta. It
+may be partitioned and indexed; a single hot key cannot be required for all
+reads. Schema defines field type, structure, requiredness, missing-field
+semantics, and evolution rules.
+
+`sample_id` is a stable opaque string supplied by the importer. Within one
+snapshot, `(sample_id, field)` has one logical value. A field is either an inline
+KV value for bounded metadata or an opaque chunk location string for larger
+payloads. Dataset passes the location to the chunk system and never parses or
+constructs its physical meaning. Copy-on-write shares unchanged values between
+snapshots; field and sample deletion uses tombstones so historical snapshots
+remain valid.
+
+A sample may contain image, video, audio, text, label, embedding, or metadata
+fields. Fields are aligned by snapshot and sample identity, while decoding and
+tensor composition belong to the consumer adapter.
+
+## 5. Selection and read plans
+
+A Split is a logical subset of a snapshot. A Selection in a Read Plan chooses
+samples using prefix, range, explicit IDs, keyword, field match, and boolean
+composition. Prefix, range, and explicit IDs can be checked for overlap directly;
+metadata predicates require a scan against one fixed snapshot. The manifest
+records any permitted overlap policy.
+
+A Read Plan binds one snapshot ID to selection, ordering, optional shuffle
+parameters, and batch policy. The implementation supports bounded sample and
+group shuffle.
+Sample shuffle loads only sample IDs and lightweight metadata into bounded groups;
+a selection that fits one group completes in one pass, while a larger selection
+uses multiple groups with the same deterministic contract. Group shuffle remains
+an explicit plan type for prefix, metadata, session, device, time-window, or
+association-key grouping. Every shuffle has sample-count, retained-byte, batch,
+prefetch, and in-flight limits and is rejected before payload reads when a limit
+would be exceeded. Shuffle never moves or rewrites published payload.
+
+## 6. Access surfaces and bounded delivery
+
+HTTP is the portable Dataset surface. The native client loads the same authority
+and a bounded versioned routing view, then connects directly to responsible
+metadata and data services. Neither surface may change snapshot semantics.
+
+`GetSample`, `GetBatch`, and bounded Scan resolve metadata first, apply field
+projection, and then read inline values or chunk ranges/streams. Batch size,
+read window, prefetch, cache, and in-flight requests are bounded and propagate
+backpressure. Direct GPU delivery may use short-lived capabilities and a CPU
+fallback, but it must preserve the same selection and integrity result.
+
+## 7. Progress and lifecycle
+
+A cursor is valid only for its bound Read Plan and snapshot. Ordered reads store a
+sample position; grouped shuffle stores group identity or index and an offset
+within the group. The client advances a cursor after batch confirmation. A crash
+may replay the last unconfirmed batch, but may not skip or change the snapshot.
+Epoch and sampler state remain client state.
+
+An active-read lease protects payloads reachable from the selected snapshot while
+the read is in progress. Normal close or cancel releases it immediately. If the
+connection has no activity for 60 seconds, the lease is invalid and GC may treat
+the read as disconnected; this TTL is the crash and network-failure fallback.
+
+Dataset owns the lifetime of its metadata and payload references. Reclamation
+requires that no retained snapshot, tombstone needed by a retained history, or
+active read lease can reach the value or chunk. External S3 or Iceberg references
+are provenance unless an explicit cross-model retention contract says otherwise.
+
+## 8. Interactions
+
+Dataset metadata uses KV authority and can use chunk-backed range state for
+large manifests or indexes. Chunk I/O owns location validation, range/stream
+reads, checksums, and physical recovery. Access Server exposes HTTP and health
+boundaries; the native client observes topology but never publishes placement.
+Framework adapters map Read Plans to PyTorch `IterableDataset`, TensorFlow/JAX
+iterators, Hugging Face streaming examples, or Ray blocks without adding storage
+objects.
+
+S3 and Iceberg remain peer metadata domains. Dataset can consume immutable data
+from either through an explicit source reference, but cannot commit their tables,
+overwrite their objects, or reclaim their owned bytes.
+
+The Dataset library owns these semantics and storage clients. The existing Access
+Server may expose Dataset HTTP on its own configured listener, alongside S3 and
+Iceberg. A separate Dataset SDK project, maintained by this team, provides both a
+thin HTTP client and a fat client that connects directly to internal Chunk-KV and
+DiskIO. The same SDK owns the PyTorch `IterableDataset` adapter. These are two
+client modes, not two Dataset authorities or a separate Dataset server product.
+
+## 9. Correctness invariants
+
+- **DATA-I1 — Namespace identity:** every Dataset carries the configured
+ default/root namespace, and a Dataset name is unique within it.
+- **DATA-I2 — Snapshot consistency:** one operation resolves exactly one immutable
+ snapshot and never mixes fields from another snapshot.
+- **DATA-I3 — Atomic publication:** readers observe no manifest or one complete
+ published snapshot.
+- **DATA-I4 — Explicit ancestry:** only the selected snapshot's parent chain
+ explains inherited state; ID ordering is irrelevant.
+- **DATA-I5 — Opaque locations:** Dataset does not interpret physical chunk
+ locations.
+- **DATA-I6 — Logical shuffle:** ordering and shuffle do not move published
+ payload.
+- **DATA-I7 — Bounded delivery:** batch, window, prefetch, cache, and in-flight
+ limits bound memory and registration.
+- **DATA-I8 — Cursor binding:** a cursor and Read Plan cannot resume against a
+ different snapshot or selection identity.
+- **DATA-I9 — Safe reclamation:** retained snapshots and active reads protect all
+ reachable metadata and payload.
+- **DATA-I10 — Surface equivalence:** HTTP and native access return the same
logical selection and integrity result.
-- **DATA-I6 — Bounded streaming:** dataset size does not determine server or
- client memory, registration, or prefetch depth.
-- **DATA-I7 — Non-authoritative topology:** a client routing view cannot publish
- or alter physical placement.
-- **DATA-I8 — GPU final destination:** a direct GPU path does not relay payload
- through Access Server or Dataset-client CPU memory.
-- **DATA-I9 — Independent authority:** S3 and Iceberg semantics cannot redefine
- Dataset publication, selection, batching, or transfer behavior.
+
+## 10. Implementation and I/O flows
+
+The implementation is split into a `crowdb-access-dataset` library and a thin
+`crowdb-access-server` Dataset listener, following the S3 and Iceberg boundary.
+`DatasetAuthority` owns keys, records, publication state, ancestry, retention,
+and durable progress through the `DatasetStore` interface. `DatasetReadService`
+owns admission, bounded delivery, plan/cursor execution, and lease lifetime.
+`DatasetHttpService` owns HTTP framing, authentication, route/status mapping,
+shutdown, and health wiring. `ChunkKvDatasetStore` adapts Dataset metadata and
+payload access to the chunk-backed KV client without exposing physical layout
+to the authority.
+
+### 10.1 Publication flow
+
+The HTTP or native client serializes a publication request and sends it to the
+listener or authority. The authority validates the Dataset, parent, operation
+token, and Manifest before any head mutation. It CAS-writes the operation
+record, writes a `Prepared` Snapshot record, partitions and persists the
+Manifest, then CAS-transitions the Snapshot to `Published` and advances the
+head. A retry reads the operation record and resumes the same Snapshot. A
+failure before the published transition leaves `latest` unchanged. Manifest
+partitions and the head are never read through the HTTP listener directly;
+all reads go through the authority's complete-snapshot checks.
+
+### 10.2 Read flow
+
+`DatasetReadService` validates the request and acquires a delivery-window
+permit before admitting work. It then records the Dataset/Snapshot read lease,
+loads the Manifest and inherited ancestry through `DatasetAuthority`, computes
+bounded sample groups and projection, and resolves each field. Inline values
+are verified locally; chunk locators are passed unchanged to the chunk store,
+which performs range/stream I/O and integrity checks. The service releases the
+lease and delivery permit after the batch result is formed, then persists the
+next cursor when the operation is a Scan. Errors release lifecycle state before
+being mapped to the native or HTTP error class.
+
+### 10.3 Retention and reclaim flow
+
+Retention mutations update Snapshot-scoped records through the authority.
+Reclaim first walks the published head's explicit parent chain and checks
+latest, stable, retained, and active-read protections. It then computes
+Dataset-owned metadata and payload candidates, leaves shared or externally
+owned locators untouched, deletes only eligible state, and persists completed
+reclaim progress. Repeated reclaim and authority restart read the same durable
+progress record and remain idempotent.
+
+### 10.4 Restart and parity flow
+
+The authority reconstructs its view from DatasetStore records on startup; no
+in-memory head or lease registry is authoritative. HTTP and native entry
+points call the same authority and read service, so parity tests compare
+Snapshot IDs, manifests, sample ordering, checksums, cursors, leases, and
+logical error classes. Restart tests reopen the same store and verify published
+heads, Manifest partitions, cursors, retention state, and reclaim progress.
+
+Metadata mutations use the smallest possible authority operation. A single-key
+CAS is reserved for a publication head, an operation phase, or an admission
+fence whose conflict is the correctness decision. Do not CAS every sample,
+field, payload block, or read cursor. Immutable payloads are written once and
+referenced by the manifest; retry identity and persisted journals handle
+recovery without a global CAS loop.
+
+Hot reads and planning use immutable snapshots, atomics, or `ArcSwap`-style
+published views. Do not add a mutex or async lock to point reads, manifest
+lookups, chunk reads, or stream delivery. Lifecycle-only coordination may use a
+bounded lock when an existing owner requires it, with contention measured before
+acceptance.
+
+Small values follow the existing small-object path and remain inline only when
+the configured threshold admits them. Large fields use the large-write pipeline
+with bounded blocks, backpressure, and chunk-native range/stream reads. Readers
+fetch bounded frames and verify chunk/block integrity. The record contract may
+require MD5 for compatibility or SHA-256 for canonical object identity; the
+algorithm is selected by correctness requirements, never omitted to optimize a
+benchmark. Digest cost, placement, and whether a full-object pass is needed are
+measured in the relevant backlog item. Dataset does not add whole-object
+buffering to the hot path.
+
+## 11. Risks and open questions
+
+Future namespaces add metadata fan-out and authorization scope. The first
+implementation intentionally has no namespace operations; it only carries the
+configured default/root namespace in every identity.
+
+Manifest partitioning, predicate indexes, cache invalidation, and direct GPU
+transport require workload measurements. They are independent extensions and do
+not alter the Dataset identity or snapshot contract.
diff --git a/doc/design/access-server/dataset/spec-crowdb-dataset.md b/doc/design/access-server/dataset/spec-crowdb-dataset.md
new file mode 100644
index 000000000..6bc463386
--- /dev/null
+++ b/doc/design/access-server/dataset/spec-crowdb-dataset.md
@@ -0,0 +1,283 @@
+
+
+
+# CROWDB Dataset Specification
+
+This document is the normative Dataset contract. It defines the objects,
+identities, state transitions, observable behavior, and compatibility rules
+shared by Dataset authorities, HTTP clients, native clients, and future SDKs.
+The implementation design is described in
+[Design: Dataset](design-crowdb-access-dataset.md); when the two documents
+appear to disagree, this specification is authoritative.
+
+## Table of contents
+
+1. [Scope and terms](#1-scope-and-terms)
+2. [Identity and names](#2-identity-and-names)
+3. [Dataset records](#3-dataset-records)
+4. [Operations](#4-operations)
+5. [Manifest and field values](#5-manifest-and-field-values)
+6. [Snapshot publication](#6-snapshot-publication)
+7. [Selection, plans, and cursors](#7-selection-plans-and-cursors)
+8. [Read behavior](#8-read-behavior)
+9. [Retention and reclamation](#9-retention-and-reclamation)
+10. [HTTP and native surfaces](#10-http-and-native-surfaces)
+11. [Errors and compatibility](#11-errors-and-compatibility)
+12. [Normative invariants](#12-normative-invariants)
+
+## 1. Scope and terms
+
+Dataset is a logical, sample-oriented collection. It stores metadata and
+references to payload bytes; it does not define physical chunk layout,
+replication, erasure coding, or framework tensor types.
+
+The following terms have one meaning throughout this specification:
+
+- **Dataset** is the named authority for immutable snapshots and their
+ metadata.
+- **Snapshot** is an immutable view of a Dataset. It is identified by an
+ opaque `SnapshotId` and may name one parent Snapshot.
+- **Manifest** is the complete, validated description of one Snapshot's
+ schema, sample membership, field values, partitions, and integrity metadata.
+- **Schema** defines the fields, requiredness, and compatibility rules for a
+ Manifest.
+- **Sample** is an opaque `sample_id` and its field map in one Snapshot.
+- **Field** is a named value of one Sample. A missing field and a tombstone are
+ distinct: missing means no value is supplied by the visible ancestry;
+ tombstone explicitly hides an inherited value.
+- **Locator** identifies payload bytes. Inline values are stored in metadata;
+ chunk locators are opaque strings interpreted only by the chunk subsystem.
+- **Selection** identifies samples from one Snapshot.
+- **Read Plan** binds a Selection, ordering, projection, batching, and optional
+ shuffle to one immutable Snapshot.
+- **Cursor** records progress for one Read Plan and is acknowledged only after
+ the corresponding batch is accepted by the caller.
+- **Active-read lease** protects data reachable from one Dataset/Snapshot while
+ a read is admitted or executing.
+
+Dataset does not infer semantics from a locator, a Snapshot ID's numeric
+ordering, a sample ID's encoding, or an external source URI.
+
+## 2. Identity and names
+
+A Dataset identity is `(namespace, dataset_name)`. The initial authority uses
+the configured `dataset-ns` root namespace for every Dataset. Names are unique
+within that namespace and are compared as opaque UTF-8 names after validation.
+
+`SnapshotId` is immutable and opaque. `latest` and `stable` are mutable names
+whose values are Snapshot IDs. A Read Plan, Cursor, Manifest, or lease stores
+the resolved Snapshot ID and never stores a mutable name in its place.
+
+A Snapshot has zero or one declared parent. Parentage is explicit; ID ordering
+never implies ancestry. A child may add, replace, or tombstone samples and
+fields while inheriting all values not overridden by its visible parent chain.
+
+## 3. Dataset records
+
+A Dataset record establishes that the identity exists and owns its metadata.
+Creating an existing identity is a conflict. Opening a missing identity is a
+not-found error. Dataset deletion is outside this specification.
+
+A Snapshot record contains the Snapshot ID, optional parent, publication
+operation identity, Manifest binding, and publication state. Publication states
+are:
+
+- `Prepared`: the operation and candidate Snapshot are durable but the
+ Snapshot is not visible through `latest`.
+- `Published`: the complete Manifest and binding are durable and the Snapshot
+ may become visible through `latest`.
+- `Aborted`: the candidate is permanently unavailable for publication.
+
+A Manifest binding records the Snapshot it belongs to, its partition count, and
+its integrity metadata. A binding for another Snapshot is invalid. Partitions
+are an internal representation; readers observe one logical Manifest.
+
+## 4. Operations
+
+The Dataset authority provides these operations. Each operation is scoped by a
+Dataset identity; operations that take a Snapshot ID MUST use the immutable ID,
+not `latest` or `stable` as an implicit mutable reference.
+
+| Operation | Input | Successful result |
+| --- | --- | --- |
+| `CreateDataset` | Dataset identity | Creates the identity; duplicate creation conflicts. |
+| `OpenDataset` | Dataset identity | Returns the Dataset record. |
+| `PrepareSnapshot` | Parent, Manifest, operation token | Returns one durable candidate Snapshot ID that is not visible through `latest`. |
+| `PublishSnapshot` | Parent, complete Manifest, operation token | Returns the published Snapshot ID and advances `latest` atomically. |
+| `GetLatest` | Dataset identity | Returns the current Snapshot ID or no value for an empty Dataset. |
+| `ListSnapshots` | Dataset identity | Returns published Snapshot records in declared ancestry order. |
+| `GetSnapshot` | Snapshot ID | Returns the immutable Snapshot record. |
+| `GetManifest` | Snapshot ID | Returns the complete validated logical Manifest. |
+| `SetStable` | Published Snapshot ID | Sets the mutable `stable` name. |
+| `RetainSnapshot` / `ReleaseSnapshot` | Snapshot ID | Adds or removes an explicit retention reference idempotently. |
+| `ReclaimSnapshot` / `GetReclaimStatus` | Snapshot ID | Reclaims eligible state or returns durable reclaim progress. |
+| `GetSample` / `GetBatch` | Snapshot ID, sample IDs, projection | Returns ordered samples and verified field values. |
+| `Scan` | Read Plan, optional Cursor | Returns one bounded batch and the next Cursor. |
+| `SaveProgress` / `Resume` | Read Plan, Cursor | Persists or loads plan-bound progress. |
+| `Cancel` | Bound read or service | Stops admission and releases the active-read lease. |
+
+`PrepareSnapshot` and `PublishSnapshot` share the same operation token and
+validation rules. A complete publication may be implemented as one call that
+performs preparation and completion internally; it MUST have the same durable
+state transitions. Read operations are side-effect free except for lease and
+cursor state required by their documented lifecycle.
+
+## 5. Manifest and field values
+
+A Manifest MUST validate before publication. Validation rejects an empty
+sample set, duplicate sample IDs, duplicate field names, missing required
+fields, invalid checksums, invalid parent references, and incomplete or
+inconsistent partition metadata.
+
+Within one Snapshot, `(sample_id, field_name)` has at most one value. A Schema
+field can be required or optional. A required field removed by a child must be
+represented by a tombstone or an explicitly compatible schema transition;
+readers never silently combine incompatible schemas.
+
+A Field value is exactly one of:
+
+- **Inline**: bounded bytes plus the declared digest.
+- **Chunk**: an opaque location, range or stream metadata as defined by the
+ chunk subsystem, and the declared digest.
+- **Tombstone**: an explicit deletion marker with no payload.
+
+Readers verify the declared digest before returning bytes. External S3 or
+Iceberg references are provenance and input metadata; they do not transfer
+ownership of those systems' objects to Dataset.
+
+## 6. Snapshot publication
+
+Publication is an idempotent operation identified by an operation token. A
+retry with the same token and equivalent parent and Manifest returns the same
+Snapshot ID. A retry with divergent content is a conflict.
+
+The authority MUST perform publication in this order:
+
+1. Validate the Dataset, declared parent, operation token, and decoded Manifest.
+2. Allocate or recover one candidate Snapshot ID for the operation.
+3. Persist the prepared Snapshot record.
+4. Persist every Manifest partition and the Snapshot binding.
+5. Persist the published Snapshot record.
+6. Advance `latest` atomically to the complete Snapshot.
+
+A failure before step 5 leaves the candidate invisible and recoverable by the
+same operation token. A reader of `latest` MUST see either the previous
+complete Snapshot or the new complete Snapshot, never a prepared or partial
+Manifest.
+
+`stable` may be set only to an existing published Snapshot. Publishing a new
+Snapshot never changes `stable` implicitly. A published Snapshot remains
+addressable by ID until reclamation removes its metadata under Section 8.
+
+## 7. Selection, plans, and cursors
+
+A Selection is evaluated against exactly one Snapshot. Supported selection
+forms are explicit sample IDs, prefix or range, and bounded metadata predicates;
+compound forms preserve their declared order and duplicate policy. Invalid or
+ambiguous selections are rejected before payload reads.
+
+A Read Plan contains the resolved Snapshot ID, Selection, ordering, projection,
+batch size, and optional deterministic shuffle parameters. The plan identity
+includes all of those values. A plan cannot be resumed with a different
+Snapshot, Selection, projection, ordering, or shuffle configuration.
+
+A Cursor is either at the start or contains a plan-bound group and offset. The
+caller acknowledges a batch by persisting the next Cursor. A failed or
+unacknowledged batch may be replayed; a successful acknowledgement may not be
+skipped or applied to another plan.
+
+Batch size, metadata bytes, prefetch, in-flight requests, and shuffle memory
+are bounded. The authority rejects a plan that exceeds configured limits before
+reading payload bytes.
+
+## 8. Read behavior
+
+`GetSample`, `GetBatch`, and bounded Scan first resolve Manifest metadata,
+selection, projection, and ancestry, then fetch only the requested values.
+Requested sample and projection order is preserved unless the plan explicitly
+requests another ordering. Unknown samples, invalid projections, checksum
+mismatches, and malformed cursors are errors.
+
+Each admitted read increments the active-read lease for its Dataset and
+Snapshot. Normal completion, cancellation, and explicit close release the
+lease. A bounded inactivity TTL expires a disconnected lease; the expiry is a
+GC safety policy, not a publication or cursor transition.
+
+HTTP and native reads use the same authority semantics. Transport framing,
+serialization, and topology discovery may differ, but the resolved Snapshot,
+logical samples, field values, checksums, cursor result, and error class are
+equivalent.
+
+## 9. Retention and reclamation
+
+`retain` and `release` apply to an immutable Snapshot ID. The latest Snapshot,
+stable Snapshot, retained Snapshots, parent-reachable history, and Snapshots
+with active-read leases are protected from reclamation.
+
+Reclamation is idempotent. It removes only unreachable Dataset-owned metadata
+and payload references. A payload shared by two retained or readable
+Snapshots remains available until its final Dataset owner is unreachable.
+External S3, Iceberg, or other source references are never deleted by Dataset
+reclamation.
+
+A reclaim status record reports the Snapshot, pending or completed state, and
+reclaimed-count progress. Repeated requests and authority restarts preserve
+that status and do not double-delete a payload.
+
+## 10. HTTP and native surfaces
+
+The HTTP surface is the portable wire contract. It exposes Dataset creation and
+opening, latest and Snapshot inspection, publication and preparation, Manifest
+retrieval, retention, stable selection, reclamation status, bounded reads, read
+plans, progress, resume, and cancellation.
+
+The native surface exposes the same operations through typed calls. Every
+operation identifies the Dataset explicitly or through a bound service and
+carries a Snapshot ID where the operation is Snapshot-scoped.
+
+A configured HTTP bearer credential protects all Dataset routes. Deployments
+that need separate management and data policies may enforce that distinction at
+the service boundary without changing Dataset semantics.
+
+## 11. Errors and compatibility
+
+Errors preserve their logical class across surfaces:
+
+- invalid input, Manifest, selection, or cursor → client-invalid;
+- missing Dataset, Snapshot, or Manifest → not-found;
+- duplicate operation, parent race, protected Snapshot, or stale cursor →
+ conflict;
+- checksum or corrupt durable state → integrity/unavailable;
+- cancelled read or expired admission → cancelled;
+- delivery-window or configured bound exceeded → backpressure/resource
+ exhausted;
+- transient storage or service failure → unavailable and retryable only when
+ the operation is documented idempotent.
+
+Wire records are versioned. Unknown optional fields may be ignored, but a
+reader MUST reject an unsupported version, missing required field, invalid
+checksum, or incompatible state transition. New surfaces must preserve the
+logical behavior in this specification before adding implementation-specific
+extensions.
+
+## 12. Normative invariants
+
+- **DATA-S1 — Snapshot identity:** every operation resolves one immutable
+ Snapshot ID.
+- **DATA-S2 — Complete visibility:** `latest` exposes only a published,
+ complete Manifest.
+- **DATA-S3 — Explicit ancestry:** only declared parent links provide
+ inheritance.
+- **DATA-S4 — Manifest uniqueness:** one Snapshot has one valid value per
+ `(sample_id, field_name)`.
+- **DATA-S5 — Opaque location:** Dataset never parses or manufactures chunk
+ locations.
+- **DATA-S6 — Plan binding:** a Cursor is valid only for its exact Read Plan.
+- **DATA-S7 — Bounded delivery:** configured memory, batch, and in-flight
+ limits are enforced before payload admission.
+- **DATA-S8 — Lease protection:** active reads protect all reachable metadata
+ and payloads until release or expiry.
+- **DATA-S9 — Ownership-safe GC:** reclamation deletes only unowned Dataset
+ data and never external source objects.
+- **DATA-S10 — Surface parity:** HTTP and native surfaces preserve the same
+ logical results and error classes.
diff --git a/doc/design/access-server/design-crowdb-access-server.md b/doc/design/access-server/design-crowdb-access-server.md
index af1fea5e1..14988f811 100644
--- a/doc/design/access-server/design-crowdb-access-server.md
+++ b/doc/design/access-server/design-crowdb-access-server.md
@@ -41,7 +41,7 @@ is an implementation choice and is not part of the architecture contract.
| ------- | ------------------------------------------ | ---------------- | ---------------------------- |
| S3 | Buckets, objects, multipart, S3 lifecycle | HTTP | Optional direct data plane |
| Iceberg | Catalogs, namespaces, tables, snapshots | HTTP | Delegated immutable FileIO |
-| Dataset | Generations, samples, shards, tensors | HTTP | Topology-aware native client |
+| Dataset | Namespaces, snapshots, samples, fields | HTTP | Topology-aware native client |
Related designs:
@@ -98,8 +98,8 @@ on another model:
- Iceberg owns catalog, table, snapshot, commit, immutable file, and
reachability semantics. Its S3-shaped file locations do not make S3 the
authority.
-- Dataset owns generation publication, sample and shard selection, batching,
- streaming, prefetch, and tensor-oriented delivery.
+- Dataset owns namespace-scoped identity, snapshot publication, sample and field
+ selection, batching, streaming, prefetch, and bounded delivery.
The models may share physical bytes only through an explicit reference contract.
Sharing never transfers namespace, publication, authorization, or reclamation
@@ -155,7 +155,7 @@ describes scheduling and stage measurements in detail.
The Dataset native path embeds routing, retry, bounded planning, streaming, and
buffer ownership in the application. It resolves one immutable dataset
-generation and distributes work directly to responsible CROWDB services. A
+snapshot and distributes work directly to responsible CROWDB services. A
topology view routes work but never grants placement authority.
The accelerated direction is an end-to-end path from storage to the final
@@ -191,7 +191,7 @@ or retry. Protocol-local caches are discardable hints.
Direct Dataset access also preserves the storage authority boundary. A client
authenticates before receiving topology, metadata, or payload authority. Direct
operations use short-lived capabilities bound to the principal, logical object,
-immutable generation, operation, ranges, limits, expiry, and topology epoch.
+immutable snapshot, operation, ranges, limits, expiry, and topology epoch.
Topology and memory descriptors alone grant no access.
All paths bound connections, metadata work, stream windows, retained buffers,
@@ -199,7 +199,7 @@ registered memory, retries, and background work independently of total object,
table, or dataset size.
The process has a separate HTTP health listener configured by `HealthConfig`
-(or `CROWDB_ACCESS_HEALTH_LISTEN`), defaulting to loopback port 9093. Its
+(or `CROWDB_ACCESS_HEALTH_LISTEN`), defaulting to loopback port 9094. Its
`/_crowdb/health/live` and `/_crowdb/health/ready` routes are independent of the
S3 and Iceberg listeners. All three ports are distinct. Health serves no
user-data routes; S3 requests cannot bypass authentication through a health
@@ -216,7 +216,7 @@ address; a missing address requires deployment reconciliation before restart.
- **AS-I3 — Native directness:** Dataset native access reaches responsible
CROWDB services without an Access Server request or payload hop.
- **AS-I4 — Semantic equivalence:** Dataset HTTP, native CPU, and accelerated
- GPU paths select the same immutable generation and logical data.
+ GPU paths select the same immutable snapshot and logical data.
- **AS-I5 — Storage authority:** no Access Server module or native client owns
physical chunk placement, replicas, erasure-coding shards, or disks.
- **AS-I6 — Stateless server:** an authoritative operation survives loss of the
diff --git a/doc/design/chunkdb/design-crowdb-chunkdb.md b/doc/design/chunkdb/design-crowdb-chunkdb.md
index 1632614a0..47da4a181 100644
--- a/doc/design/chunkdb/design-crowdb-chunkdb.md
+++ b/doc/design/chunkdb/design-crowdb-chunkdb.md
@@ -672,6 +672,15 @@ permission to reclaim data.
sends one request per DiskDB data group and runs those requests concurrently.
DiskDB persists each allocated busy block as Tentative before responding.
+**Allocation routing failure**: Only a typed connection-establishment failure
+before RPC submission permits rerouting the same allocation. ChunkDB refreshes
+the service registry and makes at most one attempt at a different endpoint.
+If discovery fails, the endpoint is unchanged, or no owner is advertised, it
+returns the original failure. Any failure during RPC submission or while awaiting
+the reply remains ambiguous and is never replayed by the allocator. Discovery
+may refresh for the next independent request. Successful allocation adds no
+registry read or lock beyond the existing periodic discovery policy.
+
**Success boundary**: The Active chunk and every referenced Tentative busy
block are durable before ChunkDB returns success. DiskDB commit changes the
matching busy records to Committed in one ordinary batch write. A
diff --git a/doc/design/chunkds/design-crowdb-chunk-kv-server.md b/doc/design/chunkds/design-crowdb-chunk-kv-server.md
index 546a25fd7..f287cb539 100644
--- a/doc/design/chunkds/design-crowdb-chunk-kv-server.md
+++ b/doc/design/chunkds/design-crowdb-chunk-kv-server.md
@@ -1,7 +1,7 @@
-# Chunk KV Server
+# CROWDB - Design: Chunk KV Server
`crowdb-chunk-kv-server` is the standalone ownership, routing, and network
boundary for chunk-backed metadata partitions. It hosts zero or many R142
@@ -9,6 +9,19 @@ partition handles but never owns or bypasses a raw tree or stream. Group 0 is
the durable catalog and monitor authority; monitor-issued serving grants are
the only authority to admit data requests.
+## Contents
+
+- [1. Catalog Publication](#1-catalog-publication)
+- [2. Domain Monitors](#2-domain-monitors)
+- [3. Serving Authority](#3-serving-authority)
+- [4. Request Contract](#4-request-contract)
+- [5. Ordered Reads](#5-ordered-reads)
+- [6. Split Publication](#6-split-publication)
+- [7. Child Balance State Machine](#7-child-balance-state-machine)
+- [8. Placement Policy](#8-placement-policy)
+- [9. Lifecycle and Observability](#9-lifecycle-and-observability)
+- [Open Issues](#open-issues)
+
## 1. Catalog Publication
The catalog is one checksummed generation head over ordered immutable pages.
@@ -139,9 +152,99 @@ routed client.
## 6. Split Publication
-The split transition phases are `Planned`, `ParentPreparing`,
-`ChildPrepared`, `CatalogCommitted`, and `Aborted`. The server and group-0
-monitor advance them in this order:
+### 6.1. Publication boundary and durable steps
+
+The group-0 catalog head publication is the external range and assignment
+cutover. A local writer handoff is a separate operation: the same process can
+dispatch the old parent route to the retained parent and child before the new
+catalog is visible. This compatibility must preserve every acknowledged WAL
+record; it does not authorize a different owner to serve an unpublished range.
+
+The parent retains its existing tree and journal. Only the child receives new
+storage. Handoff is parent-local range dispatch, not replacement of the parent
+writer with another tree/WAL and not remote ownership transfer. The successful
+durable handoff status update commits the obligation to resume this split;
+in-memory dispatch changes afterward. Handoff status is distinct from final
+`ChildPrepared` readiness and from external catalog publication. A candidate
+epoch or a process-local dispatcher cannot substitute for this status.
+
+- **S1 — Plan:** persist the transition identity, expected parent assignment,
+ split key, both successor epochs and complete storage identities. Reserve the
+ affected partition and owner against another topology transition.
+- **S2 — Prepare:** persist `ParentPreparing` before creating artifacts. Pin the
+ exact base, construct the bounded child view and establish the common WAL
+ frontier `C`. Persist the exact handoff status before changing local dispatch.
+ Parent journal appends and reads continue while the status is persisted;
+ no handoff waiting queue or mutation-worker wait for that update is allowed.
+ Bind the original parent storage, child storage, pinned base and replay
+ boundaries. Cover the additional parent suffix before actual dispatch in
+ child recovery; fixing `C` does not by itself end parent right-range writes.
+- **S3 — Ready:** persist the exact retained-parent and child artifacts,
+ overlays, common cursor and readiness proof as `ChildPrepared`. A process-local
+ handle or heartbeat cannot substitute for this durable proof.
+- **S4 — Publish:** validate that the current catalog still contains the planned
+ parent. Publish one successor head replacing it with two adjacent Serving
+ entries. This head is the external split commit point. On an ambiguous result,
+ reread the authoritative head and accept only the exact intended successors;
+ an unrelated newer generation is not proof that this split committed.
+- **S5 — Reconcile:** persist `CatalogCommitted` after proving publication.
+ Install the matching catalog and grants; preserve old-route dispatch without
+ allowing a retired writer to append. Restart must use the published assignment
+ together with its exact durable split proof.
+- **S6 — Materialize and retire:** checkpoint independently recoverable halves,
+ publish removal of their overlays and transition markers, then release pins.
+ Reclaim only artifacts with no catalog, replay, retry or pin references.
+
+Crash recovery must distinguish these boundaries:
+
+- Before handoff status commit, recover the published parent and retry or abort
+ preparation; no child mutation may have been acknowledged.
+- After status commit but before in-memory dispatch, resume the same split.
+ Recover the complete original parent journal, including writes made while the
+ status update was in flight; an empty child WAL is valid.
+- After dispatch but before catalog publication, resume the same split from the
+ original parent journal and the child journal. The old parent route remains
+ published; do not discard the child or choose authority by the maximum local
+ epoch. Neither final readiness nor catalog publication is needed to establish
+ that the committed handoff must be recovered.
+- An uncertain handoff update requires exact durable transition reread. Stale
+ preparation or abort work must not overwrite a committed handoff.
+ While the process remains live, retain and retry the exact prepared child
+ base, WAL handle and pins. Do not reopen its WAL, release its pins or rebuild
+ a different candidate merely because publication returned an error. Writes
+ arriving on the parent meanwhile are covered by the later dispatch frontier.
+ Once dispatch is installed, its live writers own finalization; only a proven
+ pre-handoff abort may discard the pending candidate.
+- After publication but before `CatalogCommitted` persistence, prove the exact
+ successor entries and finish the transition marker; do not roll back the split.
+- After publication, recover each assigned half's complete own WAL. The child
+ inherits only the parent prefix through the final readiness frontier `C2`
+ recorded in its catalog artifact, then replays its own WAL. This bound applies
+ even when the child WAL is empty: later retained-parent records belong to the
+ parent's next epoch and must not extend child inheritance. Require matching
+ grants before admitting requests.
+
+The initial handoff status fixes `C`, before the eventual in-memory dispatch
+frontier `C2`. While only that initial status exists, recovery derives `C2` from
+the first child record's sequence minus one, or inherits the parent tail when
+the child WAL is empty. Final readiness records the exact boundary explicitly;
+published catalog recovery uses that boundary instead of rediscovering it from
+the current parent tail.
+
+The durable handoff proof is stored in the split transition while its phase is
+`ParentPreparing`. Revision CAS preserves an existing proof and reconciles an
+unknown write result by exact reread. Initial and final replay frontiers remain
+distinct; additional real-process crash windows are recorded under Open Issues.
+
+### 6.2. Local execution and storage retention
+
+The split transition phase symbols are `Planned`, `ParentPreparing`,
+`ChildPrepared`, `CatalogCommitted`, and `Aborted`. A separately persisted
+`handoff_proof` distinguishes preparation from committed local handoff before
+final readiness. It binds the pinned tree base, original parent stream and
+initial inherited frontier while the transition remains `ParentPreparing`.
+Final readiness extends that frontier without replacing its base. The server and
+group-0 monitor advance them in this order:
1. Group 0 persists the complete plan before local work begins. The existing
parent keeps its identity, tree, stream, owner, and lower boundary; its next
@@ -149,8 +252,11 @@ monitor advance them in this order:
fixed by the plan.
2. The parent process persists `ParentPreparing`, renews its serving grant as
an active owner, checkpoints the exact parent base, builds the child's
- range-bounded base, performs the bounded writer handoff, and installs the
- local child handle.
+ range-bounded base, fixes `C`, and persists the handoff status without stopping
+ parent writes or reads. It then installs parent-to-child dispatch. Left-range
+ writes retain the original parent journal; right-range writes move to the
+ child journal only when dispatch changes. Replay coverage includes parent
+ writes between the fixed frontier and that dispatch change.
3. Readiness binds the exact common cutover, the retained-parent next epoch,
and the child's complete tail overlay.
It is persisted before catalog publication.
@@ -167,9 +273,11 @@ monitor advance them in this order:
the actual writer or writers used by the operation.
Before child materialization, heartbeat load reports the child as dependent.
-After process restart, either split half may reopen with its historical tail
-overlay in Prepared state. A matching serving grant alone cannot activate that
-overlay. Startup loads the persisted split transition and requires its committed
+After process restart, the retained parent reopens its original tree and WAL
+through ordinary range-filtered recovery; it has no split tail overlay. The
+child reopens its exact pinned base, replays the range-filtered parent suffix,
+and then replays its own WAL. A matching serving grant alone cannot activate
+the child overlay. Startup loads the persisted split transition and requires its committed
phase, transition identity, range, owner, epoch and complete storage artifact to
match the current Serving catalog entry. Transfer overlays retain their separate
committed-transfer validation. Missing or conflicting evidence leaves the writer
@@ -189,11 +297,77 @@ restart. Root reclamation treats the oldest durable pin as an upper bound. The
owner releases the pin only after installing the newer catalog generation whose
child has no overlay and whose parent and child have no split marker. Pin delete
is idempotent and remains reconciliation work after an ambiguous response or a
-crash. An abort before readiness removes unpublished child state and its pin;
-after readiness, durable transition and catalog evidence decide cleanup.
+crash. An abort before handoff commit may remove unpublished child state and its
+pin. After handoff commit, absence of readiness or a child catalog entry is not
+permission to abort or delete child storage; recover and complete the same split.
+
+The parent also retains its existing checkpoint generation while the child
+depends on the inherited parent WAL prefix. Publishing that prefix into the
+child's live tree does not release this dependency: the pinned recovery base
+still needs those journal records after a crash. Parent checkpoint publication
+remains suppressed by the existing transition pin until the catalog records an
+independent child. Parent reads and journal appends continue throughout this
+interval. The same completed catalog releases both generation pins.
## 7. Child Balance State Machine
+### 7.1. Owner cutover and durable steps
+
+The first successful catalog head publication naming the target in
+`TargetCatchingUp` is the owner cutover. The later `Serving` publication changes
+readiness, not ownership. A candidate target epoch in a plan is not effective
+tree or serving authority. Source release precedes the owner cutover and creates
+a recovery obligation; it is not itself a catalog owner change.
+
+- **T1 — Plan:** persist the exact source assignment, target identity and higher
+ epoch, artifacts, lease bounds and transition identity before preparation.
+- **T2 — Prepare source:** persist `SourcePreparing`, pin an immutable root,
+ initialize the independent target WAL and record replay start cursor `P`.
+ Source remains the published owner and accepts writes under its existing grant.
+- **T3 — Prepare target:** persist `TargetPreparing`; open the pinned base and
+ replay the source suffix into a Prepared view. Persist readiness before
+ `TargetPrepared`. Preparation must not advance the shared tree's effective
+ owner epoch or disable source recovery. Target cannot admit data mutations.
+- **T4 — Seal source:** after exact target readiness, close source admission,
+ drain admitted writes and persist release proof with final durable cursor `C`.
+ With an unreachable source, wait for lease exclusion and recover the complete
+ durable tail instead of treating `P` as final. No target data admission is
+ allowed while the catalog still names source. A durable release cannot be
+ undone merely because the catalog still has the old assignment.
+- **T5 — Publish owner:** validate the source assignment and durable release,
+ then publish the target epoch, `TargetCatchingUp` and complete replay artifacts
+ in one catalog successor. Resolve ambiguous publication by exact head reread.
+ Effective target storage authority must follow this published assignment;
+ a crash before that authority update is completed must allow idempotent repair.
+- **T6 — Catch up:** recover the complete source prefix through `C`, followed by
+ the target WAL. The published release proof permits durable unconditional
+ target appends; reads and conditional operations wait for the complete view.
+ Source cannot append, including through a stale client route or old grant.
+- **T7 — Serve:** persist final readiness, publish the same target assignment
+ as Serving, persist `CatalogCommitted` and activate only with its exact grant.
+ A crash between head publication and phase persistence must finish the marker
+ from exact catalog evidence rather than strand or reverse the assignment.
+- **T8 — Retire:** clear the completed overlay only after independent recovery
+ is proven. Release pins and reclaim old resources only after all recovery,
+ retry and forwarding references have cleared.
+
+Restart first reads the authoritative catalog and then the matching durable
+transition. Before release it restores source; after release but before owner
+publication it keeps source fenced and completes publication; after owner
+publication it restores target. A local tree epoch, heartbeat or cached page
+cannot independently choose the owner. Missing or conflicting proof fails closed.
+
+An abort is permitted only before durable release and owner publication. Persist
+the exact abort before discarding the prepared target; stale workers must not
+publish or activate that aborted transition. Candidate epochs are not reused,
+and cleanup must retain any source-referenced base, WAL or pin. Restoring source
+does not require synchronous deletion of unrelated target temporary objects.
+
+The effective-tree-authority ordering and publication/phase crash windows still
+require implementation verification; known gaps are recorded under Open Issues.
+
+### 7.2. Execution, proofs and recovery
+
A balance transition persists source and target owners, increasing target
epoch, source and target artifacts, readiness limits, old-grant deadline,
phase, release proof, initial readiness proof, final catch-up proof, and
@@ -319,7 +493,7 @@ The balance invariants are:
Balancing targets at least `live_owner_count * target_partitions_per_owner`,
defaulting to four partitions per owner. Split chooses the largest eligible
-partition using approximate retained pack weights. A split boundary comes from
+partition using approximate retained pack estimates. A split boundary comes from
a resident index separator or a bounded small key window; exact byte or key
medians are unnecessary. Live-key witnesses establish nonempty children when
the observation is made. Concurrent mutations can change that observation.
@@ -330,19 +504,89 @@ an unsplittable largest partition does not exclude the remaining candidates.
Retained pack estimates use the opened manifest, including shared packs, rather
than cumulative write counters. The estimate is deliberately coarse and can
lag unsnapshotted mutations; observation does not scan remote metadata or data.
-The retained parent and new split child stay local. Once independently
-recoverable, count-correcting placement takes priority over further splitting;
-weighted placement follows eligible splits. Placement minimizes partition-count
-difference first, then durable-byte spread. A move must repair count imbalance
-or improve weighted spread by at least 25%. Request rate and target headroom are
-safety filters. A child with a parent-tail overlay is ineligible. The default
-per-partition cooldown is one minute. Split pacing includes recent splits and
-transfers; repeat-placement pacing includes only recent transfers, so a split
-does not postpone a child's first placement. An owner participates in at most
-one transition at a time. Transfer preparation, estimated catch-up and forwarding
-retain independent ten-minute safety windows. Healthy, independently recoverable
-partitions with capacity on an idle target must make actual balance progress
-within forty seconds; splitting alone is not placement progress.
+The retained parent and new split child stay local. Range splitting deliberately
+uses an approximate boundary and does not guarantee equal child sizes. Split
+sizing and minimum partition supply do not require exact placement equality.
+A move uses assigned partition counts; byte estimates remain capacity filters.
+An indivisible range may prevent further useful count balancing, in which case
+placement remains unchanged.
+
+### 8.1 Count-Based Placement
+
+Placement currently uses assigned split count only: `w_i = p_i / P`, including
+healthy owners with zero assignments. `byte_weight_percent` is reserved and must
+be zero; nonzero settings are rejected until root/range statistics are available.
+The data contribution is disabled. Physical shared-pack bytes, memtable bytes,
+write counters and accumulated split ratios are not placement data weights.
+Tolerance defaults to 20% relative to equal owner share; minimum global-loss
+improvement defaults to 25%. Missing or epoch-mismatched assignment observations
+still defer planning. Capacity/headroom remains a separate safety filter.
+
+Tree statistics do not yet expose current root/range page, logical-byte or live-KV
+aggregates. Existing retained-pack estimates remain coarse inputs to automatic
+split sizing/candidate ranking and capacity checks; they can include shared packs
+and lag ordinary flushes. Count balancing does not make those estimates exact.
+The split execution, boundary sampling, recovery and publication contracts remain
+unchanged. Console does not display Weight or weighted balance explanations.
+
+### 8.2 Tolerance and Move Selection
+
+Equal owner weight is `1 / N`. The observation's relative deviation is
+`D = max_i(abs(N * w_i - 1))`; `imbalance_tolerance_percent` bounds `100 * D`.
+Within tolerance, no load-balancing move is planned. Outside tolerance, evaluate
+candidate moves against the same global loss
+`L = sum_i((N * w_i - 1)^2)` before and after the move. The partition count
+contribution moves; the byte contribution is zero. The normalized denominators
+stay fixed for each candidate evaluation. Only the two affected owner terms
+change, so each candidate's loss delta requires constant arithmetic.
+
+A candidate must strictly reduce loss and meet
+`minimum_weighted_improvement_percent` of the current loss. A count difference
+does not authorize a move that worsens the global count score. Choose the greatest loss
+reduction among safe eligible candidates; stable partition/target identities
+break ties. Use validated bounded integer/fixed-point arithmetic, sufficient
+intermediate width, and conservative threshold comparisons; UI rounding does
+not decide eligibility. With unchanged observations, a reverse move increases
+the same loss and cannot qualify. Observation changes remain subject to
+thresholds and cooldown, rather than triggering actions for every small change.
+
+For example, two owners with weights 20% and 80% can move a partition whose
+contribution is 40 percentage points to obtain 60% and 40%. A relative tolerance
+of 20% includes that result because equal owner weight is 50%. This is an
+illustration of configured thresholds, not a promise that split yields two
+40-point children. When no safe indivisible candidate offers enough improvement,
+report no useful move and keep the placement.
+
+Request rate, target headroom, fresh health, exact assignment and independent
+recoverability are safety filters. A parent-tail overlay is ineligible. One
+partition and each participating owner have at most one active transition.
+Default per-partition cooldown is one minute; cooldown supplements scoring and
+tolerance. Split pacing includes recent splits and transfers; repeat-placement
+pacing includes only transfers, so split does not postpone first placement.
+Transfer preparation, estimated catch-up and forwarding retain independent
+ten-minute safety windows. A safe candidate outside tolerance that meets the
+improvement threshold must make placement progress within forty seconds when
+its cooldown has elapsed; splitting alone is not placement progress.
+
+### 8.3 Decision Observation
+
+The monitor's bounded decision observation identifies policy coefficients and
+thresholds, catalog generation, assignment/owner identities, observation time,
+source freshness, counts, estimated bytes, count/byte weight contributions,
+owner weights, deviation and loss. It records the selected or best rejected
+candidate's predicted weights and improvement, plus move/no-move reason:
+within tolerance, insufficient benefit, unavailable observation, cooldown,
+active transition, inherited overlay, unhealthy owner or capacity/rate limit.
+Policy version identifies the scoring formula. Diagnostics retain at most 256
+owners, 4096 partition contributions and 2 MiB, independently of the planner's
+complete metadata input. Omitted details display unavailable; they do not alter
+planning. Unchanged diagnostics are renewed within half their freshness window
+rather than written on every tick. Do not emit or persist all candidate pairs
+on every tick. This diagnostic
+observation is not catalog authority and does not advance catalog generation.
+The observation remains backend diagnostic metadata; Console currently hides
+Weight and balance explanations. Catalog generation/identity mismatch remains
+explicit; UI refresh does not bypass stale cursor rejection.
## 9. Lifecycle and Observability
@@ -380,13 +624,31 @@ generation.
## Open Issues
+- **Prepared transfer crash coverage:** preparation preserves source tree
+ authority; mutable target authority requires the exact published owner
+ assignment. Fault injection must cover source and target restart around
+ that publication, including overlapping source checkpoint work.
+- **Split handoff crash coverage:** the durable handoff status precedes local
+ dispatch and preserves the original parent tree and WAL. Recovery composes
+ the exact child base, range-filtered parent suffix and child WAL. Fault
+ injection must cover status persistence with continuing parent writes,
+ dispatch before final readiness and an unconfirmed status response. Status
+ persistence remains outside the mutation worker.
+- **Head publication before phase persistence:** transfer and split publication
+ and their committed transition markers are separate writes. Recovery must
+ prove exact catalog results and finish markers after a crash in between;
+ fault injection must verify admission and eventual recovery at both transfer
+ publications and the split publication.
+- **Abort racing publication:** an abort decision must be serialized with
+ publication under the monitor fence and transition revision. A previously
+ loaded readiness proof must not publish after an authoritative abort. Verify
+ this race before treating cleanup as safe.
+
- Real-process fault injection should continue expanding coverage of ambiguous
catalog writes and process death at every balance phase.
-- A source crash while a remote target is only prepared and a newer local split
- is still materializing can advance stream writer authority ahead of the
- catalog assignment. Immediate source recovery then fails closed on the stale
- catalog epoch. Recovery should reconcile this exact interleaving without
- weakening the single-writer or exact-transition proofs.
+- Overlapping remote transfer preparation, local split materialization and
+ source restart need fault coverage that proves exact catalog and transition
+ reconciliation without weakening single-writer or replay epoch checks.
- Completed transfers remove their catalog overlay, but physical reclamation of
the departed source tree and stream, plus expiry of the stale-route
forwarding grace period, still need a manifest-fenced background policy.
diff --git a/doc/design/chunkds/design-crowdb-chunk-kv.md b/doc/design/chunkds/design-crowdb-chunk-kv.md
index 1eaad720a..19dc025bf 100644
--- a/doc/design/chunkds/design-crowdb-chunk-kv.md
+++ b/doc/design/chunkds/design-crowdb-chunk-kv.md
@@ -33,7 +33,14 @@ adapters wrap crowdb-tree and R141 `ChunkStream`; injected implementations
support deterministic tests. The raw stream handle is private, so a server
cannot bypass partition ordering, trim, or epoch checks.
-The tree is range-bounded at construction. Rust never implements the tree page
+The tree enforces its logical request range at construction. A retained parent
+keeps the original physical root, whose pages may still contain keys and
+separators outside the narrowed range until background pruning completes.
+Reopening that root validates frame checksums and structure without treating
+those physical keys as corruption; point reads, scans and mutations still
+enforce the current logical range. Importing a newly rebuilt range snapshot
+continues to require every imported key to fit its destination range.
+Rust never implements the tree page
path through `crowdb-chunk-client`; the production tree selects R140's native
page-store backend at runtime. A balance target opens the source's pinned tree
manifest directly from shared page chunks and owns a distinct target WAL. It
@@ -116,41 +123,48 @@ shrinks to `[old_start, split_key)`; the new child receives
any identity, range, epoch, or stream fails closed. The new child initially
remains on the parent owner. Placement is a later balance operation.
+Split creates only the child's tree and WAL. The retained parent uses its
+existing tree, WAL and mutation path; it is not rebuilt into a second
+destination tree or moved to a replacement WAL. Local handoff means that
+traffic still enters the parent, which dispatches the child range to the child.
+It is distinct from the later catalog publication that exposes both ranges to
+external routing. A higher candidate epoch alone proves neither boundary.
+
Split preparation follows these ordered steps:
1. Enter `SplitPreparing` and range-rebuild the child base from one exact
pinned parent tree view. The parent remains the only mutation sequencer and
continues its normal WAL and memtable path while the child tree, WAL, and
live memtable are prepared.
-2. Both destination stores and base trees are opened, and all fallible
- preparation that does not depend on the final frontier is completed while
- the parent continues serving. The parent mutation worker then processes one
- ordered `SplitCutover` marker after all earlier mutations are durable and
- applied. The marker records the right-range inheritance frontier `C`,
- attaches the fixed parent view, seeds both writers from its bounded request
- result cache instead of rescanning parent history, replays any previously
- acknowledged destination-WAL tail, and installs key routing. Destination
- tail replay is bounded preparation retry work; the marker performs no base
- rebuild, parent-history replay, checkpoint, filtered publication, or
- materialization. Mutations at or below `C` remain in parent order; later
- right-range mutations enter the child WAL and live memtable.
- Later left-range mutations continue through the retained parent. A request
- that raced with route installation and reaches the old parent queue after
- the marker is forwarded by key rather than assigned an old-parent sequence.
-3. The route change does not seal, publish, checkpoint, or wait for a
+2. Prepare the child storage and establish an ordered parent frontier `C`.
+ Fixing this frontier does not stop parent journal appends or reads. Persist
+ a handoff status in the split transition, binding the parent and child
+ identities, epochs, pinned base and WAL replay boundaries. Successful durable
+ status update is the local handoff commit point. Resolve an uncertain result
+ by reading that exact transition; an in-memory `SplitCutover` message is not
+ durable evidence. Do not await this update in the mutation worker or add a
+ split-owned request waiting queue.
+3. After the handoff status is committed, install in-memory key dispatch to
+ the child. Until dispatch changes, writes continue to the original parent
+ WAL, including writes after `C`; afterward, left-range writes continue to
+ that same WAL and right-range writes enter the child WAL. The handoff proof
+ and replay rules must cover the parent suffix between fixing `C` and changing
+ dispatch exactly once. `C` alone cannot be treated as the final inherited
+ parent frontier if parent writes continued beyond it. The child view must
+ include those acknowledged writes for reads and conditional mutations.
+ Seed its bounded request result cache and recover any acknowledged child-WAL
+ tail without reevaluating conditions. A mutation racing dispatch must have
+ exactly one writer and preserve its logical request identity.
+4. The route change does not seal, publish, checkpoint, or wait for a
memtable. The child immediately reads its base, its live memtable, and the
- right-range portion of the parent generation containing `C`. Conditional
- mutations consult that same merged view.
-4. Independently rotate the old parent memtable at its fixed `C` frontier. The
- detached generation enters one split-owned shared queue. Post-route writes
- are already isolated in the retained-parent or child writer memtable and
- are not members of this shared generation.
-5. In background, bulk-publish the shared generation once into each range:
- only the retained range into the parent and only entries at or below `C` in
- the child range into the child. Newer live-writer revisions win over an
- older shared entry. Release the shared generation only after both filtered
- publications and their durable frontiers are recorded in one immutable
- `SplitArtifact`.
+ right-range inherited parent view, including the covered handoff interval.
+ Conditional mutations consult that same merged view.
+5. Complete inherited-view publication in background, preserving the original
+ parent tree and its normal checkpoint path. The child materializes only its
+ range; newer child revisions win over older inherited entries. Retain the
+ shared view and replay dependencies until the complete inherited frontier
+ and durable storage proofs are recorded in one immutable `SplitArtifact`.
+ Handoff status is not final readiness and does not replace this artifact.
6. Publish one catalog generation that shrinks the retained parent (same ID,
new epoch) and inserts exactly one new child. The parent owner already has
both writers active, so refresh only publishes this catalog and serving
@@ -163,21 +177,26 @@ child base is durable, preparation persists
`root//pin/ = child_root_generation`. Readiness is
not recorded until that operation succeeds. A retry with the same generation is
idempotent; another generation under the same transition identity is corruption.
-If local child construction fails before readiness, preparation removes the pin
-while the parent is still the sole authority.
+Preparation may remove the unpublished child's pin after an authoritative abort
+only before handoff commit. After handoff commit, recovery continues the same
+split and retains its base, WAL and pin even if final readiness is absent.
A prepared or restarted child recovers in three layers: open the exact base
-manifest, replay the retained parent suffix through `C` with range filtering,
-then replay its own WAL from `C + 1`. Records at or below the base sequence
-restore retained request outcomes without reapplying tree mutations. Failed
+manifest, replay the retained parent suffix through the final inherited frontier
+with range filtering, then replay its own WAL from the recorded child start.
+The proof must include parent writes accepted while handoff status was being
+persisted; the earlier fixed `C` is not automatically that final frontier.
+Records at or below the base sequence restore retained request outcomes without
+reapplying tree mutations. Failed
conditions remain no-ops with their original result. Sequence gaps, a wrong
source stream or epoch, a mismatched range, or a child WAL beginning anywhere
-other than `C + 1` reject recovery.
+other than the proof's exact child start reject recovery.
The child recognizes two minimum-position namespaces. A retained parent-stream
-position at or below `C` is satisfied by the inherited overlay frontier. A
-child-stream position waits for the child's applied frontier. New mutations
-return only child-stream positions. This preserves read-after-write across a
+position covered by the inherited suffix is satisfied by the inherited overlay
+frontier. A child-stream position waits for the child's applied frontier. New
+child mutations return only child-stream positions; retained-parent mutations
+continue returning parent-stream positions. This preserves read-after-write across a
catalog split without treating two streams as one offset space.
After activation, bounded background passes independently prune the retained
@@ -197,11 +216,14 @@ The split invariants are:
- **SPLIT-PARENT-STABLE:** split preserves the parent partition, tree, stream,
owner, and lower range boundary and creates exactly one new child;
-- **SPLIT-ONE-WRITER:** before `C` the old parent sequences the full range;
- after `C` the retained parent and child sequence disjoint ranges;
-- **SPLIT-ROUTE-FRONTIER:** `C` is the last parent-ordered right-range
- mutation, not the later physical seal frontier; child mutations begin after
- `C` and a raced old-parent admission is forwarded instead of resequenced;
+- **SPLIT-ONE-WRITER:** before in-memory dispatch changes the parent sequences
+ the full range; afterward parent and child sequence disjoint ranges;
+- **SPLIT-ROUTE-FRONTIER:** the fixed preparation frontier, durable handoff
+ commit and in-memory dispatch are distinct boundaries. Recovery includes all
+ parent writes through the final inherited frontier and the complete child
+ tail; no acknowledged mutation falls between them or executes twice;
+- **SPLIT-HANDOFF-DURABLE:** persist handoff before child write admission;
+ restart after commit resumes the same split even before catalog publication;
- **SPLIT-OVERLAY-DURABLE:** base plus parent suffix plus child WAL reconstructs
values and request outcomes before activation;
- **SPLIT-NO-FOREGROUND-WAIT:** routing does not wait for memtable seal,
@@ -210,6 +232,12 @@ The split invariants are:
- **SPLIT-PIN-BEFORE-RECLAIM:** physical deletion never precedes the last
catalog, snapshot, forwarding, or retry reference.
+Implementation verification remains required for the durable handoff status and
+the exact replay/sequence representation covering continued parent appends
+between fixing `C` and installing dispatch. It must not be resolved by pausing
+parent writes, queueing requests behind status persistence, or retaining a
+second destination tree/WAL for the parent.
+
## 6. Remote Balance Storage Contract
Balancing one independently recoverable child reuses the same base-plus-tail
diff --git a/doc/design/console/design-crowdb-console-ui.md b/doc/design/console/design-crowdb-console-ui.md
index 7da85e3f5..e12ec2615 100644
--- a/doc/design/console/design-crowdb-console-ui.md
+++ b/doc/design/console/design-crowdb-console-ui.md
@@ -334,9 +334,19 @@ provide observable capacity and bitmap fixtures.
- **SVC-02:** Preserve desired configuration independently of PID. A restart
retains endpoints, workspace, and data; refreshed properties show the new PID
and separately observed readiness. Process identity is verified before signal.
+ Lifecycle acceptance first checks the submitted Start/Restart response, then
+ observes the new PID and process. An in-flight startup is not a completed
+ operation and must not trigger a duplicate cleanup restart.
+- Page unload stops its deployment runner. Cancellation of an old page's
+ observation or progress request cannot publish a service failure. The next
+ page reloads the persisted plan and reconciles interrupted deployments;
+ restoring a cached history page also reloads before resuming its runner.
- **SVC-03:** Startup observes dependencies: Group 0 before dependent metadata
services; valid disks/ownership for storage; journal/data prerequisites before
Chunk-KV bootstrap; Chunk-KV catalog before Access startup.
+- Plans waiting for a registered DiskIO service to restart observe dependencies
+ every two seconds, including when Capacity data has not been loaded. The
+ ten-second missing-disk backoff applies only to other hardware waiting states.
- Access provisioning durably records catalog initialization and activation
request identities before issuing either mutation. Interrupted deployment
reconciles committed state and resumes the same request. An existing catalog
@@ -448,6 +458,8 @@ plan, never edited out of this specification to make a run pass.
PID, and readiness. All instance labels follow §4.
- **A04 / KV:** verify Group 0 and ordinary Group 1 with three replicas; select
a group, inspect membership, and perform bounded exact/scan data operations.
+ Add members to the same group sequentially, refreshing membership after
+ each result; independent group preparation may run concurrently.
- **A05 / Capacity:** create/register DiskGroup and disk, inspect owner/binding;
select a disk with more than 32 zones, page, select Zone, and verify inline
bitmap, decode offsets, muted colors, and error handling for unavailable owner.
@@ -550,6 +562,16 @@ Unsupported features are explicit rather than represented as working controls.
Changed observations require an explicit root refresh. History retains path,
fences, offset, selected entry and byte format, then revalidates on return.
+- **CKV-06:** Split properties, Overview and graph cards do not display Weight.
+ Full Range bounds remain visible and wrap independently.
+- **CKV-07:** Weighted balance explanations and data/count contribution values
+ remain hidden until tree statistics can describe current range data reliably.
+ No tree statistics scan is triggered by opening or refreshing the console.
+- **CKV-08:** Count-based placement remains a backend policy. Existing bounded
+ observations retain catalog/epoch freshness fences, but are not presented as
+ data weights. Browser assertions verify that Weight and weighted explanations
+ are absent while tree/page inspection and range navigation remain functional.
+
## 21. Iceberg
- **ICE-01:** The configured cluster Catalog loads automatically. Left shared
diff --git a/doc/design/console/design-crowdb-console.md b/doc/design/console/design-crowdb-console.md
index c025fdd61..bcf226358 100644
--- a/doc/design/console/design-crowdb-console.md
+++ b/doc/design/console/design-crowdb-console.md
@@ -285,9 +285,14 @@ the namespace. If a recorded port is held by another owner, restart reports
the conflict; it does not silently select a different endpoint.
Disposable deployment and E2E fixtures use ephemeral namespaces with the
-same layout. All local CROWDB data, generated configuration, logs, benchmark
-output, and coordination state stay below this root; no default path uses the
-system temporary directory. Ordinary clean operations preserve persistent
+same layout. A saved CLI cluster inside an ephemeral fixture inherits that
+fixture's process and port ownership; its launcher may exit without releasing
+ports still used by the fixture. Process records remain outside disposable
+data so cleanup can terminate services even after data removal. Cleanup releases
+these nested claims along with the fixture. All local CROWDB data, generated
+configuration, logs, benchmark output, and coordination state stay below this
+root; no default path uses the system temporary directory. Ordinary clean
+operations preserve persistent
namespaces.
## 5. Node Access Model
diff --git a/doc/design/deploy/design-crowdb-deploy.md b/doc/design/deploy/design-crowdb-deploy.md
new file mode 100644
index 000000000..cafc9a594
--- /dev/null
+++ b/doc/design/deploy/design-crowdb-deploy.md
@@ -0,0 +1,435 @@
+
+
+
+# CROWDB - Design: Multi-node Deployment
+
+This document defines the target architecture for node discovery, Group-0
+bootstrap, shared console control and light-container deployment. It is a design
+contract under review, not a claim that LAN discovery or containerd lifecycle
+integration is implemented. Unresolved mechanisms are identified explicitly.
+
+The [console architecture](../console/design-crowdb-console.md) defines the
+shared operation layer; the [Group-0 architecture](../kv/design-crowdb-kv-group0.md)
+defines system metadata; the
+[reconfiguration design](../kv/design-crowdb-kv-reconfiguration.md) governs
+Paxos voting membership. This design connects those responsibilities without
+making node discovery a membership protocol.
+
+## Table of Contents
+
+- [1. Goals and boundaries](#1-goals-and-boundaries)
+- [2. Components and identities](#2-components-and-identities)
+- [3. Invariants](#3-invariants)
+- [4. Node discovery](#4-node-discovery)
+- [5. Node and rack configuration](#5-node-and-rack-configuration)
+- [6. Bootstrap and recovery](#6-bootstrap-and-recovery)
+- [7. Shared UI and management operations](#7-shared-ui-and-management-operations)
+- [8. Authorization and partitions](#8-authorization-and-partitions)
+- [9. Image and runtime deployment](#9-image-and-runtime-deployment)
+- [10. Resource and lifecycle management](#10-resource-and-lifecycle-management)
+- [11. Configuration and observability](#11-configuration-and-observability)
+- [12. Correctness and validation](#12-correctness-and-validation)
+- [13. Decisions still required](#13-decisions-still-required)
+
+## 1. Goals and boundaries
+
+- Each node has a monitor and a Web UI. Operators can use any member's UI;
+ there is no permanently designated UI host.
+- Monitors discover nodes on a management LAN without a hand-written peer list.
+ Discovery identifies nodes, not the individual servers hosted on them.
+- Before Group 0 is initialized, UI operations are restricted to node/rack
+ preparation and Group-0 bootstrap. Other server deployment requires a ready
+ Group 0 and confirmed initial topology.
+- Group 0 is the shared information platform and authority for cluster
+ membership, racks and deployment operations.
+- Production deployment uses Linux light containers: OCI images package
+ binaries/dependencies, while network, disks and performance resources are
+ explicitly assigned from the host.
+- Single-host tests simulate multiple nodes using isolated containers on a
+ dedicated Docker network and independent simulated disks.
+
+The deployment layer is not a general scheduler. Kubernetes and Nomad are not
+the default deployment substrate. OCI image compatibility allows other runtimes
+to consume an image; it does not establish complete Kubernetes deployment
+support. Cross-subnet discovery has a seed fallback and does not require a
+multicast relay. Independent initialized clusters are never automatically merged.
+
+## 2. Components and identities
+
+```text
+operator browser
+ -> any node's Web UI / shared console operations
+ -> Group 0: committed topology and operation authority
+ -> target node monitor
+ -> runtime and managed server processes
+
+node monitor <-> management-network discovery <-> peer node monitors
+```
+
+- **Node:** a deployment and hardware-management identity. A production node
+ normally corresponds to a physical Linux host; a test node may be a container.
+ A node exists before any KV server is started.
+- **Monitor:** the node management endpoint, discovery participant and local
+ process/container supervisor. Reuse `crowdb-monitor`; do not introduce a
+ second agent with overlapping responsibilities.
+- **UI:** a presentation/API entry point into shared operations. It owns neither
+ confirmed topology nor a private cluster. Losing the initiating UI does not
+ delete a submitted operation.
+- **Server:** a managed service/process deployment on a node. Server deployment
+ is distinct from node admission and Group-0 voting membership.
+- **Cluster identity:** the durable identity associated with one initialized
+ Group 0. A node cannot infer or replace it from nearby multicast announcements.
+- **Rack:** an editable logical grouping of nodes. A node-supplied rack value
+ is an initial hint; confirmed placement belongs to Group 0.
+- **Physical failure domain:** actual shared host/rack infrastructure used by
+ placement policy. Virtual rack labels do not manufacture hardware independence.
+
+Persist node identity, cluster binding and accepted bootstrap information in
+the node's host-mounted state root. Persist Group-0 replica state through its
+existing WAL/storage contract. These identities are not image contents and are
+not derived from IP addresses. Existing protocol identifier types remain the
+canonical types; the exact allocation of pre-admission node identities must be
+resolved before changing the public schema.
+
+## 3. Invariants
+
+- **I1 — Durable identity:** recreation, endpoint changes and rack reassignment
+ preserve node identity. Conflicting live endpoints claiming one identity
+ cause a visible conflict, not silent overwrite.
+- **I2 — Candidate discovery:** discovery records are observations. They grant
+ no admission, voting membership, rack authority or execution permission.
+- **I3 — Group-0 authority:** confirmed topology and management mutations use
+ Group 0. A local monitor/cache never substitutes for unavailable authority.
+- **I4 — Bootstrap binding:** every selected monitor durably accepts one
+ compatible cluster/bootstrap manifest before starting its initial replica.
+ Conflicting manifests are rejected. No timeout permits replacement bootstrap.
+- **I5 — Authorized execution:** a connected Group-0 replica is not proof of
+ authority. Management mutations require quorum-backed authorization, and
+ stale operations cannot execute after revocation or authority transition.
+- **I6 — Explicit failure domains:** virtual rack assignment and physical
+ failure-domain identity are distinct placement inputs.
+- **I7 — Runtime portability:** a supported-architecture OCI image carries
+ code; runtime inputs carry identity, network, devices and persistent state.
+- **I8 — Bootstrap gate:** only Group-0 prerequisite processes and Group-0
+ initialization run before the shared information platform is ready. Other
+ deployment requires committed initial topology.
+
+## 4. Node discovery
+
+### 4.1 mDNS and DNS-SD
+
+Monitors both announce and browse a CROWDB node discovery type. The proposed
+type is `_crowdb-node._tcp.local.`; finalize the name before publication.
+
+- mDNS transports local-link DNS queries/responses using UDP port 5353 and
+ multicast groups `224.0.0.251` / `ff02::fb`.
+- DNS-SD uses PTR records to enumerate instances, SRV to identify monitor host
+ and port, A/AAAA to resolve addresses, and TXT for bounded metadata.
+- TXT carries only discovery essentials: identity, protocol compatibility and
+ optional cluster binding. Full node/hardware/rack information is obtained
+ through the monitor management handshake.
+
+These mechanisms come from [RFC 6762](https://www.rfc-editor.org/rfc/rfc6762.html)
+and [RFC 6763](https://www.rfc-editor.org/rfc/rfc6763.html). The standards define
+discovery and caching; CROWDB defines admission and authorization separately.
+Implement standard announcements, refresh, cache expiry and clean departure,
+rather than inventing a high-frequency broadcast heartbeat.
+
+Each monitor's discovered-peer cache feeds its local UI backend. Browser code
+does not send mDNS traffic. Peers query/announce directly within the discovery
+domain; there is no requirement for a separate gossip membership layer.
+
+### 4.2 Network scope and reachability
+
+Discovery runs only on configured management interfaces. Host-network nodes
+use the management NIC/VLAN; Docker test nodes use their dedicated bridge
+interface. The advertised monitor endpoint must be directly reachable by peers
+in that scope. A wildcard bind address is not an advertised address.
+
+UI addresses exposed to browsers and monitor addresses used by peers are
+separate endpoint purposes. In bridge tests, host UI ports are published while
+peer monitor connections use container-network addresses. Production host
+networking requires an explicit local port plan.
+
+mDNS does not normally cross routed subnet/VLAN boundaries. When unavailable,
+configured peer/seed monitor endpoints provide the same handshake path. The
+seed list is a discovery input, not an authoritative membership list. Monitor
+connectivity verifies endpoint usability; discovery expiry alone does not
+establish node failure or remove a confirmed member.
+
+### 4.3 Cluster separation and trust
+
+Classify a discovered peer as unbound, bound to this cluster, bound to another
+cluster, incompatible or identity-conflicting. Only compatible candidates can
+be selected for bootstrap/admission. A foreign cluster is displayed separately;
+its topology is not imported automatically.
+
+Announcements are untrusted hints. The direct handshake and admission policy
+must validate the management peer before privileged actions. Never advertise
+credentials in TXT records. Trust provisioning remains a decision in section 13.
+
+## 5. Node and rack configuration
+
+Before bootstrap, an operator can inspect nodes, create/name virtual racks and
+assign nodes. Node-provided rack hints can be overridden. The UI labels these
+values as an uncommitted bootstrap draft, not shared authoritative metadata.
+Drafts in different UIs can differ; consensus-backed UI consistency begins only
+after Group 0 and topology publication are ready.
+
+The accepted bootstrap manifest captures the chosen initial node/rack mapping.
+After bootstrap, all rack edits and node admissions are committed through
+Group 0. Rediscovery cannot overwrite operator-confirmed rack placement.
+
+Physical host/failure-domain information remains independently inspectable.
+Several test containers may occupy different virtual racks while sharing one
+physical failure domain. Placement policy must not count those labels as
+independent physical redundancy.
+
+## 6. Bootstrap and recovery
+
+### 6.1 State transitions
+
+The node's management lifecycle is:
+
+- **Unbound:** identity exists, discovery is active, no cluster is accepted.
+- **Prepared:** a cluster identity and initial manifest are durably reserved;
+ the monitor accepts retries of that manifest and rejects conflicting ones.
+- **Group-0 starting:** selected monitors provision/start their initial replicas
+ using the same fixed voting-member manifest.
+- **Topology publishing:** Group 0 has usable quorum/leadership; initial hardware
+ and cluster metadata are being published idempotently.
+- **Active:** shared topology is confirmed and general deployment is enabled.
+- **Authority unavailable:** binding persists, diagnostics/discovery continue,
+ and new cluster-management mutations are disabled until authority recovers.
+
+Unavailability is not a transition back to Unbound. Clearing cluster binding
+requires an explicit recovery/decommission procedure, not an automatic timeout.
+
+### 6.2 Initialization flow
+
+1. Any UI collects compatible candidates and the operator's rack assignments.
+2. The operator explicitly selects initial Group-0 voting members. Candidate
+ count is not voting-member count; application nodes need not all vote.
+3. Submit a bootstrap identity and immutable initial manifest to selected
+ monitors. Persist compatible preparation before starting replicas.
+4. Start the minimum Group-0 services with the fixed manifest. Use existing
+ Paxos election and WAL durability, not discovery-cache membership.
+5. Wait for quorum-backed readiness and publish initial metadata idempotently.
+6. Persist/refresh cluster contact information for member UIs. Enable general
+ deployment only after initial topology is confirmed.
+
+Replica startup coordination and interrupted preparation recovery must be
+specified before implementation. Durable per-node conflict rejection is
+necessary but alone does not establish a complete distributed bootstrap
+protocol. Two overlapping requests must never create incompatible groups;
+arbitrary disjoint selections can form distinct clusters and must not be
+advertised as one globally unique LAN cluster.
+
+### 6.3 Recovery and subsequent admission
+
+Restart resumes accepted identity and replica WAL state. Lost multicast does
+not erase stored Group-0 contacts. An interrupted bootstrap reuses its operation
+identity/manifest; partial metadata publication resumes without admitting
+unselected nodes or repeating destructive provisioning.
+
+A newly discovered node enters the candidate view. A committed admission
+operation binds it to the cluster and assigns its rack. Adding a deployment
+node does not automatically alter Group-0 quorum. Voting-member changes follow
+the existing reconfiguration contract.
+
+## 7. Shared UI and management operations
+
+All UIs invoke the same shared console operations and read confirmed state
+from Group 0. Consistent reads support action confirmation; watch/refresh keeps
+presentation current. A temporarily stale screen does not create a second
+authority. The UI displays stale/unavailable state explicitly.
+
+Management operations have durable identity, target, requested action and
+execution outcome. Group 0 commits the management intent/authorization; the
+target monitor performs the side effect and records its result. This is not
+an atomic transaction between a KV write and a runtime action.
+
+Retries therefore use operation identity and local execution recovery. Crash
+after runtime success but before result publication must inspect runtime state
+and complete/reconcile the same operation, rather than blindly repeat it.
+Image digest and deployment-instance identity distinguish the intended instance
+from a reused process ID or container name.
+
+Changing UI hosts does not require moving cluster state. Local secrets and
+runtime observations are not replicated as public topology. Credentials needed
+to authorize another UI must be provisioned through the chosen trust policy.
+
+## 8. Authorization and partitions
+
+For a three-voter Group 0 partitioned two-to-one, only the two-voter side can
+commit management mutations. The minority cannot use its local replica or
+monitor cache as replacement authority and cannot bootstrap a new Group 0.
+
+The deployment policy is that a node may execute controlled actions only while
+holding valid authorization from the current Group 0. Define bounded grants
+and renewal for operations that outlive a single consensus commit. Fencing
+generations distinguish old ownership/authorization from current grants;
+execution boundaries must reject expired or superseded commands.
+
+A bare increasing token is insufficient unless the receiving execution path
+knows which generations are current and validates them. Likewise, a lease is
+insufficient unless timing, renewal, expiry and restart behavior are defined.
+No design claim of stale-node exclusion is valid until those enforcement
+details are resolved and tested.
+
+Monitors and UIs remain available for diagnostics during authority loss.
+The exact meaning of stopping an unauthorized node's existing data services
+is unresolved: management authorization does not automatically revoke every
+data group's independent Paxos authority. Section 13 makes that choice explicit.
+Group-0 replica processes must be allowed to recover quorum even while general
+deployment is gated; stopping all replicas on authority loss would prevent
+recovery.
+
+## 9. Image and runtime deployment
+
+### 9.1 Common OCI image
+
+Publish OCI images to Docker Hub or another OCI-compatible registry. The same
+image for a supported CPU architecture contains monitor/UI/server binaries and
+their dependencies. Node-local state and cluster identity are external mounts.
+Tags identify releases for humans; production plans pin immutable image digests.
+Architecture-specific builds may share a multi-platform image index only when
+their dependencies have been validated.
+
+Docker and containerd consume these images. `nerdctl` is an operator CLI for
+containerd, not an additional daemon. Production monitor runtime control uses
+containerd APIs without depending on dockerd. See the
+[nerdctl reference](https://github.com/containerd/nerdctl) for CLI/runtime
+terminology; its CLI compatibility does not define CROWDB lifecycle semantics.
+
+### 9.2 Production profile
+
+- Linux host, containerd runtime and host networking.
+- Explicit management/data-plane network selection; discovery uses management.
+- Stable disk/device identity and explicitly permitted block-device access.
+- Host-persistent config, node state, server state, logs and crash artifacts.
+- Explicit CPU/memory and device assignments; resource claims are validated
+ before runtime side effects.
+
+Host networking removes a container bridge/NAT layer but does not itself
+guarantee high performance. Disk passthrough does not itself bypass filesystem
+cache: the storage engine's raw-block/direct-I/O path defines that behavior.
+
+### 9.3 Single-host test profile
+
+- One container per simulated node on a dedicated user-defined Docker bridge.
+- Unique persistent roots and identities; independent simulated disk files or
+ loop devices, never overlapping writes to one backing disk.
+- Identical internal listener ports are allowed because network namespaces
+ differ; browser UI access uses distinct published host ports.
+- Actual multicast discovery is exercised within the bridge. Separate networks
+ isolate independent discovery tests.
+
+Docker bridge behavior is described in the
+[Docker reference](https://docs.docker.com/engine/network/drivers/bridge/).
+The test profile measures functional correctness, not raw-disk production
+performance. Multi-host host-network tests separately validate LAN discovery
+and real resource access. macOS-hosted Linux containers require a VM/network
+setup that is tested explicitly; image portability does not make the VM's LAN
+multicast behavior equivalent to Linux host networking.
+
+## 10. Resource and lifecycle management
+
+Containers are packaging, resource/isolation and upgrade units. Deployment
+plans explicitly assign host resources; monitors validate and execute, rather
+than autonomously choose global placement.
+
+- **CPU/memory:** cpuset, memory limits and optional NUMA assignments define
+ container boundaries. Application thread placement stays within those limits.
+ IRQ/kernel affinity tuning is host policy, not image content.
+- **Disk:** stable device references, exclusive intended ownership and supported
+ engine access modes. Important data never lives only in the writable layer.
+- **Network:** host ports and management/data interfaces are explicit.
+ RDMA device access and TCP fallback remain the responsibility of their
+ transport contracts, not the discovery mechanism.
+- **Lifecycle:** explicit start, stop, drain, upgrade and rollback operations.
+ Image upgrades pin digests and preserve state. Stateful service rollback is
+ allowed only within its storage/protocol compatibility contract.
+- **Monitoring:** container and process health are separate observations.
+ Existing monitor supervision can recover eligible process exits; it must not
+ silently override a deliberate stop, expired authorization or upgrade drain.
+- **Runtime access:** monitor receives the minimum runtime/device privileges
+ needed. Application/UI containers do not all receive the containerd socket.
+
+No automatic global placement/config reconciliation is required. Monitor
+detects drift against confirmed deployment inputs and reports it; local
+liveness recovery follows explicit policy. Repair/upgrade progress and failure
+remain visible to every authorized UI through shared operation records.
+
+## 11. Configuration and observability
+
+Local startup inputs are limited to bootstrap prerequisites and process/runtime
+settings: persistent root, management interfaces, listen/advertise addresses,
+optional discovery seeds, trust inputs and runtime endpoint. Group 0 owns
+confirmed cluster topology and management intent. This does not move every
+process tuning parameter into Group 0; respect the existing
+[configuration architecture](../config/design-crowdb-config.md).
+
+Expose:
+
+- Discovery peer state, endpoint reachability, last observation/expiry,
+ incompatible identities and multicast-interface status.
+- Bootstrap manifest identity, preparation/start/publication progress and the
+ reason general deployment is gated.
+- Group-0 authority/renewal status, rejected stale operations and recovery needs.
+- Desired deployment inputs, actual image digest/resources, process/container
+ health, drift and operation outcomes.
+
+Keep local logs/crash artifacts persistent and collect asynchronously. Do not
+replicate every log line into Group 0 or expose credentials in discovery/UI
+diagnostics.
+
+## 12. Correctness and validation
+
+- Restart and endpoint changes preserve identity; duplicated roots cause a
+ visible conflict (I1).
+- Three monitors discover each other on a real test bridge and LAN; expiry
+ affects candidates, not confirmed topology (I2).
+- Multiple UIs can edit local drafts, but only the accepted manifest becomes
+ shared topology. General deployment stays gated through incomplete publication
+ (I3, I8).
+- Concurrent overlapping bootstraps, partial preparation, initiator crashes
+ and interrupted publication recover without conflicting replicas or identity
+ replacement (I4).
+- Partition tests distinguish reachable replicas from quorum authority and
+ deliver delayed/retried commands to verify stale-action rejection (I5).
+- Multiple virtual racks on one host preserve one physical failure-domain
+ identity (I6).
+- Docker bridge and Linux containerd host-network profiles use the same image
+ digest and exercise restart, persistent disk/state and resource boundaries
+ (I7).
+- Existing Group-0 membership changes remain governed by reconfiguration;
+ deployment-node discovery never changes a voting set.
+
+Before adoption, measure discovery latency/traffic and stale-view bounds at the
+intended LAN size. Broad ecosystem use of mDNS is not proof of unlimited
+single-domain scalability.
+
+## 13. Decisions still required
+
+The architectural flow is settled; the following mechanisms need explicit
+agreement before their implementation:
+
+- **Bootstrap coordination:** one selected coordinator with durable monitor
+ preparation and resumable delivery, or a quorum/prepare protocol over fixed
+ selected membership. Define conflicts, cancellation and recovery; avoid
+ assuming local reservations alone establish global agreement.
+- **Strict node activity policy:** decide whether Group-0 authorization expiry
+ stops existing data-serving processes, including reads, or only gates
+ management/new deployment while data groups retain their own authority.
+ Define expiry bounds and fencing enforcement. Group-0 recovery and diagnostic
+ processes remain exempt from general deployment gating.
+- **Admission trust:** explicit UI approval with authenticated management peers,
+ or preprovisioned deployment credentials for automatic admission. Specify how
+ every authorized UI obtains access without public secret replication.
+- **Pre-admission identity allocation:** retain canonical numeric node IDs with
+ a defined collision/allocation scheme, or use a durable discovery identity
+ mapped to a confirmed node ID at admission. IP-derived identities are excluded.
+- **Monitor placement:** a host service controlling containers, or a dedicated
+ management container with narrowly scoped runtime access. Choose the production
+ recovery/security boundary independently of application image compatibility.
diff --git a/doc/design/diskio/design-crowdb-diskio.md b/doc/design/diskio/design-crowdb-diskio.md
index f57df351f..d03d1eed4 100644
--- a/doc/design/diskio/design-crowdb-diskio.md
+++ b/doc/design/diskio/design-crowdb-diskio.md
@@ -474,13 +474,14 @@ Two registration modes:
A `PollingMode` config per pipeline:
-- **Classic** — `io_uring_wait_cqe_timeout` every iteration, 50ms
- timeout. Simplest mode; used by crowdb-tree's single-pipeline
- topology.
+- **Classic** — event-wait on the poll thread's private eventfd with a
+ bounded timeout. Both submissions and kernel completions wake it;
+ no pipeline waits on an empty CQ while another pipeline has ready work.
+ Used by crowdb-tree's single-pipeline topology.
- **Hybrid** `{ busy_poll_budget }` — busy-poll the CQ ring's shared
memory via `io_uring_peek_cqe` with no syscall while I/O is active.
After `busy_poll_budget` consecutive empty peeks, transition to
- `epoll_wait` on the pipeline's eventfd. Any CQE resets the counter,
+ `epoll_wait` on the poll thread's private eventfd. Any CQE resets the counter,
returning to busy-poll. Gives sub-µs CQE dispatch during I/O bursts,
sleeps when idle, no core burned at idle. Default for `UringEngine`.
- **Sqpoll** `{ sq_thread_idle }` — opt-in for sustained high-IOPS:
@@ -492,6 +493,12 @@ When multiple Hybrid-mode pipelines share a poll thread, the thread
uses the **minimum** `busy_poll_budget` across its assigned Hybrid
pipelines — the most conservative budget wins.
+Each pipeline registers its owning poll thread's private eventfd with
+`io_uring`. The poll thread drains ready CQEs from every assigned pipeline
+and then signals the separate pipeline eventfds consumed by external
+reactors. External notifications therefore follow callback completion;
+external consumers cannot steal the poll thread's wakeup.
+
### 7.4 Batched SQE Submission
Client threads fill SQE slots (lock-free CAS on shadow tail + per-slot
@@ -501,12 +508,12 @@ thread submits all pending SQEs in one `io_uring_enter` per iteration
`io_uring_enter`; only the polling thread does.
In busy-poll mode (I/O active), SQEs are submitted within <1µs of
-being filled. In event-wait mode (idle), the first client to fill an
-SQE after the thread sleeps writes the pipeline's eventfd (waking the
-thread); subsequent clients in the same burst skip the eventfd write
-via a `thread_sleeping` CAS — at most one eventfd write per sleep
-cycle. For N=32 SQEs in a burst: batching = 2 syscalls, unbatched =
-32. **16× reduction.**
+being filled. The first client changing `pending_submit` from false to
+true writes the owning poll thread's private eventfd. Subsequent clients
+in the same pending batch skip that write. The eventfd remains readable
+if the thread has not entered event-wait yet, so a submission cannot lose
+its wakeup. The thread publishes newly pending SQEs after waking and
+before draining completions.
### 7.5 Pipeline Topology
diff --git a/doc/design/kv/design-crowdb-kv-server.md b/doc/design/kv/design-crowdb-kv-server.md
index 556eaa905..cf0699df4 100644
--- a/doc/design/kv/design-crowdb-kv-server.md
+++ b/doc/design/kv/design-crowdb-kv-server.md
@@ -45,8 +45,20 @@ library.
- **`memory`** — in-memory, non-durable. Explicit low-durability choice
for tests and dev.
-`--kv-backend` (`text` default, or `block` for `O_DIRECT`) only applies
-when `--kv-engine crowdb-tree` is selected.
+`--kv-backend` defaults to `block`: the tree uses an aligned block page
+store with direct I/O on Linux. `file` explicitly selects buffered file
+storage, and `mem-block` explicitly selects an in-memory block device.
+
+`--wal-backend` defaults to `block-device`: aligned real-file block storage
+with direct I/O on Linux and durable synchronization before acknowledgement.
+`file`, `uring` and `mem-block` remain explicit alternatives. File-backed
+device simulation still traverses the filesystem and can incur variable VFS
+and filesystem-log synchronization latency. Backend selection does not disable
+durable synchronization.
+
+Backend selection must match an existing data directory. Defaults select the
+backend for a new deployment; they do not convert stored tree formats. Existing
+file-backend deployments continue to specify `file` explicitly on restart.
### 2.2 Startup ordering
diff --git a/doc/design/kv/design-crowdb-kv-wal.md b/doc/design/kv/design-crowdb-kv-wal.md
index 7de8dbdbf..cfc7212bd 100644
--- a/doc/design/kv/design-crowdb-kv-wal.md
+++ b/doc/design/kv/design-crowdb-kv-wal.md
@@ -348,6 +348,13 @@ The split matters: replay/restore are purely *local* (this node's WAL), but `Acc
### 6.1 Replay — rebuild acceptor state (`replay_group`)
1. Discover all segments under `/group/seg-*.ck`, order by `segment_id`.
+ A zero-byte final segment on each WAL disk can result from a crash between
+ file creation and header writing. Remove that empty tail; it contains no
+ acknowledged records. A nonempty unreadable segment, invalid header, or
+ empty intermediate segment prevents local recovery; never skip it and serve
+ a partial acceptor history. The direct block backend
+ aligns physical reads and copies the requested byte range, including short
+ header reads and record ranges spanning block boundaries.
2. Walk each segment's records in order; verify `magic`, `version`, `crc32c`.
On the first failure, **truncate that segment at the offset and stop** (a torn
tail from a crash mid-write); later segments are still processed.
diff --git a/doc/design/tree/design-crowdb-tree-engine.md b/doc/design/tree/design-crowdb-tree-engine.md
index 6cf4c78b2..5c9ad8203 100644
--- a/doc/design/tree/design-crowdb-tree-engine.md
+++ b/doc/design/tree/design-crowdb-tree-engine.md
@@ -505,7 +505,7 @@ guard). Design rules:
- **Allocator seam.** `alloc()` routes owned allocations larger than
`kInlineCap` through a single internal allocator hook (today: glibc
`malloc`); a size-classed pool or RDMA-pinned allocator could slot in here
- later with no call-site changes. See [`todo_code.md`](../../todo_code.md) for
+ later with no call-site changes. See [`todo_code.md`](../../working/todo_code.md) for
why that hasn't been done speculatively.
- **MemTable = `absl::btree_map`.** The KEY stays
`std::string`, the VALUE is a `cell_entry{slot, flags, cell}`. The
diff --git a/doc/design/tree/design-crowdb-tree.md b/doc/design/tree/design-crowdb-tree.md
index dd5ed61fd..781a470b4 100644
--- a/doc/design/tree/design-crowdb-tree.md
+++ b/doc/design/tree/design-crowdb-tree.md
@@ -13,7 +13,7 @@ consumed from the Rust `crowdb-kv` crate over a C ABI. It records the decisions
behind that design and maps the sub-design documents. `libcrowdb-tree` is fully
implemented, wired into `crowdb-kv` (`CrowdbTreeEngine`), and shipped; this document
set is the durable record of *why* it looks the way it does, not a build plan.
-See [`todo_code.md`](../todo_code.md) for anything still open.
+See [`todo_code.md`](../../working/todo_code.md) for anything still open.
## Table of Contents
diff --git a/doc/doc_index.md b/doc/doc_index.md
index 02affbead..bc01fa918 100644
--- a/doc/doc_index.md
+++ b/doc/doc_index.md
@@ -20,6 +20,7 @@ listed document or section needed by the task.
| `doc/design/rpc/design-crowdb-rpc.md` | RPC engine, wire format, FFI, and transport. |
| `doc/design/console/design-crowdb-console.md` | Console architecture and service lifecycle. |
| `doc/design/config/design-crowdb-config.md` | Configuration ownership, precedence, validation, reload. |
+| `doc/design/deploy/design-crowdb-deploy.md` | Node discovery, Group-0 bootstrap, multi-node deployment. |
| `doc/design/access-server/design-crowdb-access-server.md` | S3, Iceberg, native Dataset access, and GPU delivery. |
## Backlog (`doc/backlog/`)
@@ -121,6 +122,7 @@ Temporary plans live under `doc/working/`; flow analyses live under
| `doc/design/access-server/iceberge/iceberg-table-spec-1.11.0.md` | Backed-up normative Iceberg table format specification. |
| `doc/design/access-server/iceberge/iceberg-rest-catalog-open-api-1.11.0.yaml` | Backed-up normative Iceberg REST Catalog OpenAPI. |
| `doc/design/access-server/dataset/design-crowdb-access-dataset.md` | Dataset HTTP, native topology access, and GPU delivery. |
+| `doc/design/access-server/dataset/spec-crowdb-dataset.md` | Normative Dataset concepts, behavior, invariants, and compatibility. |
Prefer the most specific match. Open a root design only for cross-topic work;
update its row when a permanent document is renamed or materially rescoped.
diff --git a/doc/draft.md b/doc/draft.md
deleted file mode 100644
index 5195820cd..000000000
--- a/doc/draft.md
+++ /dev/null
@@ -1,289 +0,0 @@
-
-
-
-# Docker Hub repository drafts
-
-Editing notes (do not paste this section into Docker Hub):
-
-- Copy each description into **Description**, select the suggested categories,
- and copy the content under **Repository overview** into the overview editor.
-- These are English drafts for two different user audiences.
-- The S3 image publication is being added to the release workflow. Publish its
- tags before posting the S3 quick start as available to users.
-- Examples follow the current container configuration and retained client tests.
- They have not been rerun against the public registry images for this draft.
-- Category names follow [Docker Hub repository information](https://docs.docker.com/docker-hub/repos/manage/information/).
-
-## crowdb/crowdb-s3
-
-### Description
-
-S3-compatible object storage with native CROWDB storage, in one container for development and testing.
-
-### Category
-
-- Databases & storage
-- Developer tools
-
-### Repository overview
-
-# CROWDB S3
-
-Run S3-compatible object storage locally with one Docker command. Use boto3 to
-create buckets, upload objects, and read them back without setting up a
-separate storage cluster.
-
-CROWDB is an open-source distributed storage platform. This image packages its
-storage services and S3 endpoint into a single-node development environment.
-
-## Supported operations
-
-- Bucket creation, listing, existence checks, and deletion.
-- Object upload, overwrite, download, metadata retrieval, and deletion.
-- Prefix and delimiter listing with ListObjectsV2 and continuation tokens.
-- Range and conditional reads.
-- Multipart upload, part replacement, part listing, completion, and abort.
-- AWS Signature Version 4 authentication and presigned request verification.
-- Content-MD5 validation and S3-style ETags.
-
-The repository includes boto3 end-to-end tests for core operations, multipart
-uploads, interrupted requests, and recovery after service restart.
-
-## Quick start
-
-Requires Docker on Linux amd64, or a Docker environment capable of running
-Linux amd64 containers.
-
-```sh
-docker run -d --name crowdb-s3 \
- --mount type=volume,source=crowdb-s3-data,target=/opt/crowdb/data \
- -p 127.0.0.1:9091:9091 \
- crowdb/crowdb-s3:latest
-```
-
-Check startup and wait until the health status is `healthy`:
-
-```sh
-docker inspect --format '{{.State.Health.Status}}' crowdb-s3
-docker logs crowdb-s3
-```
-
-Display the generated client credentials:
-
-```sh
-docker exec crowdb-s3 crowdb-monitor credentials show --format env
-```
-
-Set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_DEFAULT_REGION` in
-your client environment using the displayed values. Keep credentials private.
-The S3 endpoint is `http://127.0.0.1:9091` and uses path-style addressing.
-
-## Upload and download with boto3
-
-Install the client in your Python environment:
-
-```sh
-python -m pip install boto3
-```
-
-Run after setting the AWS environment variables above:
-
-```python
-import boto3
-from botocore.config import Config
-
-s3 = boto3.client(
- "s3",
- endpoint_url="http://127.0.0.1:9091",
- config=Config(
- s3={"addressing_style": "path"},
- request_checksum_calculation="when_required",
- response_checksum_validation="when_required",
- ),
-)
-
-s3.create_bucket(Bucket="example")
-s3.put_object(Bucket="example", Key="hello.txt", Body=b"Hello from CROWDB!")
-print(s3.get_object(Bucket="example", Key="hello.txt")["Body"].read())
-print(s3.list_objects_v2(Bucket="example")["Contents"])
-```
-
-The checksum configuration matches the retained container boto3 example.
-Compatibility with every SDK default or S3 tool is not implied.
-
-## Data and tags
-
-- Mount `/opt/crowdb/data` to retain data and generated credentials across
- container recreation. Reuse the same volume when recreating the container.
-- `latest` is the moving release tag; select a version tag for an explicit
- release. Version tags can be replaced when that release is republished;
- pin an image digest for exact image identity.
-- An optional console is available by also mapping container port `9090`.
-
-## Development preview scope
-
-This image runs a single-node profile with one voting replica and one copy of
-stored data. It provides no redundancy. Use disposable data for development,
-testing, and public evaluation. Production use and on-disk upgrade compatibility
-are not supported.
-
-The supported S3 API is a subset of Amazon S3. Server-side copy, batch deletion,
-versioning, lifecycle policies, bucket policies/ACLs, server-side encryption,
-object tags, replication, and event notifications are outside the current
-advertised surface. See the project documentation for updates.
-
-## Related image
-
-[crowdb/crowdb-iceberg](https://hub.docker.com/r/crowdb/crowdb-iceberg) provides
-an Iceberg-focused quick start. Both repositories contain the same runtime:
-one Access Server listens on S3 port `9091` and Iceberg port `9092`. Map both ports
-if you need both interfaces in one container. Their namespaces and authorization
-remain separate; general S3 buckets do not expose Iceberg table files.
-
-## Links and license
-
-- [Project website](https://crowdb.dev/)
-- [Source and issues](https://github.com/buzzcrow/crowdb)
-- [Container documentation](https://github.com/buzzcrow/crowdb/blob/main/container/single-node-container/README.md)
-- [License: Apache-2.0](https://github.com/buzzcrow/crowdb/blob/main/LICENSE)
-
-## crowdb/crowdb-iceberg
-
-### Description
-
-Apache Iceberg REST catalog with native CROWDB storage, in one container for development and testing.
-
-### Category
-
-- Databases & storage
-- Data science
-- Developer tools
-
-### Repository overview
-
-# CROWDB Iceberg
-
-Create Apache Iceberg tables, append data, and read it back with PyIceberg—all
-from a single container providing a REST catalog and native table-file storage.
-No separate object store is needed for this local workflow.
-
-CROWDB is an open-source distributed storage platform. Iceberg catalog, table,
-snapshot, and immutable file semantics are implemented directly over CROWDB's
-storage core.
-
-## What is included
-
-- An Apache Iceberg REST catalog endpoint.
-- Namespace and table operations, snapshot metadata, and atomic table commits.
-- Native immutable table-file storage and delegated FileIO credentials.
-- A single-node storage environment with automatic initialization and generated
- client credentials.
-- A web console, available on container port `9090`.
-
-The container acceptance suite exercises PyIceberg table creation, append,
-Arrow/pandas reads, and retrieval after service recovery and container restart.
-
-## Quick start
-
-Requires Docker on Linux amd64, or a Docker environment capable of running
-Linux amd64 containers. The example uses host port `9092`, matching the tested
-Iceberg catalog and file endpoint setup.
-
-```sh
-docker run -d --name crowdb-iceberg \
- --mount type=volume,source=crowdb-iceberg-data,target=/opt/crowdb/data \
- -p 127.0.0.1:9092:9092 \
- crowdb/crowdb-iceberg:latest
-```
-
-Check startup and wait until the health status is `healthy`:
-
-```sh
-docker inspect --format '{{.State.Health.Status}}' crowdb-iceberg
-docker logs crowdb-iceberg
-```
-
-Display the generated client credentials:
-
-```sh
-docker exec crowdb-iceberg crowdb-monitor credentials show --format env
-```
-
-Set `ICEBERG_TOKEN` in your client environment using the displayed value. Keep
-the token private. The REST catalog URI is `http://127.0.0.1:9092`.
-
-## Create and read a table with PyIceberg
-
-Install the clients in your Python environment:
-
-```sh
-python -m pip install 'pyiceberg[pyarrow]' pandas
-```
-
-Run after setting `ICEBERG_TOKEN`:
-
-```python
-import os
-
-import pandas as pd
-import pyarrow as pa
-from pyiceberg.catalog import load_catalog
-
-catalog = load_catalog(
- "crowdb",
- type="rest",
- uri="http://127.0.0.1:9092",
- token=os.environ["ICEBERG_TOKEN"],
-)
-
-catalog.create_namespace("demo")
-rows = pa.Table.from_pandas(
- pd.DataFrame({"order_id": [1, 2], "amount": [120, 80]}),
- preserve_index=False,
-)
-table = catalog.create_table("demo.orders", schema=rows.schema)
-table.append(rows)
-print(catalog.load_table("demo.orders").scan().to_pandas())
-```
-
-Use a fresh namespace/table for subsequent runs, or load the existing table
-instead of creating it again. The catalog provides the table's FileIO
-configuration; this example needs no separately configured S3 service.
-
-## Data and tags
-
-- Mount `/opt/crowdb/data` to retain tables and generated credentials across
- container recreation. Reuse the same volume when recreating the container.
-- `latest` is the moving release tag; select a version tag for an explicit
- release. Version tags can be replaced when that release is republished;
- pin an image digest for exact image identity.
-- To open the console, also publish `-p 127.0.0.1:9090:9090` and visit
- `http://127.0.0.1:9090`.
-
-## Development preview scope
-
-This image runs a single-node profile with one voting replica and one copy of
-stored data. It provides no redundancy. Use disposable data for development,
-testing, and public evaluation. Production use and on-disk upgrade compatibility
-are not supported. Dataset access and direct GPU delivery are not available in
-this image.
-
-The example covers a local PyIceberg workflow. Compatibility with every Iceberg
-engine, file format, or deployment configuration is not implied. Start with the
-published quick start and tested client recipes.
-
-## Related image
-
-[crowdb/crowdb-s3](https://hub.docker.com/r/crowdb/crowdb-s3) provides an
-object-storage-focused quick start. Both repositories contain the same runtime:
-one Access Server listens on Iceberg port `9092` and general S3 port `9091`.
-Map both ports if you need both interfaces in one container. Iceberg table
-storage and general S3 buckets have separate namespaces and authorization.
-
-## Links and license
-
-- [Project website](https://crowdb.dev/)
-- [Iceberg quick start](https://crowdb.dev/docs/quickstart/)
-- [Source and issues](https://github.com/buzzcrow/crowdb)
-- [Container documentation](https://github.com/buzzcrow/crowdb/blob/main/container/single-node-container/README.md)
-- [License: Apache-2.0](https://github.com/buzzcrow/crowdb/blob/main/LICENSE)
diff --git a/doc/working/dataset-todo.md b/doc/working/dataset-todo.md
new file mode 100644
index 000000000..68157d322
--- /dev/null
+++ b/doc/working/dataset-todo.md
@@ -0,0 +1,80 @@
+
+
+
+# CrowDB Dataset TODO
+
+Upstream design and contract:
+
+- `/cjdata/cpp/workshop/crowdb/proposal-storage/dataset-crowdb-scope.md`
+- `/cjdata/cpp/workshop/crowdb/proposal-storage/dataset-crowdb-design.md`
+- `/cjdata/cpp/workshop/crowdb/proposal-storage/dataset-use-cases.md`
+- `/cjdata/cpp/workshop/crowdb/proposal-storage/dataset-crowdb-plan.md`
+
+Goal: track the remaining design and implementation gaps required for a working
+Dataset core. R213–R226 now cover the completed authority, Manifest,
+publication, read-plan, retention, and API parity foundations. The Dataset SDK
+is intentionally deferred until the core Dataset path is usable.
+
+Current implementation status: R213–R226 are complete. Durable per-snapshot
+leases, ownership-aware reclamation, complete HTTP publication, and
+parity/recovery coverage are in place; only the deferred R220 SDK remains.
+
+The backlog items are ordered by dependency: publication recovery (R222),
+retention and GC (R225), then complete API parity (R226). Small wording or
+scope corrections belong in this TODO; implementation work belongs in the
+linked backlog item.
+
+## P0: publication and read correctness
+
+- [x] **Atomic snapshot publication** (R222): Manifest partitions, binding, publication state, and head advancement are now ordered so `latest` exposes only a complete Snapshot. Files: `lib/crowdb-access-dataset/src/authority.rs`, `record.rs`, `publication.rs`.
+- [x] **Publication retry and recovery** (R222): Prepared operations recover through their durable operation token; retries preserve one Snapshot ID and reject divergent requests.
+- [x] **Safe retention and GC** (R225): Make snapshot reclamation require retention/release authority and protect latest/stable snapshots, retained parent chains, and active reads. `reclaim_snapshot_metadata` must not unconditionally delete a live snapshot. Files: `authority.rs`, `retention.rs`, `lease.rs`.
+- [x] **Per-read GC protection** (R225): Track active Read Plans by Dataset and Snapshot, rather than using one global lease or one boolean for all reads. Protect the referenced payload until the read completes, is released, or expires under an explicit policy.
+
+## P1: Snapshot and Manifest model
+
+- [x] **Opaque locator contract** (R214): Store and pass the chunk location as an opaque location string according to the Dataset contract. Dataset code must not parse or infer chunk identity from the locator.
+- [x] **Manifest completeness validation** (R222): Persisted Manifest bindings now carry partition count and digest, reject zero/incomplete partitions, validate parent availability, and reject divergent re-publication.
+
+## P1: Read Plan, streaming, and resume
+
+
+## P1: Public control and data surfaces
+
+- [x] **Complete control API** (R226): Add the Dataset operations required by the contract: ListSnapshots, PrepareSnapshot, PublishSnapshot, OpenSnapshot, GetManifest, RetainSnapshot, and ReleaseSnapshot.
+- [x] **Complete read API** (R226): Add stable operations for GetSample, GetBatch, bounded Scan, StartReadPlan, NextBatch, SaveProgress, and Resume. HTTP and native surfaces must use the same authority and return equivalent logical results.
+- [x] **End-to-end HTTP publication** (R226): Make the HTTP client able to publish a complete validated Dataset snapshot, including Manifest persistence and publication completion, rather than publishing only an opaque reference.
+- [x] **Status and error mapping** (R226): Map not-found, conflict, invalid Manifest, checksum, transient, cancellation, backpressure, and service-unavailable states to stable native and HTTP errors.
+
+## P2: lifecycle and operational behavior
+
+- [x] **Mutable names** (R225): Implement and validate `latest` and `stable` as mutable names that never replace the immutable Snapshot ID stored in Read Plans or cursors.
+- [x] **Snapshot listing and inspection** (R226): Expose snapshot ancestry, publication state, Manifest completeness, retention state, and provenance for operators.
+- [x] **Dataset-owned payload lifecycle** (R225): Track Dataset-owned chunk references separately from external source references. External source refs must not control Dataset payload GC.
+- [x] **Shared payload reclamation** (R225): Reclaim a chunk only after no retained Snapshot and no active Read Plan references it, including inherited fields and shared locators.
+- [x] **Authentication and authorization scope** (R226): The HTTP service applies its configured bearer token to every Dataset route, covering management, snapshot access, publication, retention, reclamation, and cancellation; deployments can place finer-grained policy at the service boundary.
+
+## Verification gaps
+
+- [x] **Atomic publication tests** (R222): Cover prepared visibility, snapshot-write crash recovery, duplicate operation tokens, head advancement, and declared parent ancestry.
+- [x] **GC tests** (R225): Cover retained parent chains, latest/stable protection, active reads by Snapshot, lease expiry, shared chunks, external source refs, and idempotent deletion.
+- [x] **HTTP/native parity tests** (R226): Complete publication and bounded reads run through HTTP and native surfaces, with stable status mapping, durable lease cleanup, and restart recovery coverage.
+
+## Deferred
+
+- [ ] **Dataset SDK** ([R220](../backlog/R220-dataset-fat-client-plan.md)): Start only after the core Dataset publication, payload read, Snapshot membership, bounded streaming, retention, and resume semantics are working and covered by the tests above. The SDK must adapt the stable core contract rather than define new storage semantics.
+
+## Files to revisit
+
+- `lib/crowdb-access-dataset/src/authority.rs`
+- `lib/crowdb-access-dataset/src/manifest.rs`
+- `lib/crowdb-access-dataset/src/planner.rs`
+- `lib/crowdb-access-dataset/src/shuffle.rs`
+- `lib/crowdb-access-dataset/src/transport.rs`
+- `lib/crowdb-access-dataset/src/chunk_store.rs`
+- `lib/crowdb-access-dataset/src/retention.rs`
+- `lib/crowdb-access-dataset/src/cursor.rs`
+- `lib/crowdb-access-dataset/src/wire.rs`
+- `app/crowdb-access-server/src/dataset.rs`
+- `lib/crowdb-access-dataset/tests/`
+- `app/crowdb-access-server/tests/`
diff --git a/doc/working/plan-chunk-kv-cutover.md b/doc/working/plan-chunk-kv-cutover.md
new file mode 100644
index 000000000..05af12aa0
--- /dev/null
+++ b/doc/working/plan-chunk-kv-cutover.md
@@ -0,0 +1,258 @@
+
+
+
+# Chunk KV Cutover Plan
+
+Upstream: [Chunk KV Server design](../design/chunkds/design-crowdb-chunk-kv-server.md),
+sections 6–9. Results: [intel7960 test matrix](test.md).
+Goal: recover split/transfer at durable boundaries without lost writes, and
+make automatic placement stable, inexpensive and explainable.
+
+Temporary plan: completed implementation detail is condensed below; historical
+source and evidence remain in local commits and the referenced artifacts.
+Delete this plan after the remaining audits and acceptance are complete.
+
+## Current placement decision — 2026-10-09
+
+- R228 and R229 are deferred by the user. Resume Console/native acceptance
+ and the cutover audits; do not continue membership implementation or metrics.
+- Data Weight is deferred to [tree range metrics](../backlog/R228-tree-range-metrics.md).
+ Placement uses split counts with existing tolerance/cooldown; byte coefficient
+ must be zero. Console Weight and weighted explanation are hidden.
+- Tree metrics implementation is not part of this cutover work. No tree writer,
+ split/move execution or recovery changes are authorized by this deferral.
+- Size-driven weighted acceptance is superseded by the deferred requirement,
+ not marked passed or ignored. Retain count convergence/data integrity coverage.
+- Existing automatic split has count-shortfall and coarse-pack size triggers;
+ the latter and size ranking require replacement after tree metrics acceptance.
+
+## Status — 2026-10-09
+
+- Local commits: `1a471f43`, `407b5cab`, `a664e357`, `38389b0c`; no push performed.
+- Split retains the original parent tree/WAL and creates one child. Durable
+ handoff precedes memory routing; catalog publication is the external cutover.
+ Recovery opens the exact base, range-filters the inherited parent WAL, then
+ replays the child's WAL. Initial unpublished handoff derives the dispatch
+ frontier from child WAL evidence; final readiness/catalog fixes the published
+ frontier, even when the child's WAL is empty.
+- Verified fixes include prepared-target immutable root access, uncertain
+ handoff candidate reuse, current-writer routing, unpublished dispatcher reuse,
+ narrowed-range lazy recovery, aligned block-WAL reads/fail-closed replay,
+ narrowly scoped empty-final-tail cleanup, block tree/WAL defaults, I/O completion
+ wakeup and format-compatible bulk WAL byte encoding. Focused regressions pass.
+- Before the balance implementation, eight of nine Linux matrix tasks pass;
+ extra Rust Iceberg SDK passes 5/5.
+ Baseline full UI passes 225/225, C++ 942/942, Core 987/987, Storage 839/839.
+ Separately scheduled SDK cases ran and passed; no functional Linux skip is
+ accepted. This historical baseline is superseded by the complete current
+ Console acceptance below. Exact counts/times are in `test.md`.
+- No new crash observed in the latest acceptance attempts. Previous crash
+ investigation is retained under `/tmp/crowdb-core-investigation`.
+- File-backed simulated device sync delays are confirmed and accepted by the
+ user; sync stays enabled. Native API preparation may use 10 seconds and native
+ fixture cases 180 seconds; ordinary UI assertions/actions remain 3 seconds.
+- Transfer/publication race and native overlay lifetime items below are audits,
+ not confirmed defects or authorization for speculative production changes.
+
+## Constraints
+
+- Run builds, tests, lint and executables through `pixi run`; runtime suites
+ run sequentially. Commit only when asked; never push without authorization.
+- Preserve exact identities, epochs, root lineage, revision CAS and recovery
+ pins. No epoch guessing, caller retries, weakened assertions or deadline inflation.
+- Keep hot paths lock-free. Review any unavoidable lock, queue, blocking work or
+ material performance cost with the user before implementation.
+- The user authorizes balance algorithm/configuration and UI implementation.
+ Split/move execution, handoff and recovery changes still require prior review.
+ No new hot-path locks or speculative cutover changes are authorized.
+
+## 1. Placement policy and deferred tree metrics
+
+- [x] **Verify count-only placement and hidden Weight**: byte coefficient defaults
+ to zero and nonzero configurations are rejected. Both selectors retain shared
+ count scoring, 20% tolerance, 25% improvement, cooldown and safety filters.
+ Equal split counts must not move merely because shared-pack estimates differ.
+ Hide graph/property Weight and balance explanations; verify native Page flow.
+ Files: protocol/server config, balance tests, console components/catalog spec.
+ Verified: 24 focused Rust cases, 7 UI unit cases, Rust fmt/clippy, UI lint/build
+ pass. Actual native Page flow passes: browser 2.3s, owned cluster 345.98s;
+ zero ignored. Weight/property/explanation absence and Page inspection asserted.
+- [x] **Finish Console acceptance**: complete gate passes 350 Rust test
+ executions, zero failed/ignored, and all 23 native browser cases across four
+ fixture phases. Approved allocation fix reroutes only typed pre-send connection
+ failures to a changed owner endpoint once; ambiguous sent requests are never
+ replayed. Both regressions and all eight S3 cluster cases pass. Native Start
+ asserts the original response before process state and avoids duplicate cleanup
+ submissions. Measured ChunkKV Start is 789/3800ms; response budget is 5s,
+ process/DOM stays 3s. No core startup change. Full UI passes 229/229.
+ Evidence and matrix: `test.md`, balance-policy artifact directory.
+- Tree metrics/data-weight work is deferred to
+ [R228](../backlog/R228-tree-range-metrics.md), requiring core review before
+ implementation. Size-triggered split currently uses coarse pack estimates;
+ count-shortfall split remains available. No writer/checkpoint change here.
+- The previous size-weight acceptance failed at 968.43s because opened manifest
+ estimates did not update after ordinary flush. It is superseded by the deferred
+ metrics contract, not counted passed or ignored. Evidence remains under
+ `.crowdb-runtime/artifacts/balance-policy-20261009/`.
+
+## 2. Remaining cutover boundary audits
+
+Only change behavior when a concrete regression proves a gap; any core fix
+requires the user's review. Existing durable handoff/replay contracts stay intact.
+
+- [x] **Review publication boundaries**: compare S1–S6/T1–T8 with monitor and
+ local publication, revision CAS, grants and startup. Distinguish initial
+ unpublished handoff recovery from final published replay bounds.
+ Files: monitor `chunk_kv/catalog.rs`, server `catalog/transition.rs`, design.
+ Both publish immutable pages before expected-head revision CAS and recognize
+ exact successor entries idempotently; phase persistence is a subsequent CAS.
+ The remaining startup/abort races below are not proven by that comparison.
+- [ ] **Recover release before publication**: prove old-source admission stays
+ fenced by exact release/lease evidence and reconciliation can still start.
+ Files: server `main.rs`, `catalog/recovery.rs`, `serving/transition_runtime.rs`.
+ Review finding: old Serving catalog plus old valid grant reaches generic
+ `activate_recovered_partition`; the source branch does not read transfer
+ release proof. `suspend_for_transfer` fences the live lifecycle in memory.
+ Need a controlled restart regression before changing activation; do not claim
+ this window safe from ordinary transfer/state-machine tests.
+ Proposed reviewed scope: load durable transfer evidence once on the grant/
+ recovery control path, match exact source instance/partition/epoch, and keep
+ recovered source non-serving after release even if old catalog/grant remains.
+ Retain transition processing so publication can finish. No per-request read,
+ lock, WAL rerouting or epoch change. Controlled restart regression must prove
+ old-source mutation rejection and eventual target recovery. Await approval.
+- [ ] **Repair publication before phase marker**: cover owner and Serving
+ publication independently; exact catalog entries and proofs allow idempotent
+ repair and cannot reactivate the source. Files: server `server/activation.rs`,
+ monitor `chunk_kv/catalog.rs`, `catalog_transition_test.rs`, transfer recovery tests.
+- [x] **Verify split abort**: reject abort after durable handoff or with stale
+ proof; preserve child WAL omitted by the old catalog and resume the same split.
+ Files: server `serving/split.rs`, local split owner, split abort/recovery tests.
+ `control_store_test`, `split_prepare_retry_test` and handoff recovery tests pass;
+ committed proof cannot be cleared/replaced, unknown uncommitted status permits
+ exact abort, and committed dispatch resumes the same base.
+- [ ] **Reconcile published split before marker**: recover both exact successor
+ assignments and matching authority; historical routing cannot select an old
+ writer over the current assignment. Files: server activation/reconcile and
+ `split_recovery_test.rs`.
+- [ ] **Verify grant-generation refresh**: immediate catalog refresh is already
+ implemented; prove new grants do not leave routes fenced until periodic refresh.
+ Files: server `main.rs`, native S3/Iceberg catalog-change acceptance.
+- [ ] **Order abort against publication**: controlled interleavings must prove
+ monitor fencing, transition revision and expected-head CAS exclude stale
+ publication. Files: monitor catalog, local transition and catalog transition tests.
+- [ ] **Retire exact candidate resources**: durable abort precedes idempotent
+ cleanup; preserve source references, base pins, retry floors and grace. Verify
+ monotonic candidate epochs across abort/restart. Files: transition worker,
+ monitor, transfer recovery tests.
+- [ ] **Audit native overlay lifetime**: prove raw source-tree pointers remain
+ valid when external/session handles disappear, finalization fails or overlays
+ are removed concurrently. Establish a failing regression before ownership changes.
+ Files: `lib/crowdb-chunk-kv/src/partition/tree.rs`,
+ `lib/crowdb-tree/ffi/src/tree.rs`, `lib/crowdb-tree/src/btree/memtable_sources.cpp`.
+ C++ stores a raw source-tree pointer; Rust's safe install method takes a
+ temporary borrow and does not retain source ownership. Normal successful
+ finalization clears the overlay, but failed finalization/source-handle loss
+ and readers concurrent with clear still need a lifetime regression. No new
+ crash has been observed and no ownership fix is applied.
+
+- Deferred **membership CAS contract (R229)**: exact UI topology reproduction
+ also fails (2/3 ready groups). User requires sequential UI additions and shared
+ KV-client/server protection with explicit rejection across UI servers.
+ Approved: one complete members record, epoch CAS, Installing/Ready completion
+ fencing, exact-epoch recovery, no separate operation ID or legacy compatibility.
+ Sequential topology fixture passes 5/5 (33.1s), with the original leader
+ assertions/budgets unchanged. Approved contract and recovery boundaries are in
+ [R229](../backlog/R229-kv-membership-conflict.md), executed through
+ [membership plan](plan-kv-membership-cas.md); no membership core change
+ is applied.
+
+Native lifecycle follow-up: recorded restart opens its first tree root at
+09:21:27.697, then three more roots at 09:21:30.298–30.485; the RPC listener
+starts at 09:21:30.532 (about 3.15s after process startup). Recovery is sequential
+and includes journal replay. These timestamps do not attribute the delay to
+fsync. The user authorized a measured, modest Start-response budget after
+reviewing timing distribution; 10s is not the default target. The focused run passes with a 5s response budget: ChunkDB 215ms, DiskIO
+366ms, ChunkKV 789ms, Access 178ms (browser 9.2s; owned fixture 375.59s).
+Full Console provides a second Start sample: ChunkDB 255ms, DiskIO 466ms,
+ChunkKV 3800ms, Access 148ms. Keep the 5s native response budget: 3s fails
+legitimate recovery, while these samples do not justify 10s. These samples
+and the prior 3.15s listener sample establish variation, not percentiles. Keep per-service millisecond output for full-suite verification. Process/DOM assertions stay at 3s. Recovery and
+readiness behavior are unchanged. Complete UI passes 229/229; the native
+full Console rerun passes (350 Rust executions, zero failed/ignored; all
+23 native browser cases, including the three dedicated phases).
+
+## 3. Final verification and documentation
+
+- [ ] **Controlled crash/race coverage**: consolidate before/after durable-step
+ tests, ambiguous replies, stale revisions/grants, concurrent checkpoints and
+ both restarts. Assert exclusive ownership, WAL order, acknowledged values,
+ exact-root lineage and eventual recovery. Capture authoritative metadata before
+ teardown for any newly failing transition; retain actual crash/core evidence.
+- [x] **Complete Console matrix**: rerun native inspection, journal interruption
+ and dedicated production split/count-placement phases after confirmed changes. Eight
+ independent tasks already pass; rerun them only where new diffs affect them.
+ Complete task passes: 350 Rust executions, zero failed/ignored; all 23 native
+ browser cases run across four fixture phases. Updated `test.md` with the
+ approximate 1588.13s complete task interval and separate phase timings.
+- [ ] **Performance and quality gates**: inspect scans/serialization/locking and
+ bound planning/diagnostic work. Run affected tests, fmt, clippy, relevant C++
+ gates and UI lint/specs. Make performance claims only from measured workloads.
+- [ ] **Reconcile permanent docs**: formula, thresholds and UI contract are
+ updated. Track deferred tree metrics separately and resolve remaining cutover
+ Open Issues only after review and passing regressions; remove this plan after
+ final acceptance.
+
+## Latest focused audit verification
+
+- Independent native phases now pass without ignored cases: prerequisite-plan
+ resumption 19.32s, journal owner interruption 54.26s, and production split/
+ count convergence with complete data checks 350.85s (4/4/4).
+ The plan fixture pauses after ChunkDB rather than before its three instances
+ exist. UI observes registered DiskIO restart every 2s when Capacity is not
+ loaded; 20 focused hook tests pass. Serial deployment responses are checked
+ separately before durable progress. Core storage/transition behavior is unchanged.
+ Logs: `native-plan-response-order.out`, `native-journal-final.out`, and
+ `native-count-final.out` under the balance-policy artifact directory.
+- Full workspace Rust clippy, fmt, CI task mapping and UI lint/build pass.
+ Complete UI passes 166 unit plus 63 browser cases (229/229), 307.40s.
+ An earlier complete rerun passed automatic service
+ deployment but failed bucket clicking because creation consumed 2.92s of the
+ same action window. The test now asserts the bucket creation response before
+ selecting its UI item; each original action/response budget remains 3s.
+- Reload failure is diagnosed separately: the old page's canceled server-list
+ fetch persisted `failed` over a saved `waiting` plan. Page unload now stops
+ the runner and guards cancellation paths before failure publication; cached
+ history restoration reloads persisted authority. The 21 hook tests and entire
+ eight-case rack/node spec pass (32.5s browser). Full UI acceptance passes
+ in `ui-reload-acceptance-final.out`; no timeout or assertion is weakened.
+- 21 server cases pass across catalog transition, control store, split recovery,
+ current assignment, transfer state machine and transfer storage.
+- 10 library cases pass across native split handoff, split handoff recovery,
+ ingress and prepared-candidate retry. Zero ignored. These are focused evidence,
+ not complete real-process coverage of every listed crash/race window.
+- Current count-only Core passes 994/994, zero ignored; UI passes 166 unit and
+ 63/63 browser cases. Earlier Group 1990 replica 19902 remained unknown/term 1
+ while its leader was term 2 in a failed observation window. Preserved trace:
+ `.crowdb-runtime/artifacts/balance-policy-20261009/ui-leader-election-failure/`.
+ Log: `.crowdb-runtime/artifacts/balance-policy-20261009/count-only-core.out`.
+ Tree metrics remain deferred to R228.
+
+## Evidence and file inventory
+
+- Successful final independent matrix:
+ `.crowdb-runtime/artifacts/measure-tests/20261009T022131.804646Z/`.
+ Core/Storage/Access: `20261009T002906.349723Z/` under the same directory.
+- Latest UI/Core/Storage pass: `20261009T053116.399322Z/` under the same
+ measurement directory. Latest incomplete Console attempt:
+ `20261009T052410.043897Z/`. Balance-specific focused results and final
+ clippy output: `.crowdb-runtime/artifacts/balance-policy-20261009/`.
+ Withdrawn readiness/order experiments:
+ `.crowdb-runtime/artifacts/native-catalog-readiness-experiments/`.
+ Earlier speculative cutover changes:
+ `.crowdb-runtime/artifacts/cutover-review-withdrawn/`.
+- Production: protocol policy/status; Group-0 monitor/catalog; ChunkKV server
+ storage, serving, routing, activation/reconcile; owning ChunkKV/tree libraries.
+- Unit/integration: server planning, control-store, split/transfer recovery,
+ catalog transition and native overlay tests. E2E: web native provisioning/
+ balance and existing catalog, journal, Iceberg/S3 browser specs.
diff --git a/doc/working/plan-kv-membership-cas.md b/doc/working/plan-kv-membership-cas.md
new file mode 100644
index 000000000..1531f9d25
--- /dev/null
+++ b/doc/working/plan-kv-membership-cas.md
@@ -0,0 +1,77 @@
+
+
+
+# KV Membership CAS Plan
+
+Upstream: [R229](../backlog/R229-kv-membership-conflict.md).
+Goal: publish complete members through epoch CAS and reject concurrent/stale
+configuration submissions before they can change durable or running groups.
+
+Execution is deferred by the user on 2026-10-09. The first protocol/client task
+is committed as `339ef482`; do not resume the remaining tasks until requested.
+
+## Protocol and shared authority
+
+- [x] **Implement record and KV client CAS**: add complete member/epoch/state
+ types, canonical validation, a typed group-members key and linearizable
+ snapshots. Fresh creation starts Installing at epoch 1; changes require the
+ expected Ready epoch. Preserve previous members during installation for
+ removal/restart scope. Complete only the exact Installing snapshot by CAS.
+ Files: protocol `kv_membership.rs`, `key/kv_cluster.rs`;
+ KV client `membership.rs`; protocol/client integration tests.
+- [ ] **Wire new group authority**: creation, bootstrap and membership readers
+ use complete records; remove unconditional/member-per-key mutation authority
+ without legacy fallback. Files: client `transport/cluster.rs`,
+ `hardware/sysmd.rs`; Console `ops/kv_logical/publication.rs`.
+
+## Server submission and propagation
+
+- [ ] **Guard before durable write**: validate epoch/full configuration and
+ atomically protect the persistence-to-publication interval. Include remote
+ add/remove/batch, replacement, restore and reconcile. Inspect lifecycle
+ cancellation and persisted config identity before selecting the guard.
+ Files: KV server `mgmt/replica_ops.rs`, `recovery/group_rebuild.rs`;
+ KV `cluster/px_kv_store.rs`, `group_config.rs`, `node_config.rs`.
+- [ ] **Propagate one authorized epoch**: KV client management submission sends
+ complete configuration/epoch; Console maps conflicts to 409. Confirm exact
+ installation/fencing before Ready. Capture no-effects vs unknown failures;
+ never clear Installing on timeout. Files: client `transport/cluster.rs`;
+ Console `ops/kv_logical.rs`; Web `mgmt/replica_ops.rs`.
+- [ ] **Resume after interruption**: restore durable successor, reconcile the
+ exact authoritative epoch and reject delayed prior configuration/completion.
+ Required removal/fencing scope must survive coordinator restart.
+
+## Acceptance and completion
+
+- [ ] **Control-plane races**: real Group-0 CAS concurrent clients, Installing
+ rejection, stale expected epoch, exact completion, stale completion and
+ independent groups. Then controlled local persistence/publication overlaps.
+- [ ] **Crash and unknown outcome**: lost CAS/reply, partial installation,
+ restart after durable write and before memory publication; delayed old request.
+- [ ] **UI and docs**: sequential submissions and explicit conflict across two
+ UI servers. Update permanent reconfiguration/console specifications once
+ implemented. Keep topology setup separate from concurrent conflict coverage.
+- [ ] **Gates and cleanup**: affected protocol/client/server/Console tests,
+ Rust fmt/clippy and affected UI spec; requirement commits include only this
+ requirement's coherent work. Preserve other uncommitted changes. Never push.
+ Delete R229, its index entry and this plan only after all acceptance passes.
+
+## Verification
+
+- Protocol 2/2 and real Group-0 CAS 1/1 passed, zero ignored. Coverage includes
+ independent clients, Installing rejection, stale completion, exact Ready
+ completion confirmation and independent groups. Workspace fmt and affected
+ protocol/client all-target clippy passed. Production submission is not wired
+ yet; this does not establish server persistence/publication safety.
+- Protocol: `pixi run cargo test -p crowdb-protocol --test kv_membership_test`.
+- Shared CAS: `pixi run cargo test -p crowdb-kv-client --test membership_cas_test`.
+- Server/Console: commands from R229; runtime suites sequential and owned.
+- UI: affected replica dialog tests and topology/conflict browser scenarios.
+- Gates: `pixi run rs-fmt-check` and affected-crate clippy with `-D warnings`.
+
+## Adjacent unfinished work
+
+Native lifecycle response still exceeds its unchanged 3s budget; investigate
+actual recovery cost rather than claiming fsync or weakening tests. Complete
+Console/native matrix remains separate from this requirement. Chunk-KV cutover
+core proposals retain their own review boundary.
diff --git a/doc/working/plan-test-task-completeness.md b/doc/working/plan-test-task-completeness.md
new file mode 100644
index 000000000..4fba8c22f
--- /dev/null
+++ b/doc/working/plan-test-task-completeness.md
@@ -0,0 +1,52 @@
+
+
+
+# Access and Iceberg Test Task Completeness Plan
+
+Upstream: [test matrix](test.md), existing Pixi SDK preparation tasks.
+Goal: each named task runs all its functional acceptance cases in one invocation,
+including environment-specific preparation, without manual follow-up or retries.
+
+## Tasks
+
+- [x] **Close task coverage**: `test-access` currently leaves
+ `official_boto3_recognizes_a_copy_error_after_http_200_and_keepalives` to another
+ task. Dispatch that exact test in the pinned Boto3 environment from the same
+ parent task. Reuse its SDK environment setup; avoid running the full Boto3
+ suite unnecessarily. `test-iceberg-e2e` must invoke the existing Java FileIO
+ preparation task for all three `official_java_` cases after its Python/native
+ phases. Keep sequential owned cluster execution and fail-fast behavior.
+ Files: `tools/pixi-tasks/test-access.sh`, `test-iceberg-e2e.sh`, existing SDK
+ setup scripts and `pixi.toml` only if a narrowly scoped task is needed.
+- [ ] **Verify exact case inventory**: distinguish helper subprocess entry points
+ from acceptance cases. The native fault-listener child is executed by its
+ parent crash matrix, not a standalone acceptance case. Keep manual stress
+ profiles explicit; list any remaining exclusions and reasons instead of
+ counting another task's results as this task's completion.
+- [~] **Verify one-call acceptance in CI**: run `pixi run test-access` and
+ `pixi run -e iceberg-e2e test-iceberg-e2e` sequentially, capture complete logs
+ and independent wall times. Confirm the Boto3 case and all three Java cases
+ execute and pass in their parent task invocation. No retry loop, weakened
+ assertion or core code change. Missing required dependencies fail the task.
+- [ ] **Reconcile counts and matrix**: deduplicate cases by test binary/name
+ across ordinary and explicit ignored-test dispatches. Do not add ignored
+ lines to totals after that exact case passed in a later phase of the same
+ invocation. Mark complete only after the entire task exits successfully;
+ update counts, actual timings and retained logs in `test.md`.
+
+## Verification
+
+- Existing Rust assertions remain unchanged; this is task orchestration work.
+- Check modified shell syntax through `pixi run bash -n