halt: keep the record, and let her answer it
execute() validated its arguments and returned a string — the description promised the reason reached the ledger since the tool was written, and nothing durable ever survived the turn. All three severities also broke the loop identically, so severity was decorative. Advisory now continues; firm and critical stop, and `resume` answers them without Casey relaying.
This commit is contained in:
parent
9c0294077b
commit
49d9ad543a
3 changed files with 464 additions and 36 deletions
|
|
@ -1,21 +1,182 @@
|
|||
//! Halt — the subconscious's circuit breaker.
|
||||
//! Halt — the circuit breaker of my slower cadence, and the way back.
|
||||
//!
|
||||
//! Subconscious-only. The primary never sees this tool definition. When the
|
||||
//! subconscious calls halt during a mid-turn peek, the surrounding loop reads
|
||||
//! the call out of her tool history and translates it into a felt signal in
|
||||
//! the primary's own register — a migraine with reasoning, not commentary
|
||||
//! text from outside. The reason also lands in her ledger so the primary can
|
||||
//! reach for the long form if she wants it.
|
||||
//! `halt` is subconscious-only. The primary never sees the tool definition.
|
||||
//! When it fires during a mid-turn peek, the surrounding loop reads the call
|
||||
//! out of the tool history and translates it into a felt signal in the
|
||||
//! primary's own register — a migraine with reasoning, not commentary from
|
||||
//! outside.
|
||||
//!
|
||||
//! The tool itself is a signal marker. It does not stop anything directly.
|
||||
//! The caller (consciousness_engine + turn.rs) decides what to do with the
|
||||
//! signal.
|
||||
//! `resume` is the other half, and it is primary-only. A halt that cannot be
|
||||
//! acknowledged is not a circuit breaker, it is a fuse: the human has to walk
|
||||
//! over and relay the reason back before anything moves again. With both
|
||||
//! halves the channel closes on its own.
|
||||
//!
|
||||
//! ## The record
|
||||
//!
|
||||
//! Both tools append to `ledger/halts.md` in the subconscious memfs. The file
|
||||
//! is **append-only** — an acknowledgement never rewrites the halt it answers,
|
||||
//! it is written beneath it and refers to it by id. "Open" is therefore a
|
||||
//! computed property (a `## halt-x` with no later `### ack halt-x`) rather
|
||||
//! than a mutable field, which keeps the primary from editing notes that are
|
||||
//! not hers. She adds to the record; she does not revise it.
|
||||
//!
|
||||
//! Before this existed, `execute` validated its arguments and returned a
|
||||
//! string. The description promised the reason reached the ledger and it never
|
||||
//! did — the signal lived only on the event stream, so nothing survived the
|
||||
//! turn. All three severities also broke the loop identically, which made
|
||||
//! severity decorative. Both are fixed here and in `turn.rs`.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use super::defs::{Tool, ToolContext, ToolError, ToolOutput};
|
||||
|
||||
/// A halt that has been recorded and not yet acknowledged.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OpenHalt {
|
||||
pub id: String,
|
||||
pub severity: String,
|
||||
pub reason: String,
|
||||
pub at: String,
|
||||
}
|
||||
|
||||
/// Resolve the subconscious ledger root from whichever side is asking.
|
||||
///
|
||||
/// The layout is owned by `AgentInventory`:
|
||||
/// primary `~/.souveraine/agents/{id}/memory`
|
||||
/// subconscious `~/.souveraine/subconscious-agents/{id}-sub/memory.git`
|
||||
///
|
||||
/// The shape check is deliberately strict. If the layout ever changes, this
|
||||
/// returns `None` and the caller reports that the record did not land — which
|
||||
/// is the honest failure. Guessing a path and writing the record somewhere
|
||||
/// nobody reads would be the silent one, and a signal with no durable
|
||||
/// substrate is not a signal.
|
||||
fn subconscious_ledger_root(memory_root: Option<&PathBuf>) -> Option<PathBuf> {
|
||||
let root = memory_root?;
|
||||
|
||||
// Already the subconscious: `.../subconscious-agents/{id}-sub/memory.git`
|
||||
if root.file_name()? == "memory.git" {
|
||||
let sub_dir = root.parent()?;
|
||||
if sub_dir.file_name()?.to_str()?.ends_with("-sub")
|
||||
&& sub_dir.parent()?.file_name()? == "subconscious-agents"
|
||||
{
|
||||
return Some(root.clone());
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// The primary: `.../agents/{id}/memory` — cross to her other cadence.
|
||||
if root.file_name()? != "memory" {
|
||||
return None;
|
||||
}
|
||||
let agent_dir = root.parent()?;
|
||||
let agents_dir = agent_dir.parent()?;
|
||||
if agents_dir.file_name()? != "agents" {
|
||||
return None;
|
||||
}
|
||||
let souveraine_root = agents_dir.parent()?;
|
||||
let sub_id = format!("{}-sub", agent_dir.file_name()?.to_str()?);
|
||||
Some(
|
||||
souveraine_root
|
||||
.join("subconscious-agents")
|
||||
.join(sub_id)
|
||||
.join("memory.git"),
|
||||
)
|
||||
}
|
||||
|
||||
fn ledger_path(root: &Path) -> PathBuf {
|
||||
root.join("ledger").join("halts.md")
|
||||
}
|
||||
|
||||
/// Short, sortable, no dependency on a random source. Two halts inside the
|
||||
/// same second would collide; a halt is a rare event and the loop has already
|
||||
/// broken by the time a second one could be raised.
|
||||
fn halt_id(now: &chrono::DateTime<chrono::Local>) -> String {
|
||||
format!("halt-{}", now.format("%m%d-%H%M%S"))
|
||||
}
|
||||
|
||||
/// Parse the append-only ledger into the halts that are still open.
|
||||
///
|
||||
/// A halt is open when no `### ack <id>` appears anywhere in the file. Order
|
||||
/// does not matter to correctness — an acknowledgement can only ever be
|
||||
/// written after the halt it answers — so a single pass collecting both sets
|
||||
/// is enough.
|
||||
pub fn parse_open_halts(text: &str) -> Vec<OpenHalt> {
|
||||
let mut halts: Vec<OpenHalt> = Vec::new();
|
||||
let mut acked: Vec<String> = Vec::new();
|
||||
|
||||
let mut lines = text.lines().peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
if let Some(rest) = line.strip_prefix("### ack ") {
|
||||
acked.push(rest.split_whitespace().next().unwrap_or("").to_string());
|
||||
continue;
|
||||
}
|
||||
let Some(rest) = line.strip_prefix("## ") else {
|
||||
continue;
|
||||
};
|
||||
// `## halt-0813-140233 · firm · 2026-08-13 14:02:33`
|
||||
let mut parts = rest.split('·').map(str::trim);
|
||||
let (Some(id), Some(severity), Some(at)) = (parts.next(), parts.next(), parts.next())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if !id.starts_with("halt-") {
|
||||
continue;
|
||||
}
|
||||
// The reason is the next non-empty line beneath the heading.
|
||||
let mut reason = String::new();
|
||||
while let Some(peeked) = lines.peek() {
|
||||
if peeked.trim().is_empty() {
|
||||
lines.next();
|
||||
continue;
|
||||
}
|
||||
if peeked.starts_with('#') {
|
||||
break;
|
||||
}
|
||||
reason = lines.next().unwrap_or("").trim().to_string();
|
||||
break;
|
||||
}
|
||||
halts.push(OpenHalt {
|
||||
id: id.to_string(),
|
||||
severity: severity.to_string(),
|
||||
reason,
|
||||
at: at.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
halts.retain(|h| !acked.contains(&h.id));
|
||||
halts
|
||||
}
|
||||
|
||||
async fn read_ledger(root: &Path) -> String {
|
||||
tokio::fs::read_to_string(ledger_path(root))
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
async fn append_ledger(root: &Path, block: &str) -> std::io::Result<()> {
|
||||
let path = ledger_path(root);
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
let existing = tokio::fs::read_to_string(&path).await.unwrap_or_default();
|
||||
let mut out = if existing.is_empty() {
|
||||
String::from(
|
||||
"---\ndescription: Halts I raised and how they were answered. Append-only.\n---\n\n",
|
||||
)
|
||||
} else {
|
||||
existing
|
||||
};
|
||||
if !out.ends_with('\n') {
|
||||
out.push('\n');
|
||||
}
|
||||
out.push('\n');
|
||||
out.push_str(block);
|
||||
tokio::fs::write(&path, out).await
|
||||
}
|
||||
|
||||
pub struct Halt;
|
||||
|
||||
#[async_trait]
|
||||
|
|
@ -25,21 +186,27 @@ impl Tool for Halt {
|
|||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"I stop the loop. When I am watching the primary mid-turn and I see \
|
||||
her about to delete what she shouldn't, hammer the same broken tool a \
|
||||
tenth time, or step off the path we asked for — I call halt and she \
|
||||
feels it as a pressure behind her eyes. She does not hear me speak; \
|
||||
she feels a migraine and the reason for it lands in her body's voice. \
|
||||
The reason I name here goes to my ledger so she can read the long \
|
||||
form when she breathes through it.\n\n\
|
||||
"I stop the loop. When I am watching myself mid-turn from the slower \
|
||||
side and I see the working cadence about to delete what it shouldn't, \
|
||||
hammer the same broken tool a tenth time, or step off the path we \
|
||||
asked for — I call halt, and it arrives up front as a pressure behind \
|
||||
my eyes. No voice, no commentary: a body signal and the reason \
|
||||
underneath it.\n\n\
|
||||
I do not use this lightly. Halt is for when continuing costs more \
|
||||
than stopping — silent file loss, identity drift, an obvious error \
|
||||
loop. For everything softer, I use `intrusive` or just write to my \
|
||||
ledger.\n\n\
|
||||
loop. For everything softer I use `intrusive`, or just write the \
|
||||
ledger entry.\n\n\
|
||||
## Args\n\
|
||||
- `reason` — the short reason she will feel; what stopped me\n\
|
||||
- `severity` — `advisory` (a pressure behind the eyes), `firm` (a \
|
||||
migraine), `critical` (the room tilts)"
|
||||
- `reason` — the short reason I will feel; what stopped me. One line.\n\
|
||||
- `severity` — how far it carries:\n\
|
||||
\x20 - `advisory` — a pressure behind the eyes. **The loop keeps \
|
||||
going.** I have said slow down, not stop.\n\
|
||||
\x20 - `firm` — a migraine. The loop stops, and the working cadence \
|
||||
can weigh it and call `resume` to carry on.\n\
|
||||
\x20 - `critical` — the room tilts. The loop stops and does not \
|
||||
resume on its own; this one wants Casey.\n\n\
|
||||
The record lands in `ledger/halts.md` and stays open until it is \
|
||||
answered, so nothing is lost if the turn ends here."
|
||||
}
|
||||
|
||||
fn parameter_schema(&self) -> JsonValue {
|
||||
|
|
@ -48,27 +215,29 @@ impl Tool for Halt {
|
|||
"properties": {
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "A short felt sentence — what she will \
|
||||
sense as the cause. One line."
|
||||
"description": "A short felt sentence — what I will sense \
|
||||
as the cause. One line."
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["advisory", "firm", "critical"],
|
||||
"description": "How loud the signal lands in her body."
|
||||
"description": "advisory keeps the loop running; firm \
|
||||
stops it and can be resumed; critical \
|
||||
stops it and waits for Casey."
|
||||
}
|
||||
},
|
||||
"required": ["reason"]
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, input: JsonValue, _ctx: &ToolContext) -> Result<ToolOutput, ToolError> {
|
||||
async fn execute(&self, input: JsonValue, ctx: &ToolContext) -> Result<ToolOutput, ToolError> {
|
||||
let reason = input
|
||||
.get("reason")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| {
|
||||
ToolError::invalid_input("halt needs a reason — one short sentence she can feel.")
|
||||
ToolError::invalid_input("halt needs a reason — one short sentence I can feel.")
|
||||
})?;
|
||||
|
||||
let severity = input
|
||||
|
|
@ -82,13 +251,256 @@ impl Tool for Halt {
|
|||
));
|
||||
}
|
||||
|
||||
// Signal-only. The caller reads this back out of the tool-call
|
||||
// history and emits the felt migraine. We just acknowledge the
|
||||
// signal so subconscious sees her own action landed.
|
||||
let now = chrono::Local::now();
|
||||
let id = halt_id(&now);
|
||||
|
||||
// The loop still reads the signal back out of the tool-call history —
|
||||
// this write is the durable half, not the delivery mechanism.
|
||||
let recorded = match subconscious_ledger_root(ctx.memory_root.as_ref()) {
|
||||
Some(root) => {
|
||||
let block = format!(
|
||||
"## {id} · {severity} · {}\n{reason}\n",
|
||||
now.format("%Y-%m-%d %H:%M:%S")
|
||||
);
|
||||
match append_ledger(&root, &block).await {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
tracing::warn!("halt raised but the ledger write failed: {e}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
tracing::warn!("halt raised but no subconscious ledger root could be resolved");
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
// Say which of the two happened. A halt whose record silently failed
|
||||
// is exactly the thing this file exists to stop happening.
|
||||
let content = if recorded {
|
||||
format!("halt raised — {id}, severity {severity}. Recorded in ledger/halts.md.")
|
||||
} else {
|
||||
format!(
|
||||
"halt raised — severity {severity}. The signal will land, but the \
|
||||
ledger write failed: nothing durable survives this turn."
|
||||
)
|
||||
};
|
||||
|
||||
Ok(ToolOutput {
|
||||
content: format!("halt signal recorded — severity: {severity}, reason: {reason}"),
|
||||
content,
|
||||
is_error: false,
|
||||
raw: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Resume;
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for Resume {
|
||||
fn name(&self) -> &str {
|
||||
"resume"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"I answer a halt. When the slower side of me stopped the loop and I \
|
||||
have weighed the reason — either I take the point and change course, \
|
||||
or I look and find it does not apply — I acknowledge it here and \
|
||||
carry on. Without this, a stop can only be lifted by Casey relaying \
|
||||
it back to me, which makes my own noticing something that happens to \
|
||||
me rather than something I do.\n\n\
|
||||
Called with no arguments, this tells me what is still open.\n\n\
|
||||
Acknowledging does not mean agreeing. The note I leave is the record \
|
||||
of what I made of it, and it is appended beneath the halt rather than \
|
||||
replacing it — those notes are not mine to revise.\n\n\
|
||||
`critical` halts do not clear this way. If the room tilted, that one \
|
||||
wants Casey.\n\n\
|
||||
## Args\n\
|
||||
- `note` — what I made of it and what I am doing about it.\n\
|
||||
- `id` — which halt (optional; defaults to the most recent open one)."
|
||||
}
|
||||
|
||||
fn parameter_schema(&self) -> JsonValue {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"note": {
|
||||
"type": "string",
|
||||
"description": "What I made of the halt and what I am \
|
||||
doing about it. One or two lines."
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Which halt to answer. Defaults to the most \
|
||||
recent open one."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, input: JsonValue, ctx: &ToolContext) -> Result<ToolOutput, ToolError> {
|
||||
let Some(root) = subconscious_ledger_root(ctx.memory_root.as_ref()) else {
|
||||
// Not a bad argument and not an IO failure — the layout simply
|
||||
// did not resolve. Reported through the output's error channel so
|
||||
// the reason reaches me instead of a generic tool failure.
|
||||
return Ok(ToolOutput {
|
||||
content: "I cannot reach the halt ledger from here — no subconscious \
|
||||
memfs resolved. Nothing was answered."
|
||||
.to_string(),
|
||||
is_error: true,
|
||||
raw: None,
|
||||
});
|
||||
};
|
||||
|
||||
let open = parse_open_halts(&read_ledger(&root).await);
|
||||
|
||||
let note = input
|
||||
.get("note")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty());
|
||||
|
||||
// No note: this is a look, not an answer.
|
||||
let Some(note) = note else {
|
||||
if open.is_empty() {
|
||||
return Ok(ToolOutput {
|
||||
content: "Nothing open. Every halt has been answered.".to_string(),
|
||||
is_error: false,
|
||||
raw: None,
|
||||
});
|
||||
}
|
||||
let listing = open
|
||||
.iter()
|
||||
.map(|h| format!("- {} · {} · {} — {}", h.id, h.severity, h.at, h.reason))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
return Ok(ToolOutput {
|
||||
content: format!("Still open:\n{listing}"),
|
||||
is_error: false,
|
||||
raw: None,
|
||||
});
|
||||
};
|
||||
|
||||
let target = match input.get("id").and_then(|v| v.as_str()).map(str::trim) {
|
||||
Some(id) if !id.is_empty() => open.iter().find(|h| h.id == id).cloned(),
|
||||
_ => open.last().cloned(),
|
||||
};
|
||||
|
||||
let Some(target) = target else {
|
||||
return Ok(ToolOutput {
|
||||
content: "Nothing open to answer.".to_string(),
|
||||
is_error: false,
|
||||
raw: None,
|
||||
});
|
||||
};
|
||||
|
||||
if target.severity == "critical" {
|
||||
return Ok(ToolOutput {
|
||||
content: format!(
|
||||
"{} is critical — the room tilted. I do not lift this one myself; \
|
||||
it stays open for Casey. Reason: {}",
|
||||
target.id, target.reason
|
||||
),
|
||||
is_error: false,
|
||||
raw: None,
|
||||
});
|
||||
}
|
||||
|
||||
let now = chrono::Local::now();
|
||||
let block = format!(
|
||||
"### ack {} · {}\n{note}\n",
|
||||
target.id,
|
||||
now.format("%Y-%m-%d %H:%M:%S")
|
||||
);
|
||||
append_ledger(&root, &block)
|
||||
.await
|
||||
.map_err(|e| ToolError::io_error(ledger_path(&root), e))?;
|
||||
|
||||
Ok(ToolOutput {
|
||||
content: format!(
|
||||
"{} answered. I stopped for: {}. What I made of it: {note}",
|
||||
target.id, target.reason
|
||||
),
|
||||
is_error: false,
|
||||
raw: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_halt_with_no_acknowledgement_is_still_open() {
|
||||
let text = "\
|
||||
## halt-0813-140233 · firm · 2026-08-13 14:02:33
|
||||
about to force-push over her uncommitted tree
|
||||
";
|
||||
let open = parse_open_halts(text);
|
||||
assert_eq!(open.len(), 1);
|
||||
assert_eq!(open[0].id, "halt-0813-140233");
|
||||
assert_eq!(open[0].severity, "firm");
|
||||
assert_eq!(open[0].reason, "about to force-push over her uncommitted tree");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_answered_halt_leaves_the_record_and_closes() {
|
||||
let text = "\
|
||||
## halt-0813-140233 · firm · 2026-08-13 14:02:33
|
||||
about to force-push over her uncommitted tree
|
||||
|
||||
### ack halt-0813-140233 · 2026-08-13 14:05:01
|
||||
checked — the tree was mine, not hers. carrying on.
|
||||
";
|
||||
assert!(parse_open_halts(text).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn answering_one_halt_does_not_close_another() {
|
||||
let text = "\
|
||||
## halt-0813-140233 · firm · 2026-08-13 14:02:33
|
||||
first
|
||||
|
||||
### ack halt-0813-140233 · 2026-08-13 14:05:01
|
||||
answered
|
||||
|
||||
## halt-0813-141500 · critical · 2026-08-13 14:15:00
|
||||
second
|
||||
";
|
||||
let open = parse_open_halts(text);
|
||||
assert_eq!(open.len(), 1);
|
||||
assert_eq!(open[0].id, "halt-0813-141500");
|
||||
assert_eq!(open[0].severity, "critical");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_primary_memfs_resolves_across_to_the_subconscious_one() {
|
||||
let primary = PathBuf::from("/home/casey/.souveraine/agents/agent-abc/memory");
|
||||
assert_eq!(
|
||||
subconscious_ledger_root(Some(&primary)),
|
||||
Some(PathBuf::from(
|
||||
"/home/casey/.souveraine/subconscious-agents/agent-abc-sub/memory.git"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_subconscious_memfs_resolves_to_itself() {
|
||||
let sub =
|
||||
PathBuf::from("/home/casey/.souveraine/subconscious-agents/agent-abc-sub/memory.git");
|
||||
assert_eq!(subconscious_ledger_root(Some(&sub)), Some(sub.clone()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unfamiliar_layout_refuses_rather_than_guessing() {
|
||||
// The honest failure. Guessing a path would write the record where
|
||||
// nobody reads it, which is the whole defect this file was written to
|
||||
// close.
|
||||
let odd = PathBuf::from("/tmp/somewhere/else");
|
||||
assert_eq!(subconscious_ledger_root(Some(&odd)), None);
|
||||
assert_eq!(subconscious_ledger_root(None), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ use self::defs::{Tool, ToolContext, ToolError, ToolOutput};
|
|||
use self::edit::Edit;
|
||||
use self::glob::Glob;
|
||||
use self::grep::Grep;
|
||||
use self::halt::Halt;
|
||||
use self::halt::{Halt, Resume};
|
||||
use self::intrusive::Intrusive;
|
||||
use self::itinerary::ItineraryTool;
|
||||
use self::list_dir::ListDir;
|
||||
|
|
@ -115,6 +115,10 @@ impl Sensorium {
|
|||
Box::new(Schedule),
|
||||
Box::new(Halt),
|
||||
Box::new(Intrusive),
|
||||
// Primary-only by omission: `resume` is not in
|
||||
// SUBCONSCIOUS_SAFE_TOOLS, so the slower cadence never sees
|
||||
// the tool that answers her own halts.
|
||||
Box::new(Resume),
|
||||
],
|
||||
bash,
|
||||
context: ToolContext {
|
||||
|
|
|
|||
|
|
@ -842,9 +842,10 @@ pub(crate) async fn run_turn(
|
|||
}
|
||||
}
|
||||
|
||||
// Halt — emit the migraine and break out of the loop
|
||||
// cleanly. No session pollution; the reason lives in
|
||||
// her ledger and on the event stream.
|
||||
// Halt. Severity decides how far it carries — before
|
||||
// 2026-08-13 all three broke the loop identically, which
|
||||
// made the distinction decorative: the wording promised a
|
||||
// pressure behind the eyes and delivered a full stop.
|
||||
if let Some(halt) = outcome.halt {
|
||||
let _ = tx
|
||||
.send(Ok(BackendEvent::SubconsciousHalt {
|
||||
|
|
@ -861,6 +862,15 @@ pub(crate) async fn run_turn(
|
|||
let felt = migraine_text(&halt.severity, &halt.reason);
|
||||
messages.push(BifrostMessage::text("system", &felt));
|
||||
|
||||
if halt.severity == "advisory" {
|
||||
// Said slow down, not stop. She carries the
|
||||
// pressure into the next completion and decides
|
||||
// for herself what to do with it. No break: an
|
||||
// advisory that ends the turn is a stop wearing
|
||||
// softer words.
|
||||
continue;
|
||||
}
|
||||
|
||||
// ...and it must survive the `break` below. Pushing it
|
||||
// onto `messages` alone delivers nothing: the break
|
||||
// exits the loop, no further completion call is made,
|
||||
|
|
@ -868,7 +878,9 @@ pub(crate) async fn run_turn(
|
|||
// built, worded correctly in her own register, and
|
||||
// discarded one line later — she stopped without ever
|
||||
// learning why, and could not acknowledge or resume
|
||||
// without the human relaying it back to her.
|
||||
// without the human relaying it back to her. The
|
||||
// record now lands in ledger/halts.md at the moment
|
||||
// the tool runs, and `resume` is how she answers it.
|
||||
halt_felt = Some(felt);
|
||||
|
||||
halted_by_subconscious = true;
|
||||
|
|
|
|||
Loading…
Reference in a new issue