feat: resume pods by name

This commit is contained in:
2026-05-22 22:57:16 +09:00
parent ca2e2352f4
commit 0e562dd4d9
4 changed files with 347 additions and 57 deletions
+125 -8
View File
@@ -3,9 +3,9 @@ use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::Parser;
use manifest::{PodManifest, paths};
use manifest::{PodManifest, PodManifestConfig, paths};
use pod::{Pod, PodController, PodFactory, PromptLoader};
use session_store::{FsStore, SegmentId, Store};
use session_store::{FsStore, PodMetadataStore, SegmentId, Store};
const USER_MANIFEST_ENV: &str = "INSOMNIA_USER_MANIFEST";
@@ -47,12 +47,18 @@ struct Cli {
#[arg(long, value_name = "PATH", requires = "adopt")]
callback: Option<PathBuf>,
/// Resume or create a Pod by name. If name-keyed Pod state exists,
/// the active session/segment recorded there is restored; otherwise a
/// fresh top-level Pod is created with this name.
#[arg(long, value_name = "NAME", conflicts_with_all = ["session", "adopt"])]
pod: Option<String>,
/// Restore a Pod from an existing session. The Pod re-uses the
/// given session id and appends new turns to the same jsonl;
/// concurrent writers are prevented by the pod-registry.
/// Mutually exclusive with `--adopt` (spawned children always start
/// fresh).
#[arg(long, value_name = "UUID", conflicts_with = "adopt")]
#[arg(long, value_name = "UUID", conflicts_with_all = ["adopt", "pod"])]
session: Option<SegmentId>,
}
@@ -72,7 +78,7 @@ fn resolve_manifest_with_user_manifest_env(
"--manifest cannot be used when {USER_MANIFEST_ENV} is set"
));
}
return load_single_manifest(path);
return load_single_manifest(path, cli.pod.as_deref());
}
let factory = build_factory_with_user_manifest_path(cli, user_manifest)?;
@@ -91,14 +97,45 @@ fn user_manifest_path_from_env(value: Option<OsString>) -> Option<PathBuf> {
})
}
fn load_single_manifest(path: &Path) -> Result<(PodManifest, PromptLoader), String> {
fn load_single_manifest(
path: &Path,
pod_name_override: Option<&str>,
) -> Result<(PodManifest, PromptLoader), String> {
let toml = std::fs::read_to_string(path)
.map_err(|e| format!("failed to read manifest {}: {e}", path.display()))?;
let manifest = PodManifest::from_toml(&toml)
.map_err(|e| format!("failed to parse manifest {}: {e}", path.display()))?;
let manifest = match pod_name_override {
Some(pod_name) => match PodManifest::from_toml(&toml) {
Ok(mut manifest) => {
manifest.pod.name = pod_name.to_string();
manifest
}
Err(_) => {
let base = PodManifestConfig::from_toml(&toml)
.map_err(|e| format!("failed to parse manifest {}: {e}", path.display()))?;
let overlay = PodManifestConfig::from_toml(&pod_name_overlay_toml(pod_name))
.expect("pod name overlay TOML is generated");
PodManifest::try_from(base.merge(overlay)).map_err(|e| {
format!(
"failed to resolve manifest {} with --pod: {e}",
path.display()
)
})?
}
},
None => PodManifest::from_toml(&toml)
.map_err(|e| format!("failed to parse manifest {}: {e}", path.display()))?,
};
Ok((manifest, PromptLoader::builtins_only()))
}
fn pod_name_overlay_toml(pod_name: &str) -> String {
let mut pod = toml::value::Table::new();
pod.insert("name".into(), toml::Value::String(pod_name.to_string()));
let mut root = toml::value::Table::new();
root.insert("pod".into(), toml::Value::Table(pod));
toml::to_string(&toml::Value::Table(root)).expect("pod name overlay serialisation cannot fail")
}
fn build_factory_with_user_manifest_path(
cli: &Cli,
user_manifest: Option<PathBuf>,
@@ -129,6 +166,12 @@ fn build_factory_with_user_manifest_path(
.map_err(|e| format!("failed to parse overlay TOML: {e}"))?;
}
if let Some(pod_name) = cli.pod.as_deref() {
factory = factory
.with_overlay_toml(&pod_name_overlay_toml(pod_name))
.map_err(|e| format!("failed to apply --pod overlay: {e}"))?;
}
Ok(factory)
}
@@ -136,7 +179,7 @@ fn build_factory_with_user_manifest_path(
async fn main() -> ExitCode {
let cli = Cli::parse();
let (manifest, loader) = match resolve_manifest(&cli) {
let (mut manifest, loader) = match resolve_manifest(&cli) {
Ok(pair) => pair,
Err(e) => {
eprintln!("error: {e}");
@@ -214,6 +257,30 @@ async fn main() -> ExitCode {
return ExitCode::FAILURE;
}
}
} else if let Some(pod_name) = cli.pod.as_deref() {
manifest.pod.name = pod_name.to_string();
match store.read_by_name(pod_name) {
Ok(Some(_)) => {
match Pod::restore_from_pod_metadata(pod_name, manifest, store, loader).await {
Ok(p) => p,
Err(e) => {
eprintln!("error: failed to restore pod {pod_name}: {e}");
return ExitCode::FAILURE;
}
}
}
Ok(None) => match Pod::from_manifest(manifest, store, loader).await {
Ok(p) => p,
Err(e) => {
eprintln!("error: failed to create pod {pod_name}: {e}");
return ExitCode::FAILURE;
}
},
Err(e) => {
eprintln!("error: failed to read pod state for {pod_name}: {e}");
return ExitCode::FAILURE;
}
}
} else {
match Pod::from_manifest(manifest, store, loader).await {
Ok(p) => p,
@@ -369,6 +436,56 @@ permission = "write"
assert_eq!(manifest.pod.name, "from-env");
}
#[test]
fn pod_flag_conflicts_with_session() {
let segment_id = session_store::new_segment_id();
let segment_id = segment_id.to_string();
let err =
Cli::try_parse_from(["pod", "--pod", "agent", "--session", &segment_id]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn pod_flag_sets_requested_name_after_manifest_resolution() {
let tmp = TempDir::new().unwrap();
let manifest = tmp.path().join("manifest.toml");
write(&manifest, &manifest_toml("from-file", tmp.path()));
let cli = Cli::try_parse_from([
"pod",
"--manifest",
manifest.to_str().unwrap(),
"--pod",
"from-flag",
])
.unwrap();
let (manifest, _loader) = resolve_manifest_with_user_manifest_env(&cli, None).unwrap();
assert_eq!(manifest.pod.name, "from-flag");
}
#[test]
fn pod_flag_supplies_missing_name_for_single_manifest() {
let tmp = TempDir::new().unwrap();
let manifest = tmp.path().join("manifest.toml");
write(
&manifest,
&manifest_toml("unused", tmp.path()).replace("name = \"unused\"\n", ""),
);
let cli = Cli::try_parse_from([
"pod",
"--manifest",
manifest.to_str().unwrap(),
"--pod",
"from-flag",
])
.unwrap();
let (manifest, _loader) = resolve_manifest_with_user_manifest_env(&cli, None).unwrap();
assert_eq!(manifest.pod.name, "from-flag");
}
#[test]
fn manifest_mode_loads_single_file_with_minimal_prompt_loader() {
let tmp = TempDir::new().unwrap();