continuity: resume heartbeat artifact sequence
This commit is contained in:
parent
100d8854fb
commit
42bc665aa8
5 changed files with 120 additions and 20 deletions
|
|
@ -2,6 +2,7 @@ use anyhow::Context;
|
|||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use epiphany_core::EpiphanyCultMeshSwarmBrakeEntry;
|
||||
use epiphany_core::EpiphanyHeartbeatArtifactRetentionReceipt;
|
||||
use epiphany_core::GhostlightSceneParticipantSeed;
|
||||
use epiphany_core::HeartbeatCompleteOptions;
|
||||
use epiphany_core::HeartbeatHeatUpdateOptions;
|
||||
|
|
@ -326,7 +327,7 @@ fn main() -> Result<()> {
|
|||
serde_json::json!({
|
||||
"schemaVersion": "epiphany.heartbeat.artifact_retention_command.v0",
|
||||
"status": if receipt.is_some() { "completed" } else { "within-bound" },
|
||||
"receipt": receipt,
|
||||
"receipt": receipt.as_ref().map(retention_receipt_projection),
|
||||
"privateStateExposed": false
|
||||
})
|
||||
);
|
||||
|
|
@ -535,7 +536,7 @@ fn main() -> Result<()> {
|
|||
artifact_dir.display()
|
||||
)
|
||||
})?;
|
||||
let mut completed_iterations = 0_u64;
|
||||
let mut completed_iterations = highest_pulse_artifact_sequence(&artifact_dir)?;
|
||||
let mut completed_routines = 0_u64;
|
||||
let mut refused_pulses = 0_u64;
|
||||
loop {
|
||||
|
|
@ -604,7 +605,8 @@ fn main() -> Result<()> {
|
|||
"schemaVersion":"epiphany.heartbeat.serve_pulse.v0", "status":pulse.status,
|
||||
"owner":"heartbeat", "iteration":iteration, "artifactDir":iteration_dir,
|
||||
"acknowledgedTerminalId":pulse.acknowledged_terminal_id, "grantId":pulse.grant_id,
|
||||
"brakeId":brake.as_ref().map(|value| &value.brake_id), "retention":retention,
|
||||
"brakeId":brake.as_ref().map(|value| &value.brake_id),
|
||||
"retention":retention.as_ref().map(retention_receipt_projection),
|
||||
"privateStateExposed":false
|
||||
})
|
||||
);
|
||||
|
|
@ -636,7 +638,8 @@ fn main() -> Result<()> {
|
|||
"artifactDir": iteration_dir,
|
||||
"brakeId": brake.brake_id,
|
||||
"brakeScope": brake.scope,
|
||||
"reason": brake.reason, "retention": retention,
|
||||
"reason": brake.reason,
|
||||
"retention": retention.as_ref().map(retention_receipt_projection),
|
||||
"privateStateExposed": false
|
||||
})
|
||||
);
|
||||
|
|
@ -666,7 +669,8 @@ fn main() -> Result<()> {
|
|||
println!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"pulse": persona_pulse, "retention": retention,
|
||||
"pulse": persona_pulse,
|
||||
"retention": retention.as_ref().map(retention_receipt_projection),
|
||||
"privateStateExposed": false
|
||||
})
|
||||
);
|
||||
|
|
@ -704,7 +708,8 @@ fn main() -> Result<()> {
|
|||
"iteration": iteration,
|
||||
"artifactDir": iteration_dir,
|
||||
"routineOk": result["ok"],
|
||||
"routineArtifact": result["artifactPath"], "retention": retention,
|
||||
"routineArtifact": result["artifactPath"],
|
||||
"retention": retention.as_ref().map(retention_receipt_projection),
|
||||
"privateStateExposed": false
|
||||
})
|
||||
);
|
||||
|
|
@ -750,6 +755,51 @@ fn next_path(args: &mut impl Iterator<Item = String>, name: &str) -> Result<Path
|
|||
Ok(PathBuf::from(next_value(args, name)?))
|
||||
}
|
||||
|
||||
fn retention_receipt_projection(
|
||||
receipt: &EpiphanyHeartbeatArtifactRetentionReceipt,
|
||||
) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"schemaVersion": receipt.schema_version,
|
||||
"receiptId": receipt.receipt_id,
|
||||
"planId": receipt.plan_id,
|
||||
"status": receipt.status,
|
||||
"deletedDirectories": receipt.deleted_directories,
|
||||
"deletedFileCount": receipt.deleted_file_count,
|
||||
"deletedByteCount": receipt.deleted_byte_count,
|
||||
"completedAtUtc": receipt.completed_at_utc,
|
||||
"privateStateExposed": receipt.private_state_exposed,
|
||||
})
|
||||
}
|
||||
|
||||
fn highest_pulse_artifact_sequence(artifact_root: &Path) -> Result<u64> {
|
||||
let mut highest = 0_u64;
|
||||
for entry in fs::read_dir(artifact_root)? {
|
||||
let entry = entry?;
|
||||
let file_type = entry.file_type()?;
|
||||
if file_type.is_symlink() {
|
||||
return Err(anyhow!("heartbeat artifact root contains a symlink"));
|
||||
}
|
||||
if !file_type.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let name = entry
|
||||
.file_name()
|
||||
.into_string()
|
||||
.map_err(|_| anyhow!("heartbeat artifact directory name is not UTF-8"))?;
|
||||
let digits = name
|
||||
.strip_prefix("pulse-")
|
||||
.filter(|digits| digits.len() == 6 && digits.bytes().all(|byte| byte.is_ascii_digit()))
|
||||
.ok_or_else(|| {
|
||||
anyhow!("heartbeat artifact root contains unknown directory {name:?}")
|
||||
})?;
|
||||
highest = highest.max(digits.parse()?);
|
||||
}
|
||||
if highest >= 999_999 {
|
||||
return Err(anyhow!("heartbeat pulse artifact sequence is exhausted"));
|
||||
}
|
||||
Ok(highest)
|
||||
}
|
||||
|
||||
fn next_value(args: &mut impl Iterator<Item = String>, name: &str) -> Result<String> {
|
||||
args.next()
|
||||
.ok_or_else(|| anyhow!("{name} requires a value"))
|
||||
|
|
@ -832,6 +882,36 @@ mod brake_tests {
|
|||
assert!(active_swarm_brake(&store_arg, Some("epiphany-yggdrasil"))?.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retention_receipt_projection_names_operator_fields() {
|
||||
let projection = retention_receipt_projection(&EpiphanyHeartbeatArtifactRetentionReceipt {
|
||||
schema_version: "epiphany.heartbeat.artifact_retention_receipt.v0".into(),
|
||||
receipt_id: "receipt-1".into(),
|
||||
plan_id: "plan-1".into(),
|
||||
status: "completed".into(),
|
||||
deleted_directories: vec!["pulse-000001".into()],
|
||||
deleted_file_count: 2,
|
||||
deleted_byte_count: 3,
|
||||
completed_at_utc: "2026-08-08T21:00:00Z".into(),
|
||||
private_state_exposed: false,
|
||||
});
|
||||
assert_eq!(projection["receiptId"], "receipt-1");
|
||||
assert_eq!(projection["deletedDirectories"][0], "pulse-000001");
|
||||
assert_eq!(projection["deletedFileCount"], 2);
|
||||
assert!(projection.as_array().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resident_heartbeat_resumes_after_highest_artifact_sequence() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
fs::create_dir(temp.path().join("pulse-000003"))?;
|
||||
fs::create_dir(temp.path().join("pulse-012557"))?;
|
||||
assert_eq!(highest_pulse_artifact_sequence(temp.path())?, 12_557);
|
||||
fs::create_dir(temp.path().join("alien"))?;
|
||||
assert!(highest_pulse_artifact_sequence(temp.path()).is_err());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_scene_participant(raw: &str) -> Result<GhostlightSceneParticipantSeed> {
|
||||
|
|
|
|||
|
|
@ -53,17 +53,15 @@ pub fn retain_heartbeat_pulse_artifacts(
|
|||
return Ok(None);
|
||||
}
|
||||
let retire_count = batch_size.min(pulses.len() - retain_pulse_count);
|
||||
let protected = protected_latest_cognition_pulse(store_path.as_ref(), &artifact_root)?;
|
||||
let members = pulses
|
||||
.iter()
|
||||
.filter(|(_, path)| protected.as_deref() != path.file_name().and_then(|name| name.to_str()))
|
||||
.take(retire_count)
|
||||
.map(|(_, path)| artifact_member(&artifact_root, path))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
if let Some(protected) = protected_latest_cognition_pulse(store_path.as_ref(), &artifact_root)?
|
||||
&& members
|
||||
.iter()
|
||||
.any(|member| member.directory_name == protected)
|
||||
{
|
||||
bail!("heartbeat artifact retention refuses the latest cognition artifact pulse");
|
||||
if members.len() != retire_count {
|
||||
bail!("heartbeat artifact retention cannot fill a batch without current cognition");
|
||||
}
|
||||
let plan_id = retention_plan_id(&artifact_root, retain_pulse_count, batch_size, &members);
|
||||
let plan = EpiphanyHeartbeatArtifactRetentionPlan {
|
||||
|
|
@ -540,10 +538,10 @@ mod tests {
|
|||
},
|
||||
)?;
|
||||
|
||||
assert!(
|
||||
retain_heartbeat_pulse_artifacts(&store, &artifacts, 2, 2, "2026-08-08T21:03:01Z",)
|
||||
.is_err()
|
||||
);
|
||||
let receipt =
|
||||
retain_heartbeat_pulse_artifacts(&store, &artifacts, 2, 2, "2026-08-08T21:03:01Z")?
|
||||
.expect("retention should skip protected current cognition");
|
||||
assert!(!receipt.deleted_directories.contains(&"pulse-000001".into()));
|
||||
assert!(protected.exists());
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,28 @@
|
|||
|
||||
## Retention and release-bootstrap pass — 2026-08-08
|
||||
|
||||
Exact corrected release `100d8854` packaged 22 binaries as
|
||||
`sha256-da8be22948c6eb958edb2e7f7e03c5cc77d17463659fa14127402392194460d3`
|
||||
with witness
|
||||
`sha256-6a95957f7e7b2b47b42d2154ed8232d68ffe68d3b6bdf67bee6397fa6aab2027`.
|
||||
Its packaged runtime-spine closed all four sessions in a fresh byte-identical
|
||||
v28 copy, replayed closure idempotently, and refused post-close work. A bounded
|
||||
live retention drain then reduced heartbeat artifacts from 12,511 directories
|
||||
and 706.5 MB to 312 directories and 17.6 MB. The latest cognition pulse
|
||||
survived; 191 deletion receipts are in the heartbeat store and the drain log
|
||||
SHA-256 is `8280dd7567f839b0b038d69deb45bfb219c42092a231a2d331e0e3620a293003`.
|
||||
|
||||
Restarting the resident under `100d8854` exposed another Continuity fault:
|
||||
serve iteration restarted at `pulse-000001` instead of recovering the highest
|
||||
surviving sequence. The process was stopped after three pulses; its stdout
|
||||
SHA-256 is `4a104e39d81e0766261fff815a7d4350de31a9d5b956634a9cb39a9e5ad40b22`.
|
||||
Current source resumes from the highest exact pulse directory, refuses alien
|
||||
directories and exhausted sequence space, skips protected current cognition
|
||||
when forming retirement batches, and emits named operator receipt fields. Four
|
||||
retention and three heartbeat-binary tests pass. The resident heartbeat remains
|
||||
stopped pending an authenticated package of this correction; do not restart the
|
||||
old binary.
|
||||
|
||||
The first exact `339d5a6f` build completed its Rust 1.95 graph in 17m07s but
|
||||
failed because the isolated coordinator artifact was absent. Rebuilding that
|
||||
single target restored the cache. A detached clean-worktree retry then exposed
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -35,7 +35,7 @@ invariants:
|
|||
- Evidence and git retain history; this map retains only state that changes future decisions.
|
||||
|
||||
current_status:
|
||||
summary: Epiphany is a supervised engineering alpha on Starfire, not deployment-ready. Live v53 remains a closed complete internal circuit. The native Persona crossing is deployed and its unique permit path is proven, but Yggdrasil still lacks the Discord credential needed for one completed public consequence. A disposable byte-identical copy of sealed v28 proves bounded-worker failure terminalization, restart refusal, and locally built session closure while sealed evidence remains unchanged. Typed heartbeat pulse retention now plans exact manifests, fails closed on mutation or alien paths, recovers pending plans, protects current cognition, and receipts bounded deletion; no live deletion has occurred. Release shakedown exposed two bootstrap faults: exact worktrees did not share source-cache identity, and the runtime-spine role was not exposed by the root bundle manifest. Both are repaired locally with focused tests. The published cb91 package is authentic but rejected because its stale publisher emitted the old body without runtime-spine. Exact corrected packaging, live retention, endurance, the Persona consequence, stable Starfire firewall ownership, and Linux/Yggdrasil cognition remain open.
|
||||
summary: Epiphany is a supervised engineering alpha on Starfire, not deployment-ready. Live v53 remains a closed complete internal circuit. The native Persona crossing is deployed and its unique permit path is proven, but Yggdrasil still lacks the Discord credential needed for one completed public consequence. Exact authenticated 100d8854 packages 22 binaries including runtime-spine; its actuator closed all four v28 replay sessions, replayed idempotently, and refused post-close work. Typed live retention reduced heartbeat artifacts from 12,511 directories and 706.5 MB to 312 directories and 17.6 MB with 191 receipts while protecting current cognition. Resident restart then exposed pulse identity resetting to one; the faulty process was stopped after three pulses. Local source now recovers the highest pulse sequence, skips protected cognition in retirement batches, and emits named receipts. An authenticated package and resident restart proof of that correction remain open, followed by log rotation, endurance, the Persona consequence, stable Starfire firewall ownership, and Linux/Yggdrasil cognition.
|
||||
operational_state:
|
||||
local_worktree: retention_and_release_bootstrap_pass_ready_to_commit
|
||||
operator_wireguard: restored
|
||||
|
|
@ -58,9 +58,9 @@ current_status:
|
|||
persona_permit_probe: unique_node_rust_node_request_completed
|
||||
persona_fresh_consequence: blocked_by_absent_yggdrasil_discord_credential
|
||||
yggdrasil_epiphany_services: absent
|
||||
resident_physiology: heartbeat_and_self_running
|
||||
unresolved_authority: Modeling launch still synchronously performs an authenticated two-pass raw Repository Body observation; the next live Modeling launch must show whether Body observation remains the dominant iteration cost. Keyed storage has not earned live migration. One completed Discord consequence remains blocked by the absent Yggdrasil credential, and Windows' stable path-independent permit rule remains unapplied. Continuity crash terminalization and locally built session closure are proven on disposable historical bodies, but no authenticated package yet contains the closure actuator. Live heartbeat-v8 artifacts remain about 693 MB in 12,274 files until the typed retention release is authenticated and invoked. Resident stdout/stderr logs are a separate unbounded supervisor-owned surface. Endurance and Linux cognition remain unproven.
|
||||
next_action: Commit and push the typed retention, root-manifest actuator, and common-repository source-cache correction. Build the current publisher once, package the exact clean commit through a linked worktree, and prove the package contains runtime-spine while reusing the main source cache. Publish it, replay v28 closure with the exact actuator, then retire live heartbeat artifacts in receipted 64-directory batches. When the operator-owned Discord token appears, install it root-only on Yggdrasil and run one entirely fresh Persona consequence; never retry the two sealed requests.
|
||||
resident_physiology: self_running_heartbeat_stopped_for_sequence_recovery_release
|
||||
unresolved_authority: Modeling launch still synchronously performs an authenticated two-pass raw Repository Body observation; the next live Modeling launch must show whether Body observation remains the dominant iteration cost. Keyed storage has not earned live migration. One completed Discord consequence remains blocked by the absent Yggdrasil credential, and Windows' stable path-independent permit rule remains unapplied. Continuity crash terminalization, session closure, and bounded retention are exact-package live-proven. Pulse sequence recovery after resident restart is locally proven but not yet authenticated live. Resident stdout/stderr logs remain a separate unbounded supervisor-owned surface. Endurance and Linux cognition remain unproven.
|
||||
next_action: Commit and push heartbeat sequence recovery, protected-cognition skipping, and named receipt projection. Package and publish that exact correction, restart the resident heartbeat with explicit logs, and prove it resumes above pulse 012557 while retention remains bounded. Then address supervisor-owned log rotation and continue endurance. When the operator-owned Discord token appears, install it root-only on Yggdrasil and run one entirely fresh Persona consequence; never retry the two sealed requests.
|
||||
|
||||
active_subgoals:
|
||||
- Prove Persona, Self, Eyes, Modeling, Imagination, Hands, Soul, Mind, Continuity, and nervous-system behavior individually.
|
||||
|
|
|
|||
Reference in a new issue