Move Modeling process lifecycle under Idunn

This commit is contained in:
Meta Vens 2026-07-12 11:19:03 +01:00
commit e7d7f2961b
7 changed files with 150 additions and 18 deletions

View file

@ -230,7 +230,22 @@ fn service_launch(args: Args) -> Result<()> {
let service_args = service_serve_args(&args);
let mut command = Command::new(&command_path);
command.args(&service_args);
command.stdout(Stdio::null()).stderr(Stdio::null());
if let Some(stdout_path) = &args.stdout_artifact {
if let Some(parent) = stdout_path.parent() {
fs::create_dir_all(parent)?;
}
command.stdout(Stdio::from(fs::File::create(stdout_path)?));
} else {
command.stdout(Stdio::null());
}
if let Some(stderr_path) = &args.stderr_artifact {
if let Some(parent) = stderr_path.parent() {
fs::create_dir_all(parent)?;
}
command.stderr(Stdio::from(fs::File::create(stderr_path)?));
} else {
command.stderr(Stdio::null());
}
if let Some(cwd) = &args.cwd {
command.current_dir(cwd);
}
@ -267,7 +282,9 @@ fn service_launch(args: Args) -> Result<()> {
exit_code,
started_at,
completed_at,
None,
args.stdout_artifact
.as_ref()
.map(|path| path.display().to_string()),
);
let written = write_epiphany_cultmesh_daemon_service_lifecycle_receipt(
&args.store,
@ -284,6 +301,8 @@ fn service_launch(args: Args) -> Result<()> {
"receiptId": written.receipt_id,
"processId": written.process_id,
"exitCode": written.exit_code,
"stdoutArtifact": args.stdout_artifact,
"stderrArtifact": args.stderr_artifact,
"privateStateExposed": written.private_state_exposed,
}))?
);
@ -3170,6 +3189,8 @@ struct Args {
intent_id: Option<String>,
receipt_id: Option<String>,
artifact_ref: Option<String>,
stdout_artifact: Option<PathBuf>,
stderr_artifact: Option<PathBuf>,
}
impl Args {
@ -3210,6 +3231,8 @@ impl Args {
let mut intent_id = None;
let mut receipt_id = None;
let mut artifact_ref = None;
let mut stdout_artifact = None;
let mut stderr_artifact = None;
while let Some(arg) = values.next() {
match arg.as_str() {
@ -3339,6 +3362,16 @@ impl Args {
"--artifact-ref" => {
artifact_ref = Some(values.next().context("missing --artifact-ref value")?);
}
"--stdout-artifact" => {
stdout_artifact = Some(PathBuf::from(
values.next().context("missing --stdout-artifact value")?,
));
}
"--stderr-artifact" => {
stderr_artifact = Some(PathBuf::from(
values.next().context("missing --stderr-artifact value")?,
));
}
other => anyhow::bail!("unknown argument {other:?}"),
}
}
@ -3464,6 +3497,8 @@ impl Args {
intent_id,
receipt_id,
artifact_ref,
stdout_artifact,
stderr_artifact,
})
}
}

View file

@ -104,7 +104,6 @@ use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Stdio;
fn main() -> Result<()> {
let mut args = env::args().skip(1);
@ -14208,11 +14207,36 @@ fn resolve_repo_work_model_runtime(epiphany_root: &Path) -> Result<PathBuf> {
.ok_or_else(|| anyhow!("epiphany-openai-runtime binary is not built"))
}
fn resolve_daemon_supervisor(epiphany_root: &Path) -> Result<PathBuf> {
let exe_name = if cfg!(windows) {
"epiphany-daemon-supervisor.exe"
} else {
"epiphany-daemon-supervisor"
};
let mut candidates = Vec::new();
if let Some(target) = env::var_os("CARGO_TARGET_DIR") {
candidates.push(PathBuf::from(target).join("debug").join(exe_name));
}
candidates.push(
epiphany_root
.join("epiphany-core")
.join("target")
.join("debug")
.join(exe_name),
);
candidates.push(epiphany_root.join("target").join("debug").join(exe_name));
candidates
.into_iter()
.find(|candidate| candidate.is_file())
.ok_or_else(|| anyhow!("epiphany-daemon-supervisor binary is not built"))
}
fn launch_repo_work_modeling_worker(
epiphany_root: &Path,
workspace: &Path,
artifact_dir: &Path,
runtime_store: &Path,
local_verse_store: &Path,
request: &RepoWorkModelingRequest,
item_slug: &str,
) -> Result<Value> {
@ -14272,28 +14296,73 @@ fn launch_repo_work_modeling_worker(
},
)?;
let runtime_bin = resolve_repo_work_model_runtime(epiphany_root)?;
let supervisor_bin = resolve_daemon_supervisor(epiphany_root)?;
let stdout_path = artifact_dir.join(format!("{job_id}.stdout.log"));
let stderr_path = artifact_dir.join(format!("{job_id}.stderr.log"));
let stdout = fs::File::create(&stdout_path)?;
let stderr = fs::File::create(&stderr_path)?;
let child = Command::new(&runtime_bin)
.arg("run-worker")
let service_id = format!("idunn-{job_id}");
let output = Command::new(&supervisor_bin)
.arg("service-launch")
.arg("--store")
.arg(local_verse_store)
.arg("--runtime-id")
.arg("repo-work-local")
.arg("--daemon-id")
.arg("epiphany-daemon-modeling")
.arg("--service-id")
.arg(&service_id)
.arg("--service-command")
.arg(&runtime_bin)
.arg("--service-arg")
.arg("run-worker")
.arg("--service-arg")
.arg("--store")
.arg("--service-arg")
.arg(runtime_store)
.arg("--service-arg")
.arg("--job-id")
.arg("--service-arg")
.arg(&job_id)
.arg("--service-arg")
.arg("--cwd")
.arg("--service-arg")
.arg(workspace)
.arg("--service-arg")
.arg("--max-runtime-seconds")
.arg("--service-arg")
.arg("300")
.arg("--cwd")
.arg(workspace)
.arg("--max-runtime-seconds")
.arg("300")
.stdout(Stdio::from(stdout))
.stderr(Stdio::from(stderr))
.spawn()
.with_context(|| format!("failed to launch {}", runtime_bin.display()))?;
.arg("--stdout-artifact")
.arg(&stdout_path)
.arg("--stderr-artifact")
.arg(&stderr_path)
.arg("--reason")
.arg(format!(
"Idunn launches typed repo-work Modeling job {job_id}."
))
.output()
.with_context(|| {
format!(
"failed to invoke Idunn supervisor {}",
supervisor_bin.display()
)
})?;
if !output.status.success() {
return Err(anyhow!(
"Idunn supervisor refused repo-work Modeling launch: {}",
String::from_utf8_lossy(&output.stderr)
));
}
let lifecycle: Value = serde_json::from_slice(&output.stdout)
.context("Idunn supervisor did not emit its lifecycle receipt JSON")?;
Ok(json!({
"jobId": job_id,
"processId": child.id(),
"processId": lifecycle["processId"],
"runtimeBin": runtime_bin,
"supervisorBin": supervisor_bin,
"lifecycleOwner": "Idunn",
"lifecycleReceiptId": lifecycle["receiptId"],
"serviceId": lifecycle["serviceId"],
"stdoutArtifact": stdout_path,
"stderrArtifact": stderr_path,
"requestId": request.request_id,
@ -14749,11 +14818,15 @@ fn run_tick(args: TickArgs) -> Result<Value> {
}
None => {
action = "launch-modeling".to_string();
let lifecycle_store = local_verse_store
.clone()
.unwrap_or_else(|| workspace.join(".epiphany").join("local-verse.ccmp"));
advanced_result = launch_repo_work_modeling_worker(
&epiphany_root,
&workspace,
&artifact_dir,
&runtime_store,
&lifecycle_store,
&request,
&item_slug,
)?;
@ -16604,6 +16677,9 @@ mod authority_tests {
.next()
.expect("production source before authority tests");
assert!(!production.contains("put_repo_work_modeling_finding("));
assert!(!production.contains(".spawn()"));
assert!(production.contains("epiphany-daemon-supervisor"));
assert!(production.contains("\"lifecycleOwner\": \"Idunn\""));
assert!(!source.contains(&["RepoWork", "MapStore"].concat()));
assert!(!source.contains(&["repo-work-map", ".msgpack"].concat()));
}

View file

@ -143,6 +143,15 @@ runtime document. The obsolete direct-CLI closure smoke was deleted. A live
negative probe returned exit 1 for forged passing CLI cargo while leaving the
typed request awaiting its real Modeling worker.
Detached worker physiology is now Idunn-owned. `epiphany-work` opens the typed
runtime job and invokes `epiphany-daemon-supervisor service-launch`; it contains
no child-process `spawn`. The supervisor launches the existing model runtime,
owns Windows hidden-process behavior, redirects stdout/stderr to explicit
artifacts, and publishes an
`epiphany.cultmesh.daemon_service_lifecycle_receipt.v0` containing service and
process identity. Self receives that receipt ID as routing evidence only. Live
item `idunn-owned-modeling` crossed this boundary and reached the Bifrost gate.
The
accepted interpretation is persisted as
`epiphany.modeling.repo_work_finding.v0`; it references the passing Soul verdict,

View file

@ -204,9 +204,9 @@ Delete or demote:
## Immediate Next Action
Move repo-work Modeling process stewardship under Idunn and add a reviewed
request generation for non-passing immutable findings. The scheduler now routes
passing requests through the existing model runtime and reaches Bifrost without
Self-authored Modeling evidence; preserve that authority path. Do not overwrite
a blocked finding, launch duplicate jobs, or grow a scheduler-owned process
manager.
Design and implement a typed Mind-reviewed request-generation pointer for
non-passing immutable repo-work Modeling findings. Idunn now owns detached
worker launch through its existing lifecycle receipt mouth. Preserve the old
request/finding as immutable evidence, make one typed owner select the current
generation, and keep filesystem closure artifacts and Self from becoming
mutable routing authorities.

View file

@ -2085,6 +2085,18 @@ Live negative item `cli-modeling-authority-refusal` returned exit 1 when
`close --model-authored` supplied a forged passing finding; the error names the
required Modeling runtime route. The old CLI path is structurally unable to
publish the canonical finding.
Idunn now owns the detached worker launch itself. The repo-work scheduler no
longer calls `Command::spawn`; it invokes the existing daemon supervisor's
generic `service-launch` mouth. That mouth now accepts explicit stdout/stderr
artifact paths, owns hidden child creation, and writes its standard typed
CultMesh lifecycle receipt. Authenticated live item `idunn-owned-modeling`
returned lifecycle receipt
`daemon-service-lifecycle-receipt-idunn-repo-work-modeling-idunn-owned-modeling-launch`,
process id `45012`, a passing runtime-authored finding, closed Mind/map
admission, and overview gate `awaiting-publication` with
`bifrost-publication-missing`. Next design the typed Mind-reviewed generation
pointer for a non-passing immutable finding; do not mutate the old request or
let a filesystem close artifact become the new routing authority.
## Immediate Re-entry Instruction

Binary file not shown.

View file

@ -659,4 +659,4 @@ current_status:
- Service lifecycle receipt history is now event-only: `load_epiphany_cultmesh_daemon_service_lifecycle_receipts` filters out the `epiphany-local/daemon-service-lifecycle-receipt/latest` mirror, while `load_latest_epiphany_cultmesh_daemon_service_lifecycle_receipt` remains the latest-state reader. Focused unit test `service_lifecycle_receipt_history_excludes_latest_mirror` proves two per-receipt writes return exactly two history records and latest points at the second. Fresh smoke `verse-query-lifecycle-history-filter-smoke` and live overview/triage `local-20260618-145632-497-efac6f0d` / `local-20260618-145632-321-867947af` prove compact swarm sights still expose service lifecycle attention plus the present elevated runbook artifact without daemon pokes or service mutation. Do not use a broad unscoped cargo test filter for this check on Windows; it fans out across bin harnesses and hit a broken pipe during this pass.
- `epiphany-verse-query seed-compact` is the compact seed receipt for wrappers and read-only rites. The older `seed` command still prints the full local Verse context for explicit debugging, but compact wrapper modes now avoid full `seed`/final `query` readback after the combined overview proof exposed Windows debug stack overflow from full-context serialization pressure.
last_completed_repo_swarm_cut: Huginn live-fire has crossed the first real repo Body from Persona pressure to branch-local Hands commit and Soul/Mind closure, and now has repeatable closure and redacted-public-proof mouths. On `E:\Projects\Huginn` branch `epiphany/huginn/first-awakening`, `tools/epiphany_local_run.ps1 -Mode repo-persona-intake` accepted item `huginn-inspection-readback-livefire`, Weksa/Persona intake remained sealed, and `epiphany-work derive-plan` lowered it to model-authored safe family `repo.status_section` for `README.md` only. `tools/epiphany_local_run.ps1 -Mode repo-swarm-run` advanced four bounded pulses: run-from-plan, adopt-from-plan, execute-from-plan, and close-from-execute. Huginn commit `e9b07c38fceabbdc89ea35c2156326c80c0393a2` updated only `README.md`; closure receipt `.epiphany\work\work-close-huginn-inspection-readback-livefire.json` records status `closed`, Soul verdict `passed`, Mind state commit `repo-work-close-huginn-inspection-readback-livefire-mind-commit`, safe family `repo.status_section`, path scope `README.md`, Bifrost publication gate, and `privateStateExposed=false`. Verification also ran Huginn's TypeScript compiler through concrete Node path: `C:\Program Files\nodejs\node.exe E:\Projects\Huginn\node_modules\typescript\bin\tsc -p tsconfig.json` passed. Native verifier `epiphany-repo-livefire-closure-smoke --workspace E:\Projects\Huginn --item huginn-inspection-readback-livefire --expected-path README.md --expected-family repo.status_section --expected-gate awaiting-publication --expected-blocker bifrost-publication-missing` checks closure receipt, Mind map entry, overview gate/blocker, authority seals, and `git show` changed paths, returning `status=ok`, `soulVerdict=passed`, `publicationGate=Bifrost`, and `privateStateExposed=false`. Wrapper `tools/epiphany_local_run.ps1 -Mode repo-livefire-closure -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exposes the same read-only proof through the local operator mouth; artifact `local-20260621-190156-803-2f85ba15` printed commit, path, gate, blocker, Soul verdict, Mind commit receipt, and `privateStateExposed=False` without push, merge, publication, or target mutation. New wrapper `tools/epiphany_local_run.ps1 -Mode repo-work-public-proof -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exports the redacted proof bundle and repo-local Verse index without publishing; artifact `local-20260621-190645-345-4555b1dc` wrote `.epiphany\public\proof-bundles\repo-work-public-proof-huginn-inspection-readback-livefire.json`, proof id `repo-work-public-proof-huginn-inspection-readback-livefire`, SHA-256 `857545b199f3e37eb06c2e757e5cfc201958e46667bb1f8403fd333a22620f63`, nine artifact rows, zero publication rows, and `privateStateExposed=False`. Gjallar readback `local-20260621-190701-140-b9519ec6` sees the `REPO-WORK-PUBLIC-PROOF` row and action row from Huginn's repo-local Verse with the same SHA-256 while Bifrost publication remains missing. Launcher compact modes are now named compact operator-context modes instead of read-only modes, and `bifrost-public-proof` summaries print item, target public Verse, room, proof SHA-256, review receipts, and credit receipts so publication receipts are auditable without claiming upstream sync.
next_action: Move detached repo-work Modeling worker stewardship under Idunn and add a reviewed request generation for non-passing immutable findings. Preserve the now-live typed scheduler path from Soul request through the existing model runtime, canonical Modeling finding, immutable Mind/map admission, and Bifrost gate; Self must remain unable to author or overwrite Modeling evidence.
next_action: Design and implement a typed Mind-reviewed request-generation pointer for non-passing immutable repo-work Modeling findings. Idunn now owns detached worker launch through the existing daemon service lifecycle receipt mouth. Preserve each prior request/finding as immutable evidence; filesystem closure artifacts and Self must not become mutable routing authorities.