fix(tui): restore Agent truth after compaction
Signed-off-by: Hunter Bown <hmbown@gmail.com>
This commit is contained in:
@@ -4821,8 +4821,10 @@ impl Engine {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
Ok(mut result) => {
|
||||
if !result.messages.is_empty() || self.session.messages.is_empty() {
|
||||
self.append_compaction_agent_topology(&mut result.messages)
|
||||
.await;
|
||||
let messages_after = result.messages.len();
|
||||
let retries_used = result.retries_used;
|
||||
self.session.replace_messages(result.messages);
|
||||
@@ -5090,6 +5092,8 @@ impl Engine {
|
||||
}
|
||||
|
||||
if !compacted_messages.is_empty() || self.session.messages.is_empty() {
|
||||
self.append_compaction_agent_topology(&mut compacted_messages)
|
||||
.await;
|
||||
self.session.replace_messages(compacted_messages);
|
||||
}
|
||||
self.commit_compaction_checkpoint(summary_prompt);
|
||||
@@ -5710,6 +5714,19 @@ impl Engine {
|
||||
};
|
||||
self.session.compaction_summary_prompt = Some(summary_prompt);
|
||||
}
|
||||
|
||||
/// Capture the current session-owned Agent topology at the replacement
|
||||
/// history boundary. This is the Codewhale equivalent of Codex clearing
|
||||
/// its world-state reference after standalone compaction so the next turn
|
||||
/// receives fresh environment/subagent context instead of trusting the
|
||||
/// narrative summary as live process state.
|
||||
async fn append_compaction_agent_topology(&self, messages: &mut Vec<Message>) {
|
||||
let snapshots = {
|
||||
let manager = self.subagent_manager.read().await;
|
||||
manager.list_for_session(&self.session.id)
|
||||
};
|
||||
crate::runtime_handoff::replace_agent_topology_checkpoint(messages, &snapshots);
|
||||
}
|
||||
}
|
||||
|
||||
fn default_plugin_tools_dir() -> PathBuf {
|
||||
|
||||
@@ -735,9 +735,11 @@ impl Engine {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
Ok(mut result) => {
|
||||
// Only update if we got valid messages (never corrupt state)
|
||||
if !result.messages.is_empty() || self.session.messages.is_empty() {
|
||||
self.append_compaction_agent_topology(&mut result.messages)
|
||||
.await;
|
||||
let auto_messages_after = result.messages.len();
|
||||
let retries_used = result.retries_used;
|
||||
self.session.replace_messages(result.messages);
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
//! idempotent restore projection so creation and recognition cannot drift.
|
||||
|
||||
use crate::models::{ContentBlock, Message};
|
||||
use crate::safe_label::SafeLabel;
|
||||
use crate::tools::subagent::{AgentWorkerStatus, SubAgentResult, SubAgentStatus};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const COMPLETION_EVENT_PREFIX: &str = concat!(
|
||||
"<codewhale:runtime_event kind=\"subagent_completion\" visibility=\"internal\">\n",
|
||||
@@ -75,6 +78,19 @@ const RESTORED_CHECKPOINT_TURN_META: &str = concat!(
|
||||
const RESTORED_COMPLETION_HEADER: &str = "[Codewhale restored sub-agent checkpoint]";
|
||||
const RESTORED_COMPLETIONS_HEADER: &str = "[Codewhale restored sub-agent checkpoints]";
|
||||
const RESTORED_RUNNING_HEADER: &str = "[Codewhale restored sub-agent runtime checkpoint]";
|
||||
const RESTORED_TOPOLOGY_HEADER: &str = "[Codewhale restored Agent topology checkpoint]";
|
||||
|
||||
const AGENT_TOPOLOGY_EVENT_PREFIX: &str = concat!(
|
||||
"<codewhale:runtime_state kind=\"agent_topology\" schema=\"v1\" visibility=\"internal\">\n",
|
||||
);
|
||||
const AGENT_TOPOLOGY_EVENT_SUFFIX: &str = "\n</codewhale:runtime_state>";
|
||||
const AGENT_TOPOLOGY_TURN_META: &str = concat!(
|
||||
"<turn_meta>\n",
|
||||
"Input provenance: runtime (non-authoritative)\n",
|
||||
"Runtime state: agent_topology_v1 (authoritative)\n",
|
||||
"</turn_meta>",
|
||||
);
|
||||
const MAX_AGENT_TOPOLOGY_ROWS: usize = 24;
|
||||
|
||||
const DONE_SENTINEL_START: &str = "<codewhale:subagent.done>";
|
||||
const DONE_SENTINEL_END: &str = "</codewhale:subagent.done>";
|
||||
@@ -151,6 +167,248 @@ pub(crate) fn shell_completion_runtime_message(
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AgentTopologyCheckpoint {
|
||||
schema: &'static str,
|
||||
authority: &'static str,
|
||||
scope: &'static str,
|
||||
replaces: &'static str,
|
||||
total: usize,
|
||||
nonterminal: usize,
|
||||
terminal: usize,
|
||||
omitted: usize,
|
||||
agents: Vec<AgentTopologyRow>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AgentTopologyRow {
|
||||
agent_id: SafeLabel,
|
||||
name: SafeLabel,
|
||||
role: SafeLabel,
|
||||
status: &'static str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
parent_run_id: Option<SafeLabel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SavedAgentTopologyCheckpoint {
|
||||
schema: String,
|
||||
total: usize,
|
||||
nonterminal: usize,
|
||||
terminal: usize,
|
||||
omitted: usize,
|
||||
agents: Vec<SavedAgentTopologyRow>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SavedAgentTopologyRow {
|
||||
agent_id: String,
|
||||
name: String,
|
||||
role: String,
|
||||
status: String,
|
||||
#[serde(default)]
|
||||
parent_run_id: Option<String>,
|
||||
}
|
||||
|
||||
fn topology_status(agent: &SubAgentResult) -> &'static str {
|
||||
match agent.worker_status {
|
||||
Some(AgentWorkerStatus::Queued) => "queued",
|
||||
Some(AgentWorkerStatus::Starting) => "starting",
|
||||
Some(AgentWorkerStatus::Running) => "running",
|
||||
Some(AgentWorkerStatus::WaitingForUser) => "waiting_for_user",
|
||||
Some(AgentWorkerStatus::ModelWait) => "model_wait",
|
||||
Some(AgentWorkerStatus::RunningTool) => "running_tool",
|
||||
Some(AgentWorkerStatus::Completed) => "completed",
|
||||
Some(AgentWorkerStatus::Failed) => "failed",
|
||||
Some(AgentWorkerStatus::Cancelled) => "cancelled",
|
||||
Some(AgentWorkerStatus::Interrupted) => "interrupted",
|
||||
None => match &agent.status {
|
||||
SubAgentStatus::Running => "running",
|
||||
SubAgentStatus::Completed => "completed",
|
||||
SubAgentStatus::Interrupted(_) => "interrupted",
|
||||
SubAgentStatus::Failed(_) => "failed",
|
||||
SubAgentStatus::Cancelled => "cancelled",
|
||||
SubAgentStatus::BudgetExhausted => "budget_exhausted",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn topology_status_is_terminal(status: &str) -> bool {
|
||||
matches!(
|
||||
status,
|
||||
"completed" | "failed" | "cancelled" | "interrupted" | "budget_exhausted"
|
||||
)
|
||||
}
|
||||
|
||||
fn agent_topology_checkpoint_message(snapshots: &[SubAgentResult]) -> Message {
|
||||
// Stable ordering makes a replay byte-identical. Put non-terminal rows first
|
||||
// so a bounded projection never hides work that is still live.
|
||||
let mut agents = snapshots.iter().collect::<Vec<_>>();
|
||||
agents.sort_by(|left, right| {
|
||||
topology_status_is_terminal(topology_status(left))
|
||||
.cmp(&topology_status_is_terminal(topology_status(right)))
|
||||
.then_with(|| left.agent_id.cmp(&right.agent_id))
|
||||
});
|
||||
|
||||
let total = agents.len();
|
||||
let terminal = agents
|
||||
.iter()
|
||||
.filter(|agent| topology_status_is_terminal(topology_status(agent)))
|
||||
.count();
|
||||
let nonterminal = total.saturating_sub(terminal);
|
||||
let rows = agents
|
||||
.into_iter()
|
||||
.take(MAX_AGENT_TOPOLOGY_ROWS)
|
||||
.map(|agent| AgentTopologyRow {
|
||||
agent_id: SafeLabel::identifier(&agent.agent_id),
|
||||
name: SafeLabel::phrase(
|
||||
agent
|
||||
.nickname
|
||||
.as_deref()
|
||||
.filter(|name| !name.trim().is_empty())
|
||||
.unwrap_or(&agent.name),
|
||||
),
|
||||
role: SafeLabel::identifier(agent.agent_type.as_str()),
|
||||
status: topology_status(agent),
|
||||
parent_run_id: agent.parent_run_id.as_deref().map(SafeLabel::identifier),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let payload = AgentTopologyCheckpoint {
|
||||
schema: "codewhale.agent_topology.v1",
|
||||
authority: "runtime_current",
|
||||
scope: "current_session",
|
||||
replaces: "all_prior_agent_lifecycle_claims",
|
||||
total,
|
||||
nonterminal,
|
||||
terminal,
|
||||
omitted: total.saturating_sub(rows.len()),
|
||||
agents: rows,
|
||||
};
|
||||
let json = serde_json::to_string(&payload).unwrap_or_else(|_| {
|
||||
"{\"schema\":\"codewhale.agent_topology.v1\",\"authority\":\"runtime_unavailable\"}"
|
||||
.to_string()
|
||||
});
|
||||
Message {
|
||||
// Strict OpenAI-compatible chat templates accept only the initial
|
||||
// system message. Runtime state therefore uses role=user on the wire,
|
||||
// while the exact typed envelope + non-authoritative provenance block
|
||||
// keeps it out of the ordinary user-intent path.
|
||||
role: "user".to_string(),
|
||||
content: vec![
|
||||
ContentBlock::Text {
|
||||
text: format!("{AGENT_TOPOLOGY_EVENT_PREFIX}{json}{AGENT_TOPOLOGY_EVENT_SUFFIX}"),
|
||||
cache_control: None,
|
||||
},
|
||||
ContentBlock::Text {
|
||||
text: AGENT_TOPOLOGY_TURN_META.to_string(),
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_agent_topology_checkpoint(message: &Message) -> Option<SavedAgentTopologyCheckpoint> {
|
||||
if !is_agent_topology_checkpoint(message) {
|
||||
return None;
|
||||
}
|
||||
let ContentBlock::Text { text, .. } = message.content.first()? else {
|
||||
return None;
|
||||
};
|
||||
let json = text
|
||||
.strip_prefix(AGENT_TOPOLOGY_EVENT_PREFIX)?
|
||||
.strip_suffix(AGENT_TOPOLOGY_EVENT_SUFFIX)?;
|
||||
let mut checkpoint: SavedAgentTopologyCheckpoint = serde_json::from_str(json).ok()?;
|
||||
if checkpoint.schema != "codewhale.agent_topology.v1" {
|
||||
return None;
|
||||
}
|
||||
checkpoint.agents.truncate(MAX_AGENT_TOPOLOGY_ROWS);
|
||||
Some(checkpoint)
|
||||
}
|
||||
|
||||
fn saved_topology_status(status: &str) -> (&'static str, bool) {
|
||||
match status {
|
||||
"completed" => ("completed", true),
|
||||
"failed" => ("failed", true),
|
||||
"cancelled" => ("cancelled", true),
|
||||
"interrupted" => ("interrupted", true),
|
||||
"budget_exhausted" => ("budget_exhausted", true),
|
||||
"queued" => ("queued", false),
|
||||
"starting" => ("starting", false),
|
||||
"running" => ("running", false),
|
||||
"waiting_for_user" => ("waiting_for_user", false),
|
||||
"model_wait" => ("model_wait", false),
|
||||
"running_tool" => ("running_tool", false),
|
||||
_ => ("unknown", false),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_restored_agent_topology(checkpoint: &SavedAgentTopologyCheckpoint) -> String {
|
||||
let total = checkpoint.total.min(1_024);
|
||||
let nonterminal = checkpoint.nonterminal.min(total);
|
||||
let terminal = checkpoint.terminal.min(total);
|
||||
let omitted = checkpoint.omitted.min(total);
|
||||
let mut display = format!(
|
||||
"{RESTORED_TOPOLOGY_HEADER}\nState at save: total={total}, nonterminal={nonterminal}, terminal={terminal}, omitted={omitted}"
|
||||
);
|
||||
for agent in &checkpoint.agents {
|
||||
let id = SafeLabel::identifier(&agent.agent_id);
|
||||
let name = SafeLabel::phrase(&agent.name);
|
||||
let role = SafeLabel::identifier(&agent.role);
|
||||
let (status, terminal) = saved_topology_status(&agent.status);
|
||||
let current = if terminal {
|
||||
"terminal fact retained"
|
||||
} else {
|
||||
"historical only; prior worker process is not assumed active"
|
||||
};
|
||||
display.push_str(&format!(
|
||||
"\n- agent_id={id}, name={name}, role={role}, status_at_save={status}, resume={current}"
|
||||
));
|
||||
if let Some(parent) = agent.parent_run_id.as_deref() {
|
||||
let parent = SafeLabel::identifier(parent);
|
||||
display.push_str(&format!(", parent_run_id={parent}"));
|
||||
}
|
||||
}
|
||||
display.push_str(
|
||||
"\nAuthority: historical runtime checkpoint; newer live runtime state overrides it",
|
||||
);
|
||||
display
|
||||
}
|
||||
|
||||
fn is_agent_topology_checkpoint(message: &Message) -> bool {
|
||||
let [
|
||||
ContentBlock::Text {
|
||||
text,
|
||||
cache_control: first_cache,
|
||||
},
|
||||
ContentBlock::Text {
|
||||
text: turn_meta,
|
||||
cache_control: meta_cache,
|
||||
},
|
||||
] = message.content.as_slice()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
message.role == "user"
|
||||
&& first_cache.is_none()
|
||||
&& meta_cache.is_none()
|
||||
&& turn_meta == AGENT_TOPOLOGY_TURN_META
|
||||
&& text.starts_with(AGENT_TOPOLOGY_EVENT_PREFIX)
|
||||
&& text.ends_with(AGENT_TOPOLOGY_EVENT_SUFFIX)
|
||||
}
|
||||
|
||||
/// Install one bounded, typed Agent-topology sidecar after replacement
|
||||
/// compaction. A current empty topology is still meaningful: it overrides a
|
||||
/// narrative summary or old runtime event that says an Agent remains live.
|
||||
/// Replays are idempotent because the previous sidecar is structurally removed
|
||||
/// before the replacement is appended.
|
||||
pub(crate) fn replace_agent_topology_checkpoint(
|
||||
messages: &mut Vec<Message>,
|
||||
snapshots: &[SubAgentResult],
|
||||
) {
|
||||
messages.retain(|message| !is_agent_topology_checkpoint(message));
|
||||
messages.push(agent_topology_checkpoint_message(snapshots));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn runtime_handoff_message(text: String) -> Message {
|
||||
runtime_handoff_message_with_meta(text, SUBAGENT_HANDOFF_TURN_META)
|
||||
@@ -187,6 +445,21 @@ fn project_message_for_restore(message: &Message) -> Message {
|
||||
return message.clone();
|
||||
}
|
||||
|
||||
if is_agent_topology_checkpoint(message) {
|
||||
let display = parse_agent_topology_checkpoint(message).map_or_else(
|
||||
|| {
|
||||
format!(
|
||||
"{RESTORED_TOPOLOGY_HEADER}\n\
|
||||
State at save: unavailable (persisted topology could not be decoded safely)\n\
|
||||
Resume state: prior worker processes are not assumed active\n\
|
||||
Authority: historical runtime checkpoint; current Agent state must come from the live runtime"
|
||||
)
|
||||
},
|
||||
|checkpoint| render_restored_agent_topology(&checkpoint),
|
||||
);
|
||||
return restored_checkpoint_message(display);
|
||||
}
|
||||
|
||||
let Some(text) = raw_runtime_handoff_text(message) else {
|
||||
return message.clone();
|
||||
};
|
||||
@@ -248,6 +521,9 @@ Authority: non-authoritative runtime checkpoint"
|
||||
/// its metadata carries no provenance line at all. Someone quoting an envelope
|
||||
/// while asking about it is not matched no matter how many blocks they send.
|
||||
pub(crate) fn is_internal_runtime_handoff(message: &Message) -> bool {
|
||||
if is_agent_topology_checkpoint(message) {
|
||||
return true;
|
||||
}
|
||||
if message.role != "user" {
|
||||
return false;
|
||||
}
|
||||
@@ -612,6 +888,7 @@ pub(crate) fn restored_subagent_checkpoint_display(message: &Message) -> Option<
|
||||
RESTORED_COMPLETION_HEADER,
|
||||
RESTORED_COMPLETIONS_HEADER,
|
||||
RESTORED_RUNNING_HEADER,
|
||||
RESTORED_TOPOLOGY_HEADER,
|
||||
]
|
||||
.iter()
|
||||
.any(|header| text.starts_with(header))
|
||||
@@ -624,6 +901,45 @@ pub(crate) fn restored_subagent_checkpoint_display(message: &Message) -> Option<
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tools::subagent::{FleetRole, SubAgentAssignment};
|
||||
|
||||
fn topology_snapshot(agent_id: &str, name: &str, status: SubAgentStatus) -> SubAgentResult {
|
||||
SubAgentResult {
|
||||
name: name.to_string(),
|
||||
agent_id: agent_id.to_string(),
|
||||
context_mode: "fresh".to_string(),
|
||||
fork_context: false,
|
||||
workspace: None,
|
||||
git_branch: None,
|
||||
agent_type: FleetRole::Worker,
|
||||
assignment: SubAgentAssignment {
|
||||
objective: "not projected".to_string(),
|
||||
role: None,
|
||||
},
|
||||
model: "not-projected".to_string(),
|
||||
nickname: None,
|
||||
status,
|
||||
worker_status: None,
|
||||
runtime_permissions: None,
|
||||
parent_run_id: None,
|
||||
spawn_depth: 0,
|
||||
child_route: None,
|
||||
result: Some("raw child transcript is not projected".to_string()),
|
||||
steps_taken: 0,
|
||||
checkpoint: None,
|
||||
needs_input: None,
|
||||
duration_ms: 0,
|
||||
started_at: None,
|
||||
from_prior_session: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn message_text(message: &Message) -> &str {
|
||||
let Some(ContentBlock::Text { text, .. }) = message.content.first() else {
|
||||
panic!("expected text message")
|
||||
};
|
||||
text
|
||||
}
|
||||
|
||||
fn completion_payload(agent_id: &str, status: &str, summary: &str) -> String {
|
||||
format!(
|
||||
@@ -631,6 +947,111 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compaction_topology_replaces_stale_state_and_restore_invalidates_liveness() {
|
||||
let summary = Message {
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentBlock::Text {
|
||||
text: "Narrative handoff says the child may still be running.".to_string(),
|
||||
cache_control: None,
|
||||
}],
|
||||
};
|
||||
let mut messages = vec![summary.clone()];
|
||||
let running = topology_snapshot("agent_alpha", "Tide", SubAgentStatus::Running);
|
||||
replace_agent_topology_checkpoint(&mut messages, &[running]);
|
||||
assert_eq!(messages.len(), 2);
|
||||
let first_checkpoint = message_text(messages.last().expect("topology checkpoint"));
|
||||
assert!(first_checkpoint.contains("\"authority\":\"runtime_current\""));
|
||||
assert!(first_checkpoint.contains("\"nonterminal\":1"));
|
||||
assert!(first_checkpoint.contains("\"status\":\"running\""));
|
||||
|
||||
let running_projection = project_messages_for_restore(&messages);
|
||||
let running_display = restored_subagent_checkpoint_display(
|
||||
running_projection
|
||||
.last()
|
||||
.expect("restored running topology checkpoint"),
|
||||
)
|
||||
.expect("restored running display");
|
||||
assert!(running_display.contains("agent_id=agent_alpha"));
|
||||
assert!(running_display.contains("name=Tide"));
|
||||
assert!(running_display.contains("status_at_save=running"));
|
||||
assert!(
|
||||
running_display.contains("historical only; prior worker process is not assumed active")
|
||||
);
|
||||
|
||||
let completed = topology_snapshot(
|
||||
"agent_alpha",
|
||||
"sk-secret-credential-shaped-name",
|
||||
SubAgentStatus::Completed,
|
||||
);
|
||||
replace_agent_topology_checkpoint(&mut messages, &[completed]);
|
||||
assert_eq!(messages.len(), 2, "stale checkpoint must be replaced");
|
||||
assert_eq!(messages[0], summary);
|
||||
let replacement = message_text(messages.last().expect("replacement checkpoint"));
|
||||
assert!(replacement.contains("\"nonterminal\":0"));
|
||||
assert!(replacement.contains("\"terminal\":1"));
|
||||
assert!(replacement.contains("\"status\":\"completed\""));
|
||||
assert!(replacement.contains("sha256:"));
|
||||
assert!(!replacement.contains("sk-secret-credential-shaped-name"));
|
||||
assert!(!replacement.contains("raw child transcript"));
|
||||
assert!(!replacement.contains("not projected"));
|
||||
|
||||
let once = messages.clone();
|
||||
replace_agent_topology_checkpoint(
|
||||
&mut messages,
|
||||
&[topology_snapshot(
|
||||
"agent_alpha",
|
||||
"sk-secret-credential-shaped-name",
|
||||
SubAgentStatus::Completed,
|
||||
)],
|
||||
);
|
||||
assert_eq!(messages, once, "replay must be byte-idempotent");
|
||||
assert_eq!(
|
||||
messages
|
||||
.iter()
|
||||
.filter(|message| is_agent_topology_checkpoint(message))
|
||||
.count(),
|
||||
1,
|
||||
"repeated compaction must retain exactly one typed checkpoint"
|
||||
);
|
||||
|
||||
let projected = project_messages_for_restore(&messages);
|
||||
let display = restored_subagent_checkpoint_display(
|
||||
projected.last().expect("restored topology checkpoint"),
|
||||
)
|
||||
.expect("restored display");
|
||||
assert!(display.contains("agent_id=agent_alpha"));
|
||||
assert!(display.contains("status_at_save=completed"));
|
||||
assert!(display.contains("terminal fact retained"));
|
||||
assert!(!display.contains("prior worker processes are not assumed active"));
|
||||
assert!(!display.contains("\"status\":\"completed\""));
|
||||
assert_eq!(project_messages_for_restore(&projected), projected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_current_topology_explicitly_overrides_old_agent_claims() {
|
||||
let lookalike = Message {
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentBlock::Text {
|
||||
text: format!(
|
||||
"{AGENT_TOPOLOGY_EVENT_PREFIX}{{\"total\":99}}{AGENT_TOPOLOGY_EVENT_SUFFIX}"
|
||||
),
|
||||
cache_control: None,
|
||||
}],
|
||||
};
|
||||
let mut messages = vec![lookalike.clone()];
|
||||
replace_agent_topology_checkpoint(&mut messages, &[]);
|
||||
assert_eq!(messages.len(), 2);
|
||||
assert_eq!(
|
||||
messages[0], lookalike,
|
||||
"user-authored lookalike is not runtime state"
|
||||
);
|
||||
let checkpoint = message_text(messages.last().expect("empty topology checkpoint"));
|
||||
assert!(checkpoint.contains("\"total\":0"));
|
||||
assert!(checkpoint.contains("\"agents\":[]"));
|
||||
assert!(checkpoint.contains("all_prior_agent_lifecycle_claims"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_projection_replaces_completion_control_plane_and_is_idempotent() {
|
||||
let user_task = Message {
|
||||
|
||||
Reference in New Issue
Block a user