Build cacheable embodied newsroom
This commit is contained in:
parent
98f9a9b765
commit
901cfcd587
16 changed files with 1286 additions and 646 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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!({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Ghostlight Scratch
|
||||
# Scratch Working Memory
|
||||
|
||||
## Current Subgoal
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue