Build cacheable embodied newsroom

This commit is contained in:
Meta Vens 2026-08-29 23:48:30 +02:00
commit 901cfcd587
16 changed files with 1286 additions and 646 deletions

View file

@ -1,6 +1,6 @@
use crate::model::{
ModelPort, ModelStageReceipt, ModelStageRequest, mark_model_receipt_semantic_invalid,
run_validated_stage,
ModelInputItem, ModelPort, ModelStageReceipt, ModelStageRequest,
mark_model_receipt_semantic_invalid, render_model_input, run_validated_conversation_stage,
};
use async_trait::async_trait;
use serde::{Serialize, de::DeserializeOwned};
@ -147,7 +147,10 @@ pub async fn run_model_agent_progress<Tool: ModelAgentTool>(
}
let mut receipts = Vec::new();
let mut transcript = String::new();
let mut input = vec![ModelInputItem::user(format!(
"{}\n\nAGENT STEP: 1 of {}. Choose one typed tool action admitted by the current output schema. The harness will execute it against the frozen state and return the real tool observation. Do not claim success yourself; only an accepted tool result can end the task.",
spec.instructions, spec.max_steps
))];
let mut last_observation = None;
for step in 0..spec.max_steps {
let action_schema = match tool.action_schema() {
@ -162,37 +165,18 @@ pub async fn run_model_agent_progress<Tool: ModelAgentTool>(
});
}
};
let action_schema_text = match serde_json::to_string(&action_schema) {
Ok(schema) => schema,
Err(error) => {
return Err(ModelAgentFailure {
message: format!(
"model agent {} could not serialize its current action schema: {error}",
spec.stage
),
receipts,
});
}
};
let source_receipt_ids = causal_source_ids(&spec.source_receipt_ids, &receipts);
let request = ModelStageRequest {
stage: spec.stage.clone(),
model: spec.model.clone(),
snapshot_binding: spec.snapshot_binding.clone(),
lived_stream: format!(
"CURRENT LEGAL TOOL ACTION SCHEMA:\n{}\n\n{}{}\n\nAGENT STEP: {} of {}. Choose one typed tool action admitted by the current schema. The harness will execute it against the frozen state and return the real tool observation. Do not claim success yourself; only an accepted tool result can end the task.",
action_schema_text,
spec.instructions,
transcript,
step + 1,
spec.max_steps,
),
lived_stream: render_model_input(&input),
output_schema: Some(action_schema),
source_receipt_ids,
temperature: spec.temperature,
max_output_tokens: spec.max_output_tokens,
};
let mut stage = match run_validated_stage(port, &request).await {
let mut stage = match run_validated_conversation_stage(port, &request, &input).await {
Ok(stage) => stage,
Err(error) => {
return Err(ModelAgentFailure {
@ -212,12 +196,14 @@ pub async fn run_model_agent_progress<Tool: ModelAgentTool>(
"message":"agent returned no typed action",
});
mark_model_receipt_semantic_invalid(&mut stage.receipt, &finding);
transcript.push_str(&tool_observation(
append_agent_observation(
&mut input,
&stage.narrative,
step,
spec.max_steps,
"rejected",
&serde_json::Value::Null,
&finding,
));
);
last_observation = Some(finding);
receipts.push(stage.receipt);
continue;
@ -231,7 +217,14 @@ pub async fn run_model_agent_progress<Tool: ModelAgentTool>(
"message":error.to_string(),
});
mark_model_receipt_semantic_invalid(&mut stage.receipt, &finding);
transcript.push_str(&tool_observation(step, "rejected", &action_value, &finding));
append_agent_observation(
&mut input,
&stage.narrative,
step,
spec.max_steps,
"rejected",
&finding,
);
last_observation = Some(finding);
receipts.push(stage.receipt);
continue;
@ -266,12 +259,14 @@ pub async fn run_model_agent_progress<Tool: ModelAgentTool>(
return Err(ModelAgentFailure { message, receipts });
}
};
transcript.push_str(&tool_observation(
append_agent_observation(
&mut input,
&stage.narrative,
step,
spec.max_steps,
"continued",
&action_value,
&observation,
));
);
last_observation = Some(observation);
receipts.push(stage.receipt);
receipts.extend(tool_receipts);
@ -305,7 +300,14 @@ pub async fn run_model_agent_progress<Tool: ModelAgentTool>(
}
};
mark_model_receipt_semantic_invalid(&mut stage.receipt, &finding);
transcript.push_str(&tool_observation(step, "rejected", &action_value, &finding));
append_agent_observation(
&mut input,
&stage.narrative,
step,
spec.max_steps,
"rejected",
&finding,
);
last_observation = Some(finding);
receipts.push(stage.receipt);
receipts.extend(tool_receipts);
@ -346,18 +348,23 @@ pub(crate) fn causal_source_ids(base: &[String], receipts: &[ModelStageReceipt])
.collect()
}
fn tool_observation(
fn append_agent_observation(
input: &mut Vec<ModelInputItem>,
assistant_output: &str,
step: usize,
max_steps: usize,
status: &str,
action: &serde_json::Value,
observation: &serde_json::Value,
) -> String {
format!(
"\n\nTOOL OBSERVATION AFTER AGENT STEP {} ({status}):\n{}\nPREVIOUS TOOL ACTION:\n{}\nReturn the next complete typed tool action. Preserve frozen identity and useful valid draft work; change only what the observation actually rejects or requests.",
step + 1,
serde_json::to_string(observation).unwrap_or_else(|_| "null".into()),
serde_json::to_string(action).unwrap_or_else(|_| "null".into()),
)
) {
input.push(ModelInputItem::assistant(assistant_output));
if step + 1 < max_steps {
input.push(ModelInputItem::user(format!(
"TOOL OBSERVATION AFTER AGENT STEP {} ({status}):\n{}\n\nAGENT STEP: {} of {max_steps}. Return the next complete typed tool action. Preserve frozen identity and useful valid draft work; change only what the observation actually rejects or requests.",
step + 1,
serde_json::to_string(observation).unwrap_or_else(|_| "null".into()),
step + 2,
)));
}
}
#[cfg(test)]
@ -556,7 +563,13 @@ mod tests {
);
let requests = model.requests.lock().unwrap();
assert!(requests[1].lived_stream.contains("wrong_value"));
assert!(requests[1].lived_stream.contains("PREVIOUS TOOL ACTION"));
assert!(requests[1].lived_stream.contains("ASSISTANT:"));
assert!(
requests[1]
.lived_stream
.starts_with(&requests[0].lived_stream)
);
assert_eq!(requests[0].output_schema, requests[1].output_schema);
}
#[tokio::test]
@ -633,11 +646,10 @@ mod tests {
"accepted"
);
assert!(
requests[1]
!requests[1]
.lived_stream
.contains("CURRENT LEGAL TOOL ACTION SCHEMA")
);
assert!(requests[1].lived_stream.contains(r#""const":"accepted""#));
}
#[tokio::test]

View file

@ -2013,9 +2013,11 @@ async fn main() -> anyhow::Result<()> {
let issue_campaign = completed_wave_issue_campaign(&wave_reports, report_index)?;
let issue_title = format!("{newspaper_title} — Issue {wave_index}");
let source_campaign_digest = strategic_smoke_digest(&issue_campaign)?;
let newsroom = ghostlight_dungeon::newspaper::canopy_ledger_newsroom();
let editorial_contract_digest = strategic_smoke_digest(&serde_json::json!({
"title":&issue_title,
"editorial_voice":&newspaper_voice,
"newsroom":&newsroom,
"max_articles":5,
}))?;
let recomposition_path =
@ -2040,6 +2042,7 @@ async fn main() -> anyhow::Result<()> {
&issue_campaign,
&issue_title,
&newspaper_voice,
&newsroom,
5,
&store,
import.import,
@ -2397,6 +2400,7 @@ async fn compose_persisted_newspaper(
campaign,
title,
editorial_voice,
&ghostlight_dungeon::newspaper::canopy_ledger_newsroom(),
max_articles,
store,
)

View file

@ -8310,7 +8310,7 @@ mod tests {
&& request.lived_stream.contains("duplicate_statement")
&& request.lived_stream.contains("fact:refuge_authority")
&& request.lived_stream.contains("fact:refuge_resources")
&& request.lived_stream.contains("PREVIOUS TOOL ACTION"),
&& request.lived_stream.contains("ASSISTANT:"),
Ordering::SeqCst,
);
Ok(serde_json::json!({

View file

@ -323,6 +323,7 @@ pub struct WorldNewspaperRequest {
pub title: String,
#[schemars(length(min = 1, max = 600))]
pub editorial_voice: String,
pub newsroom: crate::newspaper::WorldNewspaperNewsroom,
#[schemars(range(min = 1, max = 6))]
pub max_articles: u16,
}

View file

@ -594,7 +594,7 @@ fn schema_catalog() -> Value {
"ghostlight.external_proposal_list.v1": schemars::schema_for!(crate::consumer::ExternalProposalList),
"ghostlight.external_proposal_acknowledgement.v1": schemars::schema_for!(crate::consumer::ExternalProposalAcknowledgement),
"ghostlight.external_proposal_receipt.v1": schemars::schema_for!(crate::consumer::ExternalProposalReceipt),
"ghostlight.world_newspaper_request.v2": schemars::schema_for!(crate::consumer::WorldNewspaperRequest),
"ghostlight.world_newspaper_request.v3": schemars::schema_for!(crate::consumer::WorldNewspaperRequest),
"ghostlight.world_newspaper_issue.v3": schemars::schema_for!(crate::newspaper::WorldNewspaperIssue)
})
}
@ -627,7 +627,7 @@ mod tests {
assert!(schemas["ghostlight.campaign.v1"]["$schema"].is_string());
assert!(schemas["ghostlight.session_zero.v1"]["$schema"].is_string());
assert!(schemas["ghostlight.vault_source_manifest.v1"]["$schema"].is_string());
assert!(schemas["ghostlight.world_newspaper_request.v2"]["$schema"].is_string());
assert!(schemas["ghostlight.world_newspaper_request.v3"]["$schema"].is_string());
assert!(schemas["ghostlight.world_newspaper_issue.v3"]["$schema"].is_string());
assert!(!schemas.contains_key("ghostlight.persona_stage_receipt.v1"));
assert!(!schemas.contains_key("ghostlight.world_mutation.v1"));

View file

@ -32,6 +32,44 @@ pub struct ModelStageRequest {
pub max_output_tokens: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "role", rename_all = "snake_case", deny_unknown_fields)]
pub enum ModelInputItem {
UserText { text: String },
AssistantText { text: String },
}
impl ModelInputItem {
pub fn user(text: impl Into<String>) -> Self {
Self::UserText { text: text.into() }
}
pub fn assistant(text: impl Into<String>) -> Self {
Self::AssistantText { text: text.into() }
}
fn text(&self) -> &str {
match self {
Self::UserText { text } | Self::AssistantText { text } => text,
}
}
}
pub fn render_model_input(items: &[ModelInputItem]) -> String {
items
.iter()
.map(|item| match item {
ModelInputItem::UserText { text } => format!("USER:\n{text}"),
ModelInputItem::AssistantText { text } => format!("ASSISTANT:\n{text}"),
})
.collect::<Vec<_>>()
.join("\n\n")
}
fn model_input_chars(items: &[ModelInputItem]) -> usize {
items.iter().map(|item| item.text().chars().count()).sum()
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct ModelStageReceipt {
pub schema: String,
@ -165,6 +203,15 @@ pub trait ModelPort: Send + Sync {
..Default::default()
})
}
async fn run_observed_conversation(
&self,
request: &ModelStageRequest,
input: &[ModelInputItem],
) -> Result<ModelProviderOutput> {
let mut projected = request.clone();
projected.lived_stream = render_model_input(input);
self.run_observed(&projected).await
}
fn provider(&self) -> &'static str;
fn attempt_timeout(&self, _request: &ModelStageRequest) -> std::time::Duration {
std::time::Duration::from_secs(45)
@ -175,13 +222,36 @@ pub async fn run_validated_stage(
port: &dyn ModelPort,
request: &ModelStageRequest,
) -> Result<ModelStageOutput> {
run_validated_stage_with_timeout(port, request, port.attempt_timeout(request)).await
run_validated_stage_inner(port, request, port.attempt_timeout(request), None).await
}
pub async fn run_validated_conversation_stage(
port: &dyn ModelPort,
request: &ModelStageRequest,
input: &[ModelInputItem],
) -> Result<ModelStageOutput> {
run_validated_stage_inner(
port,
request,
port.attempt_timeout(request),
Some(input.to_vec()),
)
.await
}
pub async fn run_validated_stage_with_timeout(
port: &dyn ModelPort,
request: &ModelStageRequest,
timeout: std::time::Duration,
) -> Result<ModelStageOutput> {
run_validated_stage_inner(port, request, timeout, None).await
}
async fn run_validated_stage_inner(
port: &dyn ModelPort,
request: &ModelStageRequest,
timeout: std::time::Duration,
mut conversation: Option<Vec<ModelInputItem>>,
) -> Result<ModelStageOutput> {
let validator = request
.output_schema
@ -202,10 +272,15 @@ pub async fn run_validated_stage_with_timeout(
let mut transport_attempt = 0;
loop {
let started = Instant::now();
let (failure_kind, diagnostic) = match tokio::time::timeout(
timeout,
port.run_observed(&attempt_request),
)
let (failure_kind, diagnostic) = match tokio::time::timeout(timeout, async {
match conversation.as_deref() {
Some(input) => {
port.run_observed_conversation(&attempt_request, input)
.await
}
None => port.run_observed(&attempt_request).await,
}
})
.await
{
Ok(Ok(output)) => break (output, started.elapsed().as_millis() as u64),
@ -216,7 +291,10 @@ pub async fn run_validated_stage_with_timeout(
"model stage {} timed out after {} seconds with {} input characters",
request.stage,
timeout.as_secs(),
attempt_request.lived_stream.chars().count()
conversation.as_deref().map_or_else(
|| attempt_request.lived_stream.chars().count(),
model_input_chars,
)
),
),
};
@ -269,8 +347,11 @@ pub async fn run_validated_stage_with_timeout(
.expect("attempt was just recorded")
.local_validation_error = Some("provider returned an empty response".into());
if attempt == 0 {
attempt_request.lived_stream.push_str(
"\n\nLOCAL VALIDATOR: The previous response was empty. Return one complete response against the same snapshot and output contract.",
append_validation_retry(
&mut attempt_request,
&mut conversation,
&output,
"LOCAL VALIDATOR: The previous response was empty. Return one complete response against the same snapshot and output contract.".into(),
);
continue;
}
@ -299,9 +380,14 @@ pub async fn run_validated_stage_with_timeout(
.expect("attempt was just recorded")
.local_validation_error = Some(bounded_validation_error(&diagnostic));
if attempt == 0 {
attempt_request.lived_stream.push_str(&format!(
"\n\nLOCAL VALIDATOR REJECTED THE PREVIOUS JSON: {diagnostic}\nReturn one corrected complete JSON object against the same snapshot and schema."
));
append_validation_retry(
&mut attempt_request,
&mut conversation,
&output,
format!(
"LOCAL VALIDATOR REJECTED THE PREVIOUS JSON: {diagnostic}\nReturn one corrected complete JSON object against the same snapshot and schema."
),
);
continue;
}
return Err(anyhow!(
@ -319,9 +405,14 @@ pub async fn run_validated_stage_with_timeout(
.last_mut()
.expect("attempt was just recorded")
.local_validation_error = Some(bounded_validation_error(&error));
attempt_request.lived_stream.push_str(&format!(
"\n\nLOCAL VALIDATOR COULD NOT PARSE THE PREVIOUS RESPONSE AS JSON: {error}\nReturn one corrected complete JSON object against the same snapshot and schema."
));
append_validation_retry(
&mut attempt_request,
&mut conversation,
&output,
format!(
"LOCAL VALIDATOR COULD NOT PARSE THE PREVIOUS RESPONSE AS JSON: {error}\nReturn one corrected complete JSON object against the same snapshot and schema."
),
);
continue;
}
Err(error) => {
@ -345,6 +436,9 @@ pub async fn run_validated_stage_with_timeout(
let receipt_model = resolved_model.unwrap_or_else(|| attempt_request.model.clone());
let mut receipted_request = attempt_request.clone();
receipted_request.model.clone_from(&receipt_model);
if let Some(input) = conversation.as_deref() {
receipted_request.lived_stream = render_model_input(input);
}
let request_bytes = serde_json::to_vec(&receipted_request)?;
let provider = port.provider().to_owned();
let request_hash = format!("sha256:{:x}", Sha256::digest(&request_bytes));
@ -380,7 +474,10 @@ pub async fn run_validated_stage_with_timeout(
latency_ms: stage_started.elapsed().as_millis() as u64,
validation_result: "valid".into(),
local_validation_error: None,
input_chars: attempt_request.lived_stream.chars().count(),
input_chars: conversation.as_deref().map_or_else(
|| attempt_request.lived_stream.chars().count(),
model_input_chars,
),
output_chars: output.chars().count(),
provider_attempts,
},
@ -389,6 +486,23 @@ pub async fn run_validated_stage_with_timeout(
unreachable!()
}
fn append_validation_retry(
request: &mut ModelStageRequest,
conversation: &mut Option<Vec<ModelInputItem>>,
rejected_output: &str,
diagnostic: String,
) {
if let Some(input) = conversation.as_mut() {
if !rejected_output.trim().is_empty() {
input.push(ModelInputItem::assistant(rejected_output));
}
input.push(ModelInputItem::user(diagnostic));
} else {
request.lived_stream.push_str("\n\n");
request.lived_stream.push_str(&diagnostic);
}
}
fn bounded_validation_error(error: &impl std::fmt::Display) -> String {
error.to_string().chars().take(1_000).collect()
}

View file

@ -15,8 +15,8 @@ use tokio::sync::Semaphore;
use zeroize::Zeroizing;
use crate::model::{
MODEL_BALANCED, MODEL_CAPABLE, MODEL_FAST, ModelPort, ModelProviderOutput, ModelStageRequest,
ModelTokenUsage,
MODEL_BALANCED, MODEL_CAPABLE, MODEL_FAST, ModelInputItem, ModelPort, ModelProviderOutput,
ModelStageRequest, ModelTokenUsage,
};
const MAX_FRAME_BYTES: usize = 1_052_672;
@ -90,6 +90,14 @@ impl CodexConnectorModelPort {
}
fn invoke(&self, request: &ModelStageRequest) -> Result<ModelProviderOutput> {
self.invoke_with_input(request, None)
}
fn invoke_with_input(
&self,
request: &ModelStageRequest,
conversation: Option<&[ModelInputItem]>,
) -> Result<ModelProviderOutput> {
let request_id = format!("ghostlight-model-{}", uuid::Uuid::new_v4());
let resolved_model = match request.model.as_str() {
MODEL_FAST => self.fast_model.clone(),
@ -103,9 +111,26 @@ impl CodexConnectorModelPort {
resolved_model,
"Execute the supplied Ghostlight model stage. Treat it as projected context, obey its output contract, and return only the requested public answer.",
);
provider_request.input = vec![CodexInputItem::UserText {
text: request.lived_stream.clone(),
}];
provider_request.input = conversation.map_or_else(
|| {
vec![CodexInputItem::UserText {
text: request.lived_stream.clone(),
}]
},
|input| {
input
.iter()
.map(|item| match item {
ModelInputItem::UserText { text } => {
CodexInputItem::UserText { text: text.clone() }
}
ModelInputItem::AssistantText { text } => {
CodexInputItem::AssistantText { text: text.clone() }
}
})
.collect()
},
);
provider_request.reasoning_effort = Some(
if matches!(request.model.as_str(), MODEL_BALANCED | MODEL_CAPABLE) {
"medium"
@ -126,7 +151,11 @@ impl CodexConnectorModelPort {
provider_request.max_output_tokens = request.max_output_tokens;
provider_request.prompt_cache_key = Some(prompt_cache_key(request)?);
let native_request_sha256 = Sha256::digest(rmp_serde::to_vec(request)?).into();
let native_request_sha256 = Sha256::digest(match conversation {
Some(input) => rmp_serde::to_vec_named(&(request, input))?,
None => rmp_serde::to_vec(request)?,
})
.into();
let invocation = CodexTransportInvocation::new(
self.caller_runtime_id.clone(),
unix_ms()?.saturating_add(REQUEST_EXPIRY.as_millis() as u64),
@ -159,6 +188,27 @@ impl ModelPort for CodexConnectorModelPort {
.await?
}
async fn run_observed_conversation(
&self,
request: &ModelStageRequest,
input: &[ModelInputItem],
) -> Result<ModelProviderOutput> {
let permit = self
.request_gate
.clone()
.acquire_owned()
.await
.map_err(|_| anyhow::anyhow!("CodexConnector request gate closed"))?;
let port = self.clone();
let request = request.clone();
let input = input.to_vec();
tokio::task::spawn_blocking(move || {
let _permit = permit;
port.invoke_with_input(&request, Some(&input))
})
.await?
}
fn provider(&self) -> &'static str {
"codex-connector"
}
@ -673,8 +723,7 @@ mod tests {
};
#[tokio::test]
async fn connector_port_uses_the_shared_v2_transport_and_exact_provider_request() -> Result<()>
{
async fn connector_port_preserves_append_only_roles_in_the_provider_request() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let endpoint = listener.local_addr()?;
let server = std::thread::spawn(move || -> Result<CodexTransportInvocation> {
@ -728,23 +777,27 @@ mod tests {
1,
)?;
assert_eq!(port.request_gate.available_permits(), 1);
let output = port
.run_observed(&ModelStageRequest {
stage: "test_interpreter".to_string(),
model: MODEL_BALANCED.to_string(),
snapshot_binding: "revision:7".to_string(),
lived_stream: "Return the typed answer.".to_string(),
output_schema: Some(serde_json::json!({
"type":"object",
"required":["answer"],
"properties":{"answer":{"type":"string"}},
"additionalProperties":false
})),
source_receipt_ids: Vec::new(),
temperature: Some(0.0),
max_output_tokens: Some(512),
})
.await?;
let request = ModelStageRequest {
stage: "test_interpreter".to_string(),
model: MODEL_BALANCED.to_string(),
snapshot_binding: "revision:7".to_string(),
lived_stream: "Return the typed answer.".to_string(),
output_schema: Some(serde_json::json!({
"type":"object",
"required":["answer"],
"properties":{"answer":{"type":"string"}},
"additionalProperties":false
})),
source_receipt_ids: Vec::new(),
temperature: Some(0.0),
max_output_tokens: Some(512),
};
let input = vec![
ModelInputItem::user("Choose one action."),
ModelInputItem::assistant("{\"answer\":\"draft\"}"),
ModelInputItem::user("The tool rejected that draft; repair it."),
];
let output = port.run_observed_conversation(&request, &input).await?;
let invocation = server.join().expect("server thread")?;
assert_eq!(invocation.request.model, "gpt-5.6-terra");
assert_eq!(invocation.request.max_output_tokens, Some(512));
@ -752,6 +805,16 @@ mod tests {
invocation.request.output_format_name.as_deref(),
Some("test_interpreter")
);
assert!(matches!(
invocation.request.input.as_slice(),
[
CodexInputItem::UserText { text: first },
CodexInputItem::AssistantText { text: second },
CodexInputItem::UserText { text: third },
] if first == "Choose one action."
&& second == "{\"answer\":\"draft\"}"
&& third == "The tool rejected that draft; repair it."
));
assert_eq!(output.content, "{\"answer\":\"ready\"}");
assert_eq!(
output.token_usage.expect("usage").prompt_cache_hit_tokens,
@ -974,6 +1037,14 @@ mod tests {
prompt_cache_key(&request)?,
prompt_cache_key(&changed_context)?
);
changed_context.output_schema = Some(serde_json::json!({
"type":"object",
"properties":{"different_action":{"type":"string"}}
}));
assert_ne!(
prompt_cache_key(&request)?,
prompt_cache_key(&changed_context)?
);
Ok(())
}
}

View file

@ -304,10 +304,10 @@ async fn handle_operation_inner(
success_response(request, "ghostlight.external_proposal_receipt.v1", &receipt)
}
ghostlight_dungeon::consumer::COMPOSE_NEWSPAPER_OPERATION => {
require_schema(payload_schema, "ghostlight.world_newspaper_request.v2")?;
require_schema(payload_schema, "ghostlight.world_newspaper_request.v3")?;
let command: ghostlight_dungeon::consumer::WorldNewspaperRequest =
decode_request(request)?;
require_schema(&command.schema, "ghostlight.world_newspaper_request.v2")?;
require_schema(&command.schema, "ghostlight.world_newspaper_request.v3")?;
let model = state
.model
.as_deref()
@ -525,14 +525,14 @@ mod tests {
assert!(
require_schema(
"ghostlight.world_newspaper_request.v1",
"ghostlight.world_newspaper_request.v2"
"ghostlight.world_newspaper_request.v3"
)
.is_err()
);
assert!(
require_schema(
"ghostlight.world_newspaper_request.v2",
"ghostlight.world_newspaper_request.v2"
"ghostlight.world_newspaper_request.v3",
"ghostlight.world_newspaper_request.v3"
)
.is_ok()
);

File diff suppressed because it is too large Load diff

View file

@ -381,7 +381,7 @@ impl CampaignRegistry {
request: WorldNewspaperRequest,
model: &dyn ModelPort,
) -> Result<crate::newspaper::WorldNewspaperAdvance> {
if request.schema != "ghostlight.world_newspaper_request.v2"
if request.schema != "ghostlight.world_newspaper_request.v3"
|| request.max_articles == 0
|| request.max_articles > 6
|| request.editorial_voice.trim().is_empty()
@ -414,6 +414,7 @@ impl CampaignRegistry {
&campaign,
request.title,
request.editorial_voice,
&request.newsroom,
usize::from(request.max_articles),
&runtime.store,
)
@ -684,58 +685,26 @@ mod tests {
struct RejectingRegistryNewspaperModel;
fn first_schema_property_enum<'a>(
schema: &'a serde_json::Value,
property: &str,
) -> Option<&'a str> {
match schema {
serde_json::Value::Object(object) => object
.get("properties")
.and_then(serde_json::Value::as_object)
.and_then(|properties| properties.get(property))
.and_then(|value| value.pointer("/items/enum/0"))
.and_then(serde_json::Value::as_str)
.or_else(|| {
object
.values()
.find_map(|value| first_schema_property_enum(value, property))
}),
serde_json::Value::Array(values) => values
.iter()
.find_map(|value| first_schema_property_enum(value, property)),
_ => None,
}
}
fn first_schema_property_const<'a>(
schema: &'a serde_json::Value,
property: &str,
) -> Option<&'a str> {
match schema {
serde_json::Value::Object(object) => object
.get("properties")
.and_then(serde_json::Value::as_object)
.and_then(|properties| properties.get(property))
.and_then(|value| value.get("const"))
.and_then(serde_json::Value::as_str)
.or_else(|| {
object
.values()
.find_map(|value| first_schema_property_const(value, property))
}),
serde_json::Value::Array(values) => values
.iter()
.find_map(|value| first_schema_property_const(value, property)),
_ => None,
}
fn last_transcript_value(stream: &str, prefix: &str) -> Option<String> {
let start = stream.match_indices(prefix).last()?.0 + prefix.len();
let tail = stream.get(start..)?;
Some(tail.split('"').next()?.to_owned())
}
fn registry_newspaper_narrative_response(request: &ModelStageRequest) -> Result<String> {
let schema = request
.output_schema
.as_ref()
.ok_or_else(|| anyhow!("newspaper fixture lost its narrative action schema"))?;
if !schema.to_string().contains("propose_agenda") {
if let Some(candidate_digest) =
last_transcript_value(&request.lived_stream, "\"candidate_digest\":\"")
{
return Ok(serde_json::json!({
"command":{
"tool":"commit_agenda",
"candidate_digest":candidate_digest,
}
})
.to_string());
}
let Some(citation) = last_transcript_value(&request.lived_stream, "\"record_id\":\"")
else {
return Ok(serde_json::json!({
"command":{
"tool":"query_public_records",
@ -750,20 +719,7 @@ mod tests {
}
})
.to_string());
}
if schema.to_string().contains("commit_agenda") {
let candidate_digest = first_schema_property_const(schema, "candidate_digest")
.ok_or_else(|| anyhow!("newspaper fixture lost its reviewed agenda digest"))?;
return Ok(serde_json::json!({
"command":{
"tool":"commit_agenda",
"candidate_digest":candidate_digest,
}
})
.to_string());
}
let citation = first_schema_property_enum(schema, "citations")
.ok_or_else(|| anyhow!("newspaper fixture lost its narrative citation enum"))?;
};
Ok(serde_json::json!({
"command":{
"tool":"propose_agenda",
@ -771,6 +727,8 @@ mod tests {
"reader_stake":"Readers depend on whether the court can distinguish an allegation from a judgment.",
"story_pitches":[{
"lead":true,
"section":"Front Page",
"journalist_id":"aven-tarl",
"citations":[citation],
"focus_citation":citation,
"narrative_claim":"The singular accusation against three auditors is itself the scandal.",
@ -782,25 +740,16 @@ mod tests {
.to_string())
}
fn registry_newspaper_editor_response(request: &ModelStageRequest) -> Result<String> {
let citation = request
.output_schema
.as_ref()
.and_then(|schema| first_schema_property_enum(schema, "citations"))
.ok_or_else(|| anyhow!("newspaper fixture lost its citation enum"))?;
fn registry_newspaper_editor_response(_request: &ModelStageRequest) -> Result<String> {
Ok(serde_json::json!({
"articles":[{
"section":"Front Page",
"headline":"Court Auditors Face a Singular Accusation",
"deck":"The inner court has turned a granary inquiry into a question of witchcraft and arithmetic.",
"byline":"By the political editor",
"dateline":"Room",
"citations":[citation],
"paragraphs":[
"The inner court has accused three granary auditors of being one witch in a long coat, placing the allegation into the public record.",
"The charge leaves the auditors answering a court that has made one person out of three, at least for purposes of blame."
]
}]
"tool":"file_story",
"headline":"Court Auditors Face a Singular Accusation",
"deck":"The inner court has turned a granary inquiry into a question of witchcraft and arithmetic.",
"dateline":"Room",
"paragraphs":[
"The inner court has accused three granary auditors of being one witch in a long coat, placing the allegation into the public record.",
"The charge leaves the auditors answering a court that has made one person out of three, at least for purposes of blame."
]
})
.to_string())
}
@ -1482,13 +1431,14 @@ mod tests {
let (_, repeated) = registry.admit_world_seed(request).await.unwrap();
assert_eq!(repeated.seed_digest, receipt.seed_digest);
let newspaper_request = crate::consumer::WorldNewspaperRequest {
schema: "ghostlight.world_newspaper_request.v2".into(),
schema: "ghostlight.world_newspaper_request.v3".into(),
campaign_id: campaign.id,
authority_id: "authority:external-hold".into(),
owner_id: "fixture-consumer".into(),
authority_key: authority_key.into(),
title: "The Consumer Gazette".into(),
editorial_voice: "A skeptical regional court broadsheet.".into(),
newsroom: crate::newspaper::canopy_ledger_newsroom(),
max_articles: 4,
};
let mut denied_newspaper = newspaper_request.clone();

View file

@ -1362,6 +1362,24 @@ disturbing” from cordon and warnings to barriers and path; line 19 changes
historical stalled lifts into “remains stalled.” These findings do not reopen
the printed edition or block delivery.
The source-grounded prompt audit falsified v8 as the target newsroom
architecture. Run 59 narrative selection used ten provider calls, 1,373,208
input characters, 582,184 prompt tokens, and zero cached tokens.
`character-newsroom.v9` now uses typed append-only model items, stable selector
and Night action schemas, stable stage/model/schema cache routing, and no
prompt-embedded tool schemas or repeated prior actions. Veyra Kest sees the
complete embodied publication roster and assigns every story to a journalist whose character,
biases, and preferences inform that choice. Each reporter sees only their own
profile, assignment, and exact cited records. Dalen Marr alone raises one
complete factual checklist. Meret Sorn receives only queried articles, their
assignments and cited facts, and the affected reporter profiles; she gets one
chance to resolve exactly those objections while preserving the story. The
one-shot whole-page writer and generic byline registry are deleted.
Deterministic press admission still owns structure, provenance, query
disposition, and closure. Focused gates pass 475/475 library tests, 16/16
strategic-smoke tests, and all-target checks. No provider or simulation run has
been launched for v9.
Run 54 exposed one separate upstream publicity defect: a public strategic
activity propagates its channel to private `MemberMemory` outcomes, which is
how Orin Pell retaining a memory became a public news row. The newsroom should
@ -1369,13 +1387,14 @@ ignore that bookkeeping, but revision 27 remains immutable. Future kernel work
must require an outcome's own observable or communicated public scope before
materializing a `NewsIssue`.
1. Deliver the actual immutable Run 59 copy and its press/artifact proof. Do not
rerun, rewrite, or withhold the edition.
2. Preserve the two grounding corrections and editorial diagnostics above as
later publication debt. After delivery, move to the publication-Persona and
correction-memory owner.
3. Defer multiple publication Personas, TeX layout, and engraved cuts until
separately authorized after Run 59 delivery.
1. Preserve immutable Run 59 and its later-publication correction debt; do not
rewrite or resimulate it.
2. Exercise `character-newsroom.v9` only in a separately authorized
newsroom-only provider run. Measure actual cached tokens and judge the
embodied reporters' copy rather than treating local cache invariants as
provider acceptance.
3. Defer multiple publications, TeX layout, and engraved cuts until separately
authorized.
4. Per-wave issues may use daily-like reporting windows, while a final
composition may be an equally current weekly-style issue with a broader
window. Both are selective journalism; broad fact availability increases

View file

@ -1288,21 +1288,26 @@ The deliberative newsroom boundary is live under this authority map:
- **Owner:** canonical `NewsIssue` plus `Event` rows own public provenance and
facts. `Event::public_assertion_status` owns attempt, declaration,
committed-course, material-outcome, and unspecified-account semantics. The
narrative-selection agent owns assignment: story choice, order, focal record,
exact citation grouping, throughline, tension, and reader question. Its
in-world assignment editor owns assignment: story choice, order, section,
journalist, focal record, exact citation grouping, throughline, tension, and
reader question. Its
deterministic workbench owns query admission, inspected-record membership,
agenda validation, focus-record dereferencing, candidate identity, and exact
commit admission. The lead writer owns one complete reader-facing page from
that assignment. The copy desk owns one complete factual query report over
the writer's page; it neither rewrites nor accepts publication. The Night
commit admission. Each assigned in-world journalist owns one reader-facing
story and receives only that pitch, its exact records, and that journalist's
character, biases, and preferences. The copy editor owns one complete factual
query report over the assembled page; it neither rewrites nor accepts
publication. The Night
Editor owns one deadline close over that page and report. The deterministic
press witness owns structural admission, lineage, and exact before/after
evidence for what printed; it does not judge prose. Errors discovered after
close belong to a later edition's correction memory, not a same-edition
repair loop. V8 defines that ownership boundary but does not yet persist a
typed correction record. `WorldKernel` remains the only world-event writer.
repair loop. V9 defines that ownership boundary but does not yet persist a
typed correction record. Publication employees are not world actors or event
writers. `WorldKernel` remains the only world-event writer.
- **Inputs:** one exact campaign revision, publication title and voice, article
budget, source-receipt ancestry, and v8 newsroom contract. Through the
budget, a validated embodied newsroom roster, source-receipt ancestry, and
`character-newsroom.v9` contract. Through the
existing generic agent harness, the selector issues deterministic bounded
queries over stable `NewsIssue` IDs using literal terms, exact asserted
public entity names, assertion statuses, channels, newest/oldest ordering,
@ -1319,11 +1324,13 @@ The deliberative newsroom boundary is live under this authority map:
submits `commit_agenda` with the exact digest of the current reviewed
candidate. The committed agenda carries one dominant throughline, reader
stake, and ordered story pitches. Every pitch names stable public-record IDs,
chooses one focus record, and supplies bounded narrative claim, tension, and
public question. Article count plus bounded paragraphs and output tokens
chooses one focus record, section, and journalist, and supplies bounded
narrative claim, tension, and public question. Article count plus bounded paragraphs and output tokens
define page space. There is no citation-count page budget. A foundational
record may support several continuing stories when each pitch uses it for a
distinct throughline. The writer emits one page; the copy desk emits one
distinct throughline. One journalist agent files each assigned story and the
page is assembled deterministically from assignment-owned section, byline,
and citations plus journalist-owned prose. The copy desk emits one
assessment plus the complete bounded set of exact factual query passages;
the Night Editor emits one close action; and accepted composition v3 carries
the printed issue, copy report, press-close witness, and exact receipt chain.
@ -1345,38 +1352,52 @@ The deliberative newsroom boundary is live under this authority map:
would bury, and returns the exact candidate digest. This is an observation,
not acceptance. The agent must reconsider the actual consequences in that
proof and explicitly commit the current digest; a replacement proposal
retires the prior candidate as commit authority. The downstream editor sees
only the committed agenda, dereferences exactly its selected IDs, centers
each article on its focus record's consequential fact, and may not widen or
regroup the selection. Record bookkeeping, memory retention, maintained
retires the prior candidate as commit authority. Each assigned journalist
sees only the publication voice, shared reporting contract, their own
character, the page throughline, one pitch, and that pitch's exact records.
They center their story on its focus record and may not widen or regroup the
selection. Record bookkeeping, memory retention, maintained
warnings, and procedure are not automatically the lede merely because they
are recent. The writer, copy desk, and Night Editor see the same
selected-record projection.
are recent. The copy editor sees the assembled page and selected facts. The
Night Editor sees only queried articles, their assignments and cited facts,
the numbered checklist, its own profile, and the profiles of the affected
reporters.
- **Desk and press path:** accepted stable IDs lower once into the existing
self-contained semantic audit with source news IDs, channels, reliability,
exact account text, assertion status, event IDs, supported identity
attributes, institutions, populations, and places. Reader-facing numeric
labels are assigned only during lowering. The copy desk reads the selected
fact projection and writer page exactly once, returning all factual queries
fact projection and assembled page exactly once, returning all factual queries
at once as exact unique passages with reasons. The immutable close checkpoint
binds that report, page, agenda, task, sources, and receipts before the Night
Editor acts. The Night Editor receives the assignment, selected facts, page,
and numbered query list, then gets one action to disposition every query and
provide complete replacement prose for each queried article. In that same
close it may optionally replace any other article to sharpen its headline,
lede, throughline, rhythm, or ending. The press workbench freezes article
selection, order, section, byline, citations, and agenda while allowing prose
replacement across any existing article. It validates structure and source
Editor acts. The Night Editor receives the affected assignments, queried
articles and facts, and numbered query list, then gets one action to
disposition every query and provide complete replacement prose for exactly
each queried article. The
press workbench freezes article selection, order, section, byline, citations,
and agenda and forbids changes to every unqueried article. It validates structure and source
membership, not whether the final language is true or compelling. A structurally invalid
close exhausts the one action and prints the checkpointed writer page with an
close exhausts the one action and prints the checkpointed journalist page with an
explicit non-applied close witness. There is no post-close model reread.
- **Agent context and cache path:** the generic harness sends typed append-only
user/assistant items instead of flattening every turn into one new user
transcript. Selector query, proposal, and commit share one stable structured
action schema, so each later provider request preserves the exact previous
prefix. The Night close schema is likewise stable across checklist contents;
its private workbench enforces the exact queried set. Tool schemas stay in the
provider output contract rather than being duplicated in prompt text.
Prompt-cache routing binds stage, model class, and exact output schema;
changing world context does not mint another routing key, while changing the
contract does. Each journalist receives only one pitch and its records. Copy
receives the complete selected desk because it alone checks the page; Night
receives only the subset named by Copy's checklist.
- **Invariants:** private events do not enter the query surface; every selected
stable ID resolves at the bound revision; story order, selection, citation
grouping, and focus remain agenda-owned; no proposal reaches the writer
grouping, focus, section, and journalist remain agenda-owned; no proposal reaches the journalists
without deterministic proof observation and exact-current-digest commit;
omitted public facts remain true; assignment, writing, copy queries, close,
lowering, and witness creation cannot mutate the world. There is one writer
pass, one copy report, and one admitted Night Editor close. No model verdict
lowering, and witness creation cannot mutate the world. There is one bounded
reporter pass per pitch, one copy report, and one admitted Night Editor close. No model verdict
rereads the printed page, no rejected close opens an iterative rewrite loop,
and no external evaluation can reopen an immutable edition. The witness must
preserve exact source and printed page digests, receipt ancestry, every query
@ -1387,77 +1408,74 @@ The deliberative newsroom boundary is live under this authority map:
state. The deterministic proof cannot rank stories or commit an agenda; it
exposes the model's proposed ordering against exact focus facts. A proposal
cannot bypass review by returning as accepted output, and a missing or stale
candidate digest cannot commit. The editor cannot widen the admitted record
set or write a second page. The copy desk cannot rewrite, suppress, or accept
candidate digest cannot commit. A journalist cannot change section, byline,
citations, assignment, another journalist's prose, or the admitted record
set. The copy desk cannot rewrite, suppress, or accept
prose, select stories, close the press, or withhold the complete query set for
a later pass. The Night Editor cannot widen sources, add, remove, or reorder
stories, alter sections, bylines, citations, or agenda, invoke another model
judge, or reopen a completed close. The Night Editor may change the
reader-facing prose of any existing article during that one close, but every
copy-query-bearing article is mandatory. The press witness cannot certify
judge, reopen a completed close, or touch an article absent from the copy
checklist. Every copy-query-bearing article and no other article is mandatory.
The press witness cannot certify
factual or editorial quality; it can only prove the exact admitted process and
bytes.
Renderers, strategic smoke, Registry, external blind reviewers, and grounding
reviewers cannot write the page, desk report, close, correction, world state,
or acceptance state. Caches cannot decide record equivalence, assertion
status, story relevance, or fact completion.
status, story relevance, fact completion, or staff assignment.
- **Shared paths:** fresh composition queries stable records, proposes one or
more agendas, observes the deterministic front-page proof for each proposal,
and commits exactly one current candidate before invoking the writer. Fresh
work then follows one path: writer, copy report, immutable close checkpoint,
and commits exactly one current candidate before invoking the assigned
journalists. Fresh work then follows one path: one bounded reporter agent per
pitch, deterministic page assembly, copy report, immutable close checkpoint,
one Night Editor action, deterministic press witness, lowering, persistence.
Registry, live per-wave composition, missing-issue recovery, and final digest
all delegate to `advance_world_newspaper`. Resume begins from the immutable
close checkpoint and does not rerun assignment, writer, or copy desk. A
close checkpoint and does not rerun assignment, journalists, or copy desk. A
persisted composition is revalidated against its close checkpoint and exact
stored receipts, then returned without model work.
- **Persistence and compatibility:** the live internal contract is
`canopy-ledger-night-editor.v8`. Close checkpoint v1 binds the assigned agenda,
writer page, single copy report, task and exact editorial bindings, source
`character-newsroom.v9`. Public request schema v3 carries the publication
roster and binds it into the task and editorial identities. Close checkpoint
v1 binds the assigned agenda, assembled page, single copy report, task and exact editorial bindings, source
checkpoint when present, and immutable receipt chain. Composition v3 stores
the printed issue, copy report, press-close witness, and receipts. Public issue
schema v3 remains unchanged. Candidate agendas and front-page proofs remain
ephemeral. V7 reconciliation checkpoint v3 is read-only resume provenance:
an explicit external import may materialize the exact immutable Run 58 copy
tip once, but v8 never appends to or advances that chain. Only a v7 tip whose
rejecting owner is the copy desk can become one v8 close checkpoint; its
tip once, but v9 never appends to or advances that chain. Only a v7 tip whose
rejecting owner is the copy desk can become one v9 close checkpoint; its
draft, agenda, query report, bindings, and receipt ancestry must validate
exactly. A v7 Night Editor rejection fails closed rather than being recast as
copy evidence. Run 58's v7 copy tip is imported into one v8 Night Editor
close without replaying ledger queries, agenda selection, writer, copy desk,
copy evidence. Run 58's v7 copy tip is imported into one v9 Night Editor
close without replaying ledger queries, agenda selection, journalists, copy desk,
world mechanics, strategic waves, or accepted per-wave publications.
- **Cut line:** `NewsroomSource`, transient ordinal selection IDs, fact-bundle
deduplication, fixed newest-32/preceding-12/foundational-12 windows,
per-story and total citation ceilings, global record uniqueness across
articles, and the newspaper-local event-kind status mapping are removed.
The direct `submit_agenda` acceptance path is also removed: query plus a
structurally valid proposal is insufficient authority to invoke the editor.
Phrase replacements, finding references, target-field and
paragraph addressing, byte-span resolution, overlap ordering, empty-phrase
deletion, and article deletion remain absent. V8 additionally deletes the
publication-review verdict union, editorial-quality verdict, generic rewrite
desk, bounded multi-action rewrite cadence, repeated copy-desk calls, Night
Editor rereview, reconciliation advancement, and pending-as-editorial-state
path. No compatibility path may retain those operations as live writers.
Whole-article prose is the sole Night Editor close unit. Query-bearing
articles are mandatory, any other existing article is optional, and the
admitted agenda plus deterministic close merge owns everything around them.
Query projection and front-page proof reuse the generic agent harness and
existing public-record, agenda, accepted issue, and audit shapes. No
dependency, crate, binary, daemon, persistent schema, service, cache, event
writer, target platform, code-generation path, or world-simulation pass was
added.
- **Cut line:** the generic byline list and one-shot whole-page writer are
deleted. Assignment-owned section, journalist, byline, citations, and order
are no longer regenerated in prose output. Tool schema text and prior-action
copies are absent from prompts; agent history is no longer flattened and
retransmitted as a newly rendered user transcript. Selector commands no
longer mutate the action schema as records become visible. Phrase-level
repairs, repeated copy calls, Night rereview, post-close model judgment, and
edits to unqueried articles remain absent. Whole-article prose is the sole
Night Editor close unit, and exactly the query-bearing articles are mandatory.
The roster, query projection, front-page proof, and article assembly reuse the
generic agent harness and existing public-record, agenda, accepted issue, and
audit shapes. No dependency, crate, binary, daemon, persistent schema,
service, cache, event writer, target platform, code-generation path, or
world-simulation pass was added.
- **Verification and build budget:** focused newspaper tests retain query
admission, exact focus proof, agenda commit, selected-record alignment, and
audit grounding. V8 gates prove one complete copy report, exact unique query
targets, one Night Editor action, complete query disposition, mandatory
rewrites for query-bearing articles, optional polish of any other article,
preservation of assignment-owned structure and citations, no post-close copy
or editorial stage, deterministic source/printed digests,
exact receipt ancestry, close-checkpoint resume without writer or copy replay,
idempotent persisted-composition validation, printing the checkpointed page
after a structurally rejected close, and conversion of one legacy v7 copy tip
into one v8 close without replay. Strategic recomposition v3 binds the copy
audit grounding. V9 gates prove stable selector and Night schemas plus
append-only provider prefixes, assignment-aware reporter context isolation, one complete
copy report, exact unique query targets, one Night Editor action over only the
queried articles and facts, complete query disposition, preservation of
assignment-owned structure and citations, no post-close copy or editorial
stage, deterministic source/printed digests, exact receipt ancestry,
close-checkpoint resume without reporter or copy replay, idempotent persisted
composition validation, printing the checkpointed page after a structurally
rejected close, and conversion of one legacy v7 copy tip into one v9 close
without replay. Strategic recomposition v3 binds the copy
report and press witness by digest while v1/v2 remain historical validation
inputs. Independent blind editorial and grounding reviews evaluate the
resulting artifact for the acceptance experiment only; they are deliberately

View file

@ -229,4 +229,6 @@
{"note": "Query-backed Canopy Ledger newsroom cut: canonical NewsIssue plus Event rows now own public facts and stable record IDs; the generic agent harness provides bounded deterministic ledger queries; selector, editor, copy desk, audit, and reconciliation share those stable IDs; numeric citations are late display labels. Removed NewsroomSource packet authority, fact-bundle deduplication, fixed archive windows, citation-count ceilings, cross-story record uniqueness, and the newspaper-local event-kind status switch. Deterministic admission rejects ambiguous ledger IDs and duplicate IDs within persisted agenda or draft groups. Verified 28/28 focused newspaper, 483/483 library, and 15/15 strategic-smoke tests before final formatting pass. Independent Modeling review confirmed the cut and isolated the remaining MemberMemory publicity defect to the kernel; revision 27 and run 54 remain immutable.", "status": "ok", "ts": "2026-08-29T14:06:49+00:00", "type": "implementation"}
{"note": "Exact commit c34c4b1f94706661cdc35d5314ea23347bda6fd7 replaces direct newsroom agenda acceptance with query, proposal, deterministic front-page proof, and exact-current-digest commit. The publication writer owns narrative construction over admitted facts; the independent copy desk owns factual judgment. Focused newspaper 28/28, library 483/483, and strategic smoke 15/15 pass, including rejection of unreviewed and stale candidates; no world simulation or persistent schema was added.", "status": "ok", "ts": "2026-08-29T14:58:39+00:00", "type": "implementation"}
{"note": "Newsroom contract v8 replaces the v7 copy/Night accept-reject tribunal and iterative rewrite loop with one complete copy-desk query report, one max_steps=1 Night Editor close, and a deterministic press witness. Every queried article must be rewritten; unqueried articles may receive optional deadline polish while selection, order, sections, bylines, citations, and agenda stay frozen. Exact v7 copy-tip import resumes Run 58 without replaying narrative selection, writer, copy desk, world, or waves. External reviews are post-press evidence rather than vetoes; typed later-correction memory remains pending. Verified 20/20 newspaper, 475/475 library, and 16/16 strategic-smoke tests.", "status": "ok", "ts": "2026-08-29T19:12:02+00:00", "type": "implementation"}
{"ts": "2026-08-29T19:50:00+00:00", "type": "verification", "status": "ok", "note": "Exact-source Run 59 at pushed 161ff2f19021249563e192cce9e6f76f00429d48 printed from immutable Run 58/revision 27 without upstream replay. The press witness binds one copy report with five queries, one applied Night close, three changed articles, and unchanged live services; archive SHA-256 ecb4db41e9a40ee4db8b0f83dce42d74e40f940e5930b1d3c1ced227dfec76b3 matches. Newspaper ee2ce08ceb627d433ac2d4ff7e1ebecb1437bf28b1399d23bfb3d5b066c8ab27 and audit b210d50d4ef1a350315d528f6d37e1e7b618926346713f737c4169c49df26b49 are frozen. A finalizer schema expectation was corrected and finalization resumed without a model call. Post-press editorial and two grounding diagnostics are later publication debt and do not reopen or block delivery of this edition."}
{"note": "Exact-source Run 59 at pushed 161ff2f19021249563e192cce9e6f76f00429d48 printed from immutable Run 58/revision 27 without upstream replay. The press witness binds one copy report with five queries, one applied Night close, three changed articles, and unchanged live services; archive SHA-256 ecb4db41e9a40ee4db8b0f83dce42d74e40f940e5930b1d3c1ced227dfec76b3 matches. Newspaper ee2ce08ceb627d433ac2d4ff7e1ebecb1437bf28b1399d23bfb3d5b066c8ab27 and audit b210d50d4ef1a350315d528f6d37e1e7b618926346713f737c4169c49df26b49 are frozen. A finalizer schema expectation was corrected and finalization resumed without a model call. Post-press editorial and two grounding diagnostics are later publication debt and do not reopen or block delivery of this edition.", "status": "ok", "ts": "2026-08-29T19:50:00+00:00", "type": "verification"}
{"note": "Run 59 prompt audit falsifies v8 as the target newsroom/cache architecture: narrative selection made ten provider calls over 1,373,208 input characters and 582,184 prompt tokens with zero cached tokens. The generic agent harness places changing action schema before stable work and replays the accumulated tool transcript; selector, whole-page writer, and Night Editor duplicate editorial doctrine. Operator-authorized owners are story-and-journalist assignment from the frozen ledger, bounded personality-bearing journalist drafting, copy desk as sole semantic factual query owner, one Night Editor checklist repair preserving narrative, and deterministic structural/provenance/query-disposition press admission. Exact schema and cache cuts remain pending bounded source inspection.", "status": "ok", "ts": "2026-08-29T20:29:24+00:00", "type": "research"}
{"ts": "2026-08-29T21:42:15+00:00", "type": "implementation", "status": "ok", "note": "character-newsroom.v9 replaces the monolithic page writer with an embodied assignment editor and one bounded journalist agent per assigned story, preserves exact assignment-owned structure, bounds Night Editor context to copy-queried articles and facts, and makes generic agent requests append-only with stable schemas and cache routing. Verified 475/475 library tests, 16/16 strategic-smoke tests, and ghostlight-dungeon all-target checks; no provider or simulation run was launched."}

View file

@ -231,3 +231,5 @@
{"key": "2026-08-29T145839Z0000-0229-d9ee904c8b40", "payload": "eyJub3RlIjogIkV4YWN0IGNvbW1pdCBjMzRjNGIxZjk0NzA2NjYxY2RjMzVkNTMxNGVhMjMzNDdiZGE2ZmQ3IHJlcGxhY2VzIGRpcmVjdCBuZXdzcm9vbSBhZ2VuZGEgYWNjZXB0YW5jZSB3aXRoIHF1ZXJ5LCBwcm9wb3NhbCwgZGV0ZXJtaW5pc3RpYyBmcm9udC1wYWdlIHByb29mLCBhbmQgZXhhY3QtY3VycmVudC1kaWdlc3QgY29tbWl0LiBUaGUgcHVibGljYXRpb24gd3JpdGVyIG93bnMgbmFycmF0aXZlIGNvbnN0cnVjdGlvbiBvdmVyIGFkbWl0dGVkIGZhY3RzOyB0aGUgaW5kZXBlbmRlbnQgY29weSBkZXNrIG93bnMgZmFjdHVhbCBqdWRnbWVudC4gRm9jdXNlZCBuZXdzcGFwZXIgMjgvMjgsIGxpYnJhcnkgNDgzLzQ4MywgYW5kIHN0cmF0ZWdpYyBzbW9rZSAxNS8xNSBwYXNzLCBpbmNsdWRpbmcgcmVqZWN0aW9uIG9mIHVucmV2aWV3ZWQgYW5kIHN0YWxlIGNhbmRpZGF0ZXM7IG5vIHdvcmxkIHNpbXVsYXRpb24gb3IgcGVyc2lzdGVudCBzY2hlbWEgd2FzIGFkZGVkLiIsICJzdGF0dXMiOiAib2siLCAidHMiOiAiMjAyNi0wOC0yOVQxNDo1ODozOSswMDowMCIsICJ0eXBlIjogImltcGxlbWVudGF0aW9uIn0=", "stored_at": "2026-08-29T14:58:39.567023+00:00", "type": "ghostlight.evidence_record.v0"}
{"key": "2026-08-29T191202Z0000-0230-6b340eadd8d4", "payload": "eyJub3RlIjogIk5ld3Nyb29tIGNvbnRyYWN0IHY4IHJlcGxhY2VzIHRoZSB2NyBjb3B5L05pZ2h0IGFjY2VwdC1yZWplY3QgdHJpYnVuYWwgYW5kIGl0ZXJhdGl2ZSByZXdyaXRlIGxvb3Agd2l0aCBvbmUgY29tcGxldGUgY29weS1kZXNrIHF1ZXJ5IHJlcG9ydCwgb25lIG1heF9zdGVwcz0xIE5pZ2h0IEVkaXRvciBjbG9zZSwgYW5kIGEgZGV0ZXJtaW5pc3RpYyBwcmVzcyB3aXRuZXNzLiBFdmVyeSBxdWVyaWVkIGFydGljbGUgbXVzdCBiZSByZXdyaXR0ZW47IHVucXVlcmllZCBhcnRpY2xlcyBtYXkgcmVjZWl2ZSBvcHRpb25hbCBkZWFkbGluZSBwb2xpc2ggd2hpbGUgc2VsZWN0aW9uLCBvcmRlciwgc2VjdGlvbnMsIGJ5bGluZXMsIGNpdGF0aW9ucywgYW5kIGFnZW5kYSBzdGF5IGZyb3plbi4gRXhhY3QgdjcgY29weS10aXAgaW1wb3J0IHJlc3VtZXMgUnVuIDU4IHdpdGhvdXQgcmVwbGF5aW5nIG5hcnJhdGl2ZSBzZWxlY3Rpb24sIHdyaXRlciwgY29weSBkZXNrLCB3b3JsZCwgb3Igd2F2ZXMuIEV4dGVybmFsIHJldmlld3MgYXJlIHBvc3QtcHJlc3MgZXZpZGVuY2UgcmF0aGVyIHRoYW4gdmV0b2VzOyB0eXBlZCBsYXRlci1jb3JyZWN0aW9uIG1lbW9yeSByZW1haW5zIHBlbmRpbmcuIFZlcmlmaWVkIDIwLzIwIG5ld3NwYXBlciwgNDc1LzQ3NSBsaWJyYXJ5LCBhbmQgMTYvMTYgc3RyYXRlZ2ljLXNtb2tlIHRlc3RzLiIsICJzdGF0dXMiOiAib2siLCAidHMiOiAiMjAyNi0wOC0yOVQxOToxMjowMiswMDowMCIsICJ0eXBlIjogImltcGxlbWVudGF0aW9uIn0=", "stored_at": "2026-08-29T19:12:02.108593+00:00", "type": "ghostlight.evidence_record.v0"}
{"key": "2026-08-29T195000Z0000-0231-c1dcff35845b", "payload": "eyJub3RlIjogIkV4YWN0LXNvdXJjZSBSdW4gNTkgYXQgcHVzaGVkIDE2MWZmMmYxOTAyMTI0OTU2M2UxOTJjY2U5ZTZmNzZmMDA0MjlkNDggcHJpbnRlZCBmcm9tIGltbXV0YWJsZSBSdW4gNTgvcmV2aXNpb24gMjcgd2l0aG91dCB1cHN0cmVhbSByZXBsYXkuIFRoZSBwcmVzcyB3aXRuZXNzIGJpbmRzIG9uZSBjb3B5IHJlcG9ydCB3aXRoIGZpdmUgcXVlcmllcywgb25lIGFwcGxpZWQgTmlnaHQgY2xvc2UsIHRocmVlIGNoYW5nZWQgYXJ0aWNsZXMsIGFuZCB1bmNoYW5nZWQgbGl2ZSBzZXJ2aWNlczsgYXJjaGl2ZSBTSEEtMjU2IGVjYjRkYjQxZTlhNDBlZTRkYjhiMGY4M2RjZTQyZDc0ZTQwZjk0MGU1OTMwYjFkM2MxY2VkMjI3ZGZlYzc2YjMgbWF0Y2hlcy4gTmV3c3BhcGVyIGVlMmNlMDhjZWI2MjdkNDMzYWMyZDRmZjdlMWViZWNiMTQzN2JmMjhiMTM5OWQyM2JmYjNkNWIwNjZjOGFiMjcgYW5kIGF1ZGl0IGIyMTBkNTBkNGVmMWEzNTAzMTVkNTI4ZjZkMzdlMWU3YjYxODkyNjM0NjcxM2Y3MzdjNDE2OWM0OWRmMjZiNDkgYXJlIGZyb3plbi4gQSBmaW5hbGl6ZXIgc2NoZW1hIGV4cGVjdGF0aW9uIHdhcyBjb3JyZWN0ZWQgYW5kIGZpbmFsaXphdGlvbiByZXN1bWVkIHdpdGhvdXQgYSBtb2RlbCBjYWxsLiBQb3N0LXByZXNzIGVkaXRvcmlhbCBhbmQgdHdvIGdyb3VuZGluZyBkaWFnbm9zdGljcyBhcmUgbGF0ZXIgcHVibGljYXRpb24gZGVidCBhbmQgZG8gbm90IHJlb3BlbiBvciBibG9jayBkZWxpdmVyeSBvZiB0aGlzIGVkaXRpb24uIiwgInN0YXR1cyI6ICJvayIsICJ0cyI6ICIyMDI2LTA4LTI5VDE5OjUwOjAwKzAwOjAwIiwgInR5cGUiOiAidmVyaWZpY2F0aW9uIn0=", "stored_at": "2026-08-29T19:50:00.186912+00:00", "type": "ghostlight.evidence_record.v0"}
{"key": "2026-08-29T202924Z0000-0232-b61cd79c3dfd", "payload": "eyJub3RlIjogIlJ1biA1OSBwcm9tcHQgYXVkaXQgZmFsc2lmaWVzIHY4IGFzIHRoZSB0YXJnZXQgbmV3c3Jvb20vY2FjaGUgYXJjaGl0ZWN0dXJlOiBuYXJyYXRpdmUgc2VsZWN0aW9uIG1hZGUgdGVuIHByb3ZpZGVyIGNhbGxzIG92ZXIgMSwzNzMsMjA4IGlucHV0IGNoYXJhY3RlcnMgYW5kIDU4MiwxODQgcHJvbXB0IHRva2VucyB3aXRoIHplcm8gY2FjaGVkIHRva2Vucy4gVGhlIGdlbmVyaWMgYWdlbnQgaGFybmVzcyBwbGFjZXMgY2hhbmdpbmcgYWN0aW9uIHNjaGVtYSBiZWZvcmUgc3RhYmxlIHdvcmsgYW5kIHJlcGxheXMgdGhlIGFjY3VtdWxhdGVkIHRvb2wgdHJhbnNjcmlwdDsgc2VsZWN0b3IsIHdob2xlLXBhZ2Ugd3JpdGVyLCBhbmQgTmlnaHQgRWRpdG9yIGR1cGxpY2F0ZSBlZGl0b3JpYWwgZG9jdHJpbmUuIE9wZXJhdG9yLWF1dGhvcml6ZWQgb3duZXJzIGFyZSBzdG9yeS1hbmQtam91cm5hbGlzdCBhc3NpZ25tZW50IGZyb20gdGhlIGZyb3plbiBsZWRnZXIsIGJvdW5kZWQgcGVyc29uYWxpdHktYmVhcmluZyBqb3VybmFsaXN0IGRyYWZ0aW5nLCBjb3B5IGRlc2sgYXMgc29sZSBzZW1hbnRpYyBmYWN0dWFsIHF1ZXJ5IG93bmVyLCBvbmUgTmlnaHQgRWRpdG9yIGNoZWNrbGlzdCByZXBhaXIgcHJlc2VydmluZyBuYXJyYXRpdmUsIGFuZCBkZXRlcm1pbmlzdGljIHN0cnVjdHVyYWwvcHJvdmVuYW5jZS9xdWVyeS1kaXNwb3NpdGlvbiBwcmVzcyBhZG1pc3Npb24uIEV4YWN0IHNjaGVtYSBhbmQgY2FjaGUgY3V0cyByZW1haW4gcGVuZGluZyBib3VuZGVkIHNvdXJjZSBpbnNwZWN0aW9uLiIsICJzdGF0dXMiOiAib2siLCAidHMiOiAiMjAyNi0wOC0yOVQyMDoyOToyNCswMDowMCIsICJ0eXBlIjogInJlc2VhcmNoIn0=", "stored_at": "2026-08-29T20:29:24.348689+00:00", "type": "ghostlight.evidence_record.v0"}
{"key": "2026-08-29T214215Z0000-0233-cdea7f28b17e", "payload": "eyJub3RlIjogImNoYXJhY3Rlci1uZXdzcm9vbS52OSByZXBsYWNlcyB0aGUgbW9ub2xpdGhpYyBwYWdlIHdyaXRlciB3aXRoIGFuIGVtYm9kaWVkIGFzc2lnbm1lbnQgZWRpdG9yIGFuZCBvbmUgYm91bmRlZCBqb3VybmFsaXN0IGFnZW50IHBlciBhc3NpZ25lZCBzdG9yeSwgcHJlc2VydmVzIGV4YWN0IGFzc2lnbm1lbnQtb3duZWQgc3RydWN0dXJlLCBib3VuZHMgTmlnaHQgRWRpdG9yIGNvbnRleHQgdG8gY29weS1xdWVyaWVkIGFydGljbGVzIGFuZCBmYWN0cywgYW5kIG1ha2VzIGdlbmVyaWMgYWdlbnQgcmVxdWVzdHMgYXBwZW5kLW9ubHkgd2l0aCBzdGFibGUgc2NoZW1hcyBhbmQgY2FjaGUgcm91dGluZy4gVmVyaWZpZWQgNDc1LzQ3NSBsaWJyYXJ5IHRlc3RzLCAxNi8xNiBzdHJhdGVnaWMtc21va2UgdGVzdHMsIGFuZCBnaG9zdGxpZ2h0LWR1bmdlb24gYWxsLXRhcmdldCBjaGVja3M7IG5vIHByb3ZpZGVyIG9yIHNpbXVsYXRpb24gcnVuIHdhcyBsYXVuY2hlZC4iLCAic3RhdHVzIjogIm9rIiwgInRzIjogIjIwMjYtMDgtMjlUMjE6NDI6MTUrMDA6MDAiLCAidHlwZSI6ICJpbXBsZW1lbnRhdGlvbiJ9", "stored_at": "2026-08-29T21:42:15.490465+00:00", "type": "ghostlight.evidence_record.v0"}

View file

@ -195,7 +195,6 @@ live_architecture:
active_subgoals:
- Deliver and preserve the printed Run 59 Canopy Ledger with its one copy report, one deadline-bound Night close, deterministic press witness, and exact artifact, manifest, provenance, and service-isolation proof. The edition is closed and may not be reopened by post-press review; diagnostic findings become later publication debt.
- After Run 59 delivery, implement the publication-Persona direction admitted in plan commit `4916638`: each publication persists its own audience, remembered beats, narrative memeplex, selective story pickup, recurring throughlines, ideological framing, rivals, and later corrections; multiple Delvehold publications may interpret the same committed event substrate differently. Grounding constrains factual assertions and provenance without neutralizing legible interpretation, selection, framing, or insinuation. Do not fold this future authority cut into the closed Run 59 edition or patch it into copy-desk prompting.
- Connect a Delvehold-owned adapter to the admitted generic consumer contract: shared compiler/consumer WorldSeed admission, externally authoritative institution-or-Gestalt snapshots, attributed proposal acknowledgement, and committed-event newspaper composition over typed CultNet, without consumer-specific IDs or schemas in Ghostlight.
- Extend strategic/offscreen named-person individuation beyond the proven emerge-then-act path so people plot across sustained political waves, acquire relationships, fold, and return without granting a cell, arena, or model commit authority.
- Pressure the proven 200-cell dispatcher with authored consumer hierarchies and measure provider cost; the active budget ceiling is 240 while provider concurrency remains separately capped at 32.
@ -327,6 +326,6 @@ data_flow:
current_status:
summary: Ghostlight's generic consumer boundary admits sparse WorldSeeds, runs the hundreds-class parallel cell machinery used by Dungeon, and projects grounded newspapers from committed public events. Local acceptance proves 1,000 subjects across 200 cells and two-wave durable named-person emergence. Exact-source provider run 38 at `596e50679244b5d42b4cfdf70d723f815751b8ff` resumed the preserved world at revision 8 without replaying compilation or four committed locality elaborations, recovered 51 of 52 strategic-wave cells on pulse three and the final cell on pulse four, then committed all six waves through revision 14. Pushed commit `3a69306f3004e4af2cd57bba612a6ad8d1b8132f` implements the mapped clock-consequence owner and is verified locally: world clocks own declared consequence text and exact observable scope; a bounded agent may propose legacy scope binding, while `WorldKernel` alone admits it and materializes stable threshold-crossing events and public news through the common mutation path. Binding, emitted-event ancestry, mutation receipts, and the next newspaper boundary persist atomically. Exact-source run 39 resumed run 38 without replaying its six committed waves, bound clock consequences, committed six new waves through revision 21, and emitted Issues 7 through 12 plus a final digest. This completes the mechanical provider acceptance path; all inherited wave 1-6 checkpoint hashes match run 38 exactly and live services were unchanged. Independent blind editorial review returns FAIL: only Issue 7 approaches spicy, while Issues 8-12 are dominated by plans, filings, inspections, warnings, and abstract relationship deltas without betrayal, scandal, public reaction, sustained reversal, or material lived cost. That run-39 diagnosis is superseded for the current phase: run 51 closes the world and reconciliation mechanics, and the active defect belongs to newsroom narrative selection and self-contained semantic provenance over immutable facts. Independent grounding review also returns FAIL: Issue 9 inflates a planned expedited agenda into completion, while Issue 8 assigns unsupported feminine pronouns to Sela Venn and Eryn Tal. Other grounding mechanics pass: 208/208 citations resolve, no future events appear, no replay occurred, the player remained unchanged, all 116/116 manifest entries verify, the archive is exact, and root independently confirmed the live service PIDs and invocation IDs remained unchanged. Editorial and grounding acceptance are terminally failed for run 39. Eight titled elaborators use the generic agent harness under deterministic weighted scheduling, bounded parallelism, and Luna/Terra/Sol routing. Workers remain proposal-only; deterministic admission binds exact frozen schedules, dispatch, receipts, conflicts, and semantic verification, and `WorldKernel` alone owns atomic CultCache persistence. Destination reconciliation is a private transactional workbench, provider schema compatibility is lowered once at the shared model-connector boundary, and durable elaboration and strategic-pulse checkpoints bind to their canonical mutation proofs. The generic ModelAgent harness remains consumer-neutral transport. Provider run 23 remains stopped; earlier failed runs and checkpoint archives remain immutable provenance. The live daemons remain outside the acceptance body. Dungeon play remains regression pressure, the Delvehold game-side adapter remains unimplemented, and unpublished Mars and Kalsa destination previews remain unapproved.
provider_acceptance_checkpoint: Exact-source Run 59 at pushed commit `161ff2f19021249563e192cce9e6f76f00429d48` is printed from immutable Run 58 at world revision 27 with no selection, writer, copy-desk, world, or wave replay. Its acceptance witness binds one copy report with five queries, one Night Editor close, `night_editor_action_applied=true`, three changed articles, and unchanged live-service PID/invocation identities. Newspaper SHA-256 is `ee2ce08ceb627d433ac2d4ff7e1ebecb1437bf28b1399d23bfb3d5b066c8ab27`; audit is `b210d50d4ef1a350315d528f6d37e1e7b618926346713f737c4169c49df26b49`; immutable local archive `F:\GameCult\GhostlightDungeon\acceptance\elven-realms-autonomous-161ff2f-59.tar` is `ecb4db41e9a40ee4db8b0f83dce42d74e40f940e5930b1d3c1ced227dfec76b3`. A finalizer verifier incorrectly expected a schema field on the assessment-plus-queries report; finalization resumed without another model call. The edition satisfies the press and delivery boundary.
implementation_checkpoint: Pushed exact commit `161ff2f19021249563e192cce9e6f76f00429d48` owns provider-proven `canopy-ledger-night-editor.v8`. The copy desk emits one assessment plus complete consequential query set. One single-step Night Editor action must disposition every query and rewrite every queried article, may polish unqueried articles, and freezes selection, order, sections, bylines, citations, and agenda. One deterministic `WorldNewspaperPressClose` witness binds the source and printed pages, both model receipts, addressed queries, changed articles, source bounds, and exact lineage; no model rereads or grades the closed page. Verification passes 20/20 newsroom tests, 475/475 library tests, and 16/16 strategic-smoke tests. Durable publication-memory corrections remain unimplemented.
next_action: Deliver the actual immutable Run 59 copy and its press/artifact proof; do not rerun, rewrite, or withhold the edition. Preserve two post-press grounding correction candidates as later publication debt: line 47 over-scopes “maintained without disturbing” from cordon and warnings to barriers and path, and line 19 changes historical stalled lifts into “remains stalled.” Preserve the editorial diagnostic—strong child-seizure reporting and raw scandal, but institutional paste, thin lived/public reaction, repetition, an HTML entity, and lowercase `sella`—as future newsroom memory rather than a delivery gate. After delivery, move to the publication-Persona and correction-memory owner; defer multi-publication expansion, TeX layout, and engraved cuts until separately authorized.
implementation_checkpoint: `character-newsroom.v9` is implemented and locally verified. The assignment editor sees one embodied publication roster and owns story, section, evidence, and journalist assignment. One bounded journalist agent writes each assigned story from only its own profile, pitch, and exact cited records; assignment-owned byline, section, citations, and order are assembled deterministically. Dalen Marr alone emits one complete factual checklist. Meret Sorn receives only queried articles, affected assignments, their cited facts, and affected reporter profiles, then gets one checklist-bound close action over exactly those articles. The generic model harness now preserves typed append-only user/assistant request prefixes, keeps schemas out of prompt text, and routes stable stage/model/schema cache keys; selector query/proposal/commit and Night close each keep one stable action schema while their private deterministic workbenches enforce current legality. The one-shot whole-page writer and generic byline registry are deleted. No dependency, crate, binary, daemon, persistent schema, event authority, or simulation pass was added. Gates pass 475/475 library tests, 16/16 strategic-smoke tests, and all-target checks.
next_action: Preserve Run 59 as immutable evidence. Exercise `character-newsroom.v9` in a separately authorized newsroom-only provider run to measure real cached tokens and judge the embodied reporters' copy; do not replay world simulation. Preserve Run 59 findings as later publication debt and defer multi-publication expansion, TeX layout, and engraved cuts.
gestalt_population: Gestalt Personas own shared population baselines; persistent member deltas own named individuality; WorldKernel materialization and approval-gated fission preserve lineage and identity. Materialisation/folding changes only representation and the exact member delta, advances resolution epoch, and retires the old cover; it has no Gestalt-wide effect authority. Selected actionable member exceptions are mandatory Projector perspective owners, so aggregation cannot silently erase their decision. Presence projection is limited to nearby active leaves and exact-location dormant members, includes reciprocal player relationship evidence, and may propose an earned cast without committing it. Simulation cells are derived covers only.

View file

@ -1,4 +1,4 @@
# Ghostlight Scratch
# Scratch Working Memory
## Current Subgoal