diff --git a/backend/deploy/e2e/docker-compose.e2e.yml b/backend/deploy/e2e/docker-compose.e2e.yml
index cedfa2af7..f5fcef319 100644
--- a/backend/deploy/e2e/docker-compose.e2e.yml
+++ b/backend/deploy/e2e/docker-compose.e2e.yml
@@ -65,8 +65,10 @@ services:
volumes:
- ./fake-codex:/opt/fake-codex:ro
- artifacts:/var/lib/codespace/artifacts
+ - spool:/var/lib/codespace/spool
ports:
- "18081:8080"
volumes:
artifacts:
+ spool:
diff --git a/backend/tests/CodeSpace.UnitTests/Settings/DurableStorageTopologyTests.cs b/backend/tests/CodeSpace.UnitTests/Settings/DurableStorageTopologyTests.cs
new file mode 100644
index 000000000..a18737531
--- /dev/null
+++ b/backend/tests/CodeSpace.UnitTests/Settings/DurableStorageTopologyTests.cs
@@ -0,0 +1,47 @@
+using YamlDotNet.RepresentationModel;
+using Shouldly;
+
+namespace CodeSpace.UnitTests.Settings;
+
+///
+/// The process spool is the recoverable source of truth until its redacted streams reach durable artifact CAS.
+/// Preparing the directory in an image is not persistence: a worker replacement must see the same bytes.
+///
+[Trait("Category", "Unit")]
+public class DurableStorageTopologyTests
+{
+ [Theory]
+ [InlineData("docker-compose.yml", "codespace-spool")]
+ [InlineData("backend/deploy/e2e/docker-compose.e2e.yml", "spool")]
+ public void Worker_spool_is_a_declared_named_volume(string relativePath, string volumeName)
+ {
+ var root = Mapping(Load(relativePath));
+ var services = Mapping(root.Children[Scalar("services")]);
+ var worker = Mapping(services.Children[Scalar("worker")]);
+ var mounts = Sequence(worker.Children[Scalar("volumes")]).Children.Select(value => ScalarValue(value)).ToArray();
+ var volumes = Mapping(root.Children[Scalar("volumes")]);
+
+ mounts.ShouldContain($"{volumeName}:{CodeSpace.Core.Settings.DurableRoots.ContainerAgentRunSpool}");
+ volumes.Children.Keys.Select(ScalarValue).ShouldContain(volumeName);
+ }
+
+ private static YamlNode Load(string relativePath)
+ {
+ using var reader = File.OpenText(Path.Combine(FindRepoRoot(), relativePath));
+ var yaml = new YamlStream();
+ yaml.Load(reader);
+ return yaml.Documents.Single().RootNode;
+ }
+
+ private static YamlMappingNode Mapping(YamlNode node) => node.ShouldBeOfType();
+ private static YamlSequenceNode Sequence(YamlNode node) => node.ShouldBeOfType();
+ private static YamlScalarNode Scalar(string value) => new(value);
+ private static string ScalarValue(YamlNode node) => node.ShouldBeOfType().Value!;
+
+ private static string FindRepoRoot()
+ {
+ for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir is not null; dir = dir.Parent)
+ if (Directory.Exists(Path.Combine(dir.FullName, "backend"))) return dir.FullName;
+ throw new InvalidOperationException("repo root not found");
+ }
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index 7c567f908..dc4dcb4ea 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -126,7 +126,11 @@ services:
# Sandbox__RequireConfinement: "true"
volumes:
- codespace-artifacts:/var/lib/codespace/artifacts
+ # The spool is the recoverable source until stdout/stderr segments reach artifact CAS. Keep it across worker
+ # replacement and share it between same-host worker replicas; production multi-host deployments require RWX.
+ - codespace-spool:/var/lib/codespace/spool
volumes:
codespace-pgdata:
codespace-artifacts:
+ codespace-spool: